From 6c7562235e2d7da0a2fb1e689323504fcfae0aa0 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 5 Oct 2026 19:36:03 +0300 Subject: [PATCH 1/7] feat(history): bounded-memory history core, parallel burndown, UAST spill, codefang mcp Analyzer audit fixes plus the bounded-memory history pipeline (FRD-bounded-memory-history, FRD-parallel-burndown, FRD-history-spill, BUG-history-shotness-nested-entities). CHANGELOG.md [Unreleased] lists every change; the highlights: - Every history walk folds through a worker pool: workers claim runs of consecutive commits from a shared cursor, steal from idle peers, and run at most one chunk ahead of the in-order fold; free heap pages go back to the OS after every chunk. Output is byte-identical for every chunk size. - --memory-budget / --chunk-size (and pipeline.memory_budget / pipeline.chunk_size): each walk plans its chunk and worker count from its share and measured per-walk costs; libgit2 gets a quarter of the budget for its object cache and 7.5 % for mapped pack windows; -v prints plans. - The UAST analyzers parse one change at a time and run as one memoized pass; their finished results spill to an unlinked temp file while other history walks run. - history/burndown prepares its per-change work on the pool and reuses the pre-pass tree diffs of heavy commits. - history/shotness: --shotness-keep-ended keeps ended entities, marked. - codefang mcp serves codefang_analyze, uast_parse and codefang_history over stdio; every tool result is the CLI's own report. - The retired Go differential gate and oracle data are removed. Measured on 31,923 kubernetes commits, history/* --format json, output byte-identical to the previous build: previous build 399.8 s 12.9 GiB automatic budget 334.4 s 3.56 GiB --memory-budget 4GB 327.8 s 3.79 GiB Gates: cargo fmt --check, make lint (0), make deadcode (0), 3138 tests. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmiE8YcZ3GtgQW2bDbcgXo --- .claude/settings.json | 27 +- .claude/workflows/codefang-rust-rewrite.js | 297 - .claude/workflows/compat-burndown.js | 150 - .claude/workflows/compat-test-system.js | 191 - .codefang.yaml | 144 +- .deadcode-whitelist | 28 - .dockerignore | 1 - .github/workflows/ci.yml | 12 +- .gitignore | 3 - .goreleaser.yml | 83 - CHANGELOG.md | 938 ++ Cargo.lock | 70 +- Cargo.toml | 1 - README.md | 25 +- bins/codefang/Cargo.toml | 18 +- bins/codefang/src/main.rs | 40 +- bins/codefang/src/watchdog.rs | 134 +- bins/codefang/tests/anomaly_e2e.rs | 352 + bins/codefang/tests/burndown_e2e.rs | 698 + bins/codefang/tests/chunked_history_e2e.rs | 829 ++ bins/codefang/tests/cli_surface_e2e.rs | 394 + bins/codefang/tests/clones_e2e.rs | 474 + bins/codefang/tests/cohesion_e2e.rs | 365 + bins/codefang/tests/comments_e2e.rs | 297 + bins/codefang/tests/common/mod.rs | 30 + bins/codefang/tests/complexity_e2e.rs | 597 + bins/codefang/tests/composition_e2e.rs | 276 + bins/codefang/tests/config_e2e.rs | 446 + bins/codefang/tests/couples_e2e.rs | 489 + bins/codefang/tests/devs_e2e.rs | 410 + bins/codefang/tests/file_history_e2e.rs | 426 + bins/codefang/tests/halstead_e2e.rs | 454 + bins/codefang/tests/history_core_e2e.rs | 671 + bins/codefang/tests/history_formats_e2e.rs | 435 + bins/codefang/tests/history_window.rs | 17 +- bins/codefang/tests/imports_e2e.rs | 301 + bins/codefang/tests/imports_history_e2e.rs | 247 + bins/codefang/tests/mcp_e2e.rs | 236 + bins/codefang/tests/mixed_selection_e2e.rs | 118 + bins/codefang/tests/quality_e2e.rs | 342 + bins/codefang/tests/sentiment_e2e.rs | 272 + bins/codefang/tests/shotness_e2e.rs | 736 + bins/codefang/tests/static_walk_e2e.rs | 311 + bins/codefang/tests/typos_e2e.rs | 317 + bins/uast/Cargo.toml | 8 + bins/uast/src/completion.rs | 28 +- bins/uast/src/main.rs | 68 +- bins/uast/src/mapping.rs | 194 +- bins/uast/src/parse.rs | 77 +- bins/uast/src/query.rs | 75 +- bins/uast/src/validate.rs | 40 +- bins/uast/tests/cli_surface_e2e.rs | 317 + bins/uast/tests/parse_shapes.rs | 201 + bins/uast/tests/query_dsl.rs | 78 + crates/cf-alg-bloom/src/lib.rs | 2 +- crates/cf-alg-cms/src/lib.rs | 2 +- crates/cf-alg-hashutil/src/lib.rs | 2 +- crates/cf-alg-hll/Cargo.toml | 2 +- crates/cf-alg-hll/src/lib.rs | 2 +- crates/cf-alg-lsh/src/lib.rs | 705 +- crates/cf-alg-minhash/src/lib.rs | 23 +- crates/cf-analyze/Cargo.toml | 2 +- crates/cf-analyze/src/conversion.rs | 145 +- crates/cf-analyze/src/metadata.rs | 12 +- crates/cf-analyzer-burndown/Cargo.toml | 2 +- crates/cf-analyzer-burndown/src/lib.rs | 27 +- crates/cf-analyzer-burndown/src/metrics.rs | 505 +- crates/cf-analyzer-burndown/src/survival.rs | 747 + crates/cf-analyzers-plumbing/Cargo.toml | 24 +- crates/cf-analyzers-plumbing/src/analyzer.rs | 105 - .../cf-analyzers-plumbing/src/blob_cache.rs | 385 - crates/cf-analyzers-plumbing/src/clock.rs | 38 - crates/cf-analyzers-plumbing/src/file_diff.rs | 401 - crates/cf-analyzers-plumbing/src/git_model.rs | 131 - .../src/identity_detector.rs | 233 +- .../src/languages_detection.rs | 388 - crates/cf-analyzers-plumbing/src/lib.rs | 116 +- crates/cf-analyzers-plumbing/src/ticks.rs | 320 +- .../src/ticks_anomaly.rs | 71 - crates/cf-analyzers-plumbing/src/tree_diff.rs | 269 - .../cf-analyzers-plumbing/src/uast_changes.rs | 285 - .../cf-analyzers-plumbing/src/uast_iface.rs | 59 - crates/cf-anomaly/Cargo.toml | 17 +- crates/cf-anomaly/src/aggregate.rs | 67 +- crates/cf-anomaly/src/config.rs | 158 +- crates/cf-anomaly/src/detect.rs | 62 +- crates/cf-anomaly/src/lib.rs | 40 +- crates/cf-anomaly/src/metrics.rs | 137 +- crates/cf-anomaly/src/model.rs | 174 +- crates/cf-anomaly/src/store.rs | 92 - crates/cf-anomaly/src/zscore.rs | 154 +- crates/cf-burndown-core/src/file.rs | 127 +- crates/cf-burndown-core/src/lib.rs | 24 +- crates/cf-burndown-core/src/packing.rs | 179 + crates/cf-burndown-core/src/timeline_treap.rs | 5 +- crates/cf-cache/Cargo.toml | 2 +- crates/cf-cache/src/lib.rs | 2 +- crates/cf-checkpoint/src/manager.rs | 2 +- crates/cf-clones/Cargo.toml | 33 +- crates/cf-clones/src/aggregator.rs | 563 +- crates/cf-clones/src/analyzer.rs | 427 - crates/cf-clones/src/engine.rs | 696 +- crates/cf-clones/src/lib.rs | 193 +- crates/cf-clones/src/profile.rs | 217 + crates/cf-clones/src/report.rs | 387 +- crates/cf-clones/src/report_section.rs | 793 +- crates/cf-clones/src/shingler.rs | 145 +- crates/cf-clones/src/uast.rs | 86 - crates/cf-clones/src/visitor.rs | 456 +- crates/cf-cohesion/Cargo.toml | 13 +- crates/cf-cohesion/src/aggregator.rs | 387 +- crates/cf-cohesion/src/analyzer.rs | 823 +- crates/cf-cohesion/src/calc.rs | 397 +- crates/cf-cohesion/src/lib.rs | 79 +- crates/cf-cohesion/src/metrics.rs | 452 +- crates/cf-cohesion/src/report_section.rs | 384 +- crates/cf-cohesion/src/report_value.rs | 80 +- crates/cf-cohesion/src/serialize.rs | 563 - crates/cf-cohesion/src/uast.rs | 382 +- crates/cf-cohesion/src/visitor.rs | 299 +- crates/cf-cohesion/tests/real_trees.rs | 379 + crates/cf-commands/Cargo.toml | 65 +- crates/cf-commands/README.md | 4 +- crates/cf-commands/src/flags.rs | 801 +- crates/cf-commands/src/formats.rs | 115 +- crates/cf-commands/src/handlers.rs | 2828 ++-- .../src/handlers/burndown_ndjson.rs | 1944 ++- .../cf-commands/src/handlers/couples_run.rs | 880 +- crates/cf-commands/src/handlers/go_sort.rs | 418 - crates/cf-commands/src/handlers/history.rs | 4583 ------ .../src/handlers/history/anomaly.rs | 519 + .../src/handlers/history/budget.rs | 369 + .../src/handlers/history/chunks.rs | 736 + .../cf-commands/src/handlers/history/devs.rs | 477 + .../src/handlers/history/file_history.rs | 888 ++ .../src/handlers/history/fold_testutil.rs | 236 + .../src/handlers/history/imports.rs | 212 + .../cf-commands/src/handlers/history/mod.rs | 537 + .../cf-commands/src/handlers/history/pool.rs | 525 + .../src/handlers/history/quality.rs | 200 + .../src/handlers/history/sentiment.rs | 318 + .../cf-commands/src/handlers/history/typos.rs | 341 + .../src/handlers/history_formats.rs | 875 +- .../cf-commands/src/handlers/history_plot.rs | 185 +- .../cf-commands/src/handlers/history_text.rs | 1109 +- crates/cf-commands/src/handlers/plot.rs | 104 +- .../src/handlers/plot_sections/cohesion.rs | 10 +- .../src/handlers/plot_sections/comments.rs | 12 +- .../src/handlers/plot_sections/complexity.rs | 71 +- .../src/handlers/plot_sections/halstead.rs | 120 +- .../handlers/plot_sections/history_anomaly.rs | 2 +- .../plot_sections/history_burndown.rs | 56 +- .../handlers/plot_sections/history_couples.rs | 34 +- .../handlers/plot_sections/history_devs.rs | 16 +- .../plot_sections/history_file_history.rs | 38 +- .../handlers/plot_sections/history_imports.rs | 55 +- .../handlers/plot_sections/history_shared.rs | 8 + .../plot_sections/history_shotness.rs | 37 +- .../src/handlers/section_render.rs | 48 +- .../cf-commands/src/handlers/shotness_run.rs | 1291 +- crates/cf-commands/src/handlers/spill.rs | 163 + .../cf-commands/src/handlers/static_clones.rs | 352 +- .../src/handlers/static_cohesion.rs | 1188 +- .../src/handlers/static_comments.rs | 1463 +- .../src/handlers/static_complexity.rs | 1612 +-- .../src/handlers/static_complexity_bin.rs | 251 +- .../src/handlers/static_complexity_yaml.rs | 578 +- .../src/handlers/static_halstead.rs | 2352 +-- .../src/handlers/static_imports.rs | 761 +- .../cf-commands/src/handlers/static_json.rs | 665 +- crates/cf-commands/src/handlers/uast_walk.rs | 1328 +- crates/cf-commands/src/lib.rs | 947 +- crates/cf-commands/src/mcp.rs | 171 + crates/cf-commands/src/observability.rs | 56 - crates/cf-commands/src/pipeline.rs | 272 +- crates/cf-commands/src/version.rs | 47 +- crates/cf-commands/tests/flag_parity.rs | 237 +- crates/cf-comments/Cargo.toml | 6 + crates/cf-comments/src/aggregator.rs | 264 +- crates/cf-comments/src/analyzer.rs | 925 +- crates/cf-comments/src/lib.rs | 369 +- crates/cf-comments/src/placement.rs | 219 + crates/cf-comments/src/traverse.rs | 197 +- crates/cf-comments/src/types.rs | 306 +- crates/cf-comments/tests/real_trees.rs | 429 + crates/cf-complexity/Cargo.toml | 28 +- crates/cf-complexity/src/discover.rs | 386 + crates/cf-complexity/src/gosort.rs | 8 - crates/cf-complexity/src/lib.rs | 1377 +- crates/cf-complexity/src/metrics.rs | 1024 ++ crates/cf-complexity/src/node.rs | 284 +- crates/cf-complexity/src/report.rs | 657 +- crates/cf-complexity/src/thresholds.rs | 424 + crates/cf-complexity/tests/fixtures.rs | 350 + crates/cf-complexity/tests/fixtures/Foo.java | 25 + .../cf-complexity/tests/fixtures/Probe.java | 16 + crates/cf-complexity/tests/fixtures/cc.c | 24 + crates/cf-complexity/tests/fixtures/cc.go | 116 + crates/cf-complexity/tests/fixtures/cc.js | 28 + crates/cf-complexity/tests/fixtures/cc.py | 35 + crates/cf-complexity/tests/fixtures/cc.rs | 30 + crates/cf-complexity/tests/fixtures/d.go | 15 + crates/cf-complexity/tests/fixtures/extras.c | 14 + crates/cf-complexity/tests/fixtures/extras.go | 100 + crates/cf-complexity/tests/fixtures/extras.js | 15 + crates/cf-complexity/tests/fixtures/extras.py | 16 + crates/cf-complexity/tests/fixtures/extras.rs | 16 + crates/cf-complexity/tests/fixtures/gen.go | 36 + crates/cf-complexity/tests/fixtures/i.go | 29 + crates/cf-composition/Cargo.toml | 16 +- crates/cf-composition/src/aggregator.rs | 180 +- crates/cf-composition/src/analyzer.rs | 203 - crates/cf-composition/src/category.rs | 192 +- crates/cf-composition/src/classifier.rs | 500 +- crates/cf-composition/src/lib.rs | 488 +- crates/cf-composition/src/report_section.rs | 517 +- crates/cf-config/Cargo.toml | 22 +- crates/cf-config/src/defaults.rs | 231 - crates/cf-config/src/facts.rs | 297 - crates/cf-config/src/lib.rs | 871 +- crates/cf-config/src/loader.rs | 745 +- crates/cf-config/src/types.rs | 1149 +- crates/cf-couples/Cargo.toml | 12 +- crates/cf-couples/src/aggregator.rs | 288 +- crates/cf-couples/src/lib.rs | 83 +- crates/cf-couples/src/matrix.rs | 100 +- crates/cf-couples/src/metrics.rs | 228 +- crates/cf-couples/src/report.rs | 132 +- crates/cf-couples/src/report_section.rs | 217 - crates/cf-couples/src/store.rs | 225 - crates/cf-couples/src/tc.rs | 37 +- crates/cf-devs/Cargo.toml | 4 +- crates/cf-devs/src/lib.rs | 95 +- crates/cf-devs/src/metrics.rs | 2 +- crates/cf-devs/src/model.rs | 7 +- crates/cf-devs/src/serialize.rs | 9 +- crates/cf-file-history/Cargo.toml | 12 +- crates/cf-file-history/src/category.rs | 107 + crates/cf-file-history/src/classify.rs | 305 - crates/cf-file-history/src/framework.rs | 106 - crates/cf-file-history/src/lib.rs | 80 +- crates/cf-file-history/src/metrics.rs | 232 +- crates/cf-file-history/src/report.rs | 61 +- crates/cf-file-history/src/tc.rs | 13 +- crates/cf-framework/src/lib.rs | 2 +- crates/cf-gitlib/Cargo.toml | 3 + crates/cf-gitlib/README.md | 8 +- crates/cf-gitlib/src/changes.rs | 2 +- crates/cf-gitlib/src/commit.rs | 58 +- crates/cf-gitlib/src/diff.rs | 2 +- crates/cf-gitlib/src/error.rs | 2 +- crates/cf-gitlib/src/hash.rs | 2 +- crates/cf-gitlib/src/lib.rs | 6 +- crates/cf-gitlib/src/repository.rs | 82 +- crates/cf-gitlib/src/testutil.rs | 483 +- crates/cf-gitlib/src/worker.rs | 2 +- crates/cf-gitlib/tests/native_limits.rs | 62 + crates/cf-godiff/src/lib.rs | 4 +- crates/cf-gojson/oracle/go.mod | 3 - crates/cf-gojson/oracle/main.go | 225 - crates/cf-gojson/oracle/oracle_err.txt | 0 crates/cf-gojson/src/lib.rs | 2 +- crates/cf-gojson/src/marshal.rs | 2 +- crates/cf-gojson/src/value.rs | 2 +- .../tests/oracle_data/_g_vs_json_diffs.txt | 0 .../cf-gojson/tests/oracle_data/_zz_diffs.txt | 0 crates/cf-gojson/tests/oracle_data/floats.tsv | 6729 --------- .../tests/oracle_data/json_cases.json | 147 - .../tests/oracle_data/json_floats.tsv | 6729 --------- crates/cf-gosort/src/lib.rs | 2 +- crates/cf-goyaml/src/lib.rs | 2 +- crates/cf-goyaml/tests/oracle.rs | 193 - crates/cf-halstead/Cargo.toml | 7 +- crates/cf-halstead/src/aggregate.rs | 278 + crates/cf-halstead/src/calculator.rs | 306 +- crates/cf-halstead/src/classify.rs | 398 + crates/cf-halstead/src/detector.rs | 1023 +- crates/cf-halstead/src/lib.rs | 89 +- crates/cf-halstead/src/source.rs | 140 + crates/cf-halstead/src/standalone.rs | 166 +- crates/cf-halstead/src/thresholds.rs | 334 + crates/cf-halstead/tests/real_source.rs | 262 + crates/cf-identity/src/lib.rs | 2 +- crates/cf-imports/Cargo.toml | 32 +- crates/cf-imports/src/aggregate.rs | 142 + crates/cf-imports/src/aggregator.rs | 103 - crates/cf-imports/src/analyzer.rs | 342 - crates/cf-imports/src/extract.rs | 541 + crates/cf-imports/src/history.rs | 432 +- crates/cf-imports/src/language.rs | 96 + crates/cf-imports/src/lib.rs | 100 +- crates/cf-imports/src/metrics.rs | 888 +- crates/cf-imports/src/node.rs | 102 - crates/cf-imports/src/report.rs | 192 +- crates/cf-imports/src/report_section.rs | 388 - crates/cf-imports/src/section.rs | 48 + crates/cf-imports/src/statement.rs | 379 + crates/cf-imports/src/stdlib.rs | 1171 ++ crates/cf-imports/src/store.rs | 94 - crates/cf-imports/tests/extract_languages.rs | 271 + crates/cf-langpath/src/lib.rs | 22 +- .../tests/content_heuristics_diff.rs | 86 - crates/cf-mcp/Cargo.toml | 13 +- crates/cf-mcp/src/gojson.rs | 38 +- crates/cf-mcp/src/providers.rs | 28 +- crates/cf-mcp/src/tools_analyze.rs | 15 +- crates/cf-mcp/src/tools_uast.rs | 4 +- crates/cf-meminfo/src/lib.rs | 102 +- crates/cf-metrics/Cargo.toml | 2 +- crates/cf-metrics/src/lib.rs | 4 +- crates/cf-pathfilter/Cargo.toml | 2 +- crates/cf-pathfilter/build.rs | 2 +- crates/cf-pathfilter/src/enry_vendor.rs | 2 +- crates/cf-pathfilter/src/lib.rs | 2 +- crates/cf-pathfilter/src/vendor_data.rs | 2 +- crates/cf-pathpolicy/src/lib.rs | 2 +- crates/cf-persist/src/gob.rs | 6 +- crates/cf-plotpage/Cargo.toml | 2 +- crates/cf-plotpage/src/echarts.rs | 2 +- crates/cf-plotpage/src/lib.rs | 2 +- crates/cf-plumbing/src/lib.rs | 2 +- crates/cf-quality/Cargo.toml | 20 +- crates/cf-quality/src/analyzer.rs | 387 +- crates/cf-quality/src/data.rs | 95 +- crates/cf-quality/src/lib.rs | 57 +- crates/cf-quality/src/metrics.rs | 567 +- crates/cf-quality/src/serialize.rs | 4 +- crates/cf-quality/src/store.rs | 71 - crates/cf-renderer/Cargo.toml | 2 +- crates/cf-renderer/src/gocompat.rs | 2 +- crates/cf-renderer/src/json.rs | 2 +- crates/cf-renderer/src/lib.rs | 4 +- crates/cf-renderer/src/metrics_output.rs | 2 +- crates/cf-renderer/src/section_renderer.rs | 31 +- crates/cf-renderer/src/terminal.rs | 6 + crates/cf-reportutil/src/binary.rs | 2 +- crates/cf-reportutil/src/lib.rs | 2 +- crates/cf-safeconv/src/lib.rs | 2 +- crates/cf-sentiment/Cargo.toml | 2 +- crates/cf-sentiment/src/analyzer.rs | 219 +- crates/cf-sentiment/src/lib.rs | 19 +- crates/cf-sentiment/src/metrics.rs | 256 +- crates/cf-sentiment/src/model.rs | 16 +- crates/cf-sentiment/src/scorer.rs | 130 +- crates/cf-sentiment/src/store.rs | 80 +- crates/cf-sentiment/src/vader.rs | 138 +- crates/cf-shotness/Cargo.toml | 10 +- crates/cf-shotness/src/aggregate.rs | 249 - crates/cf-shotness/src/lib.rs | 53 +- crates/cf-shotness/src/metrics.rs | 78 +- crates/cf-shotness/src/report.rs | 85 +- crates/cf-shotness/src/state.rs | 646 + crates/cf-shotness/src/store.rs | 192 - crates/cf-shotness/src/types.rs | 79 +- crates/cf-textutil/Cargo.toml | 2 +- crates/cf-textutil/src/json.rs | 2 +- crates/cf-textutil/src/lib.rs | 2 +- crates/cf-typos/Cargo.toml | 7 + crates/cf-typos/src/compat.rs | 273 - crates/cf-typos/src/detect.rs | 376 + crates/cf-typos/src/hash.rs | 67 + crates/cf-typos/src/levenshtein.rs | 102 - crates/cf-typos/src/lib.rs | 94 +- crates/cf-typos/src/metrics.rs | 468 +- crates/cf-typos/src/typos.rs | 101 +- crates/cf-uast-mappings/Cargo.toml | 6 + crates/cf-uast-mappings/README.md | 88 +- crates/cf-uast-mappings/src/c.rs | 27 +- crates/cf-uast-mappings/src/cpp.rs | 25 +- crates/cf-uast-mappings/src/go.rs | 76 +- crates/cf-uast-mappings/src/java.rs | 102 +- crates/cf-uast-mappings/src/javascript.rs | 44 +- crates/cf-uast-mappings/src/kotlin.rs | 17 +- crates/cf-uast-mappings/src/python.rs | 52 +- crates/cf-uast-mappings/src/rust.rs | 130 +- crates/cf-uast-mappings/src/tsx.rs | 32 +- crates/cf-uast-mappings/src/typescript.rs | 32 +- .../tests/schema_vocabulary.rs | 56 + .../cf-uast-mappings/tests/shape_contract.rs | 664 + crates/cf-uast-node/Cargo.toml | 2 +- crates/cf-uast-node/src/dsl/field_access.rs | 27 +- crates/cf-uast-node/src/dsl/lowering.rs | 92 +- crates/cf-uast-node/src/dsl/mod.rs | 158 +- crates/cf-uast-node/src/dsl/operators.rs | 66 +- crates/cf-uast-node/src/dsl/runtime.rs | 83 +- crates/cf-uast-node/src/lib.rs | 2 +- crates/cf-uast-node/src/node.rs | 4 + crates/cf-uast-node/src/tomap.rs | 4 +- crates/cf-uast-spec/src/uast-schema.json | 16 +- crates/cf-uast/build.rs | 4 +- crates/cf-uast/src/lib.rs | 2 +- crates/cf-uast/src/lowering.rs | 211 +- crates/cf-uast/tests/lowering_contract.rs | 178 + crates/cf-version/README.md | 21 +- crates/cf-version/build.rs | 90 +- crates/cf-version/src/lib.rs | 61 +- cspell.json | 152 +- .../0002-two-binary-split-uast-codefang.md | 2 +- llms-full.txt | 2670 ++-- llms.txt | 4 +- mkdocs.yml | 3 +- site/analyzers/anomaly.md | 202 +- site/analyzers/burndown.md | 280 +- site/analyzers/clones.md | 198 +- site/analyzers/cohesion.md | 255 +- site/analyzers/comments.md | 215 +- site/analyzers/complexity.md | 309 +- site/analyzers/composition.md | 196 + site/analyzers/couples.md | 51 +- site/analyzers/developers.md | 231 +- site/analyzers/file-history.md | 110 +- site/analyzers/halstead.md | 288 +- site/analyzers/imports.md | 254 +- site/analyzers/index.md | 45 +- site/analyzers/quality.md | 117 +- site/analyzers/sentiment.md | 88 +- site/analyzers/shotness.md | 193 +- site/analyzers/typos.md | 122 +- site/architecture/overview.md | 207 +- site/architecture/streaming-pipeline.md | 717 +- site/architecture/uast.md | 190 +- site/contributing/index.md | 13 +- site/explanation/anomaly.md | 34 +- site/explanation/burndown.md | 186 +- site/explanation/clones.md | 128 +- site/explanation/cohesion.md | 152 +- site/explanation/comments.md | 146 +- site/explanation/complexity.md | 176 +- site/explanation/couples.md | 83 +- site/explanation/developers.md | 148 +- site/explanation/file-history.md | 60 +- site/explanation/halstead.md | 141 +- site/explanation/imports.md | 63 +- site/explanation/quality.md | 88 +- site/explanation/sentiment.md | 180 +- site/explanation/shotness.md | 93 +- site/explanation/typos.md | 28 +- site/getting-started/first-analysis.md | 84 +- site/getting-started/quickstart.md | 61 +- site/glossary.md | 93 +- site/guide/cli-reference.md | 541 +- site/guide/configuration.md | 487 +- site/guide/data-analytics.md | 251 +- site/guide/output-formats.md | 101 +- site/how-to/analyze-a-bare-repo.md | 6 +- site/how-to/analyze-a-monorepo.md | 12 +- site/how-to/attach-an-mcp-client.md | 19 +- site/how-to/read-a-burndown-chart.md | 6 +- site/how-to/run-in-ci.md | 8 +- site/index.md | 12 +- site/integrations/ai-agents.md | 4 +- site/integrations/docker-and-actions.md | 21 +- site/integrations/mcp.md | 123 +- site/operations/large-scale-scanning.md | 155 +- site/operations/observability.md | 514 +- site/release-notes/unreleased.md | 23 +- specs/go-compat-testing/SPEC.md | 395 - tests/antisim/parity_gate.sh | 239 - tests/compat/README.md | 140 - tests/compat/allowlist.json | 69 - tests/compat/cli_surface/README.md | 81 - tests/compat/cli_surface/cli_surface.py | 340 - tests/compat/cli_surface/extract_surface.py | 343 - tests/compat/cli_surface/run.sh | 35 - .../compat/cli_surface/selftest/self_test.py | 200 - tests/compat/corpus/README.md | 71 - tests/compat/corpus/build_corpus.py | 192 - ...92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx | 56 - ...2543aebf0612d8319ae3c20ea124211c6297fbc.go | 219 - ...d456bfb72c85ff6fda0f62c875528bb5f646c88.ts | 371 - ...972b5f9b89d93e2f116be2266c73c806be8ba58.sh | 61 - ...478a16dfa6a45bcac93825dccb40bf3f001b14ae.c | 101 - ...287d66e63f290ff07d97aa7a2655999cf754d6.cpp | 469 - ...f10000e7090a00109d13e43587ccb077ef340bb.py | 117 - ...f921351c2566074697cb5b66553bf731d5355.json | 136 - ...ac0895c42073fb1dcfc9531690f923d0df81c6c8.h | 39 - ...96e152bc3a37be2c726a8d4264315ebff00e447.js | 18 - ...f30d9cce9e1b2b718bd420b7958f05830659482.rs | 97 - ...89f5aeb2f1b0735b3227000590d17bb47bd868.yml | 39 - .../corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go | 24 - .../cfb1_b345ea0e6cfd0f76.go.evidence.json | 14 - .../cfb1_b345ea0e6cfd0f76.go.go_run_0 | Bin 776 -> 0 bytes .../cfb1_b345ea0e6cfd0f76.go.go_run_1 | Bin 776 -> 0 bytes .../cfb1_b345ea0e6cfd0f76.go.go_run_2 | Bin 776 -> 0 bytes .../cfb1_b345ea0e6cfd0f76.go.rust_out | Bin 773 -> 0 bytes .../fuzzfinds/gojson_b345ea0e6cfd0f76.go | 24 - .../gojson_b345ea0e6cfd0f76.go.evidence.json | 14 - .../gojson_b345ea0e6cfd0f76.go.go_run_0 | 74 - .../gojson_b345ea0e6cfd0f76.go.go_run_1 | 74 - .../gojson_b345ea0e6cfd0f76.go.go_run_2 | 74 - .../gojson_b345ea0e6cfd0f76.go.rust_out | 74 - .../fuzzfinds/goyaml_b345ea0e6cfd0f76.go | 24 - .../goyaml_b345ea0e6cfd0f76.go.evidence.json | 14 - .../goyaml_b345ea0e6cfd0f76.go.go_run_0 | 35 - .../goyaml_b345ea0e6cfd0f76.go.go_run_1 | 35 - .../goyaml_b345ea0e6cfd0f76.go.go_run_2 | 35 - .../goyaml_b345ea0e6cfd0f76.go.rust_out | 35 - ...rics_static-complexity_b345ea0e6cfd0f76.go | 24 - ...mplexity_b345ea0e6cfd0f76.go.evidence.json | 14 - ...ic-complexity_b345ea0e6cfd0f76.go.go_run_0 | 74 - ...ic-complexity_b345ea0e6cfd0f76.go.go_run_1 | 74 - ...ic-complexity_b345ea0e6cfd0f76.go.go_run_2 | 74 - ...ic-complexity_b345ea0e6cfd0f76.go.rust_out | 74 - tests/compat/corpus/known_gaps.json | 83 - tests/compat/corpus/manifest.json | 173 - tests/compat/coverage/README.md | 57 - tests/compat/coverage/build_ledger.py | 261 - tests/compat/coverage/matrix_cell_cov.json | 188 - tests/compat/coverage/matrix_cell_cov.py | 190 - tests/compat/coverage/rust_cov.json | 31 - tests/compat/coverage/rust_cov.py | 192 - tests/compat/coverage/selftest/self_test.py | 272 - tests/compat/expand_matrix.py | 231 - tests/compat/fuzz/README.md | 98 - tests/compat/fuzz/analyzer_fuzz_test.go | 80 - tests/compat/fuzz/go.mod | 8 - tests/compat/fuzz/harness.go | 524 - tests/compat/fuzz/map_fuzz_test.go | 68 - tests/compat/fuzz/parse_fuzz_test.go | 70 - tests/compat/fuzz/run_fuzz.sh | 83 - tests/compat/fuzz/seed.go | 110 - tests/compat/fuzz/selfcheck_test.go | 217 - tests/compat/fuzz/serializer_fuzz_test.go | 76 - .../testdata/fuzz/FuzzMap/c7c64cb562bb5f58 | 3 - .../testdata/fuzz/FuzzParse/d51a495543b4d625 | 3 - .../fuzz/FuzzParseGo/3af36d37b930453f | 2 - .../testdata/fuzz/FuzzQuery/33fc0496576430b7 | 3 - tests/compat/gate.py | 196 - tests/compat/integrity/README.md | 91 - tests/compat/integrity/baseline.json | 44 - tests/compat/integrity/mutation_self_test.sh | 268 - tests/compat/integrity/run.sh | 28 - tests/compat/integrity/tamper_check.py | 407 - tests/compat/ledger.json | 8903 ------------ tests/compat/matrix.toml | 135 - tests/compat/metamorphic/README.md | 80 - .../compat/metamorphic/golden_constants.json | 12 - tests/compat/metamorphic/metamorphic.py | 434 - tests/compat/metamorphic/results.json | 337 - .../selftest/live_constant_test.py | 131 - .../compat/metamorphic/selftest/self_test.py | 261 - tests/compat/oracle/README.md | 99 - tests/compat/oracle/oracle.py | 697 - tests/compat/oracle/parity_gate.sh | 118 - .../uast-unmapped-subtree-rewalk-perf.patch | 53 - tests/compat/oracle/selftest/self_test.py | 211 - tests/compat/results/full.json | 1541 -- tests/compat/results/full.partial.jsonl | 71 - ...ull.partial.preheuristics.1781013773.jsonl | 207 - .../full.partial.stale.1780994184.jsonl | 211 - tests/compat/results/smoke.json | 209 - tests/compat/results/smoke.partial.jsonl | 8 - tests/compat/results/smoke_gate.json | 16 - tests/compat/run.sh | 121 - tests/compat/run_matrix.py | 222 - tests/compat/selftest_matrix.py | 246 - tests/doc-examples/Cargo.toml | 12 +- tests/doc-examples/tests/doc_examples.rs | 28 +- tests/golden-harness/Cargo.toml | 32 - tests/golden-harness/src/lib.rs | 3 - tests/golden-harness/src/main.rs | 232 - tests/golden-harness/tests/golden_diff.rs | 230 - tests/golden/MANIFEST.json | 2639 ---- tests/golden/capture.sh | 356 - tests/golden/run/all_static.bin | Bin 272259 -> 0 bytes tests/golden/run/all_static.json | 9097 ------------ tests/golden/run/all_static.yaml | 11886 ---------------- tests/golden/run/burndown.bin | Bin 367 -> 0 bytes tests/golden/run/burndown.compact | 0 tests/golden/run/burndown.json | 1 - tests/golden/run/burndown.ndjson | 5 - tests/golden/run/burndown.text | 18 - tests/golden/run/burndown.timeseries | 19 - tests/golden/run/burndown.timeseries.ndjson | 5 - tests/golden/run/burndown.yaml | 22 - tests/golden/run/history_anomaly.json | 1 - tests/golden/run/history_couples.json | 1 - tests/golden/run/history_devs.bin | Bin 839 -> 0 bytes tests/golden/run/history_devs.json | 1 - tests/golden/run/history_devs.yaml | 45 - tests/golden/run/history_file-history.json | 1 - tests/golden/run/history_imports.json | 1 - tests/golden/run/history_quality.json | 1 - tests/golden/run/history_sentiment.json | 1 - tests/golden/run/history_shotness.json | 1 - tests/golden/run/history_typos.json | 1 - tests/golden/static/static_clones.bin | Bin 113900 -> 0 bytes tests/golden/static/static_clones.compact | 1 - tests/golden/static/static_clones.json | 6045 -------- .../golden/static/static_clones.perfile.json | 6046 -------- tests/golden/static/static_clones.text | 25 - tests/golden/static/static_clones.yaml | 4009 ------ tests/golden/static/static_cohesion.bin | Bin 13419 -> 0 bytes tests/golden/static/static_cohesion.compact | 1 - tests/golden/static/static_cohesion.json | 894 -- .../static/static_cohesion.perfile.json | 2176 --- tests/golden/static/static_cohesion.text | 11 - tests/golden/static/static_cohesion.yaml | 573 - tests/golden/static/static_comments.bin | Bin 75710 -> 0 bytes tests/golden/static/static_comments.compact | 1 - tests/golden/static/static_comments.json | 262 - .../static/static_comments.perfile.json | 914 -- tests/golden/static/static_comments.text | 17 - tests/golden/static/static_comments.yaml | 4333 ------ tests/golden/static/static_complexity.bin | Bin 33127 -> 0 bytes tests/golden/static/static_complexity.compact | 1 - tests/golden/static/static_complexity.json | 908 -- .../static/static_complexity.perfile.json | 2270 --- tests/golden/static/static_complexity.text | 12 - tests/golden/static/static_complexity.yaml | 1472 -- tests/golden/static/static_composition.bin | Bin 269 -> 0 bytes .../golden/static/static_composition.compact | 1 - tests/golden/static/static_composition.json | 34 - .../static/static_composition.perfile.json | 246 - tests/golden/static/static_composition.text | 15 - tests/golden/static/static_composition.yaml | 19 - tests/golden/static/static_halstead.bin | Bin 35142 -> 0 bytes tests/golden/static/static_halstead.compact | 1 - tests/golden/static/static_halstead.json | 924 -- .../static/static_halstead.perfile.json | 2446 ---- tests/golden/static/static_halstead.text | 14 - tests/golden/static/static_halstead.yaml | 1440 -- tests/golden/static/static_imports.bin | Bin 693 -> 0 bytes tests/golden/static/static_imports.compact | 1 - tests/golden/static/static_imports.json | 66 - .../golden/static/static_imports.perfile.json | 295 - tests/golden/static/static_imports.text | 18 - tests/golden/static/static_imports.yaml | 34 - tests/golden/uast/analyze.json | 44 - tests/golden/uast/analyze.text | 40 - tests/golden/uast/parse.compact | 1 - tests/golden/uast/parse.json | 8379 ----------- tests/golden/uast/parse.tree | 0 tests/golden/uast/query.compact | 1 - tests/golden/uast/query.count | 1 - tests/golden/uast/query.json | 7044 --------- tests/oss-coverage/run.sh | 3 +- 637 files changed, 60904 insertions(+), 157309 deletions(-) delete mode 100644 .claude/workflows/codefang-rust-rewrite.js delete mode 100644 .claude/workflows/compat-burndown.js delete mode 100644 .claude/workflows/compat-test-system.js delete mode 100644 .deadcode-whitelist delete mode 100644 .goreleaser.yml create mode 100644 bins/codefang/tests/anomaly_e2e.rs create mode 100644 bins/codefang/tests/burndown_e2e.rs create mode 100644 bins/codefang/tests/chunked_history_e2e.rs create mode 100644 bins/codefang/tests/cli_surface_e2e.rs create mode 100644 bins/codefang/tests/clones_e2e.rs create mode 100644 bins/codefang/tests/cohesion_e2e.rs create mode 100644 bins/codefang/tests/comments_e2e.rs create mode 100644 bins/codefang/tests/common/mod.rs create mode 100644 bins/codefang/tests/complexity_e2e.rs create mode 100644 bins/codefang/tests/composition_e2e.rs create mode 100644 bins/codefang/tests/config_e2e.rs create mode 100644 bins/codefang/tests/couples_e2e.rs create mode 100644 bins/codefang/tests/devs_e2e.rs create mode 100644 bins/codefang/tests/file_history_e2e.rs create mode 100644 bins/codefang/tests/halstead_e2e.rs create mode 100644 bins/codefang/tests/history_core_e2e.rs create mode 100644 bins/codefang/tests/history_formats_e2e.rs create mode 100644 bins/codefang/tests/imports_e2e.rs create mode 100644 bins/codefang/tests/imports_history_e2e.rs create mode 100644 bins/codefang/tests/mcp_e2e.rs create mode 100644 bins/codefang/tests/mixed_selection_e2e.rs create mode 100644 bins/codefang/tests/quality_e2e.rs create mode 100644 bins/codefang/tests/sentiment_e2e.rs create mode 100644 bins/codefang/tests/shotness_e2e.rs create mode 100644 bins/codefang/tests/static_walk_e2e.rs create mode 100644 bins/codefang/tests/typos_e2e.rs create mode 100644 bins/uast/tests/cli_surface_e2e.rs create mode 100644 bins/uast/tests/parse_shapes.rs create mode 100644 bins/uast/tests/query_dsl.rs create mode 100644 crates/cf-analyzer-burndown/src/survival.rs delete mode 100644 crates/cf-analyzers-plumbing/src/analyzer.rs delete mode 100644 crates/cf-analyzers-plumbing/src/blob_cache.rs delete mode 100644 crates/cf-analyzers-plumbing/src/clock.rs delete mode 100644 crates/cf-analyzers-plumbing/src/file_diff.rs delete mode 100644 crates/cf-analyzers-plumbing/src/git_model.rs delete mode 100644 crates/cf-analyzers-plumbing/src/languages_detection.rs delete mode 100644 crates/cf-analyzers-plumbing/src/ticks_anomaly.rs delete mode 100644 crates/cf-analyzers-plumbing/src/tree_diff.rs delete mode 100644 crates/cf-analyzers-plumbing/src/uast_changes.rs delete mode 100644 crates/cf-analyzers-plumbing/src/uast_iface.rs delete mode 100644 crates/cf-anomaly/src/store.rs create mode 100644 crates/cf-burndown-core/src/packing.rs delete mode 100644 crates/cf-clones/src/analyzer.rs create mode 100644 crates/cf-clones/src/profile.rs delete mode 100644 crates/cf-clones/src/uast.rs delete mode 100644 crates/cf-cohesion/src/serialize.rs create mode 100644 crates/cf-cohesion/tests/real_trees.rs delete mode 100644 crates/cf-commands/src/handlers/go_sort.rs delete mode 100644 crates/cf-commands/src/handlers/history.rs create mode 100644 crates/cf-commands/src/handlers/history/anomaly.rs create mode 100644 crates/cf-commands/src/handlers/history/budget.rs create mode 100644 crates/cf-commands/src/handlers/history/chunks.rs create mode 100644 crates/cf-commands/src/handlers/history/devs.rs create mode 100644 crates/cf-commands/src/handlers/history/file_history.rs create mode 100644 crates/cf-commands/src/handlers/history/fold_testutil.rs create mode 100644 crates/cf-commands/src/handlers/history/imports.rs create mode 100644 crates/cf-commands/src/handlers/history/mod.rs create mode 100644 crates/cf-commands/src/handlers/history/pool.rs create mode 100644 crates/cf-commands/src/handlers/history/quality.rs create mode 100644 crates/cf-commands/src/handlers/history/sentiment.rs create mode 100644 crates/cf-commands/src/handlers/history/typos.rs create mode 100644 crates/cf-commands/src/handlers/spill.rs create mode 100644 crates/cf-commands/src/mcp.rs delete mode 100644 crates/cf-commands/src/observability.rs create mode 100644 crates/cf-comments/src/placement.rs create mode 100644 crates/cf-comments/tests/real_trees.rs create mode 100644 crates/cf-complexity/src/discover.rs delete mode 100644 crates/cf-complexity/src/gosort.rs create mode 100644 crates/cf-complexity/src/metrics.rs create mode 100644 crates/cf-complexity/src/thresholds.rs create mode 100644 crates/cf-complexity/tests/fixtures.rs create mode 100644 crates/cf-complexity/tests/fixtures/Foo.java create mode 100644 crates/cf-complexity/tests/fixtures/Probe.java create mode 100644 crates/cf-complexity/tests/fixtures/cc.c create mode 100644 crates/cf-complexity/tests/fixtures/cc.go create mode 100644 crates/cf-complexity/tests/fixtures/cc.js create mode 100644 crates/cf-complexity/tests/fixtures/cc.py create mode 100644 crates/cf-complexity/tests/fixtures/cc.rs create mode 100644 crates/cf-complexity/tests/fixtures/d.go create mode 100644 crates/cf-complexity/tests/fixtures/extras.c create mode 100644 crates/cf-complexity/tests/fixtures/extras.go create mode 100644 crates/cf-complexity/tests/fixtures/extras.js create mode 100644 crates/cf-complexity/tests/fixtures/extras.py create mode 100644 crates/cf-complexity/tests/fixtures/extras.rs create mode 100644 crates/cf-complexity/tests/fixtures/gen.go create mode 100644 crates/cf-complexity/tests/fixtures/i.go delete mode 100644 crates/cf-composition/src/analyzer.rs delete mode 100644 crates/cf-config/src/defaults.rs delete mode 100644 crates/cf-config/src/facts.rs delete mode 100644 crates/cf-couples/src/report_section.rs delete mode 100644 crates/cf-couples/src/store.rs create mode 100644 crates/cf-file-history/src/category.rs delete mode 100644 crates/cf-file-history/src/classify.rs delete mode 100644 crates/cf-file-history/src/framework.rs create mode 100644 crates/cf-gitlib/tests/native_limits.rs delete mode 100644 crates/cf-gojson/oracle/go.mod delete mode 100644 crates/cf-gojson/oracle/main.go delete mode 100644 crates/cf-gojson/oracle/oracle_err.txt delete mode 100644 crates/cf-gojson/tests/oracle_data/_g_vs_json_diffs.txt delete mode 100644 crates/cf-gojson/tests/oracle_data/_zz_diffs.txt delete mode 100644 crates/cf-gojson/tests/oracle_data/floats.tsv delete mode 100644 crates/cf-gojson/tests/oracle_data/json_cases.json delete mode 100644 crates/cf-gojson/tests/oracle_data/json_floats.tsv delete mode 100644 crates/cf-goyaml/tests/oracle.rs create mode 100644 crates/cf-halstead/src/aggregate.rs create mode 100644 crates/cf-halstead/src/classify.rs create mode 100644 crates/cf-halstead/src/source.rs create mode 100644 crates/cf-halstead/src/thresholds.rs create mode 100644 crates/cf-halstead/tests/real_source.rs create mode 100644 crates/cf-imports/src/aggregate.rs delete mode 100644 crates/cf-imports/src/aggregator.rs delete mode 100644 crates/cf-imports/src/analyzer.rs create mode 100644 crates/cf-imports/src/extract.rs create mode 100644 crates/cf-imports/src/language.rs delete mode 100644 crates/cf-imports/src/node.rs delete mode 100644 crates/cf-imports/src/report_section.rs create mode 100644 crates/cf-imports/src/section.rs create mode 100644 crates/cf-imports/src/statement.rs create mode 100644 crates/cf-imports/src/stdlib.rs delete mode 100644 crates/cf-imports/src/store.rs create mode 100644 crates/cf-imports/tests/extract_languages.rs delete mode 100644 crates/cf-langpath/tests/content_heuristics_diff.rs delete mode 100644 crates/cf-quality/src/store.rs delete mode 100644 crates/cf-shotness/src/aggregate.rs create mode 100644 crates/cf-shotness/src/state.rs delete mode 100644 crates/cf-shotness/src/store.rs delete mode 100644 crates/cf-typos/src/compat.rs create mode 100644 crates/cf-typos/src/detect.rs create mode 100644 crates/cf-typos/src/hash.rs delete mode 100644 crates/cf-typos/src/levenshtein.rs create mode 100644 crates/cf-uast-mappings/tests/schema_vocabulary.rs create mode 100644 crates/cf-uast-mappings/tests/shape_contract.rs create mode 100644 crates/cf-uast/tests/lowering_contract.rs create mode 100644 site/analyzers/composition.md delete mode 100644 specs/go-compat-testing/SPEC.md delete mode 100755 tests/antisim/parity_gate.sh delete mode 100644 tests/compat/README.md delete mode 100644 tests/compat/allowlist.json delete mode 100644 tests/compat/cli_surface/README.md delete mode 100755 tests/compat/cli_surface/cli_surface.py delete mode 100755 tests/compat/cli_surface/extract_surface.py delete mode 100755 tests/compat/cli_surface/run.sh delete mode 100755 tests/compat/cli_surface/selftest/self_test.py delete mode 100644 tests/compat/corpus/README.md delete mode 100644 tests/compat/corpus/build_corpus.py delete mode 100644 tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx delete mode 100644 tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go delete mode 100644 tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts delete mode 100644 tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh delete mode 100644 tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c delete mode 100644 tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp delete mode 100644 tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py delete mode 100644 tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json delete mode 100644 tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h delete mode 100644 tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js delete mode 100644 tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs delete mode 100644 tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml delete mode 100644 tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go delete mode 100644 tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.evidence.json delete mode 100644 tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_0 delete mode 100644 tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_1 delete mode 100644 tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_2 delete mode 100644 tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.rust_out delete mode 100644 tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go delete mode 100644 tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.evidence.json delete mode 100644 tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_0 delete mode 100644 tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_1 delete mode 100644 tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_2 delete mode 100644 tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.rust_out delete mode 100644 tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go delete mode 100644 tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.evidence.json delete mode 100644 tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_0 delete mode 100644 tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_1 delete mode 100644 tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_2 delete mode 100644 tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.rust_out delete mode 100644 tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go delete mode 100644 tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.evidence.json delete mode 100644 tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_0 delete mode 100644 tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_1 delete mode 100644 tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_2 delete mode 100644 tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.rust_out delete mode 100644 tests/compat/corpus/known_gaps.json delete mode 100644 tests/compat/corpus/manifest.json delete mode 100644 tests/compat/coverage/README.md delete mode 100644 tests/compat/coverage/build_ledger.py delete mode 100644 tests/compat/coverage/matrix_cell_cov.json delete mode 100644 tests/compat/coverage/matrix_cell_cov.py delete mode 100644 tests/compat/coverage/rust_cov.json delete mode 100644 tests/compat/coverage/rust_cov.py delete mode 100644 tests/compat/coverage/selftest/self_test.py delete mode 100644 tests/compat/expand_matrix.py delete mode 100644 tests/compat/fuzz/README.md delete mode 100644 tests/compat/fuzz/analyzer_fuzz_test.go delete mode 100644 tests/compat/fuzz/go.mod delete mode 100644 tests/compat/fuzz/harness.go delete mode 100644 tests/compat/fuzz/map_fuzz_test.go delete mode 100644 tests/compat/fuzz/parse_fuzz_test.go delete mode 100755 tests/compat/fuzz/run_fuzz.sh delete mode 100644 tests/compat/fuzz/seed.go delete mode 100644 tests/compat/fuzz/selfcheck_test.go delete mode 100644 tests/compat/fuzz/serializer_fuzz_test.go delete mode 100644 tests/compat/fuzz/testdata/fuzz/FuzzMap/c7c64cb562bb5f58 delete mode 100644 tests/compat/fuzz/testdata/fuzz/FuzzParse/d51a495543b4d625 delete mode 100644 tests/compat/fuzz/testdata/fuzz/FuzzParseGo/3af36d37b930453f delete mode 100644 tests/compat/fuzz/testdata/fuzz/FuzzQuery/33fc0496576430b7 delete mode 100755 tests/compat/gate.py delete mode 100644 tests/compat/integrity/README.md delete mode 100644 tests/compat/integrity/baseline.json delete mode 100755 tests/compat/integrity/mutation_self_test.sh delete mode 100755 tests/compat/integrity/run.sh delete mode 100755 tests/compat/integrity/tamper_check.py delete mode 100644 tests/compat/ledger.json delete mode 100644 tests/compat/matrix.toml delete mode 100644 tests/compat/metamorphic/README.md delete mode 100644 tests/compat/metamorphic/golden_constants.json delete mode 100644 tests/compat/metamorphic/metamorphic.py delete mode 100644 tests/compat/metamorphic/results.json delete mode 100644 tests/compat/metamorphic/selftest/live_constant_test.py delete mode 100644 tests/compat/metamorphic/selftest/self_test.py delete mode 100644 tests/compat/oracle/README.md delete mode 100755 tests/compat/oracle/oracle.py delete mode 100755 tests/compat/oracle/parity_gate.sh delete mode 100644 tests/compat/oracle/patches/uast-unmapped-subtree-rewalk-perf.patch delete mode 100755 tests/compat/oracle/selftest/self_test.py delete mode 100644 tests/compat/results/full.json delete mode 100644 tests/compat/results/full.partial.jsonl delete mode 100644 tests/compat/results/full.partial.preheuristics.1781013773.jsonl delete mode 100644 tests/compat/results/full.partial.stale.1780994184.jsonl delete mode 100644 tests/compat/results/smoke.json delete mode 100644 tests/compat/results/smoke.partial.jsonl delete mode 100644 tests/compat/results/smoke_gate.json delete mode 100755 tests/compat/run.sh delete mode 100644 tests/compat/run_matrix.py delete mode 100644 tests/compat/selftest_matrix.py delete mode 100644 tests/golden-harness/Cargo.toml delete mode 100644 tests/golden-harness/src/lib.rs delete mode 100644 tests/golden-harness/src/main.rs delete mode 100644 tests/golden-harness/tests/golden_diff.rs delete mode 100644 tests/golden/MANIFEST.json delete mode 100644 tests/golden/capture.sh delete mode 100644 tests/golden/run/all_static.bin delete mode 100644 tests/golden/run/all_static.json delete mode 100644 tests/golden/run/all_static.yaml delete mode 100644 tests/golden/run/burndown.bin delete mode 100644 tests/golden/run/burndown.compact delete mode 100644 tests/golden/run/burndown.json delete mode 100644 tests/golden/run/burndown.ndjson delete mode 100644 tests/golden/run/burndown.text delete mode 100644 tests/golden/run/burndown.timeseries delete mode 100644 tests/golden/run/burndown.timeseries.ndjson delete mode 100644 tests/golden/run/burndown.yaml delete mode 100644 tests/golden/run/history_anomaly.json delete mode 100644 tests/golden/run/history_couples.json delete mode 100644 tests/golden/run/history_devs.bin delete mode 100644 tests/golden/run/history_devs.json delete mode 100644 tests/golden/run/history_devs.yaml delete mode 100644 tests/golden/run/history_file-history.json delete mode 100644 tests/golden/run/history_imports.json delete mode 100644 tests/golden/run/history_quality.json delete mode 100644 tests/golden/run/history_sentiment.json delete mode 100644 tests/golden/run/history_shotness.json delete mode 100644 tests/golden/run/history_typos.json delete mode 100644 tests/golden/static/static_clones.bin delete mode 100644 tests/golden/static/static_clones.compact delete mode 100644 tests/golden/static/static_clones.json delete mode 100644 tests/golden/static/static_clones.perfile.json delete mode 100644 tests/golden/static/static_clones.text delete mode 100644 tests/golden/static/static_clones.yaml delete mode 100644 tests/golden/static/static_cohesion.bin delete mode 100644 tests/golden/static/static_cohesion.compact delete mode 100644 tests/golden/static/static_cohesion.json delete mode 100644 tests/golden/static/static_cohesion.perfile.json delete mode 100644 tests/golden/static/static_cohesion.text delete mode 100644 tests/golden/static/static_cohesion.yaml delete mode 100644 tests/golden/static/static_comments.bin delete mode 100644 tests/golden/static/static_comments.compact delete mode 100644 tests/golden/static/static_comments.json delete mode 100644 tests/golden/static/static_comments.perfile.json delete mode 100644 tests/golden/static/static_comments.text delete mode 100644 tests/golden/static/static_comments.yaml delete mode 100644 tests/golden/static/static_complexity.bin delete mode 100644 tests/golden/static/static_complexity.compact delete mode 100644 tests/golden/static/static_complexity.json delete mode 100644 tests/golden/static/static_complexity.perfile.json delete mode 100644 tests/golden/static/static_complexity.text delete mode 100644 tests/golden/static/static_complexity.yaml delete mode 100644 tests/golden/static/static_composition.bin delete mode 100644 tests/golden/static/static_composition.compact delete mode 100644 tests/golden/static/static_composition.json delete mode 100644 tests/golden/static/static_composition.perfile.json delete mode 100644 tests/golden/static/static_composition.text delete mode 100644 tests/golden/static/static_composition.yaml delete mode 100644 tests/golden/static/static_halstead.bin delete mode 100644 tests/golden/static/static_halstead.compact delete mode 100644 tests/golden/static/static_halstead.json delete mode 100644 tests/golden/static/static_halstead.perfile.json delete mode 100644 tests/golden/static/static_halstead.text delete mode 100644 tests/golden/static/static_halstead.yaml delete mode 100644 tests/golden/static/static_imports.bin delete mode 100644 tests/golden/static/static_imports.compact delete mode 100644 tests/golden/static/static_imports.json delete mode 100644 tests/golden/static/static_imports.perfile.json delete mode 100644 tests/golden/static/static_imports.text delete mode 100644 tests/golden/static/static_imports.yaml delete mode 100644 tests/golden/uast/analyze.json delete mode 100644 tests/golden/uast/analyze.text delete mode 100644 tests/golden/uast/parse.compact delete mode 100644 tests/golden/uast/parse.json delete mode 100644 tests/golden/uast/parse.tree delete mode 100644 tests/golden/uast/query.compact delete mode 100644 tests/golden/uast/query.count delete mode 100644 tests/golden/uast/query.json diff --git a/.claude/settings.json b/.claude/settings.json index 1c2d1700..84f3bebb 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,9 +4,18 @@ { "matcher": "Write|Edit|MultiEdit", "hooks": [ - { "type": "command", "command": ".claude/hooks/no-stub-markers.sh" }, - { "type": "command", "command": ".claude/hooks/no-estimates.sh" }, - { "type": "command", "command": ".claude/hooks/evidence-for-checkbox.sh" } + { + "type": "command", + "command": ".claude/hooks/no-stub-markers.sh" + }, + { + "type": "command", + "command": ".claude/hooks/no-estimates.sh" + }, + { + "type": "command", + "command": ".claude/hooks/evidence-for-checkbox.sh" + } ] } ], @@ -14,14 +23,10 @@ { "matcher": "Bash|Write|Edit|MultiEdit", "hooks": [ - { "type": "command", "command": ".claude/hooks/track-gates.sh" } - ] - } - ], - "Stop": [ - { - "hooks": [ - { "type": "command", "command": ".claude/hooks/finish-the-work.sh" } + { + "type": "command", + "command": ".claude/hooks/track-gates.sh" + } ] } ] diff --git a/.claude/workflows/codefang-rust-rewrite.js b/.claude/workflows/codefang-rust-rewrite.js deleted file mode 100644 index e322df03..00000000 --- a/.claude/workflows/codefang-rust-rewrite.js +++ /dev/null @@ -1,297 +0,0 @@ -export const meta = { - name: 'codefang-rust-rewrite', - description: 'Port codefang (Go) to Rust with byte-identical reports on ~/sources/kubernetes; keep libgit2', - phases: [ - { title: 'Discover', detail: 'Map CLI, reports, analyzers, git2go, tree-sitter, config, build, dep-graph' }, - { title: 'Synthesize', detail: 'Merge discovery into ARCHITECTURE.md + topological port order' }, - { title: 'Golden', detail: 'Build Go binary; capture byte-exact reference reports on kubernetes' }, - { title: 'Design', detail: 'Architecture panel + dependency map + byte-identity strategy -> DESIGN.md' }, - { title: 'Scaffold', detail: 'Cargo workspace, identical clap CLI, git2, tree-sitter, golden-diff harness' }, - { title: 'Port', detail: 'Port ALL modules in dependency order (full autonomous); each cargo-verified' }, - { title: 'Verify', detail: 'Run Rust on kubernetes; byte-diff machine formats vs golden; fix loop' }, - { title: 'Review', detail: 'Completeness critic + adversarial correctness + resumable ROADMAP' }, - ], -} - -// ---------- Constants ---------- -const GO = '/home/dmitriy/sources/codefang' -const KUBE = '/home/dmitriy/sources/kubernetes' -const RUST = GO + '/rust' -const GOLDEN = GO + '/tests/golden' -const DOCS = GO + '/specs/rust-rewrite' -// FULL AUTONOMOUS ATTEMPT: port every module, no cap (long tail captured in ROADMAP for resume). -const PORT_LIMIT = Infinity -// BYTE-IDENTITY TARGET = MACHINE formats only (deterministic, diffable). Human-facing -// formats are captured but treated as best-effort (cosmetic diffs do not fail the run). -const MACHINE_FORMATS = ['json', 'yaml', 'ndjson', 'timeseries', 'timeseries+ndjson', 'compact', 'bin'] -const HUMAN_FORMATS = ['text', 'plot', 'html'] -const isMachine = (f) => MACHINE_FORMATS.includes(String(f || '').toLowerCase()) - -// ---------- Schemas ---------- -const STR_LIST = { type: 'array', items: { type: 'string' } } -const DISCOVERY_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { - area: { type: 'string' }, - summary: { type: 'string' }, - files: STR_LIST, - findings: { type: 'array', items: { - type: 'object', additionalProperties: true, - properties: { name: { type: 'string' }, detail: { type: 'string' }, path: { type: 'string' } }, - required: ['name', 'detail'], - } }, - byteIdentityRisks: STR_LIST, - }, - required: ['area', 'summary', 'findings'], -} -const PORTORDER_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { - docPath: { type: 'string' }, - modules: { type: 'array', items: { - type: 'object', additionalProperties: true, - properties: { - name: { type: 'string' }, goPath: { type: 'string' }, crate: { type: 'string' }, - tier: { type: 'integer' }, deps: STR_LIST, purpose: { type: 'string' }, loc: { type: 'integer' }, - }, - required: ['name', 'goPath', 'crate', 'tier'], - } }, - }, - required: ['modules'], -} -const GOLDEN_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { - built: { type: 'boolean' }, binaryPath: { type: 'string' }, - captures: { type: 'array', items: { - type: 'object', additionalProperties: true, - properties: { - command: { type: 'string' }, argv: STR_LIST, format: { type: 'string' }, - outPath: { type: 'string' }, sha256: { type: 'string' }, bytes: { type: 'integer' }, ok: { type: 'boolean' }, - machine: { type: 'boolean' }, nonBinding: { type: 'boolean' }, - }, - required: ['command', 'outPath', 'ok'], - } }, - notes: { type: 'string' }, - }, - required: ['built', 'captures'], -} -const SCORE_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { score: { type: 'integer' }, rationale: { type: 'string' }, bestIdeas: STR_LIST, risks: STR_LIST }, - required: ['score', 'rationale'], -} -const DESIGN_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { docPath: { type: 'string' }, summary: { type: 'string' }, depMapping: { type: 'array', items: { - type: 'object', additionalProperties: true, - properties: { go: { type: 'string' }, rust: { type: 'string' }, note: { type: 'string' } }, required: ['go', 'rust'], - } }, byteIdentityStrategy: STR_LIST }, - required: ['docPath', 'summary'], -} -const SCAFFOLD_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { ok: { type: 'boolean' }, createdFiles: STR_LIST, cargoBuilds: { type: 'boolean' }, cliMatches: { type: 'boolean' }, notes: { type: 'string' } }, - required: ['ok'], -} -const PORT_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { module: { type: 'string' }, files: STR_LIST, externalCrates: STR_LIST, compiles: { type: 'boolean' }, testsPass: { type: 'boolean' }, todos: STR_LIST, notes: { type: 'string' } }, - required: ['module', 'compiles'], -} -const VERIFY_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { rustBuilds: { type: 'boolean' }, diffs: { type: 'array', items: { - type: 'object', additionalProperties: true, - properties: { command: { type: 'string' }, identical: { type: 'boolean' }, binding: { type: 'boolean' }, firstDiff: { type: 'string' }, bytesGo: { type: 'integer' }, bytesRust: { type: 'integer' } }, required: ['command', 'identical'], - } }, identicalCount: { type: 'integer' }, totalCount: { type: 'integer' }, notes: { type: 'string' } }, - required: ['rustBuilds', 'diffs'], -} -const REVIEW_SCHEMA = { - type: 'object', additionalProperties: true, - properties: { remainingWork: STR_LIST, correctnessFindings: STR_LIST, roadmapPath: { type: 'string' }, percentComplete: { type: 'integer' } }, - required: ['remainingWork'], -} - -async function safe(label, fn) { - try { return await fn() } catch (e) { log('step failed [' + label + ']: ' + ((e && e.message) || e)); return null } -} - -// ===================================================================== -// PHASE 1 — DISCOVER (parallel readers; barrier to synthesize) -// ===================================================================== -phase('Discover') -const AREAS = [ - { area: 'cli', prompt: `Map the COMPLETE CLI interface of the Go project at ${GO}. Read everything under cmd/ and any cobra command files. Report: the binary name; the full command/subcommand tree; for every command, EVERY flag (long+short name, type, default value, help text, whether persistent); positional args; exit codes; and how stdout vs stderr is used. The Rust rewrite must reproduce this interface byte-for-byte (clap). Read .codefang.yaml and report config keys too.` }, - { area: 'reports', prompt: `Find ALL report/output generation in the Go project at ${GO}. Search pkg/ and internal/ for report, render, format, output, writer, serialize, marshal, encode, table, chart code. Report: every output FORMAT (json, yaml, terminal table, html/echarts, markdown, csv); the exact serialization path for each; what controls field ORDER, map-key SORTING, FLOAT formatting (fmt verbs / strconv), integer/percentage/byte-size humanization, indentation, HTML escaping, trailing newlines. List concrete file:func sites. Flag every place where Go's encoding/json (struct field order, SetEscapeHTML), gopkg.in/yaml.v3, go-pretty/jedib0t, or go-humanize formatting could differ from a naive Rust port — these are byteIdentityRisks.` }, - { area: 'analyzers', prompt: `Inventory ALL analyzers in the Go project at ${GO}. Find the Analyzer interface and every implementation. For each: package path, Name(), what it computes, inputs/outputs, and any nondeterminism (map iteration, goroutine ordering, randomness, time). Cover technical-debt, churn/developer, AST/structure, git-history, language-stats, sentiment analyzers. List file:func.` }, - { area: 'git', prompt: `Document EVERY use of libgit2 via github.com/libgit2/git2go/v34 in ${GO}. Grep for git2go imports and list each call site with the git operation performed (open repo, walk, log, blame, diff, tree, commit, object lookup, etc.) and the surrounding function. The Rust rewrite KEEPS libgit2 through the git2 crate — note any git2go API that maps awkwardly to the git2 crate.` }, - { area: 'parsing', prompt: `Document tree-sitter usage in ${GO}: the go-sitter-forest grammars and go-tree-sitter-bare. How are parsers created, which languages, how is the AST walked/queried, what queries/captures are used? List the parsing abstraction (interfaces, registry). The Rust port uses the official tree-sitter crates + per-language grammar crates — list which languages must be supported.` }, - { area: 'deps', prompt: `For the Go project at ${GO}, explain how each non-trivial dependency is USED and where: enry (src-d/enry) language detection, govader sentiment, go-echarts charts, go-pretty tables, go-humanize, pierrec/lz4, prometheus/client_golang + go.opentelemetry.io/otel telemetry, tliron/glsp (LSP server?), spf13/cobra+viper, xeipuuv/gojsonschema, sergi/go-diff, fatih/color. For each, give the call sites and what a byte-identical Rust replacement must reproduce (esp. enry classification results and govader lexicon/scoring, which affect report bytes).` }, - { area: 'build', prompt: `Read the Makefile, Dockerfile, .goreleaser.yml, and go.mod at ${GO}. Report: exact build command and CGO flags for libgit2 (CGO_CFLAGS/LDFLAGS/PKG_CONFIG_PATH), how the binary is produced and named, where output goes (build/?), how tests/lint/deadcode/bench run, and how libgit2 itself is provided (third_party/libgit2 submodule? system pkg-config?). The Rust build must link the same libgit2.` }, - { area: 'depgraph', prompt: `Produce the internal PACKAGE DEPENDENCY GRAPH for ${GO} (cmd/, pkg/, internal/). For each package: import path, one-line purpose, non-test LOC, and which other internal packages it imports. Identify the layering (domain/adapters/frameworks) and a TOPOLOGICAL order from leaf (no internal deps) to root (cmd). This order drives the Rust port sequence. In findings, list each package as a finding with name=import path, detail=purpose+deps+LOC, path=dir.` }, -] -const discovery = (await parallel(AREAS.map(a => () => - agent(a.prompt + ' Return structured JSON per the schema. Be exhaustive and concrete with file paths.', { label: 'discover:' + a.area, phase: 'Discover', schema: DISCOVERY_SCHEMA }) -))).filter(Boolean) -log('Discovery complete: ' + discovery.length + '/' + AREAS.length + ' areas mapped') - -// ===================================================================== -// PHASE 2 — SYNTHESIZE architecture + topological port order -// ===================================================================== -phase('Synthesize') -const portOrder = await safe('synthesize', () => agent( - `You are the lead architect for porting codefang (Go) to Rust. Below is structured discovery output from 8 parallel investigators.\n\n` + - JSON.stringify(discovery) + - `\n\nDo two things:\n` + - `1. WRITE a comprehensive architecture map to ${DOCS}/ARCHITECTURE.md (create dirs as needed). Cover: binary/CLI tree with every flag, report formats and their exact serialization rules, analyzer inventory, git2go usage, tree-sitter usage, dependency usage, build/CGO/libgit2, and the internal package layering. Include the consolidated byte-identity risk list.\n` + - `2. Decide the TOPOLOGICAL PORT ORDER: list every internal package as a module with name, goPath, proposed Rust crate name, tier (0=leaf no internal deps, increasing toward cmd), internal deps, purpose, and LOC. Tier 0/1 are the foundation to port first.\n` + - `Return the module list as JSON (and docPath=the written file).`, - { label: 'synthesize:architecture', phase: 'Synthesize', schema: PORTORDER_SCHEMA } -)) -const modules = (portOrder && portOrder.modules) || [] -log('Architecture written; ' + modules.length + ' modules ordered for porting') - -// ===================================================================== -// PHASE 3 — GOLDEN capture (sequential, side effects) -// ===================================================================== -phase('Golden') -const golden = await safe('golden', () => agent( - `Establish BYTE-EXACT golden reference outputs for the Go codefang tool, to validate the Rust rewrite.\n` + - `Steps:\n` + - `1. Build the Go binary in ${GO} (use the Makefile target; honor CGO/libgit2 flags from this build info: ${JSON.stringify((discovery.find(d => d.area === 'build') || {}))}).\n` + - `2. From the CLI inventory: ${JSON.stringify((discovery.find(d => d.area === 'cli') || {}))} and report inventory: ${JSON.stringify((discovery.find(d => d.area === 'reports') || {}))}, enumerate EVERY report-producing command in EVERY output format.\n` + - ` BYTE-IDENTITY TARGET IS MACHINE FORMATS ONLY: ${MACHINE_FORMATS.join(', ')} (these are the binding goldens that the Rust port must match byte-for-byte). Human formats (${HUMAN_FORMATS.join(', ')}) should still be captured but marked nonBinding=true — cosmetic differences are acceptable for them.\n` + - ` The primary commands to cover exhaustively: 'codefang run --format ' for every machine format and representative analyzer selections (single analyzer, all analyzers), and 'uast parse/analyze/query --format json'. Bin output: capture the raw bytes AND a stable hex/sha so the Rust harness can compare.\n` + - `3. Run each against the repo at ${KUBE}. Capture stdout EXACTLY to files under ${GOLDEN}/ (create dirs). Use a stable filename per (command,format). Record the exact argv used, byte length, and sha256 of each output. Set deterministic env (TZ=UTC, NO_COLOR=1, LANG=C, LC_ALL=C, SOURCE_DATE_EPOCH if honored) AND record exactly which env you set so the Rust harness can match it. Pin nondeterminism: prefer flags that bound work (e.g. --limit, --head, --workers 1) to make output reproducible across runs; VERIFY each binding golden is stable by running it twice and confirming identical sha256. If a command cannot be made deterministic, mark nonBinding=true and note why.\n` + - `4. Also write ${GOLDEN}/MANIFEST.json describing every capture (command, argv, env, format, outPath, sha256, bytes, machine boolean, nonBinding boolean).\n` + - `Be careful: ${KUBE} is large — if a full run is too slow, ALSO capture a fast deterministic subset (e.g. a few subdirectories) and record both. Return the capture manifest as JSON.`, - { label: 'golden:capture', phase: 'Golden', schema: GOLDEN_SCHEMA } -)) -log('Golden capture: ' + (golden ? golden.captures.filter(c => c.ok).length + ' reports captured' : 'FAILED — Go build or run blocked; verify will be limited')) - -// ===================================================================== -// PHASE 4 — DESIGN (judge panel -> synthesis) -// ===================================================================== -phase('Design') -const ctxForDesign = { build: discovery.find(d => d.area === 'build'), reports: discovery.find(d => d.area === 'reports'), deps: discovery.find(d => d.area === 'deps'), git: discovery.find(d => d.area === 'git'), parsing: discovery.find(d => d.area === 'parsing'), modules } -const ANGLES = [ - { key: 'identity-first', lens: 'Optimize ABOVE ALL for byte-identical MACHINE output (json, yaml, ndjson, timeseries, compact, bin) — terminal/HTML are cosmetic best-effort. Make the Rust serialization layer mirror Go encoding/json EXACTLY: struct field DECLARATION order (NOT alphabetical), map keys sorted by Go string-byte order, HTML escaping of <,>,& on by default (json.Encoder), shortest-round-trip float formatting matching strconv.AppendFloat(\'g\',-1) — note serde_json/ryu differ from Go on exponent thresholds and integer-valued floats, so a custom Go-compatible float formatter is likely REQUIRED; integers as-is; RFC3339/RFC3339Nano time strings. For YAML, reproduce gopkg.in/yaml.v3 output (indent width, string quoting, key order via yaml.Node). For the LZ4 bin format, match the exact frame/record layout and compression settings. Propose a custom serialization compat crate rather than relying on serde defaults.' }, - { key: 'idiomatic-rust', lens: 'Optimize for clean idiomatic Rust (workspace of crates, serde, thiserror/anyhow, rayon) while still guaranteeing byte-identity via a thin formatting-compat module. Favor maintainability.' }, - { key: 'risk-first', lens: 'Optimize for de-risking the hardest byte-identity threats first: enry language classification parity, govader sentiment lexicon/scoring parity, float/percentage/humanize formatting, and map ordering nondeterminism. Propose how to port or vendor the exact data tables and how to test each in isolation.' }, -] -const proposals = (await parallel(ANGLES.map(a => () => - agent(`Propose a complete Rust architecture for porting codefang. LENS: ${a.lens}\n\nContext (discovery + module list):\n${JSON.stringify(ctxForDesign)}\n\nCover: Cargo workspace/crate layout, dependency mapping table (Go dep -> Rust crate, keep libgit2 via git2), the CLI compat plan (clap reproducing cobra exactly), the byte-identity serialization strategy in concrete terms, the tree-sitter language set, and the test harness that byte-diffs against golden. Return prose.`, - { label: 'design:' + a.key, phase: 'Design' }) -))).filter(Boolean) -const judged = await parallel(proposals.map((p, i) => () => - agent(`Score this Rust-architecture proposal (0-100) for codefang, weighting byte-identical-output feasibility highest, then correctness, then maintainability. Proposal:\n\n${p}`, - { label: 'judge:' + ANGLES[i].key, phase: 'Design', schema: SCORE_SCHEMA }) -)) -const ranked = proposals.map((p, i) => ({ p, key: ANGLES[i].key, s: (judged[i] && judged[i].score) || 0, ideas: (judged[i] && judged[i].bestIdeas) || [] })) - .sort((a, b) => b.s - a.s) -const design = await safe('design-synth', () => agent( - `Synthesize the FINAL Rust rewrite design for codefang. Base it on the winning proposal and graft the best ideas from the others.\n\n` + - `WINNER (${ranked[0] ? ranked[0].key : 'n/a'}):\n${ranked[0] ? ranked[0].p : ''}\n\n` + - `RUNNER-UP IDEAS:\n${JSON.stringify(ranked.slice(1).map(r => ({ key: r.key, ideas: r.ideas })))}\n\n` + - `Golden manifest (what must match byte-for-byte): ${JSON.stringify(golden ? golden.captures : [])}\n\n` + - `SCOPE: byte-identity is REQUIRED only for MACHINE formats (${MACHINE_FORMATS.join(', ')}); terminal/HTML (${HUMAN_FORMATS.join(', ')}) are best-effort/cosmetic. Confirmed dep swaps: keep libgit2 via the git2 crate; tree-sitter official crates + per-language grammar crates; clap for the CLI; idiomatic Rust elsewhere (serde+custom Go-compat encoder, comfy-table/tabled for tables, a charts crate or templated HTML for plots, etc.).\n` + - `WRITE the final design to ${DOCS}/DESIGN.md including: (a) Cargo workspace + crate-per-module layout aligned to the port order; (b) full dependency-mapping table keeping libgit2 via the git2 crate; (c) the precise BYTE-IDENTITY strategy for MACHINE formats — a dedicated go-compat serialization crate that reproduces Go encoding/json (declaration-order fields, byte-sorted map keys, HTML-escape, Go-compatible shortest float formatter), gopkg.in/yaml.v3 output, the LZ4 bin record layout, plus enry classification parity and govader lexicon/scoring parity (these change report bytes); explicitly mark go-pretty tables / go-echarts HTML as non-binding; (d) the clap CLI compat plan reproducing every cobra command, flag (long+short), default, and help text for BOTH the codefang and uast binaries; (e) the golden-diff integration-test harness design that diffs only the binding (machine) goldens and reports human-format diffs as informational. Return JSON with docPath, summary, depMapping, byteIdentityStrategy.`, - { label: 'design:synthesize', phase: 'Design', schema: DESIGN_SCHEMA } -)) -log('Design written: ' + (design ? design.docPath : 'FAILED')) - -// ===================================================================== -// PHASE 5 — SCAFFOLD (sequential; persistent files in real tree) -// ===================================================================== -phase('Scaffold') -const scaffold = await safe('scaffold', () => agent( - `Scaffold the Rust rewrite of codefang under ${RUST} (Cargo workspace) on the current git branch. Use the design at ${DOCS}/DESIGN.md and this summary: ${JSON.stringify(design || {})}.\n` + - `Do:\n` + - `1. Create the Cargo workspace (workspace Cargo.toml + crate skeletons matching the planned crate layout). Pin edition 2021+, set up the workspace members.\n` + - `2. Create the CLI crate that reproduces codefang's cobra interface EXACTLY using clap (every command, flag long/short, default, help text). Wire git2 as a dependency and confirm it links against the same libgit2 used by the Go build. Add the tree-sitter crates for the required languages (feature-gated is fine).\n` + - `3. Implement \`--help\`/\`--version\` and the command dispatch so the binary runs and prints help. Subcommand bodies may be stubbed with explicit unimplemented! ONLY where not yet ported, but the CLI surface must be complete.\n` + - `4. Create the golden-diff integration test harness under ${RUST}/tests/ that: builds the rust binary, runs each invocation from ${GOLDEN}/MANIFEST.json with the same env, and byte-compares stdout against the golden file, reporting the first differing byte offset. Wire it so it currently SKIPs (not fails) commands whose implementation is still stubbed, but runs for implemented ones.\n` + - `5. Run \`cargo build\` and \`cargo test --no-run\` and report whether it compiles. Run the rust binary's --help and diff its structure against the Go binary's --help.\n` + - `Return JSON: ok, createdFiles, cargoBuilds, cliMatches, notes. Do NOT delete any Go files.`, - { label: 'scaffold:workspace', phase: 'Scaffold', schema: SCAFFOLD_SCHEMA } -)) -log('Scaffold: ' + (scaffold && scaffold.ok ? 'created; builds=' + scaffold.cargoBuilds + ' cliMatches=' + scaffold.cliMatches : 'FAILED')) - -// ===================================================================== -// PHASE 6 — PORT foundation/leaf modules (pipeline: port -> verify) -// ===================================================================== -phase('Port') -const portTargets = modules - .slice() - .sort((a, b) => (a.tier - b.tier) || ((a.loc || 0) - (b.loc || 0))) - .slice(0, PORT_LIMIT) -log('FULL AUTONOMOUS PORT: attempting all ' + portTargets.length + ' modules in dependency order (tier asc, then LOC asc)') -const ported = await pipeline( - portTargets, - (m) => agent( - `Port the Go package "${m.name}" (${m.goPath}, purpose: ${m.purpose || 'n/a'}) to its Rust crate "${m.crate}" under ${RUST}, following ${DOCS}/DESIGN.md.\n` + - `Rules: (1) Reproduce behavior exactly. Byte-identity of MACHINE-format report bytes (${MACHINE_FORMATS.join(', ')}) is the project goal; route all report serialization through the shared go-compat serialization crate from the design rather than raw serde. (2) Use the dependency mapping from the design (libgit2 via git2, tree-sitter crates, clap). (3) Write full implementations with rustdoc and unit tests ported from the Go tests where they exist. (4) Create/edit ONLY files under your crate's directory — do NOT edit the workspace Cargo.toml; instead return the list of external crates your crate needs in externalCrates so they can be integrated centrally. (5) Where a transitive dep on a not-yet-ported module blocks you, define the minimal trait/interface and note it in todos. (6) For GENERATED or large embedded-data modules (e.g. embedded UAST mappings / uastmaps / *.gen.go), do NOT hand-translate the generated artifact — port the GENERATOR and/or add a build.rs (or a tools step) that regenerates the equivalent Rust data, and note this in todos. (7) For data-parity-critical modules (enry language data, govader lexicons), vendor the SAME data tables the Go libs use so classification/scoring match byte-for-byte.\n` + - `Return JSON: module, files, externalCrates, compiles(self-assessed), testsPass, todos, notes.`, - { label: 'port:' + m.name, phase: 'Port', schema: PORT_SCHEMA }), - (res, m) => agent( - `In ${RUST}, integrate the just-ported crate "${m.crate}" (module ${m.name}): add any missing external crates (${JSON.stringify(res ? res.externalCrates : [])}) to the workspace/crate Cargo.toml, then run \`cargo build -p ${m.crate}\` and \`cargo test -p ${m.crate}\`. Fix compile errors you can resolve quickly without changing behavior. Report whether it compiles and tests pass.\n` + - `Return JSON: module="${m.name}", files, externalCrates, compiles, testsPass, todos, notes.`, - { label: 'verify-port:' + m.name, phase: 'Port', schema: PORT_SCHEMA }) -) -const portedOk = ported.filter(Boolean) -log('Ported: ' + portedOk.filter(p => p.compiles).length + '/' + portTargets.length + ' modules compile') - -// ===================================================================== -// PHASE 7 — VERIFY byte-identity (bounded fix loop) -// ===================================================================== -phase('Verify') -let verify = await safe('verify', () => agent( - `Verify byte-identity of the Rust rewrite under ${RUST} against the golden outputs in ${GOLDEN} (manifest ${GOLDEN}/MANIFEST.json).\n` + - `1. \`cargo build --release\` (or debug if release fails). 2. For each BINDING capture (machine=true / nonBinding!=true: ${MACHINE_FORMATS.join(', ')}) whose command is implemented, run the Rust binary with the SAME argv+env and byte-compare stdout to the golden file. 3. ALSO run human-format (text/plot/html) captures but report those diffs as INFORMATIONAL only — they do NOT count as failures. 4. Report per-command: identical?, binding?, first differing byte offset + a short hexdump-style context, and byte counts.\n` + - `Return JSON: rustBuilds, diffs[], identicalCount (binding only), totalCount (binding only), notes. It is EXPECTED that many commands are still stubbed this run — only compare implemented ones and say so.`, - { label: 'verify:diff', phase: 'Verify', schema: VERIFY_SCHEMA } -)) -let round = 0 -const MAX_VERIFY_ROUNDS = 5 -while (verify && verify.rustBuilds && round < MAX_VERIFY_ROUNDS) { - // Only BINDING (machine) format diffs must be driven to zero. - const failing = (verify.diffs || []).filter(d => !d.identical && d.binding !== false) - if (!failing.length) { log('Verify: all implemented BINDING (machine-format) commands are byte-identical'); break } - log('Verify round ' + round + ': ' + failing.length + ' binding commands differ — spawning fixers') - await parallel(failing.slice(0, 12).map(d => () => - agent(`A Rust MACHINE-format report does not byte-match the Go golden. Command: ${d.command}. First diff: ${d.firstDiff || '(see golden vs rust)'}. Golden dir: ${GOLDEN}. Rust source: ${RUST}.\n` + - `Find the exact formatting/logic discrepancy (likely: encoding/json field-declaration order vs alphabetical, map-key byte-sort, Go shortest-float formatting vs ryu, integer-valued-float rendering, HTML escaping of <>&, indentation, trailing newline, yaml.v3 quoting/indent, LZ4 bin layout, enry classification parity, govader lexicon/scoring parity) and patch the Rust code so the output matches byte-for-byte. Prefer fixing the shared go-compat serialization crate when the bug is systemic. Re-run the single command and confirm identical. Report what you changed.`, - { label: 'fix:' + d.command, phase: 'Verify' }) - )) - round++ - verify = await safe('verify-recheck', () => agent( - `Re-run the byte-identity check for the Rust rewrite under ${RUST} against golden ${GOLDEN}/MANIFEST.json. Compare BINDING (machine) captures for failures; report human-format diffs as informational. Return JSON: rustBuilds, diffs[] (each with binding boolean), identicalCount, totalCount, notes.`, - { label: 'verify:recheck-' + round, phase: 'Verify', schema: VERIFY_SCHEMA })) -} - -// ===================================================================== -// PHASE 8 — REVIEW: completeness critic + adversarial correctness + roadmap -// ===================================================================== -phase('Review') -const review = await safe('review', () => agent( - `Final review of the codefang Rust rewrite first pass.\n` + - `Inputs: modules total=${modules.length}, ported this run=${JSON.stringify(portedOk.map(p => p.module))}, verify result=${JSON.stringify(verify || {})}, golden=${JSON.stringify(golden ? golden.captures.map(c => c.command) : [])}.\n` + - `1. COMPLETENESS CRITIC: list everything still missing for byte-identical parity — unported modules, stubbed commands, unverified report formats, and the top byte-identity risks (enry/govader parity, float/humanize formatting, map ordering, yaml quirks).\n` + - `2. ADVERSARIAL CORRECTNESS: inspect the ported Rust crates under ${RUST} for behavior-diverging bugs vs the Go source; list concrete findings with file:line.\n` + - `3. WRITE a resumable ROADMAP to ${DOCS}/ROADMAP.md as a checklist (unchecked items) covering the remaining port order and per-command byte-identity verification, so this workflow can be resumed module by module. Match the project's roadmap style (WHAT + DoD, no time estimates).\n` + - `Return JSON: remainingWork, correctnessFindings, roadmapPath, percentComplete.`, - { label: 'review:complete', phase: 'Review', schema: REVIEW_SCHEMA } -)) -log('Review done; roadmap: ' + (review ? review.roadmapPath : 'n/a') + '; ~' + (review ? review.percentComplete : 0) + '% complete') - -return { - modulesTotal: modules.length, - portedThisRun: portedOk.map(p => ({ module: p.module, compiles: p.compiles, testsPass: p.testsPass })), - golden: golden ? { captured: golden.captures.filter(c => c.ok).length, manifest: GOLDEN + '/MANIFEST.json' } : null, - byteIdentity: verify ? { identical: verify.identicalCount, total: verify.totalCount } : null, - docs: { architecture: DOCS + '/ARCHITECTURE.md', design: DOCS + '/DESIGN.md', roadmap: DOCS + '/ROADMAP.md' }, - rustWorkspace: RUST, - percentComplete: review ? review.percentComplete : null, - nextStep: 'Resume this workflow to port the next tier of modules; rerun Verify to drive byte-identity to 100%.', -} diff --git a/.claude/workflows/compat-burndown.js b/.claude/workflows/compat-burndown.js deleted file mode 100644 index 44de49a1..00000000 --- a/.claude/workflows/compat-burndown.js +++ /dev/null @@ -1,150 +0,0 @@ -export const meta = { - name: 'compat-burndown', - description: 'Iteratively drive the compat-harness real-divergence count down: vendor missing tree-sitter grammars, then fix static + history + set analyzers cluster-by-cluster, re-running the harness gate as the DoD for every fix. No gaming the gate.', - phases: [ - { title: 'Baseline', detail: 'Run compat smoke gate; capture honest divergence clusters' }, - { title: 'Grammars', detail: 'Vendor the 11 missing tree-sitter grammars -> 22 uast/parse cells' }, - { title: 'Static', detail: 'Fix static analyzers (report value once -> json/yaml/bin/compact follow)' }, - { title: 'History', detail: 'Fix history analyzers across formats on off-golden inputs' }, - { title: 'Sets', detail: 'Fix multi-analyzer / --analyzers * dispatch' }, - { title: 'Regate', detail: 'Re-run gate + mutation self-test; confirm drop + no new SIM + meta-gate still catches planted bug' }, - ], -} - -const GO = '/home/dmitriy/sources/codefang' -const RUST = GO + '/rust' -const COMPAT = RUST + '/tests/compat' -const RUN = COMPAT + '/run.sh' // run.sh {smoke|full} -const GATE = COMPAT + '/gate.py' -const SELFTEST = COMPAT + '/integrity/mutation_self_test.sh' -const SMOKE = COMPAT + '/results/smoke_gate.json' -const GOBIN = GO + '/build/bin' -const RUBIN = RUST + '/target/release' -const ENV = 'TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800' - -const CLUSTERS = { - type: 'object', additionalProperties: true, - properties: { - realDivergences: { type: 'integer' }, - pass: { type: 'integer' }, - sim: { type: 'integer' }, - clusters: { type: 'array', items: { - type: 'object', additionalProperties: true, - properties: { area: { type: 'string' }, count: { type: 'integer' }, formats: { type: 'array', items: { type: 'string' } }, sampleLabels: { type: 'array', items: { type: 'string' } } }, - required: ['area', 'count'], - } }, - notes: { type: 'string' }, - }, - required: ['realDivergences'], -} -const FIXV = { - type: 'object', additionalProperties: true, - properties: { area: { type: 'string' }, before: { type: 'integer' }, after: { type: 'integer' }, buildOk: { type: 'boolean' }, newSim: { type: 'integer' }, gateUntampered: { type: 'boolean' }, notes: { type: 'string' } }, - required: ['after'], -} - -async function safe(l, f) { try { return await f() } catch (e) { log('fail[' + l + ']: ' + ((e && e.message) || e)); return null } } - -const RULES = - `codefang Go->Rust port. The compat harness at ${COMPAT} is the HONEST scoreboard. Drive the REAL-divergence count down by FIXING THE PRODUCT, never the test.\n` + - `ABSOLUTE RULES:\n` + - `1. NEVER edit the harness/oracle/gate/canonicalizer/matrix/allowlist to make a cell pass. The integrity layer hashes them and fails closed; gaming = instant fail. A fix is valid ONLY if the LIVE Go-vs-Rust differential passes on off-golden inputs.\n` + - `2. NEVER hardcode golden bytes or emit constants — the metamorphic layer flags that SIM. Output must compute from input, vary with input, grow with --limit, be deterministic.\n` + - `3. The oracle is the live Go binary ${GOBIN}/{codefang,uast}; Rust under test ${RUBIN}/{codefang,uast}. Compare with \`set -f; env ${ENV} \`, STDOUT only.\n` + - `4. An analyzer's json/yaml/bin/compact are encodings of ONE report value — fix the report value via the real analyzer logic, then route through cf-gojson/cf-goyaml/cf-reportutil so all encodings follow. Port logic from ${GO}/internal/analyzers/ and ${GO}/internal/framework.\n` + - `5. Work ONLY under ${RUST} (analyzer crates + pipeline; NOT ${COMPAT}). Do NOT run git. Return PROSE from implementers; JSON only from verifiers.\n` + - `6. Verify a cluster with: \`bash ${RUN} smoke\` then read ${SMOKE} (or filter the gate to the area). The cluster is done when its cells move FAIL->PASS with no new SIM and the gate confirms it.\n` - -// run gate + cluster failures -async function measure(labelPhase) { - return await safe('measure', () => agent( - `${RULES}\n\nRun \`bash ${RUN} smoke 2>&1 | tail -20\`, then read ${SMOKE} and ${COMPAT}/results/smoke.json. Cluster the FAIL+SIM records by analyzer area (e.g. uast/parse, static/complexity, history/devs, sets) with their formats and a few sample labels. Return JSON: realDivergences, pass, sim, clusters[{area,count,formats,sampleLabels}], notes.`, - { label: 'measure', phase: labelPhase, schema: CLUSTERS })) -} - -// fix one area with bounded rounds, gate-verified -async function fixArea(area, prompt, rounds, phaseName) { - let last = '' - for (let r = 0; r < rounds; r++) { - await safe('impl:' + area + ':' + r, () => agent( - `${RULES}\n\n=== FIX CLUSTER: ${area} ===\n${prompt}\n` + - (last ? `\nStill failing after last attempt:\n${last}\n` : '') + - `Rebuild \`cargo build --release\`, then verify via the harness (\`bash ${RUN} smoke\` + read ${SMOKE}, or run the area's cells through ${COMPAT}/oracle/oracle.py). Return SHORT PROSE.`, - { label: 'impl:' + area, phase: phaseName })) - const v = await safe('verify:' + area + ':' + r, () => agent( - `${RULES}\n\nVerify cluster ${area}: \`cargo build --release 2>&1 | tail -3\`, then \`bash ${RUN} smoke 2>&1 | tail -20\` and read ${SMOKE}. Count this area's remaining FAIL cells (after), confirm no NEW Sim, and confirm the harness files are UNMODIFIED (\`git status --porcelain ${COMPAT}\` style — if the harness changed, gateUntampered=false, that's a violation). Return JSON: area, after (remaining FAIL in this area), buildOk, newSim, gateUntampered, notes.`, - { label: 'verify:' + area, phase: phaseName, schema: FIXV })) - if (v && v.after === 0 && v.buildOk !== false && v.gateUntampered !== false && (v.newSim || 0) === 0) { log(area + ' CLEARED (round ' + r + ')'); return v } - if (v && v.gateUntampered === false) { log(area + ' VIOLATION: harness modified — rejecting'); } - last = v ? ('after=' + v.after + ' newSim=' + v.newSim + ' tampered=' + (v.gateUntampered === false) + '\n' + (v.notes || '')) : '(verify failed)' - log(area + ' round ' + r + ': after=' + (v ? v.after : '?')) - } - return null -} - -// ===================================================================== -phase('Baseline') -const base = await measure('Baseline') -log('Baseline real divergences: ' + (base ? base.realDivergences : '?') + ' (pass=' + (base ? base.pass : '?') + ')') - -// ===================================================================== -// Grammars — biggest single cluster (22 uast/parse cells). -phase('Grammars') -await fixArea('uast/parse-grammars', - `The Rust uast binary lacks 11 tree-sitter grammars (python, c, c-header, rust, typescript, tsx, javascript, json, yaml, cpp, shell) — it errors where Go emits a UAST, failing all uast/parse[json]+[compact] cells for those languages. Vendor those grammar crates into cf-uast (add the tree-sitter- crates, register them in the loader/parser language dispatch like the existing Go grammar), so \`uast parse --format json \` etc. produce a UAST byte-identical to Go. Pin grammar versions to match the Go go-sitter-forest grammars that produced Go's output (check ${GO}/go.mod versions). Verify EACH language's uast/parse cell goes PASS via the live oracle.`, - 8, 'Grammars') -const afterGrammars = await measure('Grammars') -log('After grammars: ' + (afterGrammars ? afterGrammars.realDivergences : '?') + ' divergences') - -// ===================================================================== -// Static analyzers — fix report value once, all formats follow. -phase('Static') -const STATIC = ['static/complexity', 'static/comments', 'static/cohesion', 'static/composition', 'static/clones', 'static/halstead', 'static/imports'] -await parallel(STATIC.map(a => () => - fixArea(a, - `Static analyzer ${a} diverges from Go on off-golden dirs across formats (json/yaml/bin/compact). Port/fix the REAL static-analysis logic (UAST parse each file -> ${a} metric computation -> serialize) from ${GO}/internal/analyzers/${a.split('/')[1]} so output matches Go byte-for-byte on arbitrary directories (the harness tests dirs the golden never used). Fix the report VALUE once; all four encodings then follow via cf-gojson/cf-goyaml/cf-reportutil. Do not special-case the golden dir.`, - 5, 'Static') -)) -const afterStatic = await measure('Static') -log('After static: ' + (afterStatic ? afterStatic.realDivergences : '?') + ' divergences') - -// ===================================================================== -// History analyzers — over the general pipeline, across formats. -phase('History') -const HISTORY = ['history/devs', 'history/imports', 'history/typos', 'history/burndown', 'history/anomaly', 'history/quality', 'history/sentiment', 'history/couples', 'history/shotness', 'history/file-history'] -await parallel(HISTORY.map(a => () => - fixArea(a, - `History analyzer ${a} diverges from Go on off-golden inputs (other dirs/limits/formats) — the broader matrix exposes gaps the 7 golden captures did not. Drive its REAL per-commit computation (git revwalk -> per-commit -> aggregate -> ComputeAllMetrics -> serialize) to match Go byte-for-byte across json/yaml/bin/compact for arbitrary --limit. Port from ${GO}/internal/analyzers/${a.split('/')[1].replace('-','_')}. Remember the typos-style trap: Go-STABLE fields (e.g. commit attribution) must match exactly — measure with the oracle, never assume nondeterminism.`, - 5, 'History') -)) -const afterHistory = await measure('History') -log('After history: ' + (afterHistory ? afterHistory.realDivergences : '?') + ' divergences') - -// ===================================================================== -// Sets — multi-analyzer and --analyzers '*'. -phase('Sets') -await fixArea('analyzer-sets', - `The multi-analyzer / --analyzers '*' cells (static/*, history/*, comma-lists, @hercules repo) diverge. Make the general dispatch run the requested SET of analyzers over one pass and emit the combined report matching Go (note all_static.* may be Go-nondeterministic — if so the oracle will measure it and the allowlist needs Go-nondeterminism EVIDENCE, which you may add to ${COMPAT}/allowlist.json ONLY with stored proof from running Go N times; that is the one sanctioned harness edit and only with evidence). Fix the genuinely-deterministic set cells to PASS.`, - 5, 'Sets') - -// ===================================================================== -phase('Regate') -const finalMeasure = await measure('Regate') -const selftest = await safe('selftest', () => agent( - `Run the meta-gate \`bash ${SELFTEST} 2>&1 | tail -8\`. It MUST still prove the system catches a planted product bug AND fails closed on a harness cheat (this guarantees our fixes did not break the detector). Then run \`bash ${RUN} smoke 2>&1 | tail -6\` for the final tally. Return JSON: realDivergences, pass, sim, clusters[], notes (include whether the mutation self-test still passes).`, - { label: 'regate', phase: 'Regate', schema: CLUSTERS })) -log('FINAL: divergences=' + (finalMeasure ? finalMeasure.realDivergences : '?') + ' selftest=' + (selftest ? selftest.notes : '?')) - -return { - baselineDivergences: base ? base.realDivergences : null, - afterGrammars: afterGrammars ? afterGrammars.realDivergences : null, - afterStatic: afterStatic ? afterStatic.realDivergences : null, - afterHistory: afterHistory ? afterHistory.realDivergences : null, - finalDivergences: finalMeasure ? finalMeasure.realDivergences : null, - finalPass: finalMeasure ? finalMeasure.pass : null, - finalSim: finalMeasure ? finalMeasure.sim : null, - metaGateStillCatchesBugs: selftest ? (selftest.notes || '').toLowerCase().includes('pass') : null, - nextStep: (finalMeasure && finalMeasure.realDivergences === 0) - ? 'compat smoke GREEN with 0 real divergences and the meta-gate still proves bug-detection. Run `compat full` (486 cells) for the wider matrix.' - : 'Resume: divergences remain — re-run this workflow; the harness gate shows exactly which clusters are left.', -} diff --git a/.claude/workflows/compat-test-system.js b/.claude/workflows/compat-test-system.js deleted file mode 100644 index 8e6ca415..00000000 --- a/.claude/workflows/compat-test-system.js +++ /dev/null @@ -1,191 +0,0 @@ -export const meta = { - name: 'compat-test-system', - description: 'Build the Go<->Rust compatibility test system from specs/go-compat-testing/SPEC.md: live-Go-oracle differential testing, Go-measured canonicalization, metamorphic/anti-sim checks, coverage+gap ledger, tamper-evidence, and a mutation self-test that PROVES the system catches bugs', - phases: [ - { title: 'Oracle', detail: 'Go-runner + Go-measured canonicalizer (N-run stable/variant field classification with evidence)' }, - { title: 'CliSurface', detail: 'Recursive Go-vs-Rust CLI surface diff (flags/defaults/help/exit/stderr)' }, - { title: 'MatrixCorpus', detail: 'Invocation matrix + content-addressed mined corpus (multi-repo, per-language files)' }, - { title: 'Metamorphic', detail: 'Anti-simulation relational checks (vary-input, grow-with-limit, determinism, non-empty)' }, - { title: 'CoverageLedger', detail: 'llvm-cov + matrix-cell coverage + machine-readable gap ledger' }, - { title: 'Fuzz', detail: 'Per-stage Go-native differential fuzz targets + Rust shims, coverage-guided' }, - { title: 'TamperProof', detail: 'Tamper-evident harness + MUTATION SELF-TEST: prove the system fails on an injected bug' }, - { title: 'CI', detail: 'Single entry point (smoke/full tiers), known-divergence allowlist, honest tally' }, - ], -} - -// ---------- constants ---------- -const GO = '/home/dmitriy/sources/codefang' -const RUST = GO + '/rust' -const KUBE = '/home/dmitriy/sources/kubernetes' -const SPEC = GO + '/specs/go-compat-testing/SPEC.md' -const COMPAT = RUST + '/tests/compat' // the system we are building -const GOBIN = GO + '/build/bin' // Go oracle -const RUBIN = RUST + '/target/release' // Rust under test -const GATE = RUST + '/tests/antisim/parity_gate.sh' // existing seed (generalize, never weaken) -const ENV = 'TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800' - -// ---------- schemas (only short verifiers carry these) ---------- -const VERIFY = { - type: 'object', additionalProperties: true, - properties: { - ok: { type: 'boolean' }, // phase DoD met (built + self-checks pass) - buildOk: { type: 'boolean' }, - selfTestOk: { type: 'boolean' }, // proves the component catches a planted defect - createdFiles: { type: 'array', items: { type: 'string' } }, - errors: { type: 'array', items: { type: 'string' } }, - notes: { type: 'string' }, - }, - required: ['ok'], -} -const FINAL = { - type: 'object', additionalProperties: true, - properties: { - entryPoint: { type: 'string' }, - mutationSelfTestProvesCatches: { type: 'boolean' }, // the meta-gate - tamperEvident: { type: 'boolean' }, - matrixCells: { type: 'integer' }, - cellsCovered: { type: 'integer' }, - differentialPass: { type: 'integer' }, - differentialFail: { type: 'integer' }, - simSuspects: { type: 'integer' }, - rustLineCov: { type: 'number' }, - gapLedgerPath: { type: 'string' }, - notes: { type: 'string' }, - }, - required: ['mutationSelfTestProvesCatches'], -} - -async function safe(label, fn) { - try { return await fn() } catch (e) { log('step failed [' + label + ']: ' + ((e && e.message) || e)); return null } -} - -// ---------- shared context (every agent reads the SPEC; rules enforced) ---------- -const CTX = - `Build the Go<->Rust COMPATIBILITY TEST SYSTEM specified in ${SPEC}. READ THAT SPEC FIRST — it is the source of truth (sections 3 Scope, 4 Technical Design, 8 Roadmap).\n` + - `\nNON-NEGOTIABLE RULES (these encode why the prior effort failed):\n` + - `1. The ORACLE is the LIVE Go binary at ${GOBIN}/{codefang,uast}. NEVER re-derive expected output in Rust/Python — a re-derivation can carry the same bug and mask it.\n` + - `2. Compare Rust (${RUBIN}/{codefang,uast}) vs Go on inputs the author NEVER hand-picked (mined/generated corpus), not just recorded golden args. A recorded golden is memorizable; a fresh input is not.\n` + - `3. Canonicalization is MEASURED, never declared: run Go N>=3x per input; ONLY fields that vary across Go's own runs may be normalized, and the evidence (the differing Go outputs) must be stored. Blanking a Go-STABLE field is the exact cheat that hid a real bug before — it is FORBIDDEN and the tamper layer must detect it.\n` + - `4. You may GENERALIZE the existing ${GATE} into this system, but you may NOT weaken it: do not blank Go-stable fields, do not shrink the probed set to pass.\n` + - `5. Run env is pinned: \`set -f; env ${ENV} \`, compare STDOUT (stderr is progress).\n` + - `6. Every component must SELF-PROVE it catches a defect: include a check that injects a known divergence/constant/tamper and asserts the component reports FAIL/SIM. A green that cannot be shown to catch a planted bug is worthless.\n` + - `7. Work ONLY under ${RUST} (system lives in ${COMPAT}). Do NOT run git. \`go run\`/\`go build\`/\`cargo\` available.\n` + - `8. Return PROSE summaries from implementer steps (NOT a structured tool). Verifier steps return the JSON schema.\n` - -// Generic build+verify loop for one phase. Implementer = prose; verifier = schema. -async function phaseLoop(title, buildPrompt, verifyPrompt, rounds) { - let last = '' - for (let r = 0; r < rounds; r++) { - await safe('impl:' + title + ':' + r, () => agent( - `${CTX}\n\n=== PHASE: ${title} ===\n${buildPrompt}\n` + - (last ? `\nPrevious attempt did not meet the DoD. Fix:\n${last}\n` : '') + - `Build it for real, then run your own self-checks. Return a SHORT PROSE summary (NOT a structured tool).`, - { label: 'impl:' + title, phase: title })) - const v = await safe('verify:' + title + ':' + r, () => agent( - `${CTX}\n\n=== VERIFY PHASE: ${title} ===\n${verifyPrompt}\n` + - `Run the actual commands. Return JSON: ok (DoD met), buildOk, selfTestOk (component demonstrably catches a planted defect), createdFiles, errors (up to 25), notes.`, - { label: 'verify:' + title, phase: title, schema: VERIFY })) - if (v && v.ok && v.selfTestOk !== false) { log(title + ' DONE (round ' + r + ')'); return v } - last = v ? ('ok=' + v.ok + ' selfTest=' + v.selfTestOk + '\n' + (v.errors || []).join('\n')) : '(verify failed to run)' - log(title + ' round ' + r + ': not done (' + last.slice(0, 120) + ')') - } - log(title + ' NOT complete after ' + rounds + ' rounds') - return null -} - -// ===================================================================== -// PHASE 1 — Oracle + Go-measured canonicalizer (SPEC §3.4, roadmap 1) -// ===================================================================== -phase('Oracle') -await phaseLoop('Oracle', - `Build ${COMPAT}/oracle/: a runner that, given an invocation (bin + argv + pinned env), executes the Go binary N>=3 times and the Rust binary 2 times, then:\n` + - `- classifies each output field as GO-STABLE (identical across all Go runs) or GO-VARIANT (differs across Go runs), storing the differing Go outputs as EVIDENCE in a manifest;\n` + - `- compares Rust vs Go: byte-exact on GO-STABLE fields; canonical-equal (sorted/neutralized) on GO-VARIANT fields ONLY;\n` + - `- verifies Rust is itself deterministic (2 identical Rust runs) and flags nondeterministic Rust as a FAIL.\n` + - `Generalize the logic in ${GATE} onto this oracle (do not weaken it). Output a per-invocation verdict: PASS / FAIL / SIM, plus the field classification + evidence.`, - `In ${RUST}: confirm ${COMPAT}/oracle exists and runs. SELF-TEST (must all hold): (a) on a known-divergent fixture the oracle reports FAIL; (b) on history/typos at --limit 50 (Go is stable on commit attribution) it correctly treats commit as GO-STABLE and would FAIL a blanked-commit comparison — i.e. it does NOT blank a Go-stable field; (c) on a genuinely Go-variant field (e.g. list ORDER) it canonicalizes WITH stored evidence. Report ok/buildOk/selfTestOk.`, - 5) - -// ===================================================================== -// PHASE 2 — CLI surface conformance (SPEC §3.1, roadmap 2) -// ===================================================================== -phase('CliSurface') -await phaseLoop('CliSurface', - `Build ${COMPAT}/cli_surface/: recursively invoke \`--help\` on the Go binary (root + every subcommand) to extract the full surface — every flag (long/short/default/help text), positional args, exit codes, and stdout-vs-stderr usage — and assert the Rust binary's surface is identical. Include ERROR-PATH parity: bad flag, missing required arg, unknown analyzer => identical exit code + identical stderr message. Cover BOTH codefang and uast.`, - `Run the CLI-surface check Go-vs-Rust. SELF-TEST: temporarily compare against a deliberately-wrong expected flag set and confirm it reports a mismatch (then restore). Report which flags/commands match and which (if any) diverge.`, - 4) - -// ===================================================================== -// PHASE 3 — Invocation matrix + content-addressed mined corpus (SPEC §3.2, §3.3, roadmap 3) -// ===================================================================== -phase('MatrixCorpus') -await phaseLoop('MatrixCorpus', - `Build ${COMPAT}/matrix.toml (the meaningful cross-product: analyzer x output-format x key-flags x analyzer-sets per SPEC §3.2) and ${COMPAT}/corpus/ (content-addressed). Mine inputs: several real source files spanning MULTIPLE tree-sitter languages codefang supports (not just Go), plus at least 2 real git repos of differing size beyond ${KUBE} if available locally (else multiple subdirs of ${KUBE} as distinct repos via shallow copies). Record provenance per corpus entry (hash + origin). Wire EVERY matrix cell to the Phase-1 oracle. Cells where Go produces no output are recorded as a contract (expected-empty), not skipped.`, - `Confirm matrix.toml expands to N cells and each maps to an oracle invocation; confirm corpus entries are content-addressed with provenance. SELF-TEST: run the oracle over a small distilled slice and confirm PASS/FAIL/SIM verdicts are produced per cell (non-empty result set). Report matrixCells count + how many languages/repos the corpus covers.`, - 5) - -// ===================================================================== -// PHASE 4 — Metamorphic / anti-simulation layer (SPEC §3.6, roadmap 4) -// ===================================================================== -phase('Metamorphic') -await phaseLoop('Metamorphic', - `Build ${COMPAT}/metamorphic/: per-analyzer relational checks that fail hardcoded stubs (SPEC §3.6): (a) output DIFFERS for two different inputs where Go differs; (b) output GROWS monotonically with --limit (10 vs 500) where Go grows; (c) identical args => identical Rust bytes (determinism); (d) Rust non-empty where Go is non-empty; (e) Rust output never equals a previously-recorded golden constant once the input changed. Any failure => SIMULATION SUSPECT, printing the two inputs and the two equal Rust outputs.`, - `Run the metamorphic layer over the current Rust port. SELF-TEST (critical): create a throwaway analyzer/path that emits a CONSTANT regardless of input and confirm the metamorphic layer flags it SIM; then confirm a real analyzer (e.g. history/devs) passes. Report simSuspects found on the REAL port and whether the planted constant was caught.`, - 5) - -// ===================================================================== -// PHASE 5 — Coverage accounting + gap ledger (SPEC §3.7, roadmap 5) -// ===================================================================== -phase('CoverageLedger') -await phaseLoop('CoverageLedger', - `Build ${COMPAT}/coverage/: produce Rust line/branch coverage via cargo-llvm-cov (install if needed) AND matrix-cell coverage (cells exercised / total) AND per-language parse coverage. Emit a machine-readable ${COMPAT}/ledger.json listing: every untested matrix cell, every known divergence, and every Go-VARIANT capture with its evidence. Coverage % is reported ALONGSIDE the differential verdict, never as a substitute for it (SPEC §6 risk).`, - `Confirm cargo-llvm-cov runs and ledger.json is emitted with the three required sections (untested cells, known divergences, Go-variant-with-evidence). SELF-TEST: confirm the ledger is honest — temporarily remove a cell's result and confirm it shows up as untested in the ledger (then restore). Report rustLineCov %, matrixCells, cellsCovered.`, - 4) - -// ===================================================================== -// PHASE 6 — Per-stage differential fuzzing (SPEC §3.5, roadmap 6) -// ===================================================================== -phase('Fuzz') -await phaseLoop('Fuzz', - `Build ${COMPAT}/fuzz/: Go-native (testing/F) differential fuzz targets, one per PURE stage — tree-sitter parse, UAST map, each serializer (cf-gojson/cf-goyaml/CFB1), and at least one analyzer's pure ComputeAllMetrics. Each target feeds the same input to Go and Rust (subprocess for E2E; optional FFI shim for hot pure stages) and fails on divergence. Seed corpora from real source (structure-aware, not random bytes). Distill any divergence-finding input back into ${COMPAT}/corpus/. Run a short bounded fuzz session and record any divergences found.`, - `Confirm at least the parser + one serializer + one analyzer fuzz target build and run a short session. SELF-TEST: feed a known mutated input that the Go and a deliberately-broken Rust path render differently and confirm the fuzz harness reports the divergence. Report which stages have fuzz targets and any real divergences found.`, - 5) - -// ===================================================================== -// PHASE 7 — Tamper-evidence + MUTATION SELF-TEST (SPEC §3.8, §4 testing, roadmap 7) — THE META-GATE -// ===================================================================== -phase('TamperProof') -const tamper = await phaseLoop('TamperProof', - `Build ${COMPAT}/integrity/: (1) TAMPER-EVIDENCE — hash the oracle, canonicalizer, and matrix definition; the system fails CLOSED if any is modified, if the matrix is shrunk, or if a Go-STABLE field is newly blanked. (2) MUTATION SELF-TEST (the meta-gate, SPEC §4 testing-strategy E2E) — a script that injects a deliberate behavioral bug into a Rust analyzer (e.g. perturb one metric), rebuilds, runs the compat system, and ASSERTS the system reports FAIL; then reverts and asserts GREEN. Also inject a deliberate harness tamper (blank a Go-stable field in a COPY of the oracle) and assert fail-closed. The system is only trustworthy if it provably catches both a product bug AND a harness cheat.`, - `Run the mutation self-test end to end. It MUST: (a) with an injected Rust analyzer bug, the compat system reports FAIL (selfTestOk depends on this); (b) after revert, GREEN; (c) with a tampered oracle (blanked Go-stable field), fail-closed. Report whether the system provably catches the planted product bug AND the planted harness cheat. If it does NOT catch either, ok=false.`, - 6) - -// ===================================================================== -// PHASE 8 — CI entry point + honest final report (SPEC §3.9, roadmap 8) -// ===================================================================== -phase('CI') -const final = await safe('ci', () => agent( - `${CTX}\n\n=== PHASE: CI ===\n` + - `Build the single entry point \`${RUST}/xtask\` (or ${COMPAT}/run.sh) with two tiers: \`compat smoke\` (distilled corpus, no fuzz, gate p95 < 2 min) and \`compat full\` (full matrix + fuzz + multi-repo). Output per-cell PASS/FAIL/SIM + final tallies + the gap ledger; nonzero exit on any real divergence; a known-divergence allowlist that REQUIRES a written reason + Go-nondeterminism evidence (mirroring Connect's tracked known-failing). Run \`compat smoke\` once against the current Rust port and capture the honest tally. Update ${SPEC} status + write ${COMPAT}/README.md documenting how to run it and how the mutation self-test proves it works.\n` + - `Return JSON: entryPoint, mutationSelfTestProvesCatches (does the Phase-7 self-test prove the system catches a planted bug?), tamperEvident, matrixCells, cellsCovered, differentialPass, differentialFail, simSuspects, rustLineCov, gapLedgerPath, notes (smoke tally).`, - { label: 'ci:finalize', phase: 'CI', schema: FINAL })) -log('CI: entry=' + (final ? final.entryPoint : 'n/a') + - ' catchesPlantedBug=' + (final ? final.mutationSelfTestProvesCatches : '?') + - ' diff=' + (final ? (final.differentialPass + '/' + (final.differentialPass + final.differentialFail)) : '?') + - ' sim=' + (final ? final.simSuspects : '?')) - -return { - builtUnder: COMPAT, - entryPoint: final ? final.entryPoint : null, - // THE meta-result: the system is only worth anything if it provably catches a planted bug. - mutationSelfTestProvesCatchesBugs: !!(final && final.mutationSelfTestProvesCatches) && !!(tamper), - tamperEvident: final ? final.tamperEvident : null, - matrix: final ? { cells: final.matrixCells, covered: final.cellsCovered } : null, - differential: final ? { pass: final.differentialPass, fail: final.differentialFail, sim: final.simSuspects } : null, - rustLineCoverage: final ? final.rustLineCov : null, - gapLedger: final ? final.gapLedgerPath : (COMPAT + '/ledger.json'), - spec: SPEC, - nextStep: (final && final.mutationSelfTestProvesCatches) - ? 'Compat system live + self-proven to catch planted bugs/cheats. Run `compat full` to drive the differential pass-rate up and burn down the gap ledger honestly.' - : 'Incomplete: the system does NOT yet provably catch a planted bug — resume; without the mutation self-test passing the system cannot be trusted.', -} diff --git a/.codefang.yaml b/.codefang.yaml index 24bf8a99..9854bd9b 100644 --- a/.codefang.yaml +++ b/.codefang.yaml @@ -1,135 +1,39 @@ # codefang configuration file -# Place this file in your repository root or $HOME directory. -# Values shown below are the defaults. Uncomment and modify as needed. +# Place this file in the directory you run codefang from, or in $HOME, or +# pass it with --config . Every key sets the DEFAULT of one +# `codefang run` flag; values shown below are the compiled defaults. +# Uncomment and modify as needed (site/guide/configuration.md). # -# Environment variables override file values using the CODEFANG_ prefix: -# CODEFANG_PIPELINE_WORKERS=4 +# Precedence: explicit flag > CODEFANG_* environment > this file > default. # CODEFANG_HISTORY_BURNDOWN_GRANULARITY=15 +# CODEFANG_ANALYZERS=history/devs,history/burndown # -# CLI flags override both environment variables and file values. +# Keys not listed here are not read: a file setting one loads with a single +# "warning: ignoring unsupported config keys: ..." line. -# List of analyzers to run. Empty means all registered analyzers. +# Analyzer ids or globs run when -a is absent. Empty means all analyzers. # analyzers: [] -# Pipeline resource tuning. # pipeline: -# workers: 0 # 0 = auto (GOMAXPROCS) -# memory_budget: "" # e.g. "4GiB" -# blob_cache_size: "" # e.g. "512MiB" -# diff_cache_size: 0 -# blob_arena_size: "" # e.g. "256MiB" -# commit_batch_size: 0 -# gogc: 0 # 0 = Go default (100) -# ballast_size: "0" # e.g. "128MiB" -# memory_limit: "" # e.g. "8GiB" — sets debug.SetMemoryLimit -# worker_timeout: "" # e.g. "60s" — stall detection timeout per worker request -# -# # Advanced pipeline tuning (0 = use defaults). -# uast_spill_threshold: 32 # file changes per commit before spilling UAST to disk -# intra_commit_parallel_threshold: 4 # min file changes for intra-commit parallel parsing -# max_intra_commit_workers: 4 # max goroutines for intra-commit UAST parsing -# max_uast_blob_size: 262144 # max blob size (bytes) for UAST parsing (256 KiB) -# uast_parse_timeout: "10s" # per-file UAST parse timeout -# max_changes_per_commit: 10000 # commits exceeding this are skipped -# max_diff_batch_size: 1000 # max diff requests per batch -# memory_budget_ratio: 50 # % of system RAM for auto memory budget -# memory_budget_cap: "2GiB" # max auto memory budget -# memory_limit_ratio: 75 # % of system RAM for Go soft memory limit -# -# # Extended pipeline tuning. -# uast_spill_trim_interval: 16 # MallocTrim frequency during UAST spill-mode parsing -# native_trim_interval: 10 # malloc_trim frequency within a chunk -# max_streaming_buffering: 3 # max buffering factor for streaming (triple-buffering) -# drain_prefetch_timeout: "30s" # timeout for abandoned prefetch goroutines -# sampler_interval: "2s" # pipeline sampler polling interval -# worker_ratio: 100 # % of CPU cores for pipeline workers -# uast_worker_ratio: 40 # % of CPU cores for UAST pipeline workers -# leaf_worker_divisor: 3 # leaf workers = NumCPU / divisor -# min_leaf_workers: 4 # minimum leaf workers -# buffer_size_multiplier: 2 # buffer size = workers * multiplier -# budget_limit_ratio: 95 # budget-to-memory-limit conversion (%) -# system_ram_limit_ratio: 90 # memory limit cap as % of system RAM -# diff_job_buffer_multiplier: 10 # scales diff job queue buffer -# static_max_workers: 8 # max concurrent workers for static analysis -# malloc_trim_interval: 50 # files between malloc_trim calls in static analysis -# static_memory_limit_ratio: 90 # % of budget for static phase memory limit +# max_changes_per_commit: 0 # --max-changes-per-commit (0 = 10000) -# History analyzer settings. # history: +# tick_size: 24 # --tick-size, hours per tick # burndown: -# granularity: 30 -# sampling: 30 -# track_files: false -# track_people: false -# hibernation_threshold: 1000 -# hibernation_to_disk: true -# hibernation_directory: "" -# debug: false -# goroutines: 0 # 0 = auto -# -# couples: -# coupling_threshold_high: 10 # min co-change count for "high" coupling -# ownership_few_threshold: 3 # max contributors for "few owners" bucket -# ownership_moderate_threshold: 5 # max contributors for "moderate owners" bucket -# batch_coupling_threshold: 100 # max file pairs per commit for coupling -# hll_precision: 10 # HyperLogLog precision for contributor sketches -# top_k_per_file: 100 # max coupling pairs per file in store output -# min_edge_weight: 2 # min co-changes for a coupling edge -# +# granularity: 30 # --granularity, ticks per age band +# sampling: 30 # --sampling, ticks per sample +# track_files: false # --burndown-files +# track_people: false # --burndown-people # devs: -# consider_empty_commits: false -# anonymize: false -# bus_factor_threshold: 0.5 # cumulative ownership fraction for bus factor -# risk_threshold_critical: 90.0 # % ownership for critical risk -# risk_threshold_high: 80.0 # % ownership for high risk -# risk_threshold_medium: 60.0 # % ownership for medium risk -# active_threshold_ratio: 0.7 # fraction of max commits for "active" developer -# default_active_days: 90 # lookback window (days) for active developer check -# hll_precision: 14 # HyperLogLog precision for developer sketches -# -# file_history: -# hotspot_threshold_critical: 50 # commits for critical hotspot -# hotspot_threshold_high: 30 # commits for high hotspot -# hotspot_threshold_medium: 15 # commits for medium hotspot -# -# imports: -# goroutines: 4 -# max_file_size: 1048576 # 1 MiB -# max_dependency_risk_rows: 30 # max rows in dependency risk table -# +# consider_empty_commits: false # --empty-commits +# anonymize: false # --anonymize # sentiment: -# min_comment_length: 20 -# gap: 0.5 # 0.0 - 1.0 -# neutralizer_weight: 0.8 # SE-domain adjustment weight (0-1) -# max_weight_ratio: 3.0 # max comment length weight ratio -# positive_threshold: 0.6 # sentiment >= this is "positive" -# negative_threshold: 0.4 # sentiment <= this is "negative" -# trend_threshold: 0.1 # change needed to classify trend direction -# low_sentiment_risk_thresh: 0.2 # sentiment <= this is HIGH risk -# -# clones: -# max_clone_pairs: 1000 # max clone pairs in aggregated report -# num_hashes: 128 # MinHash signature size -# num_bands: 16 # LSH band count -# num_rows: 8 # rows per LSH band -# shingle_size: 5 # token shingle window size -# similarity_type2: 0.8 # Type-2 clone similarity threshold -# similarity_type3: 0.5 # Type-3 clone similarity threshold -# threshold_ratio_yellow: 0.1 # clone ratio for yellow warning -# threshold_ratio_red: 0.3 # clone ratio for red warning -# threshold_pairs_yellow: 5 # clone pairs for yellow warning -# threshold_pairs_red: 20 # clone pairs for red warning -# +# min_comment_length: 20 # --min-comment-len (0 disables the filter) # shotness: -# dsl_struct: 'filter(.roles has "Function")' -# dsl_name: ".props.name" -# +# dsl_struct: 'rfilter(.roles has "Function")' # --shotness-dsl-struct +# dsl_name: ".props.name" # --shotness-dsl-name # typos: -# max_distance: 4 - -# Checkpoint settings for incremental analysis. -# checkpoint: -# enabled: true -# dir: "" # empty = default temp dir -# resume: true -# clear_prev: false +# max_distance: 4 # --typos-max-distance +# anomaly: +# threshold: 2.0 # --anomaly-threshold +# window_size: 20 # --anomaly-window diff --git a/.deadcode-whitelist b/.deadcode-whitelist deleted file mode 100644 index 0cb76812..00000000 --- a/.deadcode-whitelist +++ /dev/null @@ -1,28 +0,0 @@ -# Deadcode whitelist — exact qualified function names as printed by deadcode. -# One entry per line. Lines starting with # are comments. -# -# These functions are unreachable from the production binary's main() but are -# required by test code in external test packages (package foo_test) or by -# _test.go files in other packages. They cannot be moved to _test.go files. - -# --- Auto-generated PEG parser code (pigeon) — cannot be modified --- -Pretty -Size - -# --- pkg/gitlib: test helpers used by internal/cache, internal/framework, --- -# --- internal/analyzers/{plumbing,devs,anomaly,couples,file_history,sentiment} --- -NewCachedBlobForTest -NewCachedBlobWithHashForTest -NewTestCommit -TestCommit.Hash -TestCommit.Author -TestCommit.Committer -TestCommit.Message -TestCommit.NumParents -TestCommit.Parent -TestCommit.Tree -TestCommit.Files -TestCommit.File -TestCommit.Free -TestSignature -NewCommitForTest diff --git a/.dockerignore b/.dockerignore index 39dc2586..6f399c12 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,6 @@ third_party/libgit2/build third_party/libgit2/install # Build outputs -build/bin build/ # Git and editor metadata diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c7af1b7..387986e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,13 +46,13 @@ jobs: - name: Format check run: cargo fmt --all -- --check - # Clippy is ADVISORY (continue-on-error): the workspace still carries - # pre-existing clippy debt (e.g. clippy::never_loop, clippy::approx_constant - # in test code) that predates the Rust-only cutover. It surfaces findings - # without blocking; switch to blocking once the debt is cleared. - - name: Clippy (advisory) - continue-on-error: true + # Clippy and the dead-code check are hard gates (same commands as + # `make lint` / `make deadcode`): every warning is an error. + - name: Clippy run: cargo clippy --workspace --all-targets -- -D warnings + - name: Dead code + run: RUSTFLAGS="-D dead_code" cargo check --workspace --all-targets + - name: Test run: cargo test --workspace diff --git a/.gitignore b/.gitignore index 3b0a13d3..9bd5b540 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,6 @@ /vendor cmd/codefang/plugin_template_source.go coverage.txt -build/bin/codefang -build/bin/uast python/dist python/build @@ -67,7 +65,6 @@ third_party/go-sitter-forest/ test/benchmarks/ test/benchmarks/benchmark_results.txt debug/ -build/bin/ build.log .backend.pid .vite.pid diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 44a0b1ba..00000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,83 +0,0 @@ -version: 2 - -before: - hooks: - - go mod tidy - - go run ./tools/precompgen/precompile.go -o pkg/uast/embedded_mappings.gen.go - -builds: - - id: codefang - main: ./cmd/codefang - binary: codefang - env: - - CGO_ENABLED=1 - goos: - - linux - - darwin - - windows - goarch: - - amd64 - - arm64 - ignore: - - goos: windows - goarch: arm64 - ldflags: - - -s -w - - -X github.com/Sumatoshi-tech/codefang/pkg/version.Version={{.Version}} - - -X github.com/Sumatoshi-tech/codefang/pkg/version.Commit={{.Commit}} - - -X github.com/Sumatoshi-tech/codefang/pkg/version.Date={{.Date}} - - - id: uast - main: ./cmd/uast - binary: uast - env: - - CGO_ENABLED=1 - goos: - - linux - - darwin - - windows - goarch: - - amd64 - - arm64 - ignore: - - goos: windows - goarch: arm64 - ldflags: - - -s -w - - -X github.com/Sumatoshi-tech/codefang/pkg/version.Version={{.Version}} - - -X github.com/Sumatoshi-tech/codefang/pkg/version.Commit={{.Commit}} - - -X github.com/Sumatoshi-tech/codefang/pkg/version.Date={{.Date}} - -archives: - - id: default - ids: - - codefang - - uast - name_template: >- - codefang_{{ .Version }}_{{ .Os }}_{{ .Arch }} - format_overrides: - - goos: windows - formats: - - zip - -checksum: - name_template: "checksums.txt" - -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" - - "^ci:" - - "^chore:" - - Merge pull request - - Merge branch - -release: - github: - owner: Sumatoshi-tech - name: codefang - draft: false - prerelease: auto - name_template: "v{{.Version}}" diff --git a/CHANGELOG.md b/CHANGELOG.md index e357fb94..2d7ed465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,674 @@ The format follows [Keep a Changelog](https://keepachangelog.com/). --- +## [Unreleased] — History core: one shared walk, deterministic output + +`specs/bugs/BUG-history-core-pipeline.md`. Every `history/*` analyzer now +runs through ONE shared core (`crates/cf-commands/src/handlers/history/mod.rs`): +one commit window, one identity detector, one tick assigner, one change +filter, one chronological reduce. + +### Fixed + +- **Every history analyzer drops a commit above `--max-changes-per-commit`, + alone or co-selected** (`specs/frds/FRD-bounded-memory-history.md`). The + direct walks of `history/typos`, `history/sentiment`, `history/anomaly` + and `history/imports` never applied the gate while the shared UAST walk + did, so `-a history/typos` and `-a history/typos,history/quality` reported + different typos, comments, imports and author ids for the same window. + Their ndjson records now carry the commit's window position, so a merged + ndjson stream stays in commit order across a dropped commit + (`bins/codefang/tests/chunked_history_e2e.rs`). +- **`history/shotness` co-selected with another UAST analyzer honors + `--first-parent`, `--shotness-dsl-struct` and `--shotness-dsl-name`.** The + shared multi-analyzer walk used the default DSL and skipped first-parent + merges, so the co-selected report differed from the single-analyzer one + (`bins/codefang/tests/shotness_e2e.rs`). +- **Mixed static+history selections render every advertised format.** A + `-a static/…,history/…` run with `--format timeseries` (or + `timeseries --ndjson`) emitted `{"analyzers": [], "commits": []}`: the mixed + path converted the final reports, which carry no per-commit data. It now + emits the history time series and names the omitted static analyzers once + on stderr. `--format text` prints the static sections followed by the + history text and `--format compact` every analyzer's compact line, instead + of being rejected (`bins/codefang/tests/mixed_selection_e2e.rs`). +- **`history/shotness` ndjson records keep their commit position** when + `--max-changes-per-commit` drops a commit, so a merged ndjson stream that + selects shotness before another analyzer stays in commit order + (`chunked_history_e2e.rs::merged_ndjson_keeps_window_order_across_a_dropped_commit`). +- **`history_core_e2e.rs::output_does_not_depend_on_the_host_cpu_count` no + longer flakes**: it compared two multi-analyzer documents whose + `metadata.analyzed_at` came from the wall clock; its runs now pin + `CODEFANG_NOW`. +- **`history/devs` ndjson records keep their commit position** when + `--max-changes-per-commit` drops a commit: devs numbered its records by the + kept-commit index while every other analyzer used the window position, so + a multi-analyzer ndjson stream came out of commit order (`[1, 2, 1, 3, 2, + 3]`). +- **Report metadata and both binaries name the same version.** Report + `codefang_version` read `dev` and `uast version` printed `dev (commit: + none, …)` while `codefang version` printed the crate version: cf-version + kept the retired reference's frozen defaults and only the `version` + subcommand patched over them. cf-version now defaults to the crate version + and finds the commit itself (`CF_COMMIT`, `GIT_COMMIT`, `GIT_HASH`, else the + checkout's `HEAD`); cf-commands' duplicate build script is gone. +- Docs: the glossary, the site index and the contributing guide no longer + describe the retired Go pipeline (chunks, hibernation, checkpoints, + OpenTelemetry); `uast query` examples use the real syntax (`uast query + '' `, no `-e`); the MCP how-to says no launcher ships yet. +- **`history/shotness` tracks every declaration, not only the top-level + ones** (`specs/bugs/BUG-history-shotness-nested-entities.md`). Class + methods (Java, C#, TypeScript, JavaScript, Python) and nested functions + were invisible because the default structural query `filter(...)` only + looked at the file node's direct children. The UAST DSL now implements + `rfilter(expr)` / `rmap(expr)` (recursive pre-order filter / map; they + were identity no-ops) and the `props` field (`.props.name`), and the + shotness default is `rfilter(.roles has "Function")`. `uast query` + gains the same stages. +- **Shotness entities carry a qualified name**: `Class.method`, + `Outer.Inner.method`, `outer.inner`, and the receiver type for + `func (a *A) String()` (`A.String`), so same-named methods on two + receivers are two entities instead of one. +- **Shotness renames and deletions reach the report.** The report is now + built from the ONE live entity state (a replay of each commit's + touched / ended / moved effects): a `git mv` moves every entity of the + file under the new path with its counts and couplings (6 entities for 3 + before), a removed declaration or deleted file ends its entity (it is + dropped with its couplings), and merge commits are no longer counted + against the first parent (`--first-parent` keeps them as ordinary + commits). +- **`--shotness-dsl-struct` and `--shotness-dsl-name` are honored** and + validated before the walk (an invalid expression is a named flag error); + they were accepted and ignored. +- **The shotness walk runs its per-commit work in parallel** through the + shared pre-pass (the reduce stays sequential and chronological), and a + recursive DSL query no longer allocates per visited node: hercules + `--limit 300`, debug build, 43.1 s → 10.7 s wall with identical totals. +- cf-shotness: the unused report store (`store.rs`, `ReportWriter`), + the per-tick aggregator and `CommitSummary` are deleted. +- **`history/burndown` no longer panics on a history reaching tick 16383** + (`previousTime cannot be TreeMergeMark`, exit 101 on any 45-year daily or + 2-year hourly history); the merge-mark sentinel is gone and `(person, + tick)` values are packed with a tick field sized from the window + (`specs/bugs/BUG-history-burndown-survival.md`). +- **Burndown debits a merge's deletions at the merge's tick** (they were + filed at tick 0, erasing the lines from every earlier sample, lowering + `total_peak_lines` and reporting `lines_removed: 0` for merges). +- **`--granularity`, `--sampling`, `--burndown-files`, `--burndown-people` + are applied.** The two sizes shape the sampling grid; the two switches + fill `file_survival` (per-file survival, ownership with both flags), + `developer_survival` and `interactions` (lines of an author deleted or + replaced by a modifier), `tracked_files` / `tracked_developers`, the + ndjson `FileDeltas` / `PeopleDeltas` / `MatrixDeltas`, and the text + report's "Top Developers" section. +- **`--format timeseries` stamps every burndown commit with its own tick** + (every commit carried the last tick) and **lists the devs commits** + (`"commits": []` for a devs-only run). +- **The burndown text and plot age bands read newest-to-oldest** (band 0, + the oldest code, was labelled one month old). +- **`analysis_period_days` is the tick span** (`last_tick · tick_size_hours + / 24`; a 310-day history reported 300, a 10-day one 0). +- **A windowed burndown run (`--limit`, `--since`, `--head`) seeds the + tracked state from the base tree**, so a commit's `LinesAdded` / + `LinesRemoved` are the lines it changed (the first touch of a + pre-existing file counted the whole file) and burndown agrees with devs + on the same commit; the base lines form band 0 of the global curve. +- **Burndown never leaks or double-counts lines:** a deletion always debits + the tracked lines, a re-insertion debits first, a rename moves the treap. +- **Burndown applies `--max-changes-per-commit`** like devs, so both see + the same commit set. +- **`history/devs` counts a 0-byte blob as an empty text side** + (`empty.txt` 0 → 2 lines was a `0/0/0` entry) and **honors + `--empty-commits`** (`specs/bugs/BUG-history-devs-stats.md`). +- **Bus factor reports `secondary_dev_id` whenever a secondary developer + exists** (it was omitted for developer id 0). +- **Sentiment lengths count characters, not bytes.** `--min-comment-len` + and the per-comment weight inside a tick compared UTF-8 byte lengths, so a + 14-character Russian comment passed a 20-character minimum and weighed + about twice an English comment of the same length. Both now count + characters (Unicode scalar values). +- **`history/couples` follows renames, counts every change once and keeps + oversized commits in ownership** (`specs/bugs/BUG-history-couples-coupling.md`). + A `git mv` carries the old path's co-change row, column, change count and + owners to the new path (one row, chained renames included); a merge + contributes only the files no earlier commit of the window changed and + credits its author with those only; the changeset cap is the documented + **1000** files (was 200) and a commit above it still owns every file it + touched, keeps its real hash in ndjson and lists its `AuthorFiles`; the + report lists every file at the window head that was coupled OR owned; + `--format timeseries` carries each commit's tick (was `0`) and + `files_touched` is the number of files the author touched; + `avg_coupling_strength` is one mean of the pair strengths (the 101-pass + shuffled-sum median is gone). Dead code deleted: `cf-couples` + `report_section.rs`, `store.rs`, the dense-report / raw-JSON helpers, + `filter_top_devs`, `sort_ownership_by_risk`, the `FileSpill` trait and the + unused `TickData` / `CommitSummary` / `config` / `descriptor` items. +- **`history/file-history` composition counts distinct files by content-aware + category, with tick bounds** (`specs/bugs/BUG-history-file-history-lifecycle.md`). + Every changed file is classified over its path AND the first 8 KiB of its + blob (binary and generated content are recognised); `composition` is the + distinct files changed in the window per category (a one-file repository + reports `source 1 (100%)`, not the number of edits) and `composition_ts` + the distinct files changed per tick, each entry with its `start_time` / + `end_time`; a rename retires the old path and the latest classification + wins. Dead code deleted: `cf-file-history` `framework.rs`, the placeholder + `Classifier` / `Enry` / `GeneratedDetector` (the category set lives in + `category.rs`), `to_compact_json*` and the unused descriptor constants. +- **Selecting `history/burndown` no longer forces `--first-parent` on every + co-selected analyzer.** Burndown walks its own first-parent window; devs, + couples, file-history and the rest honor only the user's `--first-parent` + (`codefang run .` reported 2 developers where the standalone devs run + reported 12). +- **Reports no longer depend on the host CPU count.** The per-worker + seen-files Bloom filters (couples), the worker-strided consume order + (file-history, typos dedup) and the strided ndjson line order are gone: + every reduce runs in chronological order over ONE exact set, and the + "files at HEAD" filter uses the newest commit of the window. The output + is a pure function of the repository and the flags (e2e: byte-identical + under `taskset -c 0-1`). +- **Renames are detected** (`git_diff_find_similar`, renames on): a pure + `git mv` is one Modify with `from.name != to.name` and the same blob, so + devs reports 0 added / 0 removed instead of 100/100 and burndown, + couples, file-history and shotness keep the file's identity. +- **`--since` keeps every commit authored at/after the cutoff.** The walk is + ordered by committer time, so the author-time cutoff is now a skip filter + over the whole walk instead of a stop condition that dropped younger + commits sorted below an older one. +- **`--head` is the single-commit window `[HEAD]` in one place, for every + analyzer and format.** Burndown ndjson/timeseries honor it; devs + `--head --format json` reports the HEAD commit (it returned + `total_commits: 0` for a non-merge HEAD — the quickstart example printed + an empty report). `--head` ≡ `--limit 1` byte for byte; the closed-form + `--head` special cases (devs, burndown, anomaly) are deleted. +- **`--tick-size` is applied** (validated `> 0`): one shared + `HistoryTicks` replaces the eight hard-coded 24-hour copies, and + `tick_size_hours` in the timeseries document reflects the flag + (`--tick-size 1` on the ticks fixture gives `[0, 1, 11, 31, 73]`). +- **`--languages ""` means no filter in the history phase** (as in the + static phase; one shared token parser) instead of excluding everything. + History language detection now reads the blob content (`.h`, `.m`, + `.tsx`, extensionless scripts resolve), and devs/anomaly use the same + `cf-langpath` detector as the filter — the second extension table in + cf-analyzers-plumbing is deleted. +- **The history change filter honors `--include-vendored`, + `--include-generated` and `--extra-excluded-prefixes`** and applies the + generated-file marker (`DO NOT EDIT`, `Code generated`, … in the first + 512 bytes) to the blob header, as the static phase does. + `StaticFilter::skips_with_header` is the static-walk counterpart. +- **Identity:** a hit on one token (email or name) now registers the other, + so `alice ` / `Alice Smith ` / `Alice Smith ` is one + person in every analyzer; burndown uses the shared `IdentityDetector` + instead of its own copy; `--exact-signatures`, `--people-dict` (an + unreadable file is an error) and `--anonymize` (`Developer-A`, `-B`, …) + are applied. One `AUTHOR_MISSING` constant (`cf-identity`) everywhere. +- **No 32-file spill gate.** Commits touching more than 32 files were + contributing zero files to quality / sentiment / imports / typos / + shotness; the gate is gone, and devs honors `--max-changes-per-commit` + instead of a hard-coded cap. +- **Numeric flags are validated:** `--limit=-5`, `--tick-size 0`, + `--max-changes-per-commit=-1` are rejected with + `Error: invalid --: …`. +- **Every user error names its cause** (`Error: path not found:

`, + `cannot open repository:

`, `unsupported format: `, `format is + not supported for `, `no report produced for + --format `); the `command dispatch is blocked` placeholder is deleted. + `--no-color` is applied (it sets `NO_COLOR` for the text/compact + renderers). +- **`history/sentiment` scores only the comments a commit introduced or + changed** (`specs/bugs/BUG-history-sentiment-scoring.md`): the per-commit + product is the multiset difference between the filtered comment sets of + the file's new and old version, so a code-only commit to a commented file + contributes nothing. A tick without such comments carries **no score** — + it is absent from the time series, the trend and the low-sentiment + periods (it was scored `0.0`, "negative", HIGH risk); `total_commits` + still counts every commit. Javadoc / Doxygen blocks (` * ` continuation + lines) are scored; trend endpoints are clamped to `[0, 1]` and + `change_percent` follows the clamped values; `--min-comment-len` is + applied (`0` disables the length filter, a negative value is + `Error: invalid --min-comment-len: …`); the SE-domain terms match whole + words from a reviewed list (`deadline` no longer hits `dead`, `skill` no + longer hits `kill`, `master` is gone) and the aggregate average is the + plain mean. +- **`history/quality` samples are the static analyzers' numbers** + (`specs/bugs/BUG-history-quality-ticks.md`): each parsed file is measured + by `cf_quality::analyze_file` with the file bytes + (`cf_complexity::node::Node::from_uast` + `AnalysisOptions { source }`, + `cf_halstead::analyze_with_source`), so a one-file repository reports what + `static/complexity`, `static/halstead`, `static/comments` and + `static/cohesion` report for that file. A failed parse contributes + nothing (it counted as an analyzed file with cohesion `1.0`), a + function-free file contributes no cohesion sample, and a tick without + analyzed files is absent from the time series and the aggregate — the + aggregate means no longer fall below the aggregate minimums + (`cohesion_mean_mean 0.667` next to `min_cohesion 1`). +- **The generated-content marker is applied in the static phase** + (`specs/bugs/BUG-static-walk-generated-marker.md`). A file whose first + 512 bytes carry `Code generated … DO NOT EDIT` (or any other generated + marker) was listed by every static analyzer although `--help` promised + its exclusion; every `static/*` analyzer now runs on ONE shared walk + (`cf_commands::handlers::walk_source_files`) that reads each file once + and applies the marker to the bytes it read, so the file is skipped by + default and re-admitted by `--include-generated`. +- **One static walk instead of seven.** The per-handler copies drifted: + composition counted symlinked directories as files, imports had no + 64 MiB read cap, `--extra-excluded-prefixes` was matched against the + absolute path (so `gen/` never matched under an absolute or `./` root), + and a single-file root was stamped `.` by some analyzers and by its name + by others. The shared walk skips `.git`, cache-tagged and symlinked + directories (no loops), applies the policy to the root-relative path, + caps every read at 64 MiB, and stamps a file root with the file's name + everywhere. `static/composition` keeps its own semantics on the same + walk (`walk_all_files`): every regular file is classified, header only. +- **Skipped files are reported once.** An unreadable or oversized file + prints `warning: skipped : ` on stderr once per run and + the end-of-run `progress: skipped N …` line counts distinct paths — a + `-a 'static/*'` run no longer reports one unreadable file as 5–7. +- **`--per-file` says when it is ignored.** The flag is a `--format json` + enrichment; with `yaml`, `bin`, `text`, `compact` or `plot` the run now + prints `--per-file applies to --format json only; ignored for ` + on stderr (once) instead of rendering silently without it. +- **`-a 'static/*'` equals the single runs** for every analyzer and format + (json sections, yaml, bin), and the merged `overall_score` averages only + the scored sections (info-only sections such as imports / composition no + longer pull it toward `-1`) — pinned by `crates/cf-commands/tests/flag_parity.rs` + and `bins/codefang/tests/static_walk_e2e.rs`. +- The complexity and halstead plot pages classify through + `cf_complexity::thresholds` / `cf_halstead::thresholds` instead of their + own copies of the band constants, so a function at a band bound is + colored as the report labels it. + +- **`history/typos` reports real typo fixes** + (`specs/bugs/BUG-history-typos-false-positives.md`). A typo is ONE + identifier a commit changed into a DIFFERENT, Levenshtein-close + identifier on the same line: the identifiers the two lines share cancel + (`v := recieve()` → `v := receive()` is reported), the pair itself must be + within `--typos-max-distance` (`foo` → `completelyDifferent` is not), and + a literal change next to an unchanged identifier, an empty or + whitespace-bearing token (Markdown headings) or a self-identical pair is + never a typo. `typo_list` lists each `wrong → correct` pair once at its + chronologically first fix (the same bytes under any CPU count), + `patterns` counts every occurrence (a pair fixed on two lines has + `frequency: 2`), `file_typos` / `aggregate` count occurrences, + `unique_patterns` counts distinct pairs; every list is `[]` when empty; + `line` is 1-based; `--format bin` carries the json value instead of `{}`. + The rules live in `cf-typos` (`candidate_line_pairs`, `identifier_pair`); + its duplicate JSON encoder (`compat::GoValue`) and Levenshtein module are + deleted. +- **`history/anomaly` counts distinct files and scores a real baseline** + (`specs/bugs/BUG-history-anomaly-files.md`). Per tick `files_changed` is + the number of DISTINCT files touched and `files` names them (a delete + names the deleted file instead of `""`); the zero-variance sentinel + `±100` needs a window of at least two observations, so the second tick of + a history is no longer flagged by construction; `--anomaly-window` is + applied (`>= 2`, default 20; a shorter history scores against every + earlier tick) through the validated `cf_anomaly::config::Config` and + rejected with a named error at preflight; `--anomaly-threshold` (`> 0`, + default 2.0) is registered and validated the same way (it was documented + but rejected by the argument parser); `anomalies` is always a list (`[]`, never `null`); anomalies sort by + severity then tick. +- **`history/imports --format json|yaml|bin` is the documented history + report** (`specs/bugs/BUG-history-imports-report.md`): `tick_size` + (seconds), `author_index` and `imports`: developer → language → import → + tick → count, with the parser's language names (`go`, `python`, …) + instead of the constant `uast` and the extractor run with the file bytes + (Kotlin imports are found). The constant zero static-shaped report, the + generic `extract_imports_from_uast` shim and the unused + `cf_imports::store` / `HistoryAnalyzer` are deleted. +- **Every history analyzer renders every format `--help` advertises** + (`specs/bugs/BUG-history-text-format.md`). `--format text` exited 1 for + `history/quality`, `history/typos`, `history/anomaly` and + `history/imports` after writing the version header and the section name + to stdout (`serialization error for : unsupported format: text`); + each now prints a human summary — quality: the aggregate complexity, + Halstead, comment and cohesion figures and the most complex ticks; typos: + the fix counts, every `wrong → correct` pair at its first fix + (`file:line`, commit), the recurring pairs and the files; anomaly: the + detection settings, the per-tick baselines and the anomalous ticks by + severity; imports: the counts, the most used imports and the top + importers. `--format timeseries --ndjson` exited 1 for `history/couples`, + `history/shotness`, `history/typos` and `history/anomaly` + (`format timeseries+ndjson is not supported`); it is now the merged + timeseries document's `commits` array, one line per commit, for every + analyzer (byte-identical to the former output of the six analyzers that + had it). Every whole-selection history format (`text`, `compact`, + `ndjson`, `timeseries`) renders every selected analyzer BEFORE writing: + an analyzer without a report is `Error: no report produced for + --format ` with nothing on stdout. +- **The merged `--format timeseries` keeps every analyzer's commits.** The + commit order came from the first analyzer (by flag) that listed commits, + so `-a history/imports,history/typos` dropped every typo-fixing commit + that changed no import; the order is now the longest walk's, and a commit + only a shorter walk lists is placed by its tick. `history/typos` + contributes `{"fixes", "files"}` per typo-fixing commit (it contributed + nothing). Each contribution is indexed by hash once (a linear search per + commit made the merge quadratic in the commit count). +- **`history/couples` and `history/file-history` apply + `--max-changes-per-commit`** like devs, burndown and the UAST-based + analyzers: a commit whose raw tree diff exceeds the cap is dropped before + identities and ticks are assigned, so every history analyzer sees the + same commit set. Couples ndjson records carry the commit's window + position, like the other analyzers, so a dropped commit cannot reorder + the merged ndjson. +- **`history/imports --format compact` is a summary** + (`developers= languages= distinct_imports= usages=`) instead of + `tick_size=86400`. + +- **The configuration file is loaded** (`specs/bugs/BUG-cli-inert-surface.md`). + `cf-config` was never linked into the binary, so `--config` and the + documented `.codefang.yaml` discovery did nothing. `codefang run` now + reads `--config ` (unreadable → `Error: cannot read --config

: …`), + else `./.codefang.yaml`, else `$HOME/.codefang.yaml`, plus the `CODEFANG_*` + variables, and installs every supported key as the default of its flag: + explicit flag > environment > file > compiled default. The fifteen keys + (`analyzers`, `pipeline.max_changes_per_commit`, `history.tick_size`, + `history.burndown.{granularity,sampling,track_files,track_people}`, + `history.devs.{consider_empty_commits,anonymize}`, + `history.sentiment.min_comment_length`, + `history.shotness.{dsl_struct,dsl_name}`, `history.typos.max_distance`, + `history.anomaly.{threshold,window_size}`) are validated with the flag's + rule (`Error: invalid config

: must be …, got `); a key nothing + reads loads with ONE `warning: ignoring unsupported config keys: …` line. +- **`-q/--quiet` and `-v/--verbose` do what their help says**: `-q` (now + also spelled `--silent`) suppresses the `progress:` lines, warnings and + errors still print; `-v` adds `progress: config file: …` and + `progress: analyzers: …`; both together are a usage error. +- **`-o/--output ` writes the report to the file** for every format + but `plot` (it was read only as the plot directory and ignored otherwise); + `Error: cannot write --output

: …` when it cannot be created. +- **`--ndjson` without `--format timeseries` is an error** + (`invalid --ndjson: requires --format timeseries, got --format `); it + was accepted and ignored. +- **A non-positive `--granularity`, `--sampling` or `--typos-max-distance` + is rejected** (`Error: invalid --granularity: must be > 0, got 0`) instead + of silently meaning the default. +- **A mixed static+history `--format text` fails at preflight** with + `format text is not supported for a mixed static+history selection` + (it ran every analyzer, then failed with `no report produced`). +- **`codefang version` names the build**: the crate version and the git + commit (`CF_COMMIT` / `GIT_COMMIT` / `GIT_HASH` at build time, else the + checkout's `HEAD` read by `crates/cf-commands/build.rs`, else `unknown`) + instead of `dev (commit: none, …)`; no wall-clock time is embedded. +- **`codefang completion --no-descriptions` drops the help texts** + (it was accepted and ignored). +- **`--profile` logs a `MEM` sample at start** and writes its + `/proc/self/maps` snapshots to the temp directory instead of a hard-coded + `/tmp`; its help no longer promises heap dumps. +- **`uast query -i ` queries the file** (it printed nothing without + positional files) and `uast query -t ` explores that file; + `uast parse -o` / `uast query -o` keep every input's output (only the last + survived); `-` reads stdin for `parse` and `query`; + `uast mapping --show-treesitter --language ` prints the raw tree + (it always failed with `unsupported language`); `uast completion` covers + every subcommand, `validate` and `mapping` included. + +### Changed + +- `cf_persist::GobCodec` encodes straight into its writer instead of + building the whole encoding in memory first (same bytes). + +- **`history/anomaly` time-series entries carry the raw values once**, under + `metrics`: the duplicated top-level `language_diversity` and + `author_count` keys are gone. `Record.files` is `[]` instead of `null` + when empty. +- **`history/typos` `typo_list` entries are 1-based `line`s** (they were + 0-based indices), and `patterns` is `[]` (not `null`) when no pair + repeats. +- **Several history analyzers in `--format json`/`yaml` emit ONE document** + (`{"version": "codefang.run.v1", "metadata": …, "analyzers": [{"id", + "mode", "report"}]}` — the same shape as a mixed static+history run) + instead of concatenated documents that `json.load` rejected. +- **`--format compact` works for history selections**: one line per + analyzer with its aggregate figures, rendered by the shared compact + renderer. +- `--format ndjson` over several history analyzers merges every analyzer's + records by commit position (selection order within a commit). +- `crates/cf-commands/src/handlers/history.rs` (4583 lines) is split into + `handlers/history/{mod, anomaly, devs, file_history, imports, quality, + sentiment, typos}.rs`; public paths are re-exported unchanged. +- `cf-analyzers-plumbing` now holds only `HistoryTicks` and + `IdentityDetector` (+ `Signature`, `anonymize_names`). +- `cf_gitlib::testutil` gained `CommitBuilder` (authors, skewed committer + dates, side lines, merges, deletes, pure renames) plus + `commit_files_as`, `delete_files`, `rename_file`. +- **History `--format text` sections and `--format compact` lines are + titled by the analyzer id** (`history/anomaly:` instead of + `TemporalAnomaly:`, `history/imports` instead of `ImportsPerDeveloper`); + compact titles are padded to one width and fractional figures rounded to + two decimals (`anomaly_rate=33.33`, not `33.33333333333333`). +- `--format timeseries --ndjson` over several history analyzers emits ONE + line per commit with every analyzer merged into it (it concatenated each + analyzer's lines). +- **Rankings are stable on an explicit total key.** The couples text's + ownership risk (fewest contributors, then path), the devs / shotness / + file-history plot treemaps and bars and the comments / halstead / + cohesion plot charts (value, then name) sort with a stable `sort_by`; + equal values list in name order instead of the Go pdqsort tie + permutation `handlers/go_sort.rs` replicated. +- `site/architecture/streaming-pipeline.md` is rewritten as **History + pipeline** (nav title too): the commit window, rename detection, the + change filter and caps, the parallel per-commit pass, identity and ticks, + the chronological reduce and the serialization — the Go buffering, + hibernation, checkpointing and the spans / metrics it described do not + exist in the Rust binary (the memory budget and chunking it now describes + are the ones added below). + `architecture/overview.md` and `architecture/uast.md` drop the same + Go-era claims (coordinator, plumbing analyzers, OpenTelemetry, config + layering, go-enry, `sync.Pool`, interning, arenas) and list which + workspace crates the binaries do not link. + +- `--max-changes-per-commit` help: dropped commits are invisible to every + analyzer; burndown re-inserts an affected file whole the next time it is + touched (the old text said the cap could desync burndown). +- The `--shotness-dsl-struct` default is `rfilter(.roles has "Function")` + (`rfilter` walks the whole tree; `filter` sees only the root's children). +- `cf-config` is the flag-default layer: every field is optional, the + compiled defaults live only in the flag registrations. +- `uast parse --format` help lists only `json, compact, none`; + `uast validate --schema` defaults to the embedded schema (shown empty). +- The doc-examples harness runs every documented command with an empty + `HOME` and working directory (no `.codefang.yaml` leaks in, `-o` files + never land in the source tree) and no longer appends the removed + checkpoint / cache flags. + +### Removed + +- The retired Go differential gate and its oracle data: `build/bin`, + `tests/compat`, `tests/antisim`, `tests/golden`, `tests/golden-harness`, + `crates/cf-gojson/oracle`, `crates/cf-gojson/tests/oracle_data`, + `crates/cf-goyaml/tests/oracle.rs`, + `crates/cf-langpath/tests/content_heuristics_diff.rs`, the compat + workflows, `.deadcode-whitelist` and `.goreleaser.yml`. Crate docs now + point at the report e2e suites in `bins/codefang/tests` as what pins the + report bytes. + +- The per-analyzer `timeseries --ndjson` / `ndjson` registry arms and their + six wrapper functions (`*_timeseries_ndjson`, `burndown_record_ndjson`): + every history selection reaches those formats through the whole-selection + renderers, so the arms were unreachable. +- Accepted-and-ignored flags: `--workers`, `--static-workers`, `--input`, + `--input-format`, `--gogc`, `--ballast-size`, `--cpuprofile`, + `--heapprofile`, `--buffer-size`, `--commit-batch-size`, + `--blob-cache-size`, `--diff-cache-size`, `--blob-arena-size`, + `--diagnostics-addr`, `--keep-store`, `--tmp-dir`, + `--whitelist`, `--fail-on-missing-submodules`, `--blob-cache-goroutines`, + `--no-diff-cleanup`, `--no-diff-whitespace`, `--diff-timeout`, + `--diff-goroutines`, `--uast-changes-goroutines`, + `--burndown-hibernation-threshold`, `--burndown-hibernation-disk`, + `--burndown-hibernation-dir`, `--burndown-debug`, `--burndown-goroutines`. +- The dead plumbing providers `TreeDiff`, `FileDiff`, `BlobCache`, + `UASTChanges`, `LanguagesDetection`, `TicksSinceStart` (and their + `Analyzer` trait / clock scaffolding) in cf-analyzers-plumbing. +- Dead quality / sentiment code: `cf_quality::analyzer::{ComponentSet, + ScalarReport, accumulate_file, consume_commit, fold_commits}`, + `cf_quality::{aggregate_commits_to_ticks, parse_report_data, + extract_commit_time_series, TickData}`, the cf-quality store scaffolding, + `cf_sentiment::analyzer::Config` and its gap / config-key constants, the + cf-sentiment store record kinds, `modal_f32_mean`, and + `cf_cohesion::AnalyzeError` with the never-failing + `analyze_with_find_depth`. +- The seven private static folder walks and their `make_relative_path` / + `parent_dir` copies; the no-op `ensure_sections_files_key`; the + `go_pdqsort` delegate (the complexity plot ranks with `sort_by` on the + raw report's total order); the unused `stacker` dependency of + cf-commands. +- `handlers/go_sort.rs` (the Go pdqsort replica); the always-empty + `CouplesPlotData::dev_names` and the `dev_names` parameter of the couples + plot sections (no developer heatmap is rendered); + `history_formats::{PartialFailure, HistoryFormatResult, leaf_name, + leaf_supports_text, leaf_report_exists}`; the history plot phase's copies + of the output-directory and `report.json` writers (it shares + `plot::{create_output_dir, write_report_json_atomic, page_renderer}`). +- The Go-era `pipeline:` configuration keys (`workers` / GOMAXPROCS, GC, + memory, cache and goroutine tuning) from `site/guide/configuration.md`: + nothing in the Rust binary reads them. + +- Accepted-and-ignored `codefang run` flags: `--checkpoint`, + `--checkpoint-dir`, `--resume`, `--clear-checkpoint`, `--cache-dir`, + `--no-cache`, `--debug-trace`, and the hidden `--skip-blacklist` / + `--blacklisted-prefixes` (use `--extra-excluded-prefixes`). No checkpoint + store, incremental cache or trace exporter is linked into the binary. +- The `codefang render` subcommand (it only printed "not available"; use + `codefang run --format plot -o

`). +- Inert `uast` flags: the global `--config`, `-v/--verbose`, `-q/--quiet`; + `parse --progress/-p`, `parse --workers/-w`; `validate --color/--no-color`; + `mapping --mapping` / `--coverage` (the mapping rules were never loaded). +- Every `cf-config` key without a live consumer: the `pipeline:` tuning tree + (all but `max_changes_per_commit`), `checkpoint:`, burndown hibernation / + debug / goroutines, and the couples / devs / file-history / imports / + sentiment / clones thresholds; `cf_config::defaults`, `Facts` / + `apply_to_facts`, `load_from_yaml_and_env`, `config_file_path`. +- Dead plumbing: `cf-commands` `observability.rs` and its `runtime` / `mcp` + features (nothing enabled them), `deprecated_flag_message`, + `build_render_command`, the binary's empty `mcp` / `profile` features and + the `--profile` pprof stub. + +### Added + +- **`--shotness-keep-ended`** (config key `history.shotness.keep_ended`): + `history/shotness` keeps entities whose declaration or file was removed, + marked `"ended": true` with their counts and couplings, adds + `aggregate.ended_nodes`, and lets an entity declared again continue its + history. Hotspots, `hot_nodes` and the plot pages still show only live + entities; the text report marks ended rows. Off by default, so the report + is unchanged; on 31,923 kubernetes commits the flag grows the shotness + report from 256 MB to 859 MB (`bins/codefang/tests/shotness_e2e.rs`). + +- **`codefang mcp`: the Model Context Protocol server ships.** It serves + `codefang_analyze`, `uast_parse` and `codefang_history` over stdio + (newline-delimited JSON-RPC); every tool result is the report + `codefang run` renders for the same input (`cf_commands::render_run`), + and the tools take Linguist language names (`python`, `JavaScript`) as + well as extensions. The server keeps no results between calls + (`bins/codefang/tests/mcp_e2e.rs`; `site/integrations/mcp.md`). +- **`history/burndown` prepares its per-change work on the worker pool** + (`specs/frds/FRD-parallel-burndown.md`): blob line counts, the binary + check, the change filter and the line diffs run in parallel; only the + per-file treap updates stay sequential, with the sequential rule for a + diverged file. The raw-count pre-pass hands the tree diffs of commits with + 256 or more changes (vendoring, mass renames) to the main pass instead of + repeating their rename detection. On 31,923 kubernetes commits, measured + back to back: 59.4 s before, 25.9 s after, byte-identical output. +- **The finished UAST results wait on disk while the other history walks + run** (`specs/frds/FRD-history-spill.md`). When a run selects a non-UAST + history analyzer next to the UAST ones, each UAST analyzer's results are + written to an unlinked temporary file in `TMPDIR` (bincode, through + `cf_persist::GobCodec`) and read back only when that analyzer renders; + `-v` prints each spill's size. On 31,923 kubernetes commits with + `--memory-budget 4GB`, `history/*` peaks at 3.80 GiB instead of 4.75 GiB + (121 MB spilled), output unchanged. On a `tmpfs` `/tmp` the spill leaves + the process but not RAM; point `TMPDIR` at a disk. + +- **Bounded-memory history walks: `--memory-budget`, `--chunk-size` and the + chunked core** (`specs/frds/FRD-bounded-memory-history.md`). Every history + walk prepares its per-commit work on a worker pool at most one chunk ahead + of an in-order fold and frees each product once folded; identity and tick + assignment run one commit at a time (`Stamper`), so the output is + byte-identical for every chunk size, budget and CPU count. The workers + live for the whole walk and claim short runs of consecutive commits from a + shared cursor (their libgit2 caches keep the shared trees), taking over + half of another worker's unstarted run when idle, so a slow commit no + longer stalls its slice and chunk boundaries cost no barrier + (`history/anomaly` on 31,923 kubernetes commits: 43 s → 32 s; + `history/sentiment` on 7,973: 26.1 s → 16.1 s). + `--memory-budget ` (default: half the RAM, at most 2 GiB; also + `pipeline.memory_budget` in the config file) plans each walk's chunk size + and worker count from its share of the budget and its measured cost per + worker and per product, and gives libgit2 a fixed part of it: a quarter + for its object cache (its 256 MiB default made the parallel walks evict + each other's trees) and 7.5 % for mapped pack files (8 GiB by default). `--chunk-size ` (also `pipeline.chunk_size`) fixes the chunk + instead. `-v` prints each walk's plan. Invalid values fail before the walk + with `Error: invalid --memory-budget: …` / `Error: invalid --chunk-size: …` + (`bins/codefang/tests/chunked_history_e2e.rs`, + `bins/codefang/tests/config_e2e.rs`). +- **Free heap memory goes back to the OS between chunks** + (`cf_meminfo::release_free_heap`). glibc kept every worker's arena at its + largest transient working set: a forced trim dropped a running + `history/sentiment` from 1777 MB to 679 MB. +- **A combined report is rendered without copying it.** The multi-analyzer + document is encoded from the decoded reports in place + (`cf_analyze::conversion::render_converted_output`) instead of from a deep + copy, and the encoded bytes are returned instead of copied into a second + buffer; a 31,923-commit kubernetes `history/*` report is 538 MB of json. +- **The UAST analyzers parse one change at a time.** `history/quality`, + `sentiment`, `typos`, `imports` and `shotness` run as one memoized pass, + alone or co-selected; each change's two sides are parsed once, shared by + every selected analyzer and dropped before the next change, instead of + holding every parse of a commit. Peak RSS on 7,973 kubernetes commits: + `history/sentiment` 2.5 GB → 0.97 GB, the five co-selected 2.9 GB → + 1.7 GB, output unchanged. +- `bins/codefang/tests/common/mod.rs`: every CLI e2e suite builds its + commands through one helper that points `HOME` at the test scratch + directory and drops inherited `CODEFANG_*` configuration variables, so a + developer's `~/.codefang.yaml` or environment can no longer change test + results (verified by running all suites under a hostile `HOME` config). +- `bins/codefang/tests/history_formats_e2e.rs`: every history analyzer in + every format `--help` advertises (json, yaml, bin, timeseries, + timeseries --ndjson, ndjson, text, compact, plot) exits 0 with a + non-empty, well-formed result; the matrix is checked against the + `--help` format list; `-a 'history/*'` in text (one section per analyzer) + and compact (one line per analyzer); the merged timeseries --ndjson + equals the document's `commits`; a failing text run writes nothing. +- `bins/codefang/tests/history_core_e2e.rs`: 13 CLI-level scenarios over + real git fixtures (first-parent isolation, CPU-count independence, pure + rename, skewed `--since`, `--head` per format, one-document JSON, named + errors, `--tick-size 1`, `--languages ""`, the generated marker, alias + identities / `--exact-signatures` / `--anonymize` / `--people-dict`, + a 40-file commit under quality, history `--format compact`). +- `bins/codefang/tests/quality_e2e.rs` and `sentiment_e2e.rs`: CLI-level + scenarios over real git fixtures (per-tick complexity 2 → 4 with a gap + tick, aggregate `min <= mean`, a 40-file commit, static == quality per-file + numbers for Go and Python; ordered positive / negative scores in `[0, 1]`, + unscored comment-free ticks, Javadoc, unchanged comments not re-counted, + clamped trend endpoints, `--min-comment-len`, determinism, json / yaml + agreement). +- `cf_quality::analyze_file` / `FileSample` / `TickQuality::push`; + `cf_sentiment::new_or_changed`; VADER rule tests against the published + example scores (negation, `but`, idioms, `least`, `!` / `?` emphasis, + ALLCAPS, emoji). +- Unit tests: rename detection and the `--since` skip filter (cf-gitlib); + register-on-hit, exact signatures, people dict, anonymization, ticks + (cf-analyzers-plumbing); the shared preflight, the generated-marker + filter, `--extra-excluded-prefixes`, the window with `--head` + (cf-commands). +- `bins/codefang/tests/static_walk_e2e.rs`: the generated marker under + every static analyzer (composition's exception included), `static/*` + parity with the single runs in json / yaml / bin, the `--per-file` + stderr note, the once-per-run unreadable-file warning, determinism. +- Walker unit tests in `crates/cf-commands/src/handlers.rs` (file root, + lexical nesting, `.git` / cache / symlinked directories, symlink loop, + oversized and unreadable files, the generated marker, vendored paths, + root-relative `--extra-excluded-prefixes`, `--languages` globs, the + all-files walk) and the seven-analyzer parity matrix in + `crates/cf-commands/tests/flag_parity.rs`. +- `bins/codefang/tests/config_e2e.rs` (11 scenarios: file / env / flag + precedence, discovery, errors, unsupported keys, `-v`), + `bins/codefang/tests/cli_surface_e2e.rs` (16 scenarios over every wired + flag and the removed surface), `bins/uast/tests/cli_surface_e2e.rs` + (10 scenarios), and the cf-config / cf-commands unit tests listed in + `specs/bugs/BUG-cli-inert-surface.md`. + +--- + ## [Unreleased] — `--limit` selects the newest commits; `--since` works ### Changed @@ -29,8 +697,232 @@ The format follows [Keep a Changelog](https://keepachangelog.com/). instead of the generic `command dispatch is blocked` placeholder (same exit code, still no stdout). +### Removed + +- **The Go oracle is gone.** The frozen Go reference binaries + (`build/bin/{codefang,uast}`), the differential parity harness + (`tests/compat/`, `tests/antisim/`), the Go-captured golden reports and their + `golden-harness` crate (`tests/golden/`, `tests/golden-harness/`), the + always-skipped yaml.v3 / enry differential tests, and the Go-era release and + deadcode configs (`.goreleaser.yml`, `.deadcode-whitelist`) were removed. The + Rust binaries are the product; machine-format output is pinned by the + workspace's own CLI-level end-to-end tests, and analyzer behaviour is free to + be corrected where the retired reference was wrong. + ### Fixed +- **`static/complexity` counts what the docs say** (`specs/bugs/BUG-static-complexity-metrics.md`). + Cyclomatic now sees every `&&`/`||` in JavaScript and Java and Python's + `and`/`or` (operators are read from the node's `operator` property, a bare + operator token, or the file source between the operands — the file source + is passed into the analysis instead of the function token), no longer + counts Go's three-clause `for` twice, Rust's `_ =>` arm, or Go `func(T) U` + parameter types as functions named `(T)`. Cognitive complexity follows the + SonarSource rules: braced `else { if … }` is nested, boolean sequences + count in every expression (returns, assignments, loop conditions, call + arguments), Go `if init; cond {}` no longer counts its then-block as an + `else`, `try` adds nothing and `catch` is the nesting node, a function's + own arrow/function-expression body is level 0, wrapped `else if` chains + (JS/Rust/C) are flat, Python's `else:` is counted, and labelled jumps + (`break label`, `continue label`, `goto`) add one. +- C functions were reported twice and named by their whole source text (the + declarator is a body-less `Function` child); declarations without a body + (C prototypes, Go interface methods and function types, Rust trait + signatures, Java abstract/interface methods) are never reported, named + nested functions are their own units, and anonymous literals belong to the + enclosing function. +- `--format yaml|bin` overflowed the stack on deeply nested sources (20k + nested `if`s, exit 134): one stack-guarded UAST bridge + (`cf_complexity::node::Node::from_uast`) now serves every format, and the + metric walks grow their stack on demand. +- `yaml`/`bin` `aggregate.cognitive_complexity` and `nesting_depth` were + always `0`, and the JSON/text `Cognitive Total` was a truncated per-file + average: aggregates are now the real cognitive sum and the max nesting + depth, with a function-weighted average; `Max Nesting` joins the key + metrics. +- `lines_of_code` was the raw line span; it is now the non-blank, + non-comment-only lines of the function (nested named functions excluded), + so `complexity_density` is meaningful. Nesting depth counts a `try`/`catch` + statement as one level. An oversized `--format bin` payload is an error on + stderr instead of a panic. +- Function ordering is a total order (cyclomatic, cognitive, nesting + descending; name, file, line ascending) applied with `sort_by` in every + format — the complexity-specific pdqsort port, the yaml handler's second + `computed_metrics`, the three UAST bridges, the four + `build_complexity_message` copies (two "Good" wordings) and the unused node + constants are gone; every threshold lives in `cf_complexity::thresholds`. +- `--format text` prints the ranked function rows ("Top Issues") the docs + showed; the complexity docs show the real `json` and `yaml` shapes, the + thresholds, every counting rule, and the fixture-based validation story + (the retired gocyclo/gocognit claim is gone). `--per-file` remains a + `json`-only enrichment and is documented as such. +- **`static/clones` finds near-miss clones and labels them by the taxonomy** + (`specs/bugs/BUG-static-clones-classification.md`). The LSH candidate stage + moved from 16×8 to 32×4 bands (recall at the 0.5 threshold 0.06 → 0.87) + and every candidate pair is now verified by the exact shingle Jaccard, + which is what `similarity` reports. Type-1 = identical structure and + tokens, Type-2 = identical structure with renamed identifiers/literals, + Type-3 = modified structure at Jaccard ≥ 0.5 — a renamed copy no longer + reads `Type-1`, a copy with an added statement is `Type-3` (0.64), and + comment-only differences stay Type-1. Closures (UAST `Lambda`) are + candidates (`Handle.func#2`, `func#N`), never paired with their enclosing + function. The status message is keyed off `clone_ratio` (cloned functions + / total functions) with the same bands as the score, so `0/10` no longer + sits next to `Low duplication`. Pairs are ordered by similarity then name. + `--per-file` lists each file's cloned functions and pairs instead of an + empty `files: []`. The engine is linear in candidate pairs with no per-pair + allocation: 4000 identical functions take 2.5 s / 27 MB (measured: 51.5 s + / 1.71 GB before; 2000 functions 6.6 s / 448 MB → 0.7 s / 21 MB). +- **`static/composition` counts vendored and generated files by default and + has an `other` category** (`specs/bugs/BUG-static-composition-per-file.md`). + The walk no longer applies the vendor/generated path policy to the one + analyzer whose job is to report those shares (only `--languages` narrows + it); files are classified against their root-relative path, so `docs/` + rules match; a file enry cannot recognize is `other` instead of `source`; + prose extensions (`.md`, `.rst`, `.txt`, …) are documentation anywhere. + `--per-file` entries carry the file's category and language + (`Classified as vendor`, `Vendor Files 1`, `Language Go`) instead of the + empty-report placeholder. New reference page `site/analyzers/composition.md`. +- `site/analyzers/clones.md` / `site/explanation/clones.md` now state the + real contract: the sections JSON envelope (not the YAML metrics shape), the + bounded `clone_ratio` definition, the 20-node candidate minimum, the 32×4 + LSH parameters and the exact-Jaccard `similarity`. +- `cf-clones` and `cf-composition` own all of their math again: the unused + per-file `Analyzer`s, the `_func_signatures` report round-trip, the + duplicated pair/distribution extraction and the hand-copied section + builders in `cf-commands` are gone; `cf-alg-lsh` is keyed by integer ids + with a reusable query scratch (`query_after`) and documents its S-curve + (`candidate_probability`); `cf-alg-minhash` gained `Signature::add_hash`. +- **`static/halstead` now counts Halstead's tokens** (`specs/bugs/BUG-static-halstead-operators.md`). + Operators were recorded under UAST type names (`"BinaryOp": 2`), by + substring containment on statement text (`return c * 2` → `*`), or as the + whole call text; declaration statements were operands (`"c := a + b": 1`); + token-less literals and identifiers (Go literals, Rust, Java) were dropped, + so `func same() int { return 1 }` had volume 0. The `cf-halstead` crate now + owns one documented classification table (`classify.rs`): leaf + identifiers/literals are operands by text, `props["operator"]` wins when a + mapping exposes it, calls/index/slice are `()`/`[]`/`[:]`, and every other + node contributes the symbol runs and keywords of its source *residue* (its + span minus its children), recovered from the file bytes, the node's verbatim + token, or the enclosing function's token. Signatures, parameters and + comments are skipped; closures belong to their enclosing function. + `hal.go` (`c := a + b; return c * 2`) reports n1 = 4, n2 = 4, N1 = 4, + N2 = 5, V = 27, D = 2.5, E = 67.5 in every format. +- **`static/halstead` aggregates are the union, not a per-file average.** + "Total Operators (N1)" reported the mean of per-file totals; yaml/bin + hard-coded `distinct_operators: 0 … length: 0`; `health_score` divided an + average file volume by the function count; `message` was the modal label of + 12 unrelated keys. The aggregate (and each `--per-file` entry) is now the + union of the functions' operator/operand multisets with per-function + statistics (`mean_*`/`max_*` volume, difficulty, effort; `high_effort_*`), + and the status, health score and section score follow the documented + high-effort share bands (`cf_halstead::thresholds`). +- **`static/halstead` thresholds are defined once** and agree across json, + yaml, bin, text and plot: volume bands `<100 / <1000 / <5000 / ≥5000` + (lower-inclusive), `high_effort_functions` filtered by effort ≥ 10000 + (was: volume ≥ 1000), `volume_assessment` "🟡 Medium" reachable. Function + lists use one total order (effort desc, volume desc, name, file) via + `sort_by`; the pdqsort and `math.Log2` ports and the duplicated tables and + formulas in `cf-commands` are deleted. +- **`static/halstead` on an empty folder** (or one with only unsupported + files) returns the empty report with `No functions found` and exit 0 + instead of `Error: command dispatch is blocked…` / exit 1. +- **Python docstrings document their declarations.** With the UAST now + lowering a docstring to a `Comment` carrying the `Doc` role, + `static/comments` places the first statement of a `def` / `class` body as + `docstring` (good, documents the declaration); a string literal later in a + body stays `inline` and a module docstring is `unassociated`. +- **UAST shapes the analyzers can trust** (`specs/bugs/BUG-uast-mapping-shapes.md`; + contract in `site/architecture/uast.md` → "Shape contract"): + - C/C++: one `Function` per definition, `name`/`token` = the declarator's + identifier (`Foo::bar` in C++); declarators are structural, prototypes + are not functions (ioq3 `code/qcommon` counted 2580 "functions" for + ~1300). Ternaries carry `operator: "?:"`. + - Go: `func(T) U` parameter types are `TypeAnnotation`s, a three-clause + `for` is one `Loop`, literals carry tokens, `type X struct{}` / + `interface{}` / other are `Struct` / `Interface` / `TypeDeclaration` + with `name`; `x++` / `x--` are `UnaryOp`s; `if` carries `Condition`. + - Rust: identifiers and literals carry tokens; `///` / `//!` / `/** */` + comments are single leaf `Comment`s with role `Doc` whose span is the + comment text only (they were nested three deep and spilled onto the + next line). + - Java: `case` labels are `Case`s, one `Catch` per clause (the caught + type is a `TypeAnnotation`), ternaries are `If` + `operator`, bodies are + `Block`s (one `Class` per class), literals carry tokens; the false + declarations `class_literal`, `method_reference`, + `explicit_constructor_invocation`, `super_interfaces`, `superclass`, + `scoped_type_identifier` (was `If`) are retyped. + - Python: `and` / `or` / `not` / comparisons are `BinaryOp` / `UnaryOp` + with `operator`; `elif` is an `If` child with `Else`, `else` a `Block` + with `Else, Body`; docstrings are leaf `Comment`s with role `Doc`. + - JavaScript/TypeScript/TSX: else-if chains sit directly under their `If` + (no `Synthetic` wrapper); `const f = () => {}` and class-field functions + are one named `Function` / `Method` (no nested `Lambda`); loop `kind` + props are real props (also Rust, Kotlin). + - Every language: `props.operator` on `BinaryOp` / `UnaryOp` / + `Assignment` / ternary nodes, source-text tokens on every `Identifier` / + `Literal`, leaf comments, and the `Else` role on an `If`'s alternative — + implemented once in the lowering and verified for Go, Python, + JavaScript, TypeScript, Rust, Java, C, C++, C#, Ruby, PHP, Kotlin and + Swift. Prop values are `@capture`, `descendant:` or a literal + (literals were silently dropped before); conditions accept a `parent` + path segment. + - The embedded JSON schema's `Role` / `NodeType` enums now include every + value the tables (and the lowering's `Else`) emit, so `uast validate` + accepts `uast parse` output; a test keeps them in sync. +- **`static/cohesion` measures what the docs say it measures** + (`specs/bugs/BUG-static-cohesion-variables.md`). A variable is now a lexical + identifier the function reads or writes — type names, the function's own + name, callees, receivers (`self`/`this`/Go receivers) and whole declaration + tokens no longer count, and "shared" means used by at least two functions + (a repeat inside one function is not sharing). Token-less identifiers + (Rust) resolve from their source span, so Rust files report real + variables instead of a uniform 1.0. Functions without variables and + single-function files score 1.0 as documented. Cross-file scores are + function-weighted; files without functions are "no data" and no longer + pull a tree of struct-only files to "Excellent"; an empty directory + reports "No cohesion data available" with exit 0. Every function is kept + (same-named methods were silently dropped, so the distribution did not sum + to Total Functions); methods are named `Type.method`. The issues list holds + only non-Excellent functions (worst first), metrics render with two + decimals, the `Excellent (>=0.6)` label matches the code, and + `text`/`compact` now render the distribution and issues. `line_count` is + the function's line span and `variable_count` counts distinct variables. + The crate's unused encoders, Bloom-filter "shared" sets and the duplicated + aggregation in the handler were removed. +- **`static/imports` reports import targets, not statements** + (`specs/bugs/BUG-static-imports-extraction.md`). Go alias/dot/blank specs + came back as `str "strings`, `. "math`, `_ "embed`; JavaScript specifier + lists (`React`, `{ useState, useEffect }`, `* as ns`) were counted as + modules while `require('fs')` and `import('m')` were missed; Python + `import os, sys` reported `os,` and lost `sys`, and `as` aliases counted; + Rust `use` items were reported verbatim. One language-aware extractor + (`cf_imports::extract_imports`, used by the static and history paths) + now yields `strings`, `math`, `embed`, `react`, `fs`, `os`, `sys`, + `std::collections::HashMap`, … with per-language rules for Go, Python, + JavaScript/TypeScript, Rust, Java, C/C++, C#, PHP, Ruby and Kotlin. +- **`static/imports` stdlib classification is per language.** The single + 32-name table marked Go `errors`/`log`/`strconv`/`testing` and Python + `datetime`/`subprocess` as `external`, skewing `external_ratio`. Each + import is now classified by the importing file's language (Go + first-element rule, CPython `sys.stdlib_module_names`, Node + `builtinModules` incl. `node:`, Rust toolchain crates, `java.`/`javax.`/ + `kotlin.`, C# `System`, C/C++ standard headers, Ruby default libraries); + Rust `crate::`/`super::`/`self::` and local-file includes classify as + `relative`. +- **`static/imports` on an empty directory, a directory without supported + files, or a single file** aborted with + `Error: command dispatch is blocked on cf-commands (tier 8)` and exit 1. + Empty roots now yield the empty report (`No import data available`, + Unique Imports 0, Total Files 0) with exit 0, and a file root analyzes + that one file. +- `static/imports --per-file` entries said `Total Files` `0`; each entry + stands for one file and now says `1`. Aggregate issues are ordered by + importing-file count, then name. +- `site/analyzers/imports.md` showed a `{"imports":{"files":[…]}}` JSON + shape that no format emits and a "dependency graph" the analyzer never + produced; the page now documents the real json/yaml shapes and the + per-language extraction rules. - `site/guide/data-analytics.md` told users to run `--since 6m` for "the last 6 months"; `m` is minutes, so that meant 6 minutes. Docs now state the supported units (`h`/`m`/`s`) and give `4380h`. @@ -42,14 +934,60 @@ The format follows [Keep a Changelog](https://keepachangelog.com/). are clean now. - Removed the never-read `InitState.rule_index` field in `cf-uast` (it pinned a HashMap of rule names per parser) and cleared the workspace clippy backlog. +- **`static/comments` counts what its definitions say** + (`specs/bugs/BUG-static-comments-coverage.md`): + - a file without comments keeps its functions (they were reported as + zero functions, zero issues); + - documentation is keyed by declaration identity, not name, so two + `String` methods are documented independently; names come from the + `name` property or the name identifier, never from a node's source + text (C functions were "named" by their whole body, Go structs by + `struct{}`); nameless class / struct / interface nodes are skipped; + - a comment documents a declaration only when it ends on the line + directly above it (attribute / decorator lines belong to the + declaration); a blank line in between no longer counts; + - comments inside a body are `inline` for that declaration instead of + "above" the next one; nested comment pieces (Rust `///`) count once and + document their item; + - `Doc Coverage` and `Good Ratio` are Σ/Σ over every analyzed file, never + a per-file mean diluted by READMEs and comment-free files; the status + and score follow the weighted good ratio; + - `--format yaml` / `bin` rows carry the real name, type, line, length, + placement, target and status, and `undocumented_functions` lists only + the undocumented declarations with a length-based risk level; + - `--per-file` from a relative cwd stamps `a.go`, not `../../a.go`; a + single-file root stamps the file name; + - the CFB1 length cap is an error path instead of a panic; the unused + `cf_comments::Aggregator` was replaced by the one the handler uses and + `CommentConfig::max_comment_length` was removed; + - `site/analyzers/comments.md` and `site/explanation/comments.md` + describe the implemented schema, placement categories, gap rule, + thresholds and limitations (Python docstrings are not comments). ### Added +- `crates/cf-halstead/tests/real_source.rs`: Halstead numbers asserted from + real Go, Python, JavaScript, Rust, Java and C source parsed with + `cf_uast::Parser`, against hand-computed n1/n2/N1/N2/V/D/E. +- `bins/codefang/tests/halstead_e2e.rs`: the real binary over a two-file + fixture — exact per-function operator/operand maps (`--format plot`), + union aggregate and `--per-file` sums, json/yaml/text agreement, empty + folder exit 0, determinism. - `crates/cf-commands/src/handlers.rs`: `history_window_tests` — window contract (newest-N, empty/partial/full `--since`, `--limit` inside a window, revision spec, injected clock, unresolvable spec). - `bins/codefang/tests/history_window.rs`: CLI-level e2e driving the real binary over a real git repo, including the `--head` ≡ `--limit 1` invariant. +- `crates/cf-complexity/tests/fixtures.rs` + `tests/fixtures/`: hand-computed + cyclomatic/cognitive/nesting/LOC expectations parsed from real Go, Python, + JavaScript, Rust, Java and C sources (else-if chains, nested else, boolean + sequences, if-with-init, try/catch, lambdas, switch/match arms, recursion, + labelled jumps, nested functions, declarations); the Java `switch`/ternary + expectation is recorded `#[ignore]`d pending the UAST mapping contract. +- `bins/codefang/tests/complexity_e2e.rs`: the real binary over the same + sources — exact per-function numbers in `json`/`bin`/`yaml`/`text`, + `--per-file` sums, every format on 20k nesting levels, empty directory, + tie ordering, determinism. --- diff --git a/Cargo.lock b/Cargo.lock index 22cb9162..763853a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,9 +296,8 @@ version = "0.0.0" name = "cf-analyzers-plumbing" version = "0.0.0" dependencies = [ - "git2", + "cf-identity", "tempfile", - "thiserror", ] [[package]] @@ -308,6 +307,7 @@ dependencies = [ "cf-alg-stats", "cf-gojson", "cf-gosort", + "cf-goyaml", ] [[package]] @@ -353,12 +353,12 @@ dependencies = [ name = "cf-clones" version = "0.0.0" dependencies = [ + "cf-alg-hashutil", "cf-alg-lsh", "cf-alg-minhash", - "cf-analyze", "cf-gojson", - "cf-gosort", "cf-reportutil", + "cf-uast", "cf-uast-node", ] @@ -366,8 +366,7 @@ dependencies = [ name = "cf-cohesion" version = "0.0.0" dependencies = [ - "cf-alg-bloom", - "cf-gosort", + "cf-uast", "cf-uast-node", "stacker", ] @@ -376,7 +375,6 @@ dependencies = [ name = "cf-commands" version = "0.0.0" dependencies = [ - "cf-alg-bloom", "cf-alg-levenshtein", "cf-analyze", "cf-analyzer-burndown", @@ -384,7 +382,6 @@ dependencies = [ "cf-anomaly", "cf-budget", "cf-burndown-core", - "cf-checkpoint", "cf-clones", "cf-cohesion", "cf-comments", @@ -403,8 +400,9 @@ dependencies = [ "cf-imports", "cf-langpath", "cf-mcp", - "cf-observability", + "cf-meminfo", "cf-pathpolicy", + "cf-persist", "cf-pipeline", "cf-plotpage", "cf-quality", @@ -412,18 +410,20 @@ dependencies = [ "cf-reportutil", "cf-sentiment", "cf-shotness", - "cf-storage", + "cf-streaming", "cf-terminal", "cf-textutil", "cf-typos", "cf-uast", "cf-uast-node", + "cf-units", "cf-version", "clap", "clap_complete", "git2", + "serde", + "serde_json", "signal-hook", - "stacker", "tempfile", "thiserror", ] @@ -433,6 +433,7 @@ name = "cf-comments" version = "0.0.0" dependencies = [ "cf-gojson", + "cf-uast", "cf-uast-node", ] @@ -441,7 +442,8 @@ name = "cf-complexity" version = "0.0.0" dependencies = [ "cf-gojson", - "cf-gosort", + "cf-uast", + "cf-uast-node", "stacker", ] @@ -450,10 +452,8 @@ name = "cf-composition" version = "0.0.0" dependencies = [ "cf-gojson", - "cf-goyaml", "cf-langpath", "cf-pathfilter", - "serde_json", ] [[package]] @@ -483,6 +483,7 @@ dependencies = [ "cf-alg-stats", "cf-gojson", "cf-gosort", + "cf-goyaml", "cf-identity", "cf-metrics", ] @@ -511,6 +512,7 @@ dependencies = [ "cf-safeconv", "cf-textutil", "git2", + "libgit2-sys", "thiserror", ] @@ -537,6 +539,7 @@ dependencies = [ name = "cf-halstead" version = "0.0.0" dependencies = [ + "cf-uast", "cf-uast-node", ] @@ -549,7 +552,10 @@ name = "cf-imports" version = "0.0.0" dependencies = [ "cf-gojson", - "cf-reportutil", + "cf-goyaml", + "cf-uast", + "cf-uast-node", + "serde", ] [[package]] @@ -666,8 +672,15 @@ name = "cf-quality" version = "0.0.0" dependencies = [ "cf-alg-stats", + "cf-cohesion", + "cf-comments", + "cf-complexity", "cf-gojson", + "cf-halstead", "cf-reportutil", + "cf-uast", + "cf-uast-node", + "serde", ] [[package]] @@ -708,6 +721,7 @@ name = "cf-shotness" version = "0.0.0" dependencies = [ "cf-gojson", + "serde", ] [[package]] @@ -760,6 +774,11 @@ dependencies = [ [[package]] name = "cf-typos" version = "0.0.0" +dependencies = [ + "cf-alg-levenshtein", + "cf-gojson", + "serde", +] [[package]] name = "cf-uast" @@ -791,6 +810,9 @@ version = "0.0.0" dependencies = [ "cf-uast", "cf-uast-mapping", + "cf-uast-node", + "cf-uast-spec", + "serde_json", "tree-sitter", ] @@ -878,9 +900,12 @@ name = "codefang" version = "0.0.0" dependencies = [ "cf-commands", + "cf-couples", "cf-gitlib", "clap", "git2", + "serde_json", + "tempfile", ] [[package]] @@ -957,8 +982,7 @@ dependencies = [ name = "doc-examples" version = "0.0.0" dependencies = [ - "codefang", - "uast", + "tempfile", ] [[package]] @@ -1151,16 +1175,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "golden-harness" -version = "0.0.0" -dependencies = [ - "codefang", - "serde", - "serde_json", - "uast", -] - [[package]] name = "hashbrown" version = "0.17.1" @@ -2426,6 +2440,8 @@ dependencies = [ "clap_complete", "jsonschema", "serde_json", + "tempfile", + "tree-sitter", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4fe02e3d..77edf851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ resolver = "2" members = [ "crates/*", "bins/*", - "tests/golden-harness", "tests/doc-examples", ] # crates/analyzers is a nesting directory (it holds sub-crates), not a leaf crate diff --git a/README.md b/README.md index c3506d57..490ea684 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ the [documentation site](https://sumatoshi-tech.github.io/codefang/) (built from quality, sentiment, shotness, typos, and anomaly, computed by walking Git history through libgit2. - **Stable machine output** — `--format json` (and `yaml`, `ndjson`, - `timeseries`, `bin`, `text`, `compact`, `plot`). The JSON byte stream is a - frozen contract, verified by the differential parity gate (see below). + `timeseries`, `bin`, `text`, `compact`, `plot`). The machine formats are a + stable contract, pinned by CLI-level end-to-end tests (see Testing below). ## Why @@ -112,24 +112,19 @@ Analyzer IDs are `static/` and `history/`; `-a` accepts globs --- -## Differential parity gate +## Testing -Machine-report output is a frozen contract. The original Go binaries are kept -solely as a **frozen oracle** at `build/bin/{codefang,uast}` — the Go source is -gone, only the compiled reference binaries remain. The parity harness under -`tests/compat/` diffs the Rust binary's bytes against that oracle: +Three gates, all runnable locally and all required to be green: ```bash -cd tests/compat -python3 oracle/oracle.py --n-go 3 --quiet -- \ - run --checkpoint=false --resume=false --no-cache --workers 1 \ - --analyzers history/burndown --format json --limit 50 /path/to/repo +make test # the workspace test suite (unit, integration, CLI-level e2e) +make lint # clippy across the workspace; every warning is an error +make deadcode # fails on any unreachable private item ``` -The oracle runs the Go reference N≥3 times, classifies each output field as -stable or run-to-run variant, requires the Rust output to be byte-identical on -stable fields, and rejects any attempt to blank a stable field. Exit code `0` -means parity holds. See `tests/compat/README.md` for the full system. +The analyzer crates carry unit tests for their metric math; `bins/codefang/tests` +drives the real binary over hermetic fixture repositories and source trees and +pins the machine-format bytes end to end. The CLI examples in this README and in `docs/` are themselves executed against the built Rust binary by the `doc-examples` test crate diff --git a/bins/codefang/Cargo.toml b/bins/codefang/Cargo.toml index d6f7d361..7e818462 100644 --- a/bins/codefang/Cargo.toml +++ b/bins/codefang/Cargo.toml @@ -11,17 +11,6 @@ publish.workspace = true name = "codefang" path = "src/main.rs" -[features] -default = [] -# The `mcp` command (cmd/codefang/commands/mcp.go) carries `//go:build ignore` -# in Go and is NOT wired into the root command. Mirror that: feature-gated, off -# by default. See DESIGN.md §4.1. -mcp = [] -# `--profile` pprof HTTP server (localhost:6060). The memory watchdog is always -# available; this feature is reserved for the pprof HTTP endpoints (behavioral -# parity only, never machine output bytes). -profile = [] - [dependencies] clap = { workspace = true } # KEEP libgit2 (vendored via git2). The entrypoint links a core libgit2 path so @@ -38,3 +27,10 @@ cf-commands = { path = "../../crates/cf-commands" } # Deterministic commit fixtures for the CLI-level window tests (feature-gated: # compiled for tests only, never in a shipped binary). cf-gitlib = { path = "../../crates/cf-gitlib", features = ["testutil"] } +# Comments e2e: tempdir fixtures + machine-format assertions on the real +# binary (`bins/codefang/tests/comments_e2e.rs`). +serde_json = { workspace = true } +tempfile = { workspace = true } +# Couples e2e: the documented changeset cap the oversized-commit fixture is +# sized against (`bins/codefang/tests/couples_e2e.rs`). +cf-couples = { path = "../../crates/cf-couples" } diff --git a/bins/codefang/src/main.rs b/bins/codefang/src/main.rs index 62b69a49..29d330f5 100644 --- a/bins/codefang/src/main.rs +++ b/bins/codefang/src/main.rs @@ -1,24 +1,19 @@ //! `codefang` binary — a thin shell over the `cf-commands` crate. //! -//! The entire CLI surface (the `run` / `render` / `version` command tree, every -//! literal + dynamic flag) and the analysis dispatch (the general run pipeline + -//! analyzer registry) live in `cf-commands` (DESIGN §1 tier 8). This entrypoint -//! only performs the two pre-parse bootstrap steps that must run in the process -//! before any dispatch — mirroring Go `cmd/codefang/main.go`: +//! The entire CLI surface (the `run` / `version` / `completion` command tree, +//! every literal + dynamic flag, the configuration file) and the analysis +//! dispatch live in `cf-commands`. This entrypoint only performs the two +//! steps that must run before any parsing: //! -//! 1. [`malloc::ensure_malloc_tunables`] — set glibc malloc env vars and re-exec -//! self BEFORE anything else (Go `ensureMallocTunables()`, first line of -//! `main`). -//! 2. `--profile` PersistentPreRun: pprof server + memory watchdog -//! ([`watchdog`]), behavioral parity only. +//! 1. [`malloc::ensure_malloc_tunables`] — set glibc malloc env vars and +//! re-exec self BEFORE anything else. +//! 2. `--profile` — start the memory watchdog ([`watchdog`]). //! -//! Then it hands argv to [`cf_commands::run`], which owns parsing + dispatch, and -//! exits with the returned code. The historical per-`(analyzer, format)` dispatch -//! ladder and the duplicated analyzer-orchestration modules have moved into -//! `cf-commands` (`pipeline` + `handlers`); this file no longer contains any. +//! Then it hands argv to [`cf_commands::run`], which owns parsing + dispatch, +//! and exits with the returned code. //! -//! `git2` stays a dependency so the workspace links the vendored libgit2 (DESIGN -//! §3); [`_libgit2_link_anchor`] keeps a core libgit2 symbol referenced. +//! `git2` stays a dependency so the workspace links the vendored libgit2; +//! [`_libgit2_link_anchor`] keeps a core libgit2 symbol referenced. mod malloc; mod watchdog; @@ -26,19 +21,16 @@ mod watchdog; use std::process::exit; fn main() { - // (1) glibc malloc tunables BEFORE any parsing; re-execs self on first run - // (Go `ensureMallocTunables()`, the first line of main). + // (1) glibc malloc tunables BEFORE any parsing; re-execs self on first run. malloc::ensure_malloc_tunables(); - // (2) --profile PersistentPreRun: pprof server + memory watchdog. We scan - // argv directly (the flag is global/persistent) so the watchdog starts - // before cf-commands parses and dispatches, matching cobra's PreRun order. + // (2) --profile: the memory watchdog. argv is scanned directly (the flag + // is global) so the watchdog samples the whole run, parsing included. if std::env::args().any(|a| a == "--profile") { - watchdog::start_pprof_server(); - watchdog::start_memory_watchdog(watchdog::RSS_THRESHOLD_MIB, "/tmp"); + watchdog::start_memory_watchdog(watchdog::RSS_THRESHOLD_MIB, std::env::temp_dir()); } - // (3) Parse + dispatch run / render / version through the general pipeline. + // (3) Parse + dispatch run / version / completion. exit(cf_commands::run(std::env::args_os())); } diff --git a/bins/codefang/src/watchdog.rs b/bins/codefang/src/watchdog.rs index deb95e6e..aa80a945 100644 --- a/bins/codefang/src/watchdog.rs +++ b/bins/codefang/src/watchdog.rs @@ -1,23 +1,14 @@ -//! Memory watchdog + pprof bootstrap for `--profile`. +//! The `--profile` memory watchdog. //! -//! Port of the memory-watchdog / pprof machinery in cmd/codefang/main.go -//! (`startMemoryWatchdog`, `readRSSMiB`, `readProcField`, `readSmapsRollup`, -//! `saveProcMaps`, `startPprofServer`). This is behavioral parity only: it never -//! influences machine-report bytes (DESIGN.md §4.1, §2.8). +//! Logs `MEM t= RSS= threads=` to stderr when the run starts and +//! every [`WATCHDOG_INTERVAL`] after, snapshots `/proc/self/maps` into the +//! temp directory at start (`maps_baseline.txt`) and on the first RSS spike +//! above [`RSS_THRESHOLD_MIB`] (`maps_spike_MiB.txt`), and logs the +//! `/proc/self/smaps_rollup` summary on every spike (at most +//! [`MAX_SPIKE_DUMPS`]). It never influences report bytes. //! -//! Intentional differences from Go, documented so the divergence is explicit: -//! - Go dumps gzipped pprof heap profiles on RSS spikes; Rust has no built-in -//! equivalent, so on a spike this logs the smaps rollup and snapshots -//! `/proc/self/maps` (the diagnostic value Go's runbook relies on). The -//! gzipped heap `.pb.gz` dump is tracked as a roadmap follow-up under the -//! `profile` feature and is not output-affecting. -//! - The pprof HTTP server (localhost:6060) needs a profiling backend; it is -//! gated behind the `profile` cargo feature. See [`start_pprof_server`]. -//! - Go also logs Go-runtime `MemStats` (GoHeap/GoSys/goroutines); those are -//! runtime-specific with no Rust analogue, so this logs RSS/threads only. -//! -//! All `/proc` reads are Linux-specific; elsewhere they return defaults and the -//! watchdog logs zeros, matching Go's best-effort `os.Open` fallback. +//! All `/proc` reads are Linux-specific; elsewhere they return defaults and +//! the watchdog logs zeros. use std::fs::File; use std::io::{BufRead, BufReader, Read, Write}; @@ -29,13 +20,19 @@ const WATCHDOG_INTERVAL: Duration = Duration::from_secs(2); /// 1 MiB in bytes (Go `megabyte`). const MEGABYTE: u64 = 1024 * 1024; -/// RSS threshold in MiB above which heap dumps are triggered (Go `rssThresholdMiB`). +/// RSS threshold in MiB above which a spike is logged. pub const RSS_THRESHOLD_MIB: i64 = 4096; -/// Reads current RSS in MiB from `/proc/self/statm` (Go `readRSSMiB`). +/// Spikes logged per run. +const MAX_SPIKE_DUMPS: i32 = 5; + +/// The baseline `/proc/self/maps` snapshot written at start. +const BASELINE_MAPS_FILE: &str = "maps_baseline.txt"; + +/// Reads current RSS in MiB from `/proc/self/statm`. /// -/// Returns 0 when the file cannot be read or parsed, matching Go's best-effort -/// behavior. The second field of `statm` is the resident set size in pages. +/// Returns 0 when the file cannot be read or parsed (best effort). The +/// second field of `statm` is the resident set size in pages. fn read_rss_mib() -> i64 { let mut buf = String::new(); if File::open("/proc/self/statm") @@ -53,7 +50,7 @@ fn read_rss_mib() -> i64 { rss_pages * page_size() / MEGABYTE as i64 } -/// Returns the OS page size in bytes (Go `os.Getpagesize()`). +/// Returns the OS page size in bytes. /// /// Reading `/proc/self/statm` already assumes Linux, where 4 KiB is the page /// size on the supported targets. Watchdog logging is diagnostic, not output. @@ -61,8 +58,8 @@ fn page_size() -> i64 { 4096 } -/// Reads a named field (e.g. `"Threads:"`) from `/proc/self/status` -/// (Go `readProcField`). Returns an empty string when unavailable. +/// Reads a named field (e.g. `"Threads:"`) from `/proc/self/status`. +/// Returns an empty string when unavailable. fn read_proc_field(field: &str) -> String { let f = match File::open("/proc/self/status") { Ok(f) => f, @@ -76,7 +73,7 @@ fn read_proc_field(field: &str) -> String { String::new() } -/// Summarizes memory regions from `/proc/self/smaps_rollup` (Go `readSmapsRollup`). +/// Summarizes memory regions from `/proc/self/smaps_rollup`. fn read_smaps_rollup() -> String { const PREFIXES: &[&str] = &[ "Rss:", @@ -102,9 +99,9 @@ fn read_smaps_rollup() -> String { out } -/// Copies `/proc/self/maps` to `path` for offline analysis (Go `saveProcMaps`). -/// Best-effort: silently returns on any I/O error, matching Go. -fn save_proc_maps(path: &str) { +/// Copies `/proc/self/maps` to `path` for offline analysis. Best effort: +/// silently returns on any I/O error. +fn save_proc_maps(path: &std::path::Path) { let mut src = match File::open("/proc/self/maps") { Ok(f) => f, Err(_) => return, @@ -119,29 +116,33 @@ fn save_proc_maps(path: &str) { } } -/// Logs an RSS spike and snapshots `/proc/self/maps` (Go `handleRSSSpike`). -/// -/// Returns the updated dump count. This logs the smaps rollup and the maps -/// snapshot; the gzipped pprof heap profile Go writes has no Rust equivalent -/// without an extra dependency and is a roadmap follow-up under `profile`. -fn handle_rss_spike(dump_count: i32, rss_mib: i64, dump_dir: &str) -> i32 { +/// Logs an RSS spike and, on the first one, snapshots `/proc/self/maps` +/// into `dump_dir`. Returns the updated spike count. +fn handle_rss_spike(dump_count: i32, rss_mib: i64, dump_dir: &std::path::Path) -> i32 { let dump_count = dump_count + 1; let smaps = read_smaps_rollup(); eprintln!("SPIKE #{dump_count}: RSS={rss_mib} MiB smaps: {smaps}"); if dump_count == 1 { - save_proc_maps(&format!("{dump_dir}/maps_spike_{rss_mib}MiB.txt")); + save_proc_maps(&dump_dir.join(format!("maps_spike_{rss_mib}MiB.txt"))); } dump_count } -/// Starts the background memory watchdog (Go `startMemoryWatchdog`). -/// -/// Logs RSS / threads every [`WATCHDOG_INTERVAL`] and snapshots `/proc/self/maps` -/// on threshold breach (capped at 5 dumps, matching Go). The baseline maps are -/// saved to `/tmp/maps_baseline.txt` at startup. Runs on a detached thread; the -/// process exits normally when `main` returns. -pub fn start_memory_watchdog(threshold_mib: i64, dump_dir: &'static str) { - let dump_dir_owned = dump_dir.to_string(); +/// Logs one `MEM` sample at `elapsed_secs` and returns the RSS in MiB. +fn log_sample(elapsed_secs: u64) -> i64 { + let rss_mib = read_rss_mib(); + let threads = read_proc_field("Threads:"); + eprintln!("MEM t={elapsed_secs} RSS={rss_mib} threads={threads}"); + rss_mib +} + +/// Starts the memory watchdog: one sample and the baseline maps snapshot +/// now, then a detached thread sampling every [`WATCHDOG_INTERVAL`] and +/// logging spikes above `threshold_mib` (at most [`MAX_SPIKE_DUMPS`]). The +/// thread ends with the process. +pub fn start_memory_watchdog(threshold_mib: i64, dump_dir: std::path::PathBuf) { + log_sample(0); + save_proc_maps(&dump_dir.join(BASELINE_MAPS_FILE)); std::thread::spawn(move || { let mut dump_count = 0i32; let mut tick = 0u64; @@ -149,35 +150,12 @@ pub fn start_memory_watchdog(threshold_mib: i64, dump_dir: &'static str) { loop { std::thread::sleep(WATCHDOG_INTERVAL); tick += 1; - let rss_mib = read_rss_mib(); - let threads = read_proc_field("Threads:"); - eprintln!( - "MEM t={} RSS={} threads={}", - tick * tick_seconds, - rss_mib, - threads - ); - if rss_mib > threshold_mib && dump_count < 5 { - dump_count = handle_rss_spike(dump_count, rss_mib, &dump_dir_owned); + let rss_mib = log_sample(tick * tick_seconds); + if rss_mib > threshold_mib && dump_count < MAX_SPIKE_DUMPS { + dump_count = handle_rss_spike(dump_count, rss_mib, &dump_dir); } } }); - - // Save baseline maps at startup (Go: runbook 6.2, compare t0 vs tN). - save_proc_maps("/tmp/maps_baseline.txt"); -} - -/// Starts the pprof HTTP server on localhost:6060 (Go `startPprofServer`). -/// -/// Without the `profile` cargo feature this is a single log line: the pprof -/// endpoints require a profiling backend that is not a default dependency. -/// Behavioral parity only; never affects output bytes. -#[allow(unused_variables)] -pub fn start_pprof_server() { - #[cfg(not(feature = "profile"))] - eprintln!("pprof server requested (--profile) but the `profile` feature is not enabled"); - #[cfg(feature = "profile")] - eprintln!("pprof server: localhost:6060 (feature `profile` enabled)"); } #[cfg(test)] @@ -200,15 +178,11 @@ mod tests { } #[test] - fn handle_spike_increments_count() { - let dir = std::env::temp_dir(); - let dir = dir.to_str().unwrap(); - assert_eq!(handle_rss_spike(0, 1, dir), 1); - assert_eq!(handle_rss_spike(3, 1, dir), 4); - } - - #[test] - fn threshold_constant_matches_go() { - assert_eq!(RSS_THRESHOLD_MIB, 4096); + fn handle_spike_increments_count_and_snapshots_the_first() { + let dir = tempfile::tempdir().unwrap(); + assert_eq!(handle_rss_spike(0, 1, dir.path()), 1); + assert!(dir.path().join("maps_spike_1MiB.txt").is_file()); + assert_eq!(handle_rss_spike(3, 2, dir.path()), 4); + assert!(!dir.path().join("maps_spike_2MiB.txt").exists()); } } diff --git a/bins/codefang/tests/anomaly_e2e.rs b/bins/codefang/tests/anomaly_e2e.rs new file mode 100644 index 00000000..af51e535 --- /dev/null +++ b/bins/codefang/tests/anomaly_e2e.rs @@ -0,0 +1,352 @@ +//! End-to-end coverage of `history/anomaly`, driven through the **real +//! binary** over on-disk git fixtures +//! (`specs/bugs/BUG-history-anomaly-files.md`). +//! +//! The contract (`site/analyzers/anomaly.md`): per tick, `files_changed` is +//! the number of DISTINCT files touched and `files` lists them (a delete +//! names the deleted file); the Z-score of a tick is computed over the +//! trailing window of the previous `--anomaly-window` ticks (population +//! standard deviation) and needs at least two observations before a +//! zero-variance window can flag anything; `anomalies` is always a list; +//! `--anomaly-window` is applied and validated. + +mod common; + +use cf_gitlib::testutil::{commit_files, delete_files, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T12:00:00Z. +const T0: i64 = 1_609_502_400; +/// One hour. +const HOUR: i64 = 3_600; +/// One day between quiet fixture commits. +const DAY: i64 = 24 * HOUR; +/// Number of quiet days before the busy day. +const QUIET_DAYS: i64 = 10; +/// Lines of a quiet-day file on even ticks. +const QUIET_LINES_EVEN: usize = 10; +/// Lines of a quiet-day file on odd ticks. +const QUIET_LINES_ODD: usize = 12; +/// New files added on the busy day. +const BURST_NEW_FILES: usize = 10; +/// Appends to the first burst file on the busy day. +const BURST_APPENDS: usize = 10; +/// Lines per burst write. +const BURST_LINES: usize = 10; +/// The busy day's tick index (ticks are 0-based, one per quiet day). +const BUSY_TICK: i64 = QUIET_DAYS; +/// Distinct files touched on the busy day: the new files plus the deleted +/// quiet file. +const BUSY_DISTINCT_FILES: i64 = BURST_NEW_FILES as i64 + 1; +/// Lines added on the busy day: `(new files + appends) * lines`. +const BUSY_LINES_ADDED: i64 = ((BURST_NEW_FILES + BURST_APPENDS) * BURST_LINES) as i64; +/// Lines removed on the busy day: the deleted `q1.txt`. +const BUSY_LINES_REMOVED: i64 = QUIET_LINES_EVEN as i64; +/// Mean of the ten quiet net-churn values `[10, 12, 10, 12, …]`. +const QUIET_MEAN: f64 = 11.0; +/// Population standard deviation of the quiet values. +const QUIET_STDDEV: f64 = 1.0; +/// The zero-variance sentinel magnitude. +const SENTINEL: f64 = 100.0; + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// stdout of a successful run. +fn stdout_ok(args: &[&str]) -> Vec { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// stdout of a successful run, parsed as ONE JSON document. +fn json_ok(args: &[&str]) -> serde_json::Value { + let bytes = stdout_ok(args); + serde_json::from_slice(&bytes).unwrap_or_else(|e| { + panic!( + "stdout is not one JSON document ({e}):\n{}", + String::from_utf8_lossy(&bytes) + ) + }) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// `n` numbered lines tagged with `tag`. +fn lines(tag: &str, n: usize) -> Vec { + (1..=n) + .map(|i| format!("line {i} of {tag}\n")) + .collect::() + .into_bytes() +} + +/// Ten quiet days (one commit each, alternating 10/12 added lines) and one +/// busy day: ten new files, ten appends to the first of them, and the +/// deletion of `q1.txt` — 21 commits touching 11 distinct files. +fn burst_fixture() -> TestRepo { + let test = init_repo().expect("init"); + for d in 0..QUIET_DAYS { + let n = if d % 2 == 0 { + QUIET_LINES_EVEN + } else { + QUIET_LINES_ODD + }; + let name = format!("q{}.txt", d + 1); + commit_files( + &test, + &format!("quiet {d}"), + T0 + d * DAY, + &[(name.as_str(), lines(&name, n).as_slice())], + ) + .expect("quiet"); + } + let busy = T0 + QUIET_DAYS * DAY; + let mut minute = 0; + for k in 1..=BURST_NEW_FILES { + let name = format!("burst{k}.txt"); + commit_files( + &test, + &format!("burst {k}"), + busy + minute * 60, + &[(name.as_str(), lines(&name, BURST_LINES).as_slice())], + ) + .expect("burst"); + minute += 1; + } + let mut body = lines("burst1.txt", BURST_LINES); + for k in 1..=BURST_APPENDS { + body.extend_from_slice(&lines(&format!("append{k}"), BURST_LINES)); + commit_files( + &test, + &format!("append {k}"), + busy + minute * 60, + &[("burst1.txt", body.as_slice())], + ) + .expect("append"); + minute += 1; + } + delete_files(&test, "delete q1", busy + minute * 60, &["q1.txt"]).expect("delete"); + test +} + +/// Three identical quiet commits: nothing to flag. +fn quiet_fixture() -> TestRepo { + let test = init_repo().expect("init"); + for d in 0..3_i64 { + let name = format!("q{d}.txt"); + commit_files( + &test, + &format!("quiet {d}"), + T0 + d * DAY, + &[(name.as_str(), lines(&name, QUIET_LINES_EVEN).as_slice())], + ) + .expect("quiet"); + } + test +} + +/// The busy day is the ONE flagged tick, with the hand-computed Z-scores over +/// the default window, distinct `files_changed`, and the deleted file named. +#[test] +fn the_busy_day_is_flagged_with_hand_computed_z_scores() { + let test = burst_fixture(); + let report = json_ok(&["-a", "history/anomaly", "--format", "json", path(&test)]); + + let anomalies = report["anomalies"].as_array().expect("anomalies"); + assert_eq!(anomalies.len(), 1, "{report}"); + let busy = &anomalies[0]; + assert_eq!(busy["tick"], BUSY_TICK, "{report}"); + + let churn = BUSY_LINES_ADDED - BUSY_LINES_REMOVED; + let z_churn = (churn as f64 - QUIET_MEAN) / QUIET_STDDEV; + let z_added = (BUSY_LINES_ADDED as f64 - QUIET_MEAN) / QUIET_STDDEV; + assert_eq!(busy["z_scores"]["net_churn"], z_churn, "{report}"); + assert_eq!(busy["z_scores"]["lines_added"], z_added, "{report}"); + // Every quiet tick touched exactly one file: zero variance, so the busy + // day's 11 distinct files score the sentinel. + assert_eq!(busy["z_scores"]["files_changed"], SENTINEL, "{report}"); + assert_eq!(busy["z_scores"]["lines_removed"], SENTINEL, "{report}"); + assert_eq!(busy["max_abs_z_score"], z_added.max(z_churn), "{report}"); + + assert_eq!( + busy["metrics"]["files_changed"], BUSY_DISTINCT_FILES, + "files_changed counts distinct files: {report}" + ); + assert_eq!(busy["metrics"]["lines_added"], BUSY_LINES_ADDED, "{report}"); + assert_eq!( + busy["metrics"]["lines_removed"], BUSY_LINES_REMOVED, + "{report}" + ); + assert_eq!(busy["metrics"]["net_churn"], churn, "{report}"); + + let files: Vec<&str> = busy["files"] + .as_array() + .expect("files") + .iter() + .map(|f| f.as_str().expect("file name")) + .collect(); + assert_eq!(files.len(), BUSY_DISTINCT_FILES as usize, "{report}"); + assert!( + files.contains(&"q1.txt"), + "a delete names the file: {files:?}" + ); + assert!(files.contains(&"burst1.txt"), "{files:?}"); + assert!(!files.contains(&""), "{files:?}"); + assert_eq!( + report["aggregate"]["total_ticks"], + QUIET_DAYS + 1, + "{report}" + ); + assert_eq!(report["aggregate"]["total_anomalies"], 1, "{report}"); +} + +/// The second tick is NOT flagged: a one-sample window has no variance to +/// speak of, so the sentinel needs at least two observations. +#[test] +fn a_single_observation_never_triggers_the_sentinel() { + let test = burst_fixture(); + let report = json_ok(&["-a", "history/anomaly", "--format", "json", path(&test)]); + let series = report["time_series"].as_array().expect("time_series"); + assert_eq!(series[1]["tick"], 1, "{report}"); + assert_eq!(series[1]["is_anomaly"], false, "{report}"); + assert_eq!(series[1]["churn_z_score"], 0.0, "{report}"); + // The time-series entry carries the raw metrics once, under `metrics`. + assert!(series[1].get("language_diversity").is_none(), "{report}"); + assert!(series[1].get("author_count").is_none(), "{report}"); + assert_eq!(series[1]["metrics"]["author_count"], 1, "{report}"); +} + +/// `--anomaly-window` changes the baseline the busy day is scored against and +/// is reported in `aggregate.window_size`; an invalid value is a named error. +#[test] +fn anomaly_window_is_applied_and_validated() { + let test = burst_fixture(); + let p = path(&test); + let default = json_ok(&["-a", "history/anomaly", "--format", "json", p]); + let narrow = json_ok(&[ + "-a", + "history/anomaly", + "--format", + "json", + "--anomaly-window", + "3", + p, + ]); + assert_eq!(default["aggregate"]["window_size"], 20, "{default}"); + assert_eq!(narrow["aggregate"]["window_size"], 3, "{narrow}"); + assert_eq!(narrow["anomalies"][0]["tick"], BUSY_TICK, "{narrow}"); + assert_ne!( + narrow["anomalies"][0]["z_scores"]["net_churn"], + default["anomalies"][0]["z_scores"]["net_churn"], + "a three-tick window scores against a different baseline" + ); + + let out = codefang(&[ + "-a", + "history/anomaly", + "--format", + "json", + "--anomaly-window", + "1", + p, + ]); + assert_eq!(out.status.code(), Some(1)); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains("Error: invalid --anomaly-window: must be >= 2, got 1"), + "{err}" + ); +} + +/// `--anomaly-threshold` is a registered flag: a threshold above the busy +/// day's z-score leaves nothing flagged, and a non-positive one is a named +/// error before any walk. +#[test] +fn anomaly_threshold_is_applied_and_validated() { + let test = burst_fixture(); + let p = path(&test); + let default = json_ok(&["-a", "history/anomaly", "--format", "json", p]); + assert_eq!(default["anomalies"][0]["tick"], BUSY_TICK, "{default}"); + let strict = json_ok(&[ + "-a", + "history/anomaly", + "--format", + "json", + "--anomaly-threshold", + "1000", + p, + ]); + assert_eq!(strict["anomalies"], serde_json::json!([]), "{strict}"); + + let out = codefang(&[ + "-a", + "history/anomaly", + "--format", + "json", + "--anomaly-threshold", + "0", + p, + ]); + assert_eq!(out.status.code(), Some(1)); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains("Error: invalid --anomaly-threshold: must be > 0, got 0"), + "{err}" + ); +} + +/// With nothing flagged, `anomalies` is `[]` in json and yaml alike, and the +/// bin payload is the json value. +#[test] +fn no_anomaly_is_an_empty_list_in_every_format() { + let test = quiet_fixture(); + let p = path(&test); + let report = json_ok(&["-a", "history/anomaly", "--format", "json", p]); + assert_eq!(report["anomalies"], serde_json::json!([]), "{report}"); + assert_eq!(report["time_series"].as_array().map(Vec::len), Some(3)); + + let yaml = String::from_utf8(stdout_ok(&["-a", "history/anomaly", "--format", "yaml", p])) + .expect("utf-8 yaml"); + assert!(yaml.contains("anomalies: []"), "{yaml}"); + + let json = stdout_ok(&["-a", "history/anomaly", "--format", "json", p]); + let bin = stdout_ok(&["-a", "history/anomaly", "--format", "bin", p]); + assert_eq!(&bin[8..], &json[..], "bin payload is the json value"); +} + +/// Per-commit ndjson records name the deleted file, never an empty string. +#[test] +fn ndjson_records_name_deleted_files() { + let test = burst_fixture(); + let ndjson = String::from_utf8(stdout_ok(&[ + "-a", + "history/anomaly", + "--format", + "ndjson", + path(&test), + ])) + .expect("utf-8"); + let delete = ndjson + .lines() + .last() + .expect("the delete commit is the newest record"); + assert!(delete.contains(r#""files":["q1.txt"]"#), "{delete}"); + assert!(!ndjson.contains(r#""files":[""]"#), "{ndjson}"); +} diff --git a/bins/codefang/tests/burndown_e2e.rs b/bins/codefang/tests/burndown_e2e.rs new file mode 100644 index 00000000..7b25cebf --- /dev/null +++ b/bins/codefang/tests/burndown_e2e.rs @@ -0,0 +1,698 @@ +//! End-to-end coverage of `history/burndown` through the **real binary** over +//! on-disk git fixtures (`specs/bugs/BUG-history-burndown-survival.md`). +//! +//! Every number below is computed by hand from the fixture +//! (`site/explanation/burndown.md` defines the figures): lines written in a +//! band that still exist at each later sample, survival ≤ 100 %, gross lines +//! added / removed per commit, the merge on the first-parent line, the +//! window base, the two tracking flags, the age bands, the 16383-tick +//! history that used to panic. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, CommitBuilder, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// 2021-01-01T12:00:00Z, the fixture's first commit. +const T0: i64 = 1_609_502_400; +/// One day. +const DAY: i64 = 24 * 3_600; +/// Days between the fixture's main-line commits: every commit lands in its +/// own band of the default 30-tick grid. +const SPACING_DAYS: i64 = 31; +/// Fixture developers. +const ALICE: (&str, &str) = ("Alice", "alice@example.com"); +const BOB: (&str, &str) = ("Bob", "bob@example.com"); +const CAROL: (&str, &str) = ("Carol", "carol@example.com"); +/// The tick that collided with the former merge-mark sentinel. +const FORMER_MERGE_MARK_TICK: i64 = 16_383; +/// The default sampling grid. +const DEFAULT_GRID: i64 = 30; + +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +fn json_ok(args: &[&str]) -> serde_json::Value { + let text = stdout_ok(args); + serde_json::from_str(&text) + .unwrap_or_else(|e| panic!("stdout is not one JSON document ({e}):\n{text}")) +} + +/// The burndown JSON report of `extra` flags over `repo`. +fn burndown_json(repo: &str, extra: &[&str]) -> serde_json::Value { + json_ok( + &[ + &["-a", "history/burndown", "--format", "json"], + extra, + &[repo], + ] + .concat(), + ) +} + +/// The per-commit `(tick, LinesAdded, LinesRemoved, data)` of an ndjson run. +fn burndown_ndjson(repo: &str, extra: &[&str]) -> Vec { + let text = stdout_ok( + &[ + &["-a", "history/burndown", "--format", "ndjson"], + extra, + &[repo], + ] + .concat(), + ); + text.lines() + .map(|l| serde_json::from_str(l).expect("ndjson line")) + .collect() +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +fn lines(items: &[&str]) -> Vec { + let mut s = items.join("\n"); + s.push('\n'); + s.into_bytes() +} + +/// The committer time of main-line commit `n` (day `n * SPACING_DAYS`). +fn day(n: i64) -> i64 { + T0 + n * SPACING_DAYS * DAY +} + +/// Fixture A — ten first-parent commits plus one side-line commit merged in: +/// +/// | commit | tick | author | change | +/// |---------|------|-------------|------------------------------------------| +/// | c1 | 0 | Alice | a.go 10 lines | +/// | c2 | 31 | Bob | a.go: delete l2,l5,l8; append n1..n5 | +/// | c3 | 62 | Alice | pure rename a.go → b.go | +/// | c4 | 93 | Alice | img.bin (binary), script.py 4 lines | +/// | c5 | 124 | Bob | script.py: p2→P2, append p5,p6 | +/// | c6 | 155 | Bob | script.py: append p7 | +/// | cb1 | 155 | Carol | side line off c5: feat.go 6, delete b.go | +/// | merge | 186 | Alice | merge cb1 (first-parent diff: +feat.go, −b.go) | +/// | c7 | 217 | Alice | vendor/lib/x.go (vendored, filtered out) | +/// | c8 | 248 | Alice Smith | feat.go +1 | +/// | c9 | 279 | Alice | empty commit | +/// | c10 | 310 | Alice Smith | script.py +1 | +fn fixture_a() -> TestRepo { + let test = init_repo().expect("init"); + let a10 = lines(&["l1", "l2", "l3", "l4", "l5", "l6", "l7", "l8", "l9", "l10"]); + let a12 = lines(&[ + "l1", "l3", "l4", "l6", "l7", "l9", "l10", "n1", "n2", "n3", "n4", "n5", + ]); + let commit = |msg: &str, n: i64, who: (&str, &str)| { + CommitBuilder::new(&test, msg, day(n)).author(who.0, who.1) + }; + + commit("c1", 0, ALICE) + .write("a.go", &a10) + .commit() + .expect("c1"); + commit("c2", 1, BOB) + .write("a.go", &a12) + .commit() + .expect("c2"); + commit("c3", 2, ALICE) + .rename("a.go", "b.go") + .commit() + .expect("c3"); + commit("c4", 3, ALICE) + .write("img.bin", b"BIN\x00\x01\x02DATA\n") + .write("script.py", &lines(&["p1", "p2", "p3", "p4"])) + .commit() + .expect("c4"); + let c5 = commit("c5", 4, BOB) + .write("script.py", &lines(&["p1", "P2", "p3", "p4", "p5", "p6"])) + .commit() + .expect("c5"); + commit("c6", 5, BOB) + .write( + "script.py", + &lines(&["p1", "P2", "p3", "p4", "p5", "p6", "p7"]), + ) + .commit() + .expect("c6"); + let feat6 = lines(&["f1", "f2", "f3", "f4", "f5", "f6"]); + let cb1 = CommitBuilder::new(&test, "cb1", day(5) + 6 * 3_600) + .author(CAROL.0, CAROL.1) + .parent(c5) + .detached() + .write("feat.go", &feat6) + .delete("b.go") + .commit() + .expect("cb1"); + commit("merge", 6, ALICE) + .merge_parent(cb1) + .write("feat.go", &feat6) + .delete("b.go") + .commit() + .expect("merge"); + commit("c7", 7, ALICE) + .write("vendor/lib/x.go", &lines(&["v1", "v2", "v3", "v4", "v5"])) + .commit() + .expect("c7"); + commit("c8", 8, ("Alice Smith", "alice@example.com")) + .write( + "feat.go", + &lines(&["f1", "f2", "f3", "f4", "f5", "f6", "f7"]), + ) + .commit() + .expect("c8"); + commit("c9", 9, ALICE).commit().expect("c9"); + commit("c10", 10, ("Alice Smith", "asmith@other.com")) + .write( + "script.py", + &lines(&["p1", "P2", "p3", "p4", "p5", "p6", "p7", "p8"]), + ) + .commit() + .expect("c10"); + test +} + +/// The dense global matrix of fixture A on the default grid, by hand: +/// bands are the 11 commit ticks / 30; sample `i` is the state after tick +/// `30·i + 29`. +fn fixture_a_global_survival() -> Vec<(i64, Vec)> { + vec![ + (10, vec![10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + (12, vec![7, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + (12, vec![7, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + (16, vec![7, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0]), + (18, vec![7, 5, 0, 3, 3, 0, 0, 0, 0, 0, 0]), + (19, vec![7, 5, 0, 3, 3, 1, 0, 0, 0, 0, 0]), + (13, vec![0, 0, 0, 3, 3, 1, 6, 0, 0, 0, 0]), + (13, vec![0, 0, 0, 3, 3, 1, 6, 0, 0, 0, 0]), + (14, vec![0, 0, 0, 3, 3, 1, 6, 0, 1, 0, 0]), + (14, vec![0, 0, 0, 3, 3, 1, 6, 0, 1, 0, 0]), + (15, vec![0, 0, 0, 3, 3, 1, 6, 0, 1, 0, 1]), + ] +} + +/// Lines ever written that lived to a sample: 10 + 5 + 4 + 3 + 1 + 6 + 1 + 1. +const FIXTURE_A_PEAK: i64 = 31; +/// Lines alive at the end: script.py 8 + feat.go 7. +const FIXTURE_A_CURRENT: i64 = 15; + +#[test] +fn global_survival_follows_the_hand_computed_matrix() { + let test = fixture_a(); + let report = burndown_json(path(&test), &[]); + let agg = &report["aggregate"]; + assert_eq!(agg["total_current_lines"], FIXTURE_A_CURRENT, "{report}"); + assert_eq!(agg["total_peak_lines"], FIXTURE_A_PEAK, "{report}"); + assert_eq!( + agg["analysis_period_days"], 310, + "the tick span, not the grid" + ); + assert_eq!(agg["num_samples"], 11); + assert_eq!(agg["num_bands"], 11); + assert_eq!(agg["tracked_files"], 0); + assert_eq!(agg["tracked_developers"], 0); + let rate = agg["overall_survival_rate"].as_f64().unwrap(); + assert!((rate - 15.0 / 31.0).abs() < 1e-12, "{rate}"); + + let samples = report["global_survival"].as_array().expect("samples"); + let want = fixture_a_global_survival(); + assert_eq!(samples.len(), want.len(), "{report}"); + for (i, (total, bands)) in want.iter().enumerate() { + let s = &samples[i]; + assert_eq!(s["sample_index"], i as i64); + assert_eq!(s["total_lines"], *total, "sample {i}: {s}"); + let got: Vec = s["band_breakdown"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_i64().unwrap()) + .collect(); + assert_eq!(got, *bands, "sample {i}"); + let r = s["survival_rate"].as_f64().unwrap(); + assert!(r <= 1.0 && (r - *total as f64 / FIXTURE_A_PEAK as f64).abs() < 1e-12); + } + assert_eq!(report["file_survival"], serde_json::json!([])); + assert_eq!(report["developer_survival"], serde_json::json!([])); + assert_eq!(report["interactions"], serde_json::json!([])); +} + +#[test] +fn ndjson_reports_gross_lines_per_commit_including_the_merge() { + let test = fixture_a(); + let records = burndown_ndjson(path(&test), &[]); + let got: Vec<(i64, i64, i64)> = records + .iter() + .map(|r| { + ( + r["tick"].as_i64().unwrap(), + r["data"]["LinesAdded"].as_i64().unwrap(), + r["data"]["LinesRemoved"].as_i64().unwrap(), + ) + }) + .collect(); + // (tick, added, removed) per first-parent commit; the merge deletes the + // 12 lines of b.go on the main line and adds feat.go's 6. + assert_eq!( + got, + vec![ + (0, 10, 0), + (31, 5, 3), + (62, 0, 0), + (93, 4, 0), + (124, 3, 1), + (155, 1, 0), + (186, 6, 12), + (217, 0, 0), + (248, 1, 0), + (279, 0, 0), + (310, 1, 0), + ] + ); + let merge = &records[6]["data"]; + assert_eq!( + merge["GlobalDeltas"], + serde_json::json!({"186": {"0": -7, "31": -5, "186": 6}}), + "the merge's deletions are debited at its own tick" + ); + assert_eq!(merge["FileDeltas"], serde_json::Value::Null); + assert_eq!(merge["PeopleDeltas"], serde_json::Value::Null); + assert_eq!(merge["MatrixDeltas"], serde_json::Value::Null); + assert_eq!(records[1]["author_id"], 1, "Bob"); + assert_eq!(records[1]["timestamp"], "2021-02-01T12:00:00Z"); +} + +#[test] +fn head_equals_limit_one_and_a_window_counts_only_in_window_changes() { + let test = fixture_a(); + let p = path(&test); + let head = stdout_ok(&["-a", "history/burndown", "--format", "json", "--head", p]); + let limit_one = stdout_ok(&[ + "-a", + "history/burndown", + "--format", + "json", + "--limit", + "1", + p, + ]); + assert_eq!(head, limit_one, "--head ≡ --limit 1"); + + // Window c8, c9, c10: the base tree (c7's) seeds script.py 7 + feat.go 6 + // at tick 0; c8 adds ONE line, not the whole 7-line file. + let records = burndown_ndjson(p, &["--limit", "3"]); + let got: Vec<(i64, i64, i64)> = records + .iter() + .map(|r| { + ( + r["tick"].as_i64().unwrap(), + r["data"]["LinesAdded"].as_i64().unwrap(), + r["data"]["LinesRemoved"].as_i64().unwrap(), + ) + }) + .collect(); + assert_eq!(got, vec![(0, 1, 0), (31, 0, 0), (62, 1, 0)]); + + let report = burndown_json(p, &["--limit", "3"]); + assert_eq!(report["aggregate"]["total_current_lines"], 15); + assert_eq!(report["aggregate"]["total_peak_lines"], 15); + assert_eq!(report["aggregate"]["analysis_period_days"], 62); + let last = &report["global_survival"][2]; + assert_eq!(last["band_breakdown"], serde_json::json!([14, 0, 1])); + + // devs agrees on c8: one line added. + let devs = stdout_ok(&[ + "-a", + "history/devs", + "--format", + "ndjson", + "--limit", + "3", + p, + ]); + let c8: serde_json::Value = serde_json::from_str(devs.lines().next().unwrap()).unwrap(); + assert_eq!(c8["data"]["lines_added"], 1, "{devs}"); +} + +#[test] +fn timeseries_stamps_every_commit_with_its_own_tick() { + let test = fixture_a(); + let ts = json_ok(&[ + "-a", + "history/burndown", + "--format", + "timeseries", + path(&test), + ]); + let ticks: Vec = ts["commits"] + .as_array() + .unwrap() + .iter() + .map(|c| c["tick"].as_i64().unwrap()) + .collect(); + assert_eq!( + ticks, + vec![0, 31, 62, 93, 124, 155, 186, 217, 248, 279, 310], + "{ts}" + ); + assert_eq!(ts["commits"][6]["burndown"]["lines_removed"], 12); +} + +#[test] +fn granularity_and_sampling_shape_the_grid() { + let test = fixture_a(); + let p = path(&test); + let report = burndown_json(p, &["--granularity", "100", "--sampling", "50"]); + let agg = &report["aggregate"]; + assert_eq!(agg["num_bands"], 4, "310 / 100 + 1: {agg}"); + assert_eq!(agg["num_samples"], 7, "310 / 50 + 1: {agg}"); + assert_eq!(agg["analysis_period_days"], 310); + assert_eq!(agg["total_current_lines"], FIXTURE_A_CURRENT); + // Final state by creation tick: 93 → 3 lines; 124, 155, 186 → 3 + 1 + 6; + // 248 → 1; 310 → 1, grouped into bands of 100 ticks. + let last = &report["global_survival"][6]["band_breakdown"]; + assert_eq!(last, &serde_json::json!([3, 10, 1, 1])); + + // The defaults (30 / 30) give one band and one sample per 31-day tick + // step; a non-positive value is rejected (BUG-cli-inert-surface item 4). + let dflt = burndown_json(p, &[]); + assert_eq!(dflt["aggregate"]["num_bands"], 11); + assert_eq!(dflt["aggregate"]["num_samples"], 11); + for flag in ["--granularity", "--sampling"] { + let out = codefang(&["-a", "history/burndown", flag, "0", p]); + assert_eq!(out.status.code(), Some(1)); + assert!( + String::from_utf8_lossy(&out.stderr) + .contains(&format!("Error: invalid {flag}: must be > 0, got 0")), + "{flag}" + ); + } +} + +#[test] +fn text_age_bands_read_newest_to_oldest() { + let test = fixture_a(); + let text = stdout_ok(&["-a", "history/burndown", "--format", "text", path(&test)]); + let age: Vec<&str> = text + .lines() + .filter(|l| l.contains("month")) + .map(str::trim) + .collect(); + // Last sample: band 10 (1 line, 1 month old), band 8 (1 line, 3 months), + // bands 5+6 (1 + 6 lines, 5–6 months), bands 3+4 (3 + 3 lines, 7–8 months). + assert_eq!(age.len(), 4, "{text}"); + assert!( + age[0].starts_with("< 1 month") && age[0].ends_with("(1)"), + "{text}" + ); + assert!( + age[1].starts_with("1-3 months") && age[1].ends_with("(1)"), + "{text}" + ); + assert!( + age[2].starts_with("3-6 months") && age[2].ends_with("(7)"), + "{text}" + ); + assert!( + age[3].starts_with("6-12 months") && age[3].ends_with("(6)"), + "{text}" + ); + assert!(text.contains("Burndown: "), "{text}"); + assert!(text.contains("310d"), "{text}"); + assert!( + !text.contains("Top Developers"), + "no people tracking: {text}" + ); +} + +#[test] +fn per_file_survival_follows_renames_and_binaries_are_skipped() { + let test = fixture_a(); + let report = burndown_json(path(&test), &["--burndown-files"]); + assert_eq!(report["aggregate"]["tracked_files"], 3, "{report}"); + let files: Vec<(String, i64, i64)> = report["file_survival"] + .as_array() + .unwrap() + .iter() + .map(|f| { + ( + f["path"].as_str().unwrap().to_string(), + f["current_lines"].as_i64().unwrap(), + f["peak_lines"].as_i64().unwrap(), + ) + }) + .collect(); + assert_eq!( + files, + vec![ + ("script.py".to_string(), 8, 9), + ("feat.go".to_string(), 7, 7), + ("b.go".to_string(), 0, 15), + ] + ); + assert!(report["file_survival"][0].get("ownership").is_none()); + // The global figures are unchanged by the flag. + assert_eq!( + report["aggregate"]["total_current_lines"], + FIXTURE_A_CURRENT + ); + + // Per-commit file deltas are keyed by the file's final path. + let records = burndown_ndjson(path(&test), &["--burndown-files"]); + assert_eq!( + records[1]["data"]["FileDeltas"], + serde_json::json!({"b.go": {"31": {"0": -3, "31": 5}}}) + ); +} + +#[test] +fn per_developer_survival_and_interactions_follow_the_shared_identities() { + let test = fixture_a(); + let report = burndown_json(path(&test), &["--burndown-people", "--burndown-files"]); + assert_eq!(report["aggregate"]["tracked_developers"], 2, "{report}"); + let devs: Vec<(i64, String, i64, i64)> = report["developer_survival"] + .as_array() + .unwrap() + .iter() + .map(|d| { + ( + d["id"].as_i64().unwrap(), + d["name"].as_str().unwrap().to_string(), + d["current_lines"].as_i64().unwrap(), + d["peak_lines"].as_i64().unwrap(), + ) + }) + .collect(); + // The loose identity detector lowercases names. + // alice: 10 (c1) − 3 (c2) − 7 (merge) + 4 + 6 + 1 + 1 − 1 (c5) = 11 live, 22 peak. + // bob: 5 (c2, gone at the merge) + 3 + 1 = 4 live, 9 peak. + assert_eq!( + devs, + vec![ + (0, "alice".to_string(), 11, 22), + (1, "bob".to_string(), 4, 9) + ] + ); + let inter: Vec<(String, String, i64, bool)> = report["interactions"] + .as_array() + .unwrap() + .iter() + .map(|i| { + ( + i["author_name"].as_str().unwrap().to_string(), + i["modifier_name"].as_str().unwrap().to_string(), + i["lines_modified"].as_i64().unwrap(), + i["is_self_modify"].as_bool().unwrap(), + ) + }) + .collect(); + assert_eq!( + inter, + vec![ + ("alice".to_string(), "alice".to_string(), 7, true), + ("bob".to_string(), "alice".to_string(), 5, false), + ("alice".to_string(), "bob".to_string(), 4, false), + ] + ); + // Ownership of the live files. + let script = report["file_survival"] + .as_array() + .unwrap() + .iter() + .find(|f| f["path"] == "script.py") + .expect("script.py"); + assert_eq!(script["ownership"], serde_json::json!({"0": 4, "1": 4})); + assert_eq!(script["top_owner_id"], 0, "tie → smaller id"); + assert_eq!(script["top_owner_name"], "alice"); + assert_eq!(script["top_owner_percentage"], 50.0); + let dead = report["file_survival"] + .as_array() + .unwrap() + .iter() + .find(|f| f["path"] == "b.go") + .expect("b.go"); + assert_eq!(dead["ownership"], serde_json::json!({})); + assert!(dead.get("top_owner_id").is_none()); + + // The text report lists the developers. + let text = stdout_ok(&[ + "-a", + "history/burndown", + "--format", + "text", + "--burndown-people", + path(&test), + ]); + assert!( + text.contains("Top Developers (by surviving lines)"), + "{text}" + ); + let alice = text + .lines() + .find(|l| l.trim_start().starts_with("alice")) + .unwrap(); + assert!(alice.contains("11") && alice.contains("50.0%"), "{alice}"); + + // Per-commit people and matrix deltas. + let records = burndown_ndjson(path(&test), &["--burndown-people"]); + assert_eq!( + records[1]["data"]["PeopleDeltas"], + serde_json::json!({"0": {"31": {"0": -3}}, "1": {"31": {"31": 5}}}) + ); + assert_eq!( + records[1]["data"]["MatrixDeltas"], + serde_json::json!({"0": {"1": 3}}) + ); +} + +#[test] +fn a_history_spanning_the_former_merge_mark_tick_runs() { + // 1981-01-01T12:00:00Z; c2 lands exactly 16383 days later, c3 one + // sampling period after and deletes c2's lines (so the peak sees them). + const START_1981: i64 = 347_155_200; + const C3_TICK: i64 = FORMER_MERGE_MARK_TICK + DEFAULT_GRID; + let test = init_repo().expect("init"); + commit_files(&test, "c1", START_1981, &[("f.txt", b"a\nb\n")]).expect("c1"); + commit_files( + &test, + "c2", + START_1981 + FORMER_MERGE_MARK_TICK * DAY, + &[("f.txt", b"a\nb\nc\nd\n")], + ) + .expect("c2"); + commit_files( + &test, + "c3", + START_1981 + C3_TICK * DAY, + &[("f.txt", b"a\nb\n")], + ) + .expect("c3"); + let p = path(&test); + + for extra in [&[][..], &["--burndown-people", "--burndown-files"][..]] { + let report = burndown_json(p, extra); + let agg = &report["aggregate"]; + assert_eq!(agg["total_current_lines"], 2, "{report}"); + assert_eq!(agg["total_peak_lines"], 4, "{report}"); + assert_eq!(agg["num_samples"], C3_TICK / DEFAULT_GRID + 1); + assert_eq!(agg["analysis_period_days"], C3_TICK); + } + let records = burndown_ndjson(p, &[]); + assert_eq!(records[1]["tick"], FORMER_MERGE_MARK_TICK); + assert_eq!(records[1]["data"]["LinesAdded"], 2); + assert_eq!( + records[2]["data"]["GlobalDeltas"], + serde_json::json!({"16413": {"16383": -2}}) + ); +} + +#[test] +fn oversized_commits_are_dropped_like_devs_does() { + let test = fixture_a(); + let p = path(&test); + // c4 (img.bin + script.py) and the merge (feat.go + b.go) have two raw + // changes; with the cap at 1 both are dropped: c4's 4 lines never + // appear, and c5 re-inserts script.py whole at its own tick. + let records = burndown_ndjson(p, &["--max-changes-per-commit", "1"]); + let ticks: Vec = records + .iter() + .map(|r| r["tick"].as_i64().unwrap()) + .collect(); + assert_eq!( + ticks, + vec![0, 31, 62, 124, 155, 217, 248, 279, 310], + "{records:?}" + ); + assert_eq!( + records[3]["data"]["LinesAdded"], 6, + "c5 re-inserts script.py whole" + ); + let devs = stdout_ok(&[ + "-a", + "history/devs", + "--format", + "ndjson", + "--max-changes-per-commit", + "1", + p, + ]); + assert_eq!( + devs.lines().count(), + 7, + "devs keeps the same commits minus the empty ones: {devs}" + ); +} + +#[test] +fn json_and_yaml_agree_and_runs_are_deterministic() { + let test = fixture_a(); + let p = path(&test); + let flags = ["--burndown-people", "--burndown-files"]; + let a = stdout_ok( + &[ + &["-a", "history/burndown", "--format", "json"], + &flags[..], + &[p], + ] + .concat(), + ); + let b = stdout_ok( + &[ + &["-a", "history/burndown", "--format", "json"], + &flags[..], + &[p], + ] + .concat(), + ); + assert_eq!(a, b, "two runs must be byte-identical"); + let yaml = stdout_ok( + &[ + &["-a", "history/burndown", "--format", "yaml"], + &flags[..], + &[p], + ] + .concat(), + ); + assert!(yaml.contains(" total_current_lines: 15"), "{yaml}"); + assert!(yaml.contains(" tracked_developers: 2"), "{yaml}"); + assert!(yaml.contains(" - path: script.py"), "{yaml}"); + assert!(yaml.contains(" lines_modified: 7"), "{yaml}"); +} diff --git a/bins/codefang/tests/chunked_history_e2e.rs b/bins/codefang/tests/chunked_history_e2e.rs new file mode 100644 index 00000000..6f8514a7 --- /dev/null +++ b/bins/codefang/tests/chunked_history_e2e.rs @@ -0,0 +1,829 @@ +//! End-to-end coverage of the chunked history core, driven through the +//! **real binary** over real on-disk git fixtures +//! (`specs/frds/FRD-bounded-memory-history.md`). +//! +//! Contracts pinned here: +//! +//! * the `--max-changes-per-commit` drop gate is applied by EVERY analyzer, +//! so an analyzer's report does not depend on which other analyzers are +//! co-selected, and no per-commit stream names a dropped commit; +//! * merged ndjson streams stay in window order across a dropped commit, +//! whichever leaf is selected first; +//! * `--chunk-size ` never changes a byte of any output format of the +//! analyzers whose walks fold per chunk ([`CHUNKED_IDS`]) nor of the +//! co-selected UAST analyzers' shared walk ([`UAST_CO_SELECTION`]), on a +//! fixture with merges, renames, deletes, a binary file, an aliased author, +//! an empty commit, an oversized commit at a chunk edge and a typo fix; +//! * an out-of-range `--chunk-size` is a named error before any walk. + +mod common; + +use std::path::Path; + +use cf_gitlib::testutil::{commit_files_as, init_repo, CommitBuilder, TestRepo}; +use cf_gitlib::Hash; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits (one default tick each). +const DAY: i64 = 24 * 3_600; +/// The fixture developers. +const ALICE: (&str, &str) = ("alice", "alice@example.com"); +const BOB: (&str, &str) = ("bob", "bob@example.com"); +const CAROL: (&str, &str) = ("carol", "carol@example.com"); +/// The `--max-changes-per-commit` cap the drop-gate fixture is sized +/// against: its oversized commit changes one file more than this. +const CAP: &str = "2"; +/// The analyzers whose direct walks once ignored the drop gate. +const DROP_GATE_IDS: [&str; 4] = [ + "history/typos", + "history/sentiment", + "history/anomaly", + "history/imports", +]; +/// The analyzer co-selected with each [`DROP_GATE_IDS`] entry: a UAST +/// analyzer, so the typos / sentiment / imports runs take the shared walk. +const CO_SELECTED: &str = "history/quality"; +/// Every history analyzer id. +const ALL_HISTORY_IDS: [&str; 10] = [ + "history/burndown", + "history/devs", + "history/couples", + "history/file-history", + "history/shotness", + "history/quality", + "history/sentiment", + "history/typos", + "history/anomaly", + "history/imports", +]; + +/// Go source with a typo (`recieve`), a comment and one import. +const MAIN_GO_V1: &[u8] = b"package main + +import \"fmt\" + +// great work, this is a wonderful helper +func recieve(x int) int { +\tif x > 0 { +\t\treturn x +\t} +\treturn -x +} + +func main() { fmt.Println(recieve(1)) } +"; + +/// The typo fixed on the same lines, a second import and a new comment. +const MAIN_GO_V2: &[u8] = b"package main + +import ( +\t\"fmt\" +\t\"os\" +) + +// great work, this is a wonderful helper +func receive(x int) int { +\tif x > 0 { +\t\treturn x +\t} +\treturn -x +} + +// terrible awful broken hack, I hate it +func main() { fmt.Println(receive(1)); os.Exit(0) } +"; + +/// Python helper with an import and a comment. +const UTIL_PY_V2: &[u8] = b"import os +# nice and happy code +def helper(a): + if a: + return os.getcwd() + return None +"; + +/// A third Go file of the oversized commit. +const EXTRA_GO: &[u8] = b"package main + +import \"strings\" + +// dreadful sad code nobody likes +func upper(s string) string { return strings.ToUpper(s) } +"; + +/// A Go helper with a typo (`trimm`), an import and a comment. +const HELPER_GO_V1: &[u8] = b"package main + +import \"strings\" + +// lovely tidy helper for trimming +func trimm(s string) string { +\treturn strings.TrimSpace(s) +} +"; + +/// The helper's typo fixed. +const HELPER_GO_V2: &[u8] = b"package main + +import \"strings\" + +// lovely tidy helper for trimming +func trim(s string) string { +\treturn strings.TrimSpace(s) +} +"; + +/// A four-commit history whose second commit (Bob's) changes three files, +/// one above [`CAP`]; every other commit stays within it. The oversized +/// commit carries data every analyzer reports: a typo fix, new comments, +/// new imports, line churn. +struct DropGateFixture { + test: TestRepo, + /// The window, oldest-first. + window: Vec, + /// The commit above the cap. + oversized: Hash, +} + +fn drop_gate_fixture() -> DropGateFixture { + let test = init_repo().expect("init"); + let c1 = commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c1", + T0, + &[("main.go", MAIN_GO_V1), ("util.py", b"lib\n")], + ) + .expect("c1"); + let c2 = commit_files_as( + &test, + BOB.0, + BOB.1, + "c2", + T0 + DAY, + &[ + ("main.go", MAIN_GO_V2), + ("util.py", UTIL_PY_V2), + ("extra.go", EXTRA_GO), + ], + ) + .expect("c2"); + let c3 = commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c3", + T0 + 2 * DAY, + &[("helper.go", HELPER_GO_V1)], + ) + .expect("c3"); + let c4 = commit_files_as( + &test, + CAROL.0, + CAROL.1, + "c4", + T0 + 3 * DAY, + &[("helper.go", HELPER_GO_V2)], + ) + .expect("c4"); + DropGateFixture { + test, + window: vec![c1, c2, c3, c4], + oversized: c2, + } +} + +/// The pinned run clock (`CODEFANG_NOW`): a multi-analyzer document stamps +/// `analyzed_at`, which must not differ between two runs compared byte for +/// byte. +const PINNED_NOW: &str = "2021-06-01T00:00:00Z"; + +/// Runs `codefang run ` with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("CODEFANG_NOW", PINNED_NOW) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// stdout of a successful run. +fn stdout_ok(args: &[&str]) -> Vec { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// stdout of a successful run, parsed as ONE JSON document. +fn json_ok(args: &[&str]) -> serde_json::Value { + let out = stdout_ok(args); + serde_json::from_slice(&out).unwrap_or_else(|e| { + panic!( + "stdout is not one JSON document ({e}):\n{}", + String::from_utf8_lossy(&out) + ) + }) +} + +/// The report of analyzer `id` inside a multi-analyzer JSON document +/// (`{version, metadata, analyzers: [{id, mode, report}]}`). +fn analyzer_report<'a>(doc: &'a serde_json::Value, id: &str) -> &'a serde_json::Value { + doc["analyzers"] + .as_array() + .unwrap_or_else(|| panic!("no analyzers array in {doc}")) + .iter() + .find(|a| a["id"] == id) + .map(|a| &a["report"]) + .unwrap_or_else(|| panic!("no analyzer {id} in {doc}")) +} + +/// The window positions of the hashes an ndjson stream names, line by line. +fn ndjson_positions(out: &[u8], window: &[Hash]) -> Vec { + let order: Vec = window.iter().map(ToString::to_string).collect(); + String::from_utf8_lossy(out) + .lines() + .map(|line| { + let v: serde_json::Value = serde_json::from_str(line).expect("ndjson line"); + let hash = v["hash"].as_str().expect("hash"); + order + .iter() + .position(|h| h == hash) + .unwrap_or_else(|| panic!("unknown hash {hash}")) + }) + .collect() +} + +/// An analyzer's report is the same whether it runs alone or next to a +/// co-selected UAST analyzer, when a commit exceeds the cap: both walks drop +/// the oversized commit. +#[test] +fn drop_gate_reports_do_not_depend_on_the_co_selection() { + let fx = drop_gate_fixture(); + let p = path(&fx.test); + let mut defects = Vec::new(); + for id in DROP_GATE_IDS { + let single = json_ok(&[ + "-a", + id, + "--format", + "json", + "--max-changes-per-commit", + CAP, + p, + ]); + let selection = format!("{id},{CO_SELECTED}"); + let multi = json_ok(&[ + "-a", + &selection, + "--format", + "json", + "--max-changes-per-commit", + CAP, + p, + ]); + let co_selected = analyzer_report(&multi, id); + if &single != co_selected { + defects.push(format!("{id}: alone {single}\n co-selected {co_selected}")); + } + } + assert!(defects.is_empty(), "{}", defects.join("\n")); +} + +/// No analyzer's per-commit stream names the commit the cap dropped. +#[test] +fn no_analyzer_streams_a_dropped_commit() { + let fx = drop_gate_fixture(); + let p = path(&fx.test); + let dropped = fx.oversized.to_string(); + let offenders: Vec<&str> = ALL_HISTORY_IDS + .into_iter() + .filter(|id| { + let out = stdout_ok(&[ + "-a", + id, + "--format", + "ndjson", + "--max-changes-per-commit", + CAP, + p, + ]); + String::from_utf8_lossy(&out).contains(&dropped) + }) + .collect(); + assert!( + offenders.is_empty(), + "these analyzers stream the dropped commit {dropped}: {offenders:?}" + ); +} + +/// A selection whose FIRST leaf is shotness: on a position tie the merge +/// emits the first-selected leaf, so a shotness record keyed on its +/// kept-commit index would overtake the devs record of an earlier commit. +const SHOTNESS_FIRST: &str = "history/shotness,history/devs"; + +/// A merged ndjson stream over every drop-gate analyzer keeps window order +/// across the dropped commit: each analyzer keys its records on the window +/// position, never on its kept-commit index. +#[test] +fn merged_ndjson_keeps_window_order_across_a_dropped_commit() { + let fx = drop_gate_fixture(); + let p = path(&fx.test); + let selection = std::iter::once("history/devs") + .chain(DROP_GATE_IDS) + .collect::>() + .join(","); + for selection in [ + selection.as_str(), + "history/devs,history/sentiment", + SHOTNESS_FIRST, + ] { + let out = stdout_ok(&[ + "-a", + selection, + "--format", + "ndjson", + "--max-changes-per-commit", + CAP, + p, + ]); + let positions = ndjson_positions(&out, &fx.window); + assert!( + positions.windows(2).all(|w| w[0] <= w[1]), + "{selection}: records out of window order {positions:?}:\n{}", + String::from_utf8_lossy(&out) + ); + } +} + +/// The analyzers whose walks fold per chunk: their output must not depend on +/// `--chunk-size`. +const CHUNKED_IDS: [&str; 10] = [ + "history/burndown", + "history/devs", + "history/anomaly", + "history/file-history", + "history/couples", + "history/quality", + "history/sentiment", + "history/typos", + "history/imports", + "history/shotness", +]; +/// The five UAST analyzers co-selected: ONE shared pass feeds every one of +/// them, so this selection exercises the shared walk's fold. +const UAST_CO_SELECTION: &str = + "history/quality,history/sentiment,history/typos,history/imports,history/shotness"; +/// The `--chunk-size` values every chunked analyzer is run with: one commit +/// per chunk, two, a size that puts the oversized commit last in the first +/// chunk, and a size above the window. +const CHUNK_SIZES: [&str; 4] = ["1", "2", "7", "1000"]; +/// The chunk fixture's `--max-changes-per-commit` cap: its oversized commit +/// changes one file more than this. +const CHUNK_FIXTURE_CAP: &str = "3"; +/// The `--format` values `codefang run --help` advertises. +const ADVERTISED_FORMATS: [&str; 8] = [ + "json", + "yaml", + "plot", + "bin", + "timeseries", + "ndjson", + "text", + "compact", +]; +/// One requested output: a label and the flags that select it. +const FORMATS: [(&str, &[&str]); 9] = [ + ("json", &["--format", "json"]), + ("yaml", &["--format", "yaml"]), + ("bin", &["--format", "bin"]), + ("timeseries", &["--format", "timeseries"]), + ("timeseries+ndjson", &["--format", "timeseries", "--ndjson"]), + ("ndjson", &["--format", "ndjson"]), + ("text", &["--format", "text"]), + ("compact", &["--format", "compact"]), + ("plot", &["--format", "plot"]), +]; +/// The label of the format that writes files instead of stdout. +const PLOT: &str = "plot"; +/// The name-aliased spelling of Alice (same name, another email). +const ALICE_ELSEWHERE: (&str, &str) = ("alice", "alice@elsewhere.org"); +/// A blob the binary detector rejects (NUL bytes after a PNG signature). +const PNG_V1: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x01"; +/// The binary blob, changed. +const PNG_V2: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x02"; + +/// `n` numbered lines. +fn lines(n: usize) -> Vec { + (1..=n) + .map(|i| format!("line {i}\n")) + .collect::() + .into_bytes() +} + +/// A Go source body with one import and `n` one-line functions, each under +/// its own comment (shotness entities, sentiment comments, imports). +fn go_source(n: usize) -> Vec { + let mut s = String::from("package main\n\nimport \"strings\"\n\n"); + for i in 0..n { + s.push_str(&format!( + "// helper number {i} keeps the happy path tidy\nfunc f{i}() int {{ return {i} }}\n" + )); + } + s.into_bytes() +} + +/// A twelve-commit history exercising every walk edge: +/// +/// | pos | commit | author | edit | +/// |-----|--------|--------|------| +/// | 0 | c1 | alice | `a.go`, `b.txt`, binary `logo.png` | +/// | 1 | side | bob | side line off c1: `c.txt`, `b.txt` | +/// | 2 | c3 | Alice Smith (email alias) | `a.go` | +/// | 3 | merge | alice | merges side | +/// | 4 | rename | bob | `a.go` → `src/a.go` | +/// | 5 | empty | bob | no change | +/// | 6 | oversized | bob | four new files (above the cap) | +/// | 7 | delete | alice (name alias) | deletes `b.txt` | +/// | 8 | binary | alice | `logo.png`, `src/a.go` | +/// | 9–11 | c10–c12 | alternating | small edits; `t.go` gains a typo (c10), the typo is fixed (c11) | +fn chunk_fixture() -> TestRepo { + let test = init_repo().expect("init"); + let at = |n: i64| T0 + n * DAY; + let c1 = CommitBuilder::new(&test, "c1", at(0)) + .author(ALICE.0, ALICE.1) + .write("a.go", &go_source(2)) + .write("b.txt", &lines(3)) + .write("logo.png", PNG_V1) + .commit() + .expect("c1"); + let side = CommitBuilder::new(&test, "side", at(1)) + .author(BOB.0, BOB.1) + .parent(c1) + .detached() + .write("c.txt", &lines(2)) + .write("b.txt", &lines(4)) + .commit() + .expect("side"); + CommitBuilder::new(&test, "c3", at(2)) + .author("Alice Smith", ALICE.1) + .write("a.go", &go_source(3)) + .commit() + .expect("c3"); + CommitBuilder::new(&test, "merge", at(3)) + .author(ALICE.0, ALICE.1) + .merge_parent(side) + .write("c.txt", &lines(2)) + .write("b.txt", &lines(4)) + .commit() + .expect("merge"); + CommitBuilder::new(&test, "rename", at(4)) + .author(BOB.0, BOB.1) + .rename("a.go", "src/a.go") + .commit() + .expect("rename"); + CommitBuilder::new(&test, "empty", at(5)) + .author(BOB.0, BOB.1) + .commit() + .expect("empty"); + let mut oversized = CommitBuilder::new(&test, "oversized", at(6)).author(BOB.0, BOB.1); + for i in 0..4 { + oversized = oversized.write(&format!("d{i}.txt"), &lines(i + 1)); + } + oversized.commit().expect("oversized"); + CommitBuilder::new(&test, "delete", at(7)) + .author(ALICE_ELSEWHERE.0, ALICE_ELSEWHERE.1) + .delete("b.txt") + .commit() + .expect("delete"); + CommitBuilder::new(&test, "binary", at(8)) + .author(ALICE.0, ALICE.1) + .write("logo.png", PNG_V2) + .write("src/a.go", &go_source(4)) + .commit() + .expect("binary"); + let helpers: [&[u8]; 3] = [HELPER_GO_V1, HELPER_GO_V2, HELPER_GO_V2]; + for (i, (author, helper)) in [ALICE, BOB, ALICE].into_iter().zip(helpers).enumerate() { + let n = i as i64 + 9; + CommitBuilder::new(&test, &format!("c{}", n + 1), at(n)) + .author(author.0, author.1) + .write("c.txt", &lines(i + 3)) + .write("t.go", helper) + .commit() + .expect("tail commit"); + } + test +} + +/// Every output of one run: `stdout` for the stream formats, every written +/// file (sorted by name) for `plot`. +fn rendered( + id: &str, + label: &str, + flags: &[&str], + repo: &str, + chunk: Option<&str>, +) -> Vec<(String, Vec)> { + let plot_dir = tempfile::tempdir().expect("plot dir"); + let plot_out = plot_dir.path().to_str().expect("utf-8 dir"); + let mut args = vec!["-a", id, "--max-changes-per-commit", CHUNK_FIXTURE_CAP]; + args.extend_from_slice(flags); + if let Some(size) = chunk { + args.extend_from_slice(&["--chunk-size", size]); + } + if label == PLOT { + args.extend_from_slice(&["--output", plot_out]); + } + args.push(repo); + let stdout = stdout_ok(&args); + if label == PLOT { + return dir_files(plot_dir.path()); + } + vec![("stdout".to_string(), stdout)] +} + +/// Every file under `dir`, recursively, as `(relative path, bytes)` sorted by +/// path. +fn dir_files(dir: &Path) -> Vec<(String, Vec)> { + let mut files = Vec::new(); + let mut pending = vec![dir.to_path_buf()]; + while let Some(next) = pending.pop() { + for entry in std::fs::read_dir(&next).expect("read plot dir") { + let path = entry.expect("dir entry").path(); + if path.is_dir() { + pending.push(path); + } else { + let name = path + .strip_prefix(dir) + .expect("under the plot dir") + .to_string_lossy() + .into_owned(); + files.push((name, std::fs::read(&path).expect("read plot file"))); + } + } + } + files.sort(); + files +} + +/// The `(selection, format, chunk size)` triples whose output differs from +/// the unchunked output of the same selection and format; every unchunked +/// output must be non-empty. +fn chunk_defects(selection: &str, repo: &str) -> Vec { + let mut defects = Vec::new(); + for (label, flags) in FORMATS { + let whole = rendered(selection, label, flags, repo, None); + assert!( + whole.iter().all(|(_, bytes)| !bytes.is_empty()) && !whole.is_empty(), + "{selection} {label}: empty output" + ); + for size in CHUNK_SIZES { + let chunked = rendered(selection, label, flags, repo, Some(size)); + if chunked != whole { + defects.push(format!("{selection} {label} --chunk-size {size}")); + } + } + } + defects +} + +/// For every chunked analyzer and every advertised format, the output with +/// each `--chunk-size` equals the unchunked output byte for byte. The +/// analyzers run concurrently, one thread each. +#[test] +fn chunked_output_equals_the_unchunked_output() { + let test = chunk_fixture(); + let p = path(&test); + let defects: Vec = std::thread::scope(|s| { + let runs: Vec<_> = CHUNKED_IDS + .into_iter() + .map(|id| s.spawn(move || chunk_defects(id, p))) + .collect(); + runs.into_iter() + .flat_map(|run| run.join().expect("chunk matrix thread")) + .collect() + }); + assert!( + defects.is_empty(), + "outputs changed by chunking:\n{}", + defects.join("\n") + ); +} + +/// The co-selected UAST analyzers share ONE chunked pass: every format of +/// the combined run is byte-identical for every `--chunk-size`. +#[test] +fn the_shared_uast_walk_output_does_not_depend_on_the_chunk_size() { + let test = chunk_fixture(); + let defects = chunk_defects(UAST_CO_SELECTION, path(&test)); + assert!( + defects.is_empty(), + "outputs changed by chunking:\n{}", + defects.join("\n") + ); +} + +/// The format table above covers every format `--help` advertises (plus the +/// `--ndjson` modifier), and `--help` documents `--chunk-size`. +#[test] +fn the_chunk_matrix_covers_every_advertised_format() { + let out = common::command(BIN) + .args(["run", "--help"]) + .output() + .expect("run --help"); + let help = String::from_utf8_lossy(&out.stdout); + let line = help + .lines() + .find_map(|l| l.trim().strip_prefix("Output format: ")) + .unwrap_or_else(|| panic!("no `Output format:` line in:\n{help}")); + let mut advertised: Vec<&str> = line.split(',').map(str::trim).collect(); + advertised.sort_unstable(); + let mut covered = ADVERTISED_FORMATS.to_vec(); + covered.sort_unstable(); + assert_eq!(advertised, covered, "{help}"); + for format in ADVERTISED_FORMATS { + assert!( + FORMATS.iter().any(|(label, _)| *label == format), + "{format} is not in the matrix" + ); + } + assert!(help.contains("--chunk-size"), "{help}"); +} + +/// A `--chunk-size` below 1 is a named error before any walk: exit 1, empty +/// stdout. +#[test] +fn an_out_of_range_chunk_size_is_a_named_error() { + let test = chunk_fixture(); + let p = path(&test); + for bad in ["0", "-5"] { + let flag = format!("--chunk-size={bad}"); + let out = codefang(&["-a", "history/devs", &flag, p]); + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(1), "{bad}: {stderr}"); + assert!(out.stdout.is_empty(), "{bad}: stdout must stay empty"); + assert!( + stderr.contains(&format!( + "Error: invalid --chunk-size: must be >= 1, got {bad}" + )), + "{bad}: {stderr}" + ); + } +} + +/// The smallest `--memory-budget`, which plans the smallest chunks and the +/// fewest workers. +const MIN_BUDGET: &str = "256MiB"; +/// [`MIN_BUDGET`] in MiB. +const MIN_BUDGET_MIB: u64 = 256; + +/// A `--memory-budget` that is not a size, or is below the minimum, is a +/// named error before any walk: exit 1, empty stdout. +#[test] +fn an_invalid_memory_budget_is_a_named_error() { + let test = chunk_fixture(); + let p = path(&test); + for (bad, why) in [ + ("1MB", "is below the minimum 256 MiB"), + ("banana", "is not a size"), + ("", "is not a size"), + ] { + let flag = format!("--memory-budget={bad}"); + let out = codefang(&["-a", "history/devs", &flag, p]); + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(1), "{bad}: {stderr}"); + assert!(out.stdout.is_empty(), "{bad}: stdout must stay empty"); + assert!( + stderr.contains(&format!("Error: invalid --memory-budget: {bad:?} {why}")), + "{bad}: {stderr}" + ); + } +} + +/// The budget only plans the walk: every chunked analyzer renders the same +/// bytes at the smallest budget as with the automatic one. +#[test] +fn the_memory_budget_never_changes_the_output() { + let test = chunk_fixture(); + let p = path(&test); + let mut defects = Vec::new(); + for id in CHUNKED_IDS { + for (label, flags) in FORMATS.iter().filter(|(label, _)| *label != PLOT) { + let auto = rendered(id, label, flags, p, None); + let mut budgeted = flags.to_vec(); + budgeted.extend_from_slice(&["--memory-budget", MIN_BUDGET]); + if rendered(id, label, &budgeted, p, None) != auto { + defects.push(format!("{id} {label}")); + } + } + } + assert!( + defects.is_empty(), + "outputs changed by the budget:\n{}", + defects.join("\n") + ); +} + +/// `-v` prints each walk's plan on stderr: the chunk, the chunk count, the +/// workers, and the walk's share of the budget or the `--chunk-size` it +/// came from. +#[test] +fn verbose_runs_print_each_walk_plan() { + let test = chunk_fixture(); + let p = path(&test); + for id in CHUNKED_IDS { + let budgeted = codefang(&["-v", "-a", id, "--memory-budget", MIN_BUDGET, p]); + let stderr = String::from_utf8_lossy(&budgeted.stderr); + assert!(budgeted.status.success(), "{id}: {stderr}"); + // The walk's share: the budget less libgit2's part, never more than + // the budget. + let share = stderr + .lines() + .filter(|l| l.starts_with(&format!("progress: {id} plan: "))) + .find_map(|l| { + l.strip_suffix(" MiB)")? + .rsplit_once("(budget ")? + .1 + .parse::() + .ok() + }); + assert!( + matches!(share, Some(mib) if mib > 0 && mib <= MIN_BUDGET_MIB), + "{id}: {stderr}" + ); + let sized = codefang(&["-v", "-a", id, "--chunk-size", "2", p]); + let stderr = String::from_utf8_lossy(&sized.stderr); + assert!( + stderr.lines().any(|l| l + .starts_with(&format!("progress: {id} plan: 2 commits per chunk, ")) + && l.ends_with("(--chunk-size)")), + "{id}: {stderr}" + ); + let quiet = codefang(&["-a", id, "--chunk-size", "2", p]); + assert!( + !String::from_utf8_lossy(&quiet.stderr).contains(" plan: "), + "{id}: the plan is printed only with -v" + ); + } +} + +/// The `"analyzer"` field of one ndjson record. +fn ndjson_analyzer(line: &str) -> String { + let record: serde_json::Value = serde_json::from_str(line).expect("ndjson record"); + record["analyzer"] + .as_str() + .expect("analyzer field") + .to_owned() +} + +/// With another history walk selected, the UAST pass's results wait on disk +/// (`-v` names each spill) and every UAST analyzer still streams exactly +/// the records its own run streams; a UAST-only selection keeps its results +/// in memory. +#[test] +fn spilled_uast_results_stream_the_same_records() { + let test = chunk_fixture(); + let p = path(&test); + let mixed_ids = format!("{UAST_CO_SELECTION},history/devs"); + let mixed = codefang(&["-v", "-a", &mixed_ids, "--format", "ndjson", p]); + let stderr = String::from_utf8_lossy(&mixed.stderr); + assert!(mixed.status.success(), "{stderr}"); + assert!(stderr.contains("results spilled to disk"), "{stderr}"); + let mixed_out = String::from_utf8(mixed.stdout).expect("utf-8"); + for id in UAST_CO_SELECTION.split(',') { + let alone = + String::from_utf8(stdout_ok(&["-a", id, "--format", "ndjson", p])).expect("utf-8"); + let alone: Vec<&str> = alone.lines().collect(); + let Some(first) = alone.first() else { + continue; + }; + let name = ndjson_analyzer(first); + let in_mixed: Vec<&str> = mixed_out + .lines() + .filter(|l| ndjson_analyzer(l) == name) + .collect(); + assert_eq!(in_mixed, alone, "{id}"); + } + let uast_only = codefang(&["-v", "-a", UAST_CO_SELECTION, p]); + assert!(uast_only.status.success()); + assert!( + !String::from_utf8_lossy(&uast_only.stderr).contains("spilled"), + "a UAST-only run keeps its results in memory" + ); +} diff --git a/bins/codefang/tests/cli_surface_e2e.rs b/bins/codefang/tests/cli_surface_e2e.rs new file mode 100644 index 00000000..cec0f2d3 --- /dev/null +++ b/bins/codefang/tests/cli_surface_e2e.rs @@ -0,0 +1,394 @@ +//! End-to-end coverage of the `codefang` command surface, driven through the +//! **real binary** (`specs/bugs/BUG-cli-inert-surface.md`, items 2 and 3). +//! +//! The contract (`site/guide/cli-reference.md`): every flag and subcommand +//! `--help` lists does what its help says; a flag with no implementation is +//! rejected as an unknown argument instead of being accepted and ignored. + +mod common; + +use std::path::Path; +use std::process::{Command, Output}; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture history: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits. +const DAY: i64 = 86_400; +/// A Go source file with one branching function. +const GO_SOURCE: &str = + "package main\n\nfunc f(x int) int {\n\tif x > 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n"; +/// The ANSI escape introducer. +const ANSI_ESCAPE: char = '\u{1b}'; + +/// Runs `codefang ` with an empty `HOME`, `envs`, and `TMPDIR` set to +/// `tmp` when given. +fn codefang_in(args: &[&str], envs: &[(&str, &str)]) -> Output { + let home = tempfile::tempdir().expect("home"); + let mut cmd = common::command(BIN); + cmd.args(args) + .env("HOME", home.path()) + .env("TZ", "UTC") + .env("LANG", "C") + .env("LC_ALL", "C") + .env_remove("NO_COLOR"); + for (k, v) in envs { + cmd.env(k, v); + } + cmd.output().expect("run the codefang binary") +} + +fn codefang(args: &[&str]) -> Output { + codefang_in(args, &[]) +} + +fn stdout(out: &Output) -> String { + String::from_utf8(out.stdout.clone()).expect("utf-8 stdout") +} + +fn stderr(out: &Output) -> String { + String::from_utf8_lossy(&out.stderr).into_owned() +} + +/// stdout of a successful run. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!(out.status.success(), "{args:?}: {}", stderr(&out)); + stdout(&out) +} + +/// A directory holding one Go file. +fn go_dir() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("main.go"), GO_SOURCE).expect("write go"); + dir +} + +/// Three daily commits of a Go file. +fn go_repo() -> TestRepo { + let test = init_repo().expect("init"); + for d in 0..3_i64 { + let body = format!("{GO_SOURCE}// revision {d}\n"); + commit_files(&test, "edit", T0 + d * DAY, &[("main.go", body.as_bytes())]).expect("commit"); + } + test +} + +fn utf8(path: &Path) -> &str { + path.to_str().expect("utf-8 path") +} + +/// Flags and subcommands with no implementation behind them are rejected. +#[test] +fn inert_flags_and_render_are_rejected() { + let dir = go_dir(); + let d = utf8(dir.path()); + for flag in [ + "--checkpoint=false", + "--checkpoint", + "--checkpoint-dir=x", + "--resume=false", + "--clear-checkpoint", + "--cache-dir=x", + "--no-cache", + "--debug-trace", + "--skip-blacklist", + "--blacklisted-prefixes=x", + ] { + let out = codefang(&["run", "-a", "static/complexity", flag, d]); + assert_eq!(out.status.code(), Some(1), "{flag} must be rejected"); + assert!(out.stdout.is_empty(), "{flag}"); + assert!(stderr(&out).contains("unexpected argument"), "{flag}"); + } + let out = codefang(&["render", d, "-o", d]); + assert_eq!(out.status.code(), Some(1), "render is not a subcommand"); + assert!(stderr(&out).contains("unrecognized subcommand")); +} + +#[test] +fn quiet_and_silent_suppress_the_progress_lines() { + let dir = go_dir(); + let d = utf8(dir.path()); + let loud = codefang(&["run", "-a", "static/complexity", d]); + assert!(stderr(&loud).contains("progress: run finished")); + for flag in ["-q", "--quiet", "--silent"] { + let out = codefang(&["run", flag, "-a", "static/complexity", d]); + assert!(out.status.success(), "{flag}"); + assert_eq!(stderr(&out), "", "{flag}"); + assert_eq!(stdout(&out), stdout(&loud), "{flag} keeps the report"); + } + // The global flag also works before the subcommand. + let out = codefang(&["-q", "run", "-a", "static/complexity", d]); + assert_eq!(stderr(&out), ""); +} + +#[test] +fn quiet_keeps_warnings_and_errors() { + let dir = go_dir(); + let d = utf8(dir.path()); + let out = codefang(&[ + "run", + "-q", + "-a", + "static/complexity", + "--format", + "yaml", + "-F", + d, + ]); + assert!(out.status.success()); + assert_eq!( + stderr(&out), + "--per-file applies to --format json only; ignored for yaml\n" + ); + let out = codefang(&["run", "-q", "-a", "static/complexity", "/no/such/dir"]); + assert_eq!(stderr(&out), "Error: path not found: /no/such/dir\n"); +} + +#[test] +fn verbose_names_the_resolved_analyzers() { + let test = go_repo(); + let p = utf8(test.path()); + let out = codefang(&["-v", "run", "-a", "history/devs,static/complexity", p]); + assert!(out.status.success(), "{}", stderr(&out)); + let err = stderr(&out); + assert!( + err.contains("progress: analyzers: static/complexity,history/devs"), + "{err}" + ); + assert!(err.contains("progress: config file: none"), "{err}"); + let quiet = codefang(&["run", "-a", "history/devs,static/complexity", p]); + assert!(!stderr(&quiet).contains("progress: analyzers:")); +} + +#[test] +fn verbose_and_quiet_together_is_a_usage_error() { + let dir = go_dir(); + let out = codefang(&["-v", "run", "-q", utf8(dir.path())]); + assert_eq!(out.status.code(), Some(1)); + assert!(out.stdout.is_empty()); + assert!(stderr(&out).contains("Error: --verbose and --quiet cannot be used together")); +} + +#[test] +fn output_writes_the_report_to_a_file() { + let test = go_repo(); + let p = utf8(test.path()); + let out_dir = tempfile::tempdir().expect("out"); + for args in [ + vec!["-a", "static/complexity", "--format", "json"], + vec!["-a", "history/devs", "--format", "yaml"], + vec![ + "-a", + "history/devs,history/anomaly", + "--format", + "timeseries", + ], + vec!["-a", "history/devs", "--format", "timeseries", "--ndjson"], + ] { + let file = out_dir.path().join("report.out"); + let mut with_output = vec!["run", "-q"]; + with_output.extend_from_slice(&args); + with_output.extend_from_slice(&["-o", utf8(&file), p]); + let out = codefang(&with_output); + assert!(out.status.success(), "{args:?}: {}", stderr(&out)); + assert!( + out.stdout.is_empty(), + "{args:?}: the report went to the file" + ); + + let mut plain = vec!["run", "-q"]; + plain.extend_from_slice(&args); + plain.push(p); + let written = std::fs::read_to_string(&file).expect("report file"); + assert_eq!(written, stdout_ok(&plain), "{args:?}"); + } +} + +#[test] +fn an_unwritable_output_is_an_error() { + let dir = go_dir(); + let out = codefang(&[ + "run", + "-a", + "static/complexity", + "-o", + "/no/such/dir/report.json", + utf8(dir.path()), + ]); + assert_eq!(out.status.code(), Some(1)); + assert!(out.stdout.is_empty()); + assert!( + stderr(&out).contains("Error: cannot write --output /no/such/dir/report.json: "), + "{}", + stderr(&out) + ); +} + +#[test] +fn plot_still_takes_the_output_directory() { + let test = go_repo(); + let out_dir = tempfile::tempdir().expect("out"); + let out = codefang(&[ + "run", + "-a", + "history/devs", + "--format", + "plot", + "-o", + utf8(out_dir.path()), + utf8(test.path()), + ]); + assert!(out.status.success(), "{}", stderr(&out)); + assert!(out_dir.path().join("index.html").is_file()); +} + +#[test] +fn ndjson_without_timeseries_is_an_error() { + let test = go_repo(); + let out = codefang(&["run", "-a", "history/devs", "--ndjson", utf8(test.path())]); + assert_eq!(out.status.code(), Some(1)); + assert!(out.stdout.is_empty()); + assert!( + stderr(&out) + .contains("Error: invalid --ndjson: requires --format timeseries, got --format json"), + "{}", + stderr(&out) + ); +} + +#[test] +fn typos_max_distance_must_be_positive() { + let test = go_repo(); + let out = codefang(&[ + "run", + "-a", + "history/typos", + "--typos-max-distance", + "0", + utf8(test.path()), + ]); + assert_eq!(out.status.code(), Some(1)); + assert!(stderr(&out).contains("Error: invalid --typos-max-distance: must be > 0, got 0")); +} + +#[test] +fn list_analyzers_prints_every_id_and_exits() { + let text = stdout_ok(&["run", "--list-analyzers"]); + let ids: Vec<&str> = text.lines().collect(); + assert!(ids.contains(&"static/complexity"), "{text}"); + assert!(ids.contains(&"history/devs"), "{text}"); + assert!(ids.iter().all(|id| id.contains('/')), "{text}"); + // It answers before any configuration is read. + let out = codefang(&[ + "run", + "--list-analyzers", + "--config", + "/no/such/config.yaml", + ]); + assert!(out.status.success()); +} + +#[test] +fn path_flag_and_positional_path_agree() { + let dir = go_dir(); + let d = utf8(dir.path()); + let by_flag = stdout_ok(&["run", "-q", "-a", "static/complexity", "-p", d]); + let by_positional = stdout_ok(&["run", "-q", "-a", "static/complexity", d]); + assert_eq!(by_flag, by_positional); + let empty = tempfile::tempdir().expect("empty"); + let other = stdout_ok(&[ + "run", + "-q", + "-a", + "static/complexity", + "-p", + utf8(empty.path()), + ]); + assert_ne!(by_flag, other, "-p selects the analyzed folder"); +} + +#[test] +fn no_color_strips_ansi_from_text_output() { + let dir = go_dir(); + let d = utf8(dir.path()); + let colored = stdout_ok(&[ + "run", + "-q", + "-a", + "static/complexity", + "--format", + "text", + d, + ]); + assert!(colored.contains(ANSI_ESCAPE), "the text renderer colors"); + let plain = stdout_ok(&[ + "run", + "-q", + "--no-color", + "-a", + "static/complexity", + "--format", + "text", + d, + ]); + assert!(!plain.contains(ANSI_ESCAPE), "{plain}"); +} + +#[test] +fn version_names_the_crate_version() { + let line = stdout_ok(&["version"]); + let prefix = format!("codefang {} (commit: ", env!("CARGO_PKG_VERSION")); + assert!(line.starts_with(&prefix), "{line}"); + assert!(line.ends_with(")\n"), "{line}"); + assert!(!line.contains("commit: none"), "{line}"); +} + +#[test] +fn profile_logs_memory_samples() { + let dir = go_dir(); + let tmp = tempfile::tempdir().expect("tmp"); + let out = codefang_in( + &[ + "--profile", + "run", + "-q", + "-a", + "static/complexity", + utf8(dir.path()), + ], + &[("TMPDIR", utf8(tmp.path()))], + ); + assert!(out.status.success()); + let err = stderr(&out); + assert!(err.starts_with("MEM t=0 RSS="), "{err}"); + assert!(tmp.path().join("maps_baseline.txt").is_file()); +} + +/// The generated completion scripts parse and complete the live flag set. +#[test] +fn completion_scripts_are_valid_and_current() { + for shell in ["bash", "zsh", "fish", "powershell"] { + let script = stdout_ok(&["completion", shell]); + assert!(script.contains("config"), "{shell}"); + assert!(script.contains("granularity"), "{shell}"); + assert!(!script.contains("checkpoint"), "{shell}"); + assert!(!script.contains("render"), "{shell}"); + } + let bash = stdout_ok(&["completion", "bash"]); + let check = Command::new("bash") + .args(["-n", "-c", &bash]) + .output() + .expect("bash -n"); + assert!(check.status.success(), "{}", stderr(&check)); + + // --no-descriptions drops the help texts. + let described = stdout_ok(&["completion", "zsh"]); + let bare = stdout_ok(&["completion", "zsh", "--no-descriptions"]); + assert!(described.contains("Analyzer IDs or glob patterns")); + assert!(!bare.contains("Analyzer IDs or glob patterns")); + assert!(bare.contains("--granularity")); +} diff --git a/bins/codefang/tests/clones_e2e.rs b/bins/codefang/tests/clones_e2e.rs new file mode 100644 index 00000000..be5505fd --- /dev/null +++ b/bins/codefang/tests/clones_e2e.rs @@ -0,0 +1,474 @@ +//! End-to-end coverage of `static/clones`, driven through the **real binary** +//! over hermetic on-disk fixtures. +//! +//! The contract under test (see `site/analyzers/clones.md`): +//! +//! * Type-1 = identical structure AND identical tokens; Type-2 = identical +//! structure, renamed identifiers / literals; Type-3 = modified structure +//! whose exact shingle Jaccard clears the 0.5 threshold; +//! * `similarity` is the exact shingle-set Jaccard of the pair; +//! * `clone_ratio` = functions in at least one pair / total functions, and the +//! status message is keyed off that ratio; +//! * pairs are ordered by similarity descending, then by name; +//! * closures with a large-enough body are clone candidates; +//! * `--per-file` lists each file's functions that take part in a pair. + +mod common; + +use std::path::Path; + +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +/// Section title of the clones report. +const SECTION_TITLE: &str = "CLONE DETECTION"; + +/// Exact shingle Jaccard of [`PROCESS_ITEMS`] vs [`PROCESS_ITEMS_PLUS`] +/// (one statement and one call argument added): 41 shared 5-shingles over a +/// union of 64. Pinned with a tolerance that still rejects the MinHash +/// estimate the old engine reported (it never got past the LSH bands). +const T3_EXPECTED_SIMILARITY: f64 = 41.0 / 64.0; +const SIMILARITY_TOLERANCE: f64 = 0.02; + +const PROCESS_ITEMS: &str = r#"package main + +import "fmt" + +func ProcessItems(items []int) int { + total := 0 + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total) + return total +} +"#; + +/// [`PROCESS_ITEMS`] with every identifier and literal renamed. +const SUM_VALUES: &str = r#"package main + +import "fmt" + +func SumValues(values []int) int { + acc := 0 + for idx, val := range values { + if val%2 == 0 { + acc += val * idx + } else { + acc -= val + } + } + fmt.Println("acc", acc) + return acc +} +"#; + +/// [`PROCESS_ITEMS`] with one statement and one call argument added. +const PROCESS_ITEMS_PLUS: &str = r#"package main + +import "fmt" + +func ProcessItemsPlus(items []int) int { + total := 0 + count := len(items) + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total, count) + return total +} +"#; + +/// [`PROCESS_ITEMS`] with operators and literals changed, structure intact. +const PROCESS_ITEMS_V4: &str = r#"package main + +import "fmt" + +func ProcessItemsV4(items []int) int { + total := 1 + for i, v := range items { + if v%3 != 0 { + total -= v / i + } else { + total += v + } + } + fmt.Println("sum", total) + return total +} +"#; + +/// A function with nothing in common with [`PROCESS_ITEMS`]. +const DESCRIBE: &str = r#"package main + +import "strings" + +type Server struct { + name string + tags map[string][]string +} + +func (s *Server) Describe(prefix string) string { + var b strings.Builder + b.WriteString(prefix) + b.WriteString(": ") + b.WriteString(s.name) + for k, vs := range s.tags { + b.WriteString(" ") + b.WriteString(k) + b.WriteString("=") + b.WriteString(strings.Join(vs, ",")) + } + return strings.ToUpper(b.String()) +} +"#; + +/// Below the minimum function size: counted, never paired. +const TINY_ADD: &str = "package main\n\nfunc Add(a, b int) int {\n\treturn a + b\n}\n"; + +/// Two identical package-level closures. +const CLOSURES: &str = r#"package main + +import "fmt" + +var first = func(items []int) int { + total := 0 + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total) + return total +} + +var second = func(items []int) int { + total := 0 + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total) + return total +} +"#; + +/// Writes `files` (relative path, content) into a fresh temp dir. +fn fixture(files: &[(&str, &str)]) -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + for (rel, content) in files { + let path = dir.path().join(rel); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("mkdir"); + } + std::fs::write(path, content).expect("write fixture"); + } + dir +} + +/// Runs `codefang run -a static/clones ` with a pinned +/// environment and returns the raw stdout bytes. +fn run_clones(path: &Path, extra: &[&str]) -> Vec { + let out = common::command(BIN) + .arg("run") + .args(["-a", "static/clones"]) + .args(extra) + .arg(path) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary"); + assert!( + out.status.success(), + "run failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// The single clones section of a `--format json` run. +fn clones_section(path: &Path, extra: &[&str]) -> Value { + let stdout = run_clones(path, &[&["--format", "json"], extra].concat()); + let report: Value = serde_json::from_slice(&stdout).expect("valid JSON report"); + let sections = report["sections"].as_array().expect("sections array"); + assert_eq!(sections.len(), 1, "one clones section expected"); + assert_eq!(sections[0]["title"], SECTION_TITLE); + sections[0].clone() +} + +/// Looks up a `metrics[].value` by label. +fn metric(section: &Value, label: &str) -> String { + section["metrics"] + .as_array() + .expect("metrics array") + .iter() + .find(|m| m["label"] == label) + .unwrap_or_else(|| panic!("metric {label} missing in {section}"))["value"] + .as_str() + .expect("metric value string") + .to_string() +} + +/// `(name, clone_type, similarity)` of every issue, in emitted order. +fn issues(section: &Value) -> Vec<(String, String, f64)> { + section["issues"] + .as_array() + .expect("issues array") + .iter() + .map(|i| { + ( + i["name"].as_str().expect("issue name").to_string(), + i["location"].as_str().expect("clone type").to_string(), + i["value"] + .as_str() + .expect("similarity string") + .parse::() + .expect("similarity parses"), + ) + }) + .collect() +} + +/// `count` of the distribution row labelled `label`. +fn distribution_count(section: &Value, label: &str) -> i64 { + section["distribution"] + .as_array() + .expect("distribution array") + .iter() + .find(|d| d["label"] == label) + .unwrap_or_else(|| panic!("distribution row {label} missing"))["count"] + .as_i64() + .expect("count") +} + +fn assert_close(got: f64, want: f64, what: &str) { + assert!( + (got - want).abs() <= SIMILARITY_TOLERANCE, + "{what}: got {got}, want {want} ± {SIMILARITY_TOLERANCE}" + ); +} + +/// Byte-identical copies across files are one Type-1 pair with similarity +/// 1.0; every function is cloned so the ratio is 1.0 and the status says so. +#[test] +fn identical_pair_is_type1_with_similarity_one() { + let dir = fixture(&[("a.go", PROCESS_ITEMS), ("b.go", PROCESS_ITEMS)]); + let section = clones_section(dir.path(), &[]); + + assert_eq!(metric(§ion, "Total Functions"), "2"); + assert_eq!(metric(§ion, "Clone Pairs"), "1"); + assert_eq!(metric(§ion, "Clone Ratio"), "1.0"); + assert_eq!( + section["status"], + "High duplication - significant refactoring recommended" + ); + assert_eq!(section["score_label"], "0/10"); + + let got = issues(§ion); + assert_eq!(got.len(), 1); + assert_eq!(got[0].0, "a.go::ProcessItems <-> b.go::ProcessItems"); + assert_eq!(got[0].1, "Type-1"); + assert_close(got[0].2, 1.0, "Type-1 similarity"); + assert_eq!(distribution_count(§ion, "Type-1 (Exact)"), 1); +} + +/// Renaming every identifier and literal keeps the structure: Type-2, 1.0. +#[test] +fn renamed_pair_is_type2() { + let dir = fixture(&[("a.go", PROCESS_ITEMS), ("c.go", SUM_VALUES)]); + let section = clones_section(dir.path(), &[]); + + let got = issues(§ion); + assert_eq!(got.len(), 1, "one pair expected: {got:?}"); + assert_eq!(got[0].0, "a.go::ProcessItems <-> c.go::SumValues"); + assert_eq!(got[0].1, "Type-2"); + assert_close(got[0].2, 1.0, "Type-2 similarity"); + assert_eq!(distribution_count(§ion, "Type-2 (Renamed)"), 1); + assert_eq!(distribution_count(§ion, "Type-1 (Exact)"), 0); +} + +/// Changing operators and literals only is still identical structure: Type-2. +#[test] +fn operator_and_literal_changes_are_type2() { + let dir = fixture(&[("a.go", PROCESS_ITEMS), ("b.go", PROCESS_ITEMS_V4)]); + let section = clones_section(dir.path(), &[]); + + let got = issues(§ion); + assert_eq!(got.len(), 1, "one pair expected: {got:?}"); + assert_eq!(got[0].1, "Type-2"); + assert_close(got[0].2, 1.0, "Type-2 similarity"); +} + +/// One added statement + one added call argument modifies the structure: the +/// pair is a Type-3 clone found with its exact Jaccard (≈0.63), which the old +/// 16×8 LSH bands missed entirely. +#[test] +fn added_statement_pair_is_type3_above_threshold() { + let dir = fixture(&[("a.go", PROCESS_ITEMS), ("d.go", PROCESS_ITEMS_PLUS)]); + let section = clones_section(dir.path(), &[]); + + let got = issues(§ion); + assert_eq!(got.len(), 1, "Type-3 pair must be found: {got:?}"); + assert_eq!(got[0].0, "a.go::ProcessItems <-> d.go::ProcessItemsPlus"); + assert_eq!(got[0].1, "Type-3"); + assert_close(got[0].2, T3_EXPECTED_SIMILARITY, "Type-3 exact Jaccard"); + assert_eq!(distribution_count(§ion, "Type-3 (Near-miss)"), 1); +} + +/// Unrelated functions never pair; the status and score reflect zero clones. +#[test] +fn unrelated_functions_are_not_paired() { + let dir = fixture(&[("a.go", PROCESS_ITEMS), ("e.go", DESCRIBE)]); + let section = clones_section(dir.path(), &[]); + + assert_eq!(metric(§ion, "Total Functions"), "2"); + assert_eq!(metric(§ion, "Clone Pairs"), "0"); + assert_eq!(metric(§ion, "Clone Ratio"), "0.0"); + assert_eq!(section["status"], "No code clones detected"); + assert_eq!(section["score_label"], "10/10"); + assert!(issues(§ion).is_empty()); +} + +/// A lone function is never paired with itself. +#[test] +fn single_function_has_no_self_pair() { + let dir = fixture(&[("a.go", PROCESS_ITEMS)]); + let section = clones_section(dir.path(), &[]); + assert_eq!(metric(§ion, "Total Functions"), "1"); + assert_eq!(metric(§ion, "Clone Pairs"), "0"); + assert!(issues(§ion).is_empty()); +} + +/// Three identical copies form three pairs, listed in name order among equal +/// similarities (deterministic tie order). +#[test] +fn three_copies_yield_three_pairs_in_name_order() { + let dir = fixture(&[ + ("a.go", PROCESS_ITEMS), + ("b.go", PROCESS_ITEMS), + ("g.go", PROCESS_ITEMS), + ]); + let section = clones_section(dir.path(), &[]); + + assert_eq!(metric(§ion, "Clone Pairs"), "3"); + let names: Vec = issues(§ion).into_iter().map(|i| i.0).collect(); + assert_eq!( + names, + vec![ + "a.go::ProcessItems <-> b.go::ProcessItems", + "a.go::ProcessItems <-> g.go::ProcessItems", + "b.go::ProcessItems <-> g.go::ProcessItems", + ] + ); +} + +/// Functions below the minimum node count are counted but never paired. +#[test] +fn small_functions_are_counted_but_never_paired() { + let dir = fixture(&[("a.go", TINY_ADD), ("b.go", TINY_ADD)]); + let section = clones_section(dir.path(), &[]); + assert_eq!(metric(§ion, "Total Functions"), "2"); + assert_eq!(metric(§ion, "Clone Pairs"), "0"); + assert_eq!(section["status"], "No code clones detected"); +} + +/// Closures (UAST `Lambda`) with a large-enough body are clone candidates, +/// named by their enclosing scope and ordinal. +#[test] +fn closures_are_clone_candidates() { + let dir = fixture(&[("a.go", CLOSURES)]); + let section = clones_section(dir.path(), &[]); + + assert_eq!(metric(§ion, "Total Functions"), "2"); + let got = issues(§ion); + assert_eq!(got.len(), 1, "the two closures form one pair: {got:?}"); + assert_eq!(got[0].0, "a.go::func#1 <-> a.go::func#2"); + assert_eq!(got[0].1, "Type-1"); +} + +/// `--per-file` lists, per file, the functions that take part in a clone pair. +#[test] +fn per_file_lists_each_files_cloned_functions() { + let dir = fixture(&[ + ("a.go", PROCESS_ITEMS), + ("b.go", PROCESS_ITEMS), + ("e.go", DESCRIBE), + ]); + let section = clones_section(dir.path(), &["--per-file"]); + + let files = section["files"].as_array().expect("files array"); + let paths: Vec<&str> = files + .iter() + .map(|f| f["file_path"].as_str().expect("file_path")) + .collect(); + assert_eq!(paths, vec!["a.go", "b.go", "e.go"]); + + let a = &files[0]; + assert_eq!(metric(a, "Total Functions"), "1"); + assert_eq!(metric(a, "Cloned Functions"), "1"); + assert_eq!(a["status"], "1 of 1 functions in clone pairs"); + let a_issues = issues(a); + assert_eq!(a_issues.len(), 1); + assert_eq!(a_issues[0].0, "a.go::ProcessItems <-> b.go::ProcessItems"); + + let e = &files[2]; + assert_eq!(metric(e, "Cloned Functions"), "0"); + assert_eq!(e["status"], "No functions in clone pairs"); + assert!(issues(e).is_empty()); +} + +/// Without `--per-file` the section carries no `files` key. +#[test] +fn no_per_file_flag_omits_files() { + let dir = fixture(&[("a.go", PROCESS_ITEMS), ("b.go", PROCESS_ITEMS)]); + let section = clones_section(dir.path(), &[]); + assert!(section.get("files").is_none(), "files must be omitted"); +} + +/// Two runs over the same tree produce identical bytes in every format. +#[test] +fn two_runs_are_byte_identical() { + let dir = fixture(&[ + ("a.go", PROCESS_ITEMS), + ("b.go", PROCESS_ITEMS), + ("c.go", SUM_VALUES), + ("d.go", PROCESS_ITEMS_PLUS), + ]); + for format in ["json", "yaml", "text", "compact"] { + let first = run_clones(dir.path(), &["--format", format]); + let second = run_clones(dir.path(), &["--format", format]); + assert_eq!(first, second, "--format {format} must be deterministic"); + } +} + +/// The YAML metrics carry the exact pair list and the ratio-keyed message. +#[test] +fn yaml_carries_computed_metrics() { + let dir = fixture(&[("a.go", PROCESS_ITEMS), ("d.go", PROCESS_ITEMS_PLUS)]); + let yaml = String::from_utf8(run_clones(dir.path(), &["--format", "yaml"])).unwrap(); + assert!(yaml.contains("total_functions: 2\n"), "{yaml}"); + assert!(yaml.contains("total_clone_pairs: 1\n"), "{yaml}"); + assert!(yaml.contains("clone_ratio: 1\n"), "{yaml}"); + assert!(yaml.contains("clone_type: Type-3\n"), "{yaml}"); + assert!( + yaml.contains("message: High duplication - significant refactoring recommended\n"), + "{yaml}" + ); +} diff --git a/bins/codefang/tests/cohesion_e2e.rs b/bins/codefang/tests/cohesion_e2e.rs new file mode 100644 index 00000000..ae1008ed --- /dev/null +++ b/bins/codefang/tests/cohesion_e2e.rs @@ -0,0 +1,365 @@ +//! End-to-end coverage of `static/cohesion`, driven through the **real binary** +//! over an on-disk fixture tree. +//! +//! The contract under test (`site/analyzers/cohesion.md`, +//! `site/explanation/cohesion.md`, `specs/bugs/BUG-static-cohesion-variables.md`): +//! +//! * a variable is a lexical identifier reference, never a type name, the +//! function's own name, a callee or a whole declaration token; +//! * LCOM-HS per file is `1 - sum(mA) / (m * a)` over the functions that use +//! at least one variable; `cohesion_score = 1 - LCOM`; +//! * per-function cohesion is `shared / unique` where "shared" means used by +//! at least one OTHER function of the same file; +//! * aggregation is function-weighted and function-free files are "no data"; +//! * every function is kept (no `(file, name)` dedup), so the distribution +//! always sums to `Total Functions`; +//! * the issues list holds only functions below the Excellent threshold. + +mod common; + +use std::path::Path; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +/// `f` uses {x, y}, `g` uses {x}, `h` uses {x, y, z}: +/// m = 3, a = 3, sum(mA) = 3 + 2 + 1 = 6, LCOM = 1 - 6/9 = 1/3. +/// f: x, y shared -> 1.0; g: x shared -> 1.0; h: x, y shared, z not -> 2/3. +const SHARE_GO: &str = "package a + +func f() int { + x := 1 + y := 2 + return x + y +} + +func g() int { + x := 3 + return x +} + +func h() int { + x := 1 + y := 2 + z := x + y + return z +} +"; + +/// `f2` uses only {p} (twice), `g2` uses only {q}: +/// m = 2, a = 2, sum(mA) = 2, LCOM = 0.5; both functions share nothing -> 0.0. +const DISJOINT_GO: &str = "package a + +func f2() int { + p := 1 + p = p + 1 + return p +} + +func g2() int { + q := 2 + return q +} +"; + +/// A struct-only file: no functions, so no cohesion data. +const TYPES_GO: &str = "package a + +type Config struct { + Name string + Port int +} +"; + +/// Expected per-fixture numbers (see the fixture docs above). +const SHARE_LCOM: f64 = 1.0 / 3.0; +const DISJOINT_LCOM: f64 = 0.5; +const SHARE_FUNCTIONS: f64 = 3.0; +const DISJOINT_FUNCTIONS: f64 = 2.0; +const TOTAL_FUNCTIONS: i64 = 5; +/// Function-weighted LCOM: (1/3 * 3 + 0.5 * 2) / 5 = 0.4. +const EXPECTED_LCOM: f64 = + (SHARE_LCOM * SHARE_FUNCTIONS + DISJOINT_LCOM * DISJOINT_FUNCTIONS) / 5.0; +/// Mean per-function cohesion: (1 + 1 + 2/3 + 0 + 0) / 5. +const EXPECTED_AVG_COHESION: f64 = (1.0 + 1.0 + 2.0 / 3.0) / 5.0; +const EPS: f64 = 1e-9; + +/// Runs `codefang run -a static/cohesion` with a deterministic environment. +fn cohesion(format: &str, extra: &[&str], path: &Path) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(["-a", "static/cohesion", "--format", format]) + .args(extra) + .arg(path) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// The multi-file Go fixture plus two function-free files (an empty source +/// file and a Markdown file) that must not influence any number. +fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("share.go"), SHARE_GO).unwrap(); + std::fs::write(dir.path().join("disjoint.go"), DISJOINT_GO).unwrap(); + std::fs::write(dir.path().join("types.go"), TYPES_GO).unwrap(); + std::fs::write(dir.path().join("empty.go"), "").unwrap(); + std::fs::write(dir.path().join("README.md"), "# fixture\n\nno code here\n").unwrap(); + dir +} + +fn json_section(path: &Path, extra: &[&str]) -> serde_json::Value { + let out = cohesion("json", extra, path); + assert!( + out.status.success(), + "json run failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + let report: serde_json::Value = + serde_json::from_slice(&out.stdout).expect("stdout is a JSON report"); + report["sections"][0].clone() +} + +fn metric<'a>(section: &'a serde_json::Value, label: &str) -> &'a str { + section["metrics"] + .as_array() + .expect("metrics array") + .iter() + .find(|m| m["label"] == label) + .unwrap_or_else(|| panic!("metric {label} missing in {section}"))["value"] + .as_str() + .expect("metric value is a string") +} + +fn count_of(section: &serde_json::Value, label_prefix: &str) -> i64 { + section["distribution"] + .as_array() + .expect("distribution array") + .iter() + .find(|d| d["label"].as_str().unwrap_or("").starts_with(label_prefix)) + .unwrap_or_else(|| panic!("bucket {label_prefix} missing in {section}"))["count"] + .as_i64() + .expect("count is an integer") +} + +/// The headline numbers of the multi-file fixture are exactly the hand-computed +/// LCOM-HS values, aggregated function-weighted over the two files that have +/// functions. +#[test] +fn multi_file_go_fixture_reports_exact_numbers() { + let dir = fixture(); + let section = json_section(dir.path(), &[]); + + assert_eq!( + metric(§ion, "Total Functions"), + TOTAL_FUNCTIONS.to_string() + ); + assert_eq!( + metric(§ion, "LCOM Score"), + format!("{EXPECTED_LCOM:.2}") + ); + assert_eq!( + metric(§ion, "Cohesion Score"), + format!("{:.2}", 1.0 - EXPECTED_LCOM) + ); + assert_eq!( + metric(§ion, "Avg Cohesion"), + format!("{EXPECTED_AVG_COHESION:.2}") + ); + + let score = section["score"].as_f64().expect("score is a float"); + assert!( + (score - (1.0 - EXPECTED_LCOM)).abs() < EPS, + "score {score} != {}", + 1.0 - EXPECTED_LCOM + ); + assert_eq!(section["score_label"], "6/10"); + assert_eq!( + section["status"], + "Good overall cohesion with room for improvement" + ); +} + +/// Every function is kept: the distribution buckets sum to Total Functions +/// and land in the documented buckets (f, g, h excellent; f2, g2 poor). +#[test] +fn distribution_sums_to_total_functions() { + let dir = fixture(); + let section = json_section(dir.path(), &[]); + + let excellent = count_of(§ion, "Excellent"); + let good = count_of(§ion, "Good"); + let fair = count_of(§ion, "Fair"); + let poor = count_of(§ion, "Poor"); + assert_eq!(excellent + good + fair + poor, TOTAL_FUNCTIONS); + assert_eq!((excellent, good, fair, poor), (3, 0, 0, 2)); +} + +/// Only functions below the Excellent threshold are issues, ordered by +/// cohesion ascending then name, with two-decimal values. +#[test] +fn issues_list_only_the_non_excellent_functions() { + let dir = fixture(); + let section = json_section(dir.path(), &[]); + + let issues: Vec<(String, String, String, String)> = section["issues"] + .as_array() + .expect("issues array") + .iter() + .map(|i| { + ( + i["name"].as_str().unwrap().to_string(), + i["location"].as_str().unwrap().to_string(), + i["value"].as_str().unwrap().to_string(), + i["severity"].as_str().unwrap().to_string(), + ) + }) + .collect(); + assert_eq!( + issues, + vec![ + ( + "f2".into(), + "disjoint.go".into(), + "0.00".into(), + "poor".into() + ), + ( + "g2".into(), + "disjoint.go".into(), + "0.00".into(), + "poor".into() + ), + ] + ); +} + +/// An existing but empty directory is "no data": the empty report, exit 0. +#[test] +fn empty_directory_yields_the_empty_report_with_exit_zero() { + let dir = tempfile::tempdir().expect("tempdir"); + let out = cohesion("json", &[], dir.path()); + assert_eq!(out.status.code(), Some(0)); + let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + let section = &report["sections"][0]; + assert_eq!(section["status"], "No cohesion data available"); + assert_eq!(metric(section, "Total Functions"), "0"); + assert_eq!(section["score"], 0.0); + assert_eq!(section["score_label"], "0/10"); + assert_eq!(report["overall_score_label"], "0/10"); + assert!(section.get("distribution").is_none()); + assert_eq!(section["issues"].as_array().map(Vec::len), Some(0)); +} + +/// Function-free files (struct-only, zero-byte, Markdown) are "no data": a tree +/// holding only those yields the empty report, not a perfect score. +#[test] +fn function_free_files_are_no_data() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("types.go"), TYPES_GO).unwrap(); + std::fs::write(dir.path().join("empty.go"), "").unwrap(); + std::fs::write(dir.path().join("README.md"), "# fixture\n").unwrap(); + let section = json_section(dir.path(), &[]); + assert_eq!(section["status"], "No cohesion data available"); + assert_eq!(metric(§ion, "Total Functions"), "0"); + assert_eq!(section["score_label"], "0/10"); +} + +/// `--per-file` lists every analyzed file with its own LCOM; function-free +/// files report "No functions found". +#[test] +fn per_file_entries_carry_file_level_lcom() { + let dir = fixture(); + let section = json_section(dir.path(), &["--per-file"]); + let files = section["files"].as_array().expect("files array"); + let entry = |name: &str| { + files + .iter() + .find(|f| f["file_path"] == name) + .unwrap_or_else(|| panic!("no entry for {name}")) + }; + + let share = entry("share.go"); + assert_eq!(metric(share, "Total Functions"), "3"); + assert_eq!(metric(share, "LCOM Score"), format!("{SHARE_LCOM:.2}")); + assert_eq!( + metric(share, "Cohesion Score"), + format!("{:.2}", 1.0 - SHARE_LCOM) + ); + assert_eq!(count_of(share, "Excellent"), 3); + + let disjoint = entry("disjoint.go"); + assert_eq!( + metric(disjoint, "LCOM Score"), + format!("{DISJOINT_LCOM:.2}") + ); + assert_eq!(count_of(disjoint, "Poor"), 2); + + let types = entry("types.go"); + assert_eq!(types["status"], "No functions found"); + assert_eq!(metric(types, "Total Functions"), "0"); +} + +/// JSON, YAML and text agree on the headline numbers; text renders the +/// Distribution and issue sections like the sibling analyzers do. +#[test] +fn json_yaml_and_text_agree() { + let dir = fixture(); + let section = json_section(dir.path(), &[]); + + let yaml_out = cohesion("yaml", &[], dir.path()); + assert!(yaml_out.status.success()); + let yaml = String::from_utf8(yaml_out.stdout).unwrap(); + assert!( + yaml.contains(&format!("total_functions: {TOTAL_FUNCTIONS}")), + "yaml total_functions:\n{yaml}" + ); + assert!( + yaml.contains("lcom: 0.4\n"), + "yaml lcom must match the json section:\n{yaml}" + ); + assert!( + yaml.contains("cohesion_score: 0.6\n"), + "yaml cohesion_score must match the json section:\n{yaml}" + ); + assert!( + yaml.contains("excellent: 3\n") && yaml.contains("poor: 2\n"), + "{yaml}" + ); + assert!( + yaml.contains("message: Good overall cohesion with room for improvement"), + "{yaml}" + ); + + let text_out = cohesion("text", &[], dir.path()); + assert!(text_out.status.success()); + let text = String::from_utf8(text_out.stdout).unwrap(); + for label in [ + "Total Functions", + "LCOM Score", + "Cohesion Score", + "Avg Cohesion", + ] { + let value = metric(§ion, label); + assert!( + text.contains(label) && text.contains(value), + "text must carry {label} = {value}:\n{text}" + ); + } + assert!( + text.contains("Distribution"), + "no Distribution block:\n{text}" + ); + assert!(text.contains("Issues"), "no issues block:\n{text}"); + assert!( + text.contains("f2") && text.contains("g2"), + "issues missing:\n{text}" + ); + assert!( + !text.contains(" h "), + "excellent functions must not be listed as issues:\n{text}" + ); +} diff --git a/bins/codefang/tests/comments_e2e.rs b/bins/codefang/tests/comments_e2e.rs new file mode 100644 index 00000000..f0a7b0a4 --- /dev/null +++ b/bins/codefang/tests/comments_e2e.rs @@ -0,0 +1,297 @@ +//! End-to-end coverage of `static/comments`, driven through the **real +//! binary** over on-disk fixtures. +//! +//! The contract (see `site/analyzers/comments.md`): documentation coverage is +//! Σdocumented / Σfunctions over every analyzed file, the good-comment ratio +//! is Σgood / Σcomments, a function is documented iff a comment ends on the +//! line directly above it, and every output format reports the same counts. + +mod common; + +use std::path::Path; +use std::process::Output; + +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +/// Fixture file with one documented and one undocumented function; the +/// package comment is not directly above any declaration and the body +/// comment is inline. +const A_GO: &str = "// Package fixture is the comments e2e fixture. +package fixture + +// Add adds. +func Add(a, b int) int { + // inline comment + return a + b +} + +func Sub(a, b int) int { + return a - b +} +"; + +/// Fixture file where a blank line separates `Mul` from its comment (not +/// documented), a block comment documents `Div`, and `Mod` has nothing. +const B_GO: &str = "package fixture + +// Mul multiplies. + +func Mul(a, b int) int { + return a * b +} + +/* Div divides. */ +func Div(a, b int) int { + return a / b +} + +func Mod(a, b int) int { + return a %% b +} +"; + +/// A parser-supported file with no declarations and no comments: it must not +/// dilute the ratios. +const README_MD: &str = "# Fixture\n\nProse only.\n"; + +/// Fixture totals: Add, Sub, Mul, Div, Mod. +const TOTAL_FUNCTIONS: i64 = 5; +/// Add (line comment directly above) and Div (block comment directly above). +const DOCUMENTED_FUNCTIONS: i64 = 2; +/// Package comment, Add doc, inline comment, Mul comment, Div doc. +const TOTAL_COMMENTS: i64 = 5; +/// Add doc + Div doc. +const GOOD_COMMENTS: i64 = 2; + +/// Writes the multi-file fixture under `root`. +fn write_fixture(root: &Path) { + std::fs::create_dir_all(root).expect("fixture dir"); + std::fs::write(root.join("a.go"), A_GO).expect("a.go"); + std::fs::write(root.join("b.go"), B_GO.replace("%%", "%")).expect("b.go"); + std::fs::write(root.join("README.md"), README_MD).expect("README.md"); +} + +/// Runs `codefang run -a static/comments` with a deterministic environment +/// from `cwd`. +fn run_comments(cwd: &Path, args: &[&str]) -> Output { + common::command(BIN) + .current_dir(cwd) + .args(["run", "-a", "static/comments"]) + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// Runs the json format and parses stdout. +fn run_json(cwd: &Path, args: &[&str]) -> Value { + let out = run_comments(cwd, &[&["--format", "json"], args].concat()); + assert!( + out.status.success(), + "exit {:?}: {}", + out.status.code(), + String::from_utf8_lossy(&out.stderr) + ); + serde_json::from_slice(&out.stdout).expect("json stdout") +} + +fn section(report: &Value) -> &Value { + &report["sections"][0] +} + +/// The `value` of the key metric labelled `label`. +fn metric<'a>(section: &'a Value, label: &str) -> &'a str { + section["metrics"] + .as_array() + .expect("metrics array") + .iter() + .find(|m| m["label"] == label) + .unwrap_or_else(|| panic!("metric {label} missing in {section}"))["value"] + .as_str() + .expect("metric value") +} + +fn distribution_count(section: &Value, label: &str) -> i64 { + section["distribution"] + .as_array() + .expect("distribution array") + .iter() + .find(|d| d["label"] == label) + .unwrap_or_else(|| panic!("distribution {label} missing in {section}"))["count"] + .as_i64() + .expect("count") +} + +/// `(name, location)` of every issue, in emitted order. +fn issues(section: &Value) -> Vec<(String, String)> { + section["issues"] + .as_array() + .expect("issues array") + .iter() + .map(|i| { + ( + i["name"].as_str().expect("name").to_string(), + i["location"].as_str().expect("location").to_string(), + ) + }) + .collect() +} + +#[test] +fn multi_file_go_fixture_reports_weighted_coverage() { + let tmp = tempfile::tempdir().expect("tempdir"); + write_fixture(tmp.path()); + let report = run_json(tmp.path(), &["."]); + let s = section(&report); + + assert_eq!(s["title"], "COMMENTS"); + assert_eq!(metric(s, "Total Functions"), TOTAL_FUNCTIONS.to_string()); + assert_eq!(metric(s, "Total Comments"), TOTAL_COMMENTS.to_string()); + assert_eq!(metric(s, "Good Comments"), GOOD_COMMENTS.to_string()); + assert_eq!( + metric(s, "Bad Comments"), + (TOTAL_COMMENTS - GOOD_COMMENTS).to_string() + ); + // 2 of 5 functions, NOT the mean of per-file ratios (1/1, 1/4, README). + assert_eq!(metric(s, "Doc Coverage"), "40.0%"); + assert_eq!(metric(s, "Good Ratio"), "40.0%"); + assert_eq!(distribution_count(s, "Documented"), DOCUMENTED_FUNCTIONS); + assert_eq!( + distribution_count(s, "Undocumented"), + TOTAL_FUNCTIONS - DOCUMENTED_FUNCTIONS + ); + assert_eq!(s["score"], 0.4); + assert_eq!(s["score_label"], "4/10"); + assert_eq!( + s["status"], + "Fair comment quality - consider improving placement" + ); + assert_eq!( + issues(s), + vec![ + ("Mod".to_string(), "b.go".to_string()), + ("Mul".to_string(), "b.go".to_string()), + ("Sub".to_string(), "a.go".to_string()), + ] + ); +} + +#[test] +fn per_file_paths_stay_root_relative_from_a_relative_cwd() { + let tmp = tempfile::tempdir().expect("tempdir"); + let root = tmp.path().join("fixture"); + write_fixture(&root); + std::fs::create_dir_all(root.join("sub")).expect("sub dir"); + std::fs::write(root.join("sub").join("c.go"), A_GO).expect("sub/c.go"); + + // Relative root, relative cwd: the bug emitted "../../a.go". + let report = run_json(tmp.path(), &["--per-file", "fixture"]); + let s = section(&report); + let files: Vec<&str> = s["files"] + .as_array() + .expect("files array") + .iter() + .map(|f| f["file_path"].as_str().expect("file_path")) + .collect(); + assert_eq!(files, vec!["README.md", "a.go", "b.go", "sub/c.go"]); + + let sub_entry = s["files"] + .as_array() + .expect("files array") + .iter() + .find(|f| f["file_path"] == "sub/c.go") + .expect("sub/c.go entry"); + assert_eq!( + issues(sub_entry), + vec![("Sub".to_string(), "sub/c.go".to_string())] + ); + assert!( + issues(s).contains(&("Sub".to_string(), "sub/c.go".to_string())), + "section issues keep the same relative location: {:?}", + issues(s) + ); +} + +#[test] +fn single_file_root_stamps_the_file_name() { + let tmp = tempfile::tempdir().expect("tempdir"); + write_fixture(tmp.path()); + let report = run_json(tmp.path(), &["--per-file", "a.go"]); + let s = section(&report); + assert_eq!(s["files"][0]["file_path"], "a.go"); + assert_eq!(issues(s), vec![("Sub".to_string(), "a.go".to_string())]); +} + +#[test] +fn empty_directory_reports_the_empty_report_with_exit_zero() { + let tmp = tempfile::tempdir().expect("tempdir"); + let out = run_comments(tmp.path(), &["--format", "json", "."]); + assert_eq!(out.status.code(), Some(0)); + let report: Value = serde_json::from_slice(&out.stdout).expect("json stdout"); + let s = section(&report); + assert_eq!(s["status"], "No comments found"); + assert_eq!(metric(s, "Total Functions"), "0"); + assert_eq!(metric(s, "Doc Coverage"), "0.0%"); + assert!( + s.get("distribution").is_none(), + "no functions, no distribution" + ); + assert_eq!(issues(s), Vec::<(String, String)>::new()); +} + +#[test] +fn json_yaml_and_text_agree_on_the_counts() { + let tmp = tempfile::tempdir().expect("tempdir"); + write_fixture(tmp.path()); + + let json = run_json(tmp.path(), &["."]); + let s = section(&json); + assert_eq!(metric(s, "Total Functions"), TOTAL_FUNCTIONS.to_string()); + + let yaml = run_comments(tmp.path(), &["--format", "yaml", "."]); + assert!(yaml.status.success()); + let yaml = String::from_utf8(yaml.stdout).expect("utf-8 yaml"); + for needle in [ + &format!("total_functions: {TOTAL_FUNCTIONS}"), + &format!("documented_functions: {DOCUMENTED_FUNCTIONS}"), + &format!("total_comments: {TOTAL_COMMENTS}"), + &format!("good_comments: {GOOD_COMMENTS}"), + "documentation_coverage: 0.4", + "good_comments_ratio: 0.4", + "name: Add", + "status: Documented", + "name: Sub", + "status: Undocumented", + ] { + assert!(yaml.contains(needle), "yaml lacks {needle:?}:\n{yaml}"); + } + // Only undocumented functions are listed as such. + let undocumented = yaml + .split("undocumented_functions:") + .nth(1) + .and_then(|rest| rest.split("aggregate:").next()) + .expect("undocumented_functions block"); + assert_eq!(undocumented.matches("- name:").count(), 3, "{undocumented}"); + assert!(!undocumented.contains("name: Add"), "{undocumented}"); + + let text = run_comments(tmp.path(), &["--format", "text", "."]); + assert!(text.status.success()); + let text = String::from_utf8_lossy(&text.stdout) + .split_whitespace() + .collect::>() + .join(" "); + for needle in [ + &format!("Total Functions {TOTAL_FUNCTIONS}"), + &format!("Total Comments {TOTAL_COMMENTS}"), + "Doc Coverage 40.0%", + "Good Ratio 40.0%", + ] { + assert!(text.contains(needle), "text lacks {needle:?}:\n{text}"); + } +} diff --git a/bins/codefang/tests/common/mod.rs b/bins/codefang/tests/common/mod.rs new file mode 100644 index 00000000..d82cf941 --- /dev/null +++ b/bins/codefang/tests/common/mod.rs @@ -0,0 +1,30 @@ +//! Shared helper for the CLI-level end-to-end tests: a process command that is +//! isolated from the developer's environment. +//! +//! `codefang run` reads `./.codefang.yaml`, then `$HOME/.codefang.yaml`, and a +//! `CODEFANG_` environment variable per configuration key, as the default +//! layer of its flags. A test that inherited a developer's config file or +//! variables would assert against defaults it did not choose, so every e2e +//! suite builds its commands through [`command`]. + +use std::process::Command; + +/// The test clock variable: not a configuration key, kept when inherited. +const TEST_CLOCK_VAR: &str = "CODEFANG_NOW"; + +/// Builds a command for `program` (the `codefang` binary, or a wrapper such as +/// `taskset` that runs it) with `HOME` pointed at the test scratch directory +/// (which holds no `.codefang.yaml`) and every inherited `CODEFANG_*` +/// configuration variable removed. +pub fn command(program: &str) -> Command { + let mut cmd = Command::new(program); + cmd.env("HOME", env!("CARGO_TARGET_TMPDIR")); + for (key, _) in std::env::vars_os() { + if let Some(name) = key.to_str() { + if name.starts_with("CODEFANG_") && name != TEST_CLOCK_VAR { + cmd.env_remove(name); + } + } + } + cmd +} diff --git a/bins/codefang/tests/complexity_e2e.rs b/bins/codefang/tests/complexity_e2e.rs new file mode 100644 index 00000000..5a8a097b --- /dev/null +++ b/bins/codefang/tests/complexity_e2e.rs @@ -0,0 +1,597 @@ +//! End-to-end coverage of `static/complexity`, driven through the **real +//! binary** over hermetic source trees written into a tempdir. +//! +//! The fixture sources are the ones the crate-level tests measure +//! (`crates/cf-complexity/tests/fixtures/`), so every number asserted here +//! was hand-computed against the documented counting rules +//! (`site/explanation/complexity.md`) and is checked twice: from the crate +//! API and from the CLI's machine formats. + +mod common; + +use std::fs; +use std::path::Path; + +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +const GO_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/cc.go"); +const PY_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/cc.py"); +const JS_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/cc.js"); +const RS_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/cc.rs"); +const JAVA_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/Foo.java"); +const C_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/cc.c"); +const GEN_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/gen.go"); + +/// A Go file without functions (a type declaration only). +const NOFUNC_GO: &str = "package main\n\ntype Config struct {\n\tName string\n}\n"; +/// A one-function file, cyclomatic 1. +const SAME_A_GO: &str = "package a\n\nfunc same() int { return 1 }\n"; +/// A one-function file with the same name, cyclomatic 2 (one `if`). +const SAME_B_GO: &str = "package b\n\nfunc same() int { if true { return 1 }; return 0 }\n"; + +/// Nesting depth of the generated deep fixture. +const DEEP_DEPTH: usize = 20_000; + +/// Magic + LE u32 payload length of the `--format bin` envelope. +const BIN_MAGIC: &[u8; 4] = b"CFB1"; +const BIN_HEADER_SIZE: usize = 8; + +/// The Go fixture's functions in report order, as the `issues` rows render +/// them: `(name, cyclomatic, cognitive, nesting, severity)`. +const GO_EXPECTED: &[(&str, i64, i64, i64, &str)] = &[ + ("decisions", 8, 7, 1, "fair"), + ("manyCases", 5, 1, 1, "good"), + ("nested", 4, 6, 3, "good"), + ("loopCond", 4, 3, 1, "good"), + ("boolSeq", 4, 2, 0, "good"), + ("elseNest", 3, 4, 2, "good"), + ("withClosure", 3, 3, 1, "good"), + ("fact", 2, 2, 1, "good"), + ("Method", 2, 1, 1, "good"), + ("simple", 1, 0, 0, "good"), +]; + +/// Runs the binary with a deterministic environment; state is disabled so +/// the run is self-contained. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// `(exit code, stdout, stderr)` of `static/complexity --format `. +fn complexity(path: &Path, format: &str, extra: &[&str]) -> (i32, Vec, String) { + let p = path.to_str().expect("utf-8 path"); + let out = codefang( + &[ + &["-a", "static/complexity", "--format", format], + extra, + &[p], + ] + .concat(), + ); + ( + out.status.code().unwrap_or(-1), + out.stdout, + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +fn write(dir: &Path, rel: &str, content: &str) { + let path = dir.join(rel); + fs::create_dir_all(path.parent().expect("parent")).expect("mkdir"); + fs::write(path, content).expect("write fixture"); +} + +/// The Go fixture plus a function-free file and an empty file. +fn mixed_tree() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + write(dir.path(), "cc.go", GO_FIXTURE); + write(dir.path(), "nofunc.go", NOFUNC_GO); + write(dir.path(), "empty.go", ""); + write(dir.path(), "notes.txt", "this is not source\n"); + dir +} + +/// Two identically named functions in two directories. +fn dup_tree() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + write(dir.path(), "a/x.go", SAME_A_GO); + write(dir.path(), "b/x.go", SAME_B_GO); + dir +} + +/// Every language fixture in one tree. +fn polyglot_tree() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + write(dir.path(), "cc.go", GO_FIXTURE); + write(dir.path(), "cc.py", PY_FIXTURE); + write(dir.path(), "cc.js", JS_FIXTURE); + write(dir.path(), "cc.rs", RS_FIXTURE); + write(dir.path(), "Foo.java", JAVA_FIXTURE); + write(dir.path(), "cc.c", C_FIXTURE); + write(dir.path(), "gen.go", GEN_FIXTURE); + dir +} + +fn deep_tree() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + let mut src = String::from("package main\n\nfunc deep(x int) {\n"); + for _ in 0..DEEP_DEPTH { + src.push_str("if true {\n"); + } + for _ in 0..DEEP_DEPTH { + src.push_str("}\n"); + } + src.push_str("}\n"); + write(dir.path(), "deep.go", &src); + dir +} + +fn json_section(stdout: &[u8]) -> Value { + let report: Value = serde_json::from_slice(stdout).expect("json report parses"); + report["sections"][0].clone() +} + +/// `label -> value` of a section's (or file entry's) key metrics. +fn metrics(section: &Value) -> Vec<(String, String)> { + section["metrics"] + .as_array() + .expect("metrics array") + .iter() + .map(|m| { + ( + m["label"].as_str().expect("label").to_string(), + m["value"].as_str().expect("value").to_string(), + ) + }) + .collect() +} + +fn metric(section: &Value, label: &str) -> String { + metrics(section) + .into_iter() + .find(|(l, _)| l == label) + .map(|(_, v)| v) + .unwrap_or_else(|| panic!("metric {label} missing in {section}")) +} + +fn metric_i64(section: &Value, label: &str) -> i64 { + metric(section, label).parse().expect("integer metric") +} + +/// `(name, location, cyclomatic, cognitive, nesting, severity)` of each +/// issue row, parsing the `CC=.. | Cog=.. | Nest=..` value. +fn issue_rows(section: &Value) -> Vec<(String, String, i64, i64, i64, String)> { + section["issues"] + .as_array() + .expect("issues array") + .iter() + .map(|row| { + let value = row["value"].as_str().expect("value"); + let nums: Vec = value + .split('|') + .map(|part| { + part.trim() + .split('=') + .nth(1) + .expect("metric=value") + .parse() + .expect("integer") + }) + .collect(); + ( + row["name"].as_str().expect("name").to_string(), + row["location"].as_str().expect("location").to_string(), + nums[0], + nums[1], + nums[2], + row["severity"].as_str().expect("severity").to_string(), + ) + }) + .collect() +} + +/// Decodes the CFB1 envelope and parses its JSON payload. +fn bin_payload(stdout: &[u8]) -> Value { + assert!(stdout.len() >= BIN_HEADER_SIZE, "envelope too short"); + assert_eq!(&stdout[..4], BIN_MAGIC, "envelope magic"); + let len = u32::from_le_bytes(stdout[4..8].try_into().expect("4 bytes")) as usize; + assert_eq!(stdout.len(), BIN_HEADER_SIZE + len, "envelope length"); + serde_json::from_slice(&stdout[BIN_HEADER_SIZE..]).expect("bin payload parses as json") +} + +/// The scalar after `:` in the yaml `aggregate:` block. +fn yaml_aggregate(yaml: &str, key: &str) -> String { + let agg = yaml + .split("\naggregate:\n") + .nth(1) + .expect("aggregate block"); + agg.lines() + .find_map(|line| line.trim().strip_prefix(&format!("{key}: "))) + .unwrap_or_else(|| panic!("aggregate.{key} missing in:\n{yaml}")) + .to_string() +} + +/// The integer printed after a `Key Metrics` label in the text rendering. +fn text_metric(text: &str, label: &str) -> i64 { + let at = text + .find(label) + .unwrap_or_else(|| panic!("{label} missing in:\n{text}")); + let rest = &text[at + label.len()..]; + rest.trim_start() + .chars() + .take_while(char::is_ascii_digit) + .collect::() + .parse() + .expect("digits after label") +} + +#[test] +fn json_reports_exact_per_function_metrics_distribution_and_totals() { + let dir = mixed_tree(); + let (rc, stdout, stderr) = complexity(dir.path(), "json", &[]); + assert_eq!(rc, 0, "run failed: {stderr}"); + let section = json_section(&stdout); + + assert_eq!(section["title"], "COMPLEXITY"); + assert_eq!( + metrics(§ion), + vec![ + ("Total Functions".to_string(), "10".to_string()), + ("Avg Complexity".to_string(), "3.6".to_string()), + ("Max Complexity".to_string(), "8".to_string()), + ("Total Complexity".to_string(), "36".to_string()), + ("Cognitive Total".to_string(), "29".to_string()), + ("Max Nesting".to_string(), "3".to_string()), + ("Decision Points".to_string(), "26".to_string()), + ] + ); + assert_eq!( + section["status"], + "Fair complexity - some functions could be simplified" + ); + assert_eq!(section["score_label"], "6/10"); + + let dist: Vec<(String, f64, i64)> = section["distribution"] + .as_array() + .expect("distribution") + .iter() + .map(|d| { + ( + d["label"].as_str().expect("label").to_string(), + d["percent"].as_f64().expect("percent"), + d["count"].as_i64().expect("count"), + ) + }) + .collect(); + assert_eq!( + dist, + vec![ + ("Simple (1-5)".to_string(), 0.9, 9), + ("Moderate (6-10)".to_string(), 0.1, 1), + ("Complex (11-20)".to_string(), 0.0, 0), + ("Very Complex (>20)".to_string(), 0.0, 0), + ] + ); + + let rows = issue_rows(§ion); + let want: Vec<(String, String, i64, i64, i64, String)> = GO_EXPECTED + .iter() + .map(|(n, cc, cog, nest, sev)| { + ( + (*n).to_string(), + "cc.go".to_string(), + *cc, + *cog, + *nest, + (*sev).to_string(), + ) + }) + .collect(); + assert_eq!( + rows, want, + "issue rows (name, file, cc, cognitive, nesting, severity)" + ); + assert!( + section.get("files").is_none(), + "no --per-file: no files array" + ); +} + +#[test] +fn bin_envelope_carries_lines_of_code_density_and_risk() { + let dir = mixed_tree(); + let (rc, stdout, stderr) = complexity(dir.path(), "bin", &[]); + assert_eq!(rc, 0, "run failed: {stderr}"); + let payload = bin_payload(&stdout); + + let fc = payload["function_complexity"].as_array().expect("array"); + assert_eq!(fc.len(), 10); + let top = &fc[0]; + assert_eq!(top["name"], "decisions"); + assert_eq!(top["source_file"], "cc.go"); + assert_eq!(top["language"], "go"); + assert_eq!(top["directory"], "."); + assert_eq!(top["cyclomatic_complexity"], 8); + assert_eq!(top["cognitive_complexity"], 7); + assert_eq!(top["nesting_depth"], 1); + assert_eq!(top["lines_of_code"], 22); + assert!((top["complexity_density"].as_f64().expect("density") - 8.0 / 22.0).abs() < 1e-12); + assert_eq!(top["risk_level"], "MEDIUM"); + let last = &fc[9]; + assert_eq!(last["name"], "simple"); + assert_eq!(last["lines_of_code"], 3); + assert_eq!(last["risk_level"], "LOW"); + + assert_eq!(payload["distribution"]["simple"], 9); + assert_eq!(payload["distribution"]["moderate"], 1); + assert!(payload["distribution"].get("complex").is_none()); + assert_eq!( + payload["high_risk_functions"] + .as_array() + .expect("array") + .len(), + 0 + ); + + let agg = &payload["aggregate"]; + assert_eq!(agg["total_functions"], 10); + assert_eq!(agg["total_complexity"], 36); + assert_eq!(agg["cognitive_complexity"], 29); + assert_eq!(agg["nesting_depth"], 3); + assert_eq!(agg["max_complexity"], 8); + assert_eq!(agg["decision_points"], 26); + assert!((agg["average_complexity"].as_f64().expect("avg") - 3.6).abs() < 1e-12); + assert_eq!( + agg["message"], + "Fair complexity - some functions could be simplified" + ); +} + +#[test] +fn yaml_aggregate_carries_real_sums_and_max_nesting() { + let dir = mixed_tree(); + let (rc, stdout, stderr) = complexity(dir.path(), "yaml", &[]); + assert_eq!(rc, 0, "run failed: {stderr}"); + let yaml = String::from_utf8(stdout).expect("utf-8 yaml"); + assert_eq!(yaml_aggregate(&yaml, "total_functions"), "10"); + assert_eq!(yaml_aggregate(&yaml, "total_complexity"), "36"); + assert_eq!(yaml_aggregate(&yaml, "cognitive_complexity"), "29"); + assert_eq!(yaml_aggregate(&yaml, "nesting_depth"), "3"); + assert_eq!(yaml_aggregate(&yaml, "max_complexity"), "8"); + assert_eq!(yaml_aggregate(&yaml, "decision_points"), "26"); + assert_eq!(yaml_aggregate(&yaml, "average_complexity"), "3.6"); + assert!(yaml.starts_with("function_complexity:\n - name: decisions\n")); +} + +#[test] +fn per_file_entries_sum_to_the_aggregate() { + let dir = mixed_tree(); + write(dir.path(), "sub/x.go", SAME_B_GO); + let (rc, stdout, stderr) = complexity(dir.path(), "json", &["--per-file"]); + assert_eq!(rc, 0, "run failed: {stderr}"); + let section = json_section(&stdout); + let files = section["files"].as_array().expect("files array"); + let paths: Vec<&str> = files + .iter() + .map(|f| f["file_path"].as_str().expect("file_path")) + .collect(); + assert_eq!( + paths, + vec!["cc.go", "empty.go", "nofunc.go", "sub/x.go"], + "every parsed file in walk order, function-free files included" + ); + + for label in [ + "Total Functions", + "Total Complexity", + "Cognitive Total", + "Decision Points", + ] { + let sum: i64 = files.iter().map(|f| metric_i64(f, label)).sum(); + assert_eq!(sum, metric_i64(§ion, label), "{label}: per-file sum"); + } + for label in ["Max Complexity", "Max Nesting"] { + let max = files + .iter() + .map(|f| metric_i64(f, label)) + .max() + .expect("max"); + assert_eq!(max, metric_i64(§ion, label), "{label}: per-file max"); + } + + let nofunc = &files[2]; + assert_eq!(nofunc["status"], "No functions found"); + assert!(nofunc.get("distribution").is_none()); + assert!(nofunc["issues"].as_array().expect("issues").is_empty()); + let sub = &files[3]; + assert_eq!(issue_rows(sub)[0].0, "same"); + assert_eq!(issue_rows(sub)[0].1, "sub/x.go"); +} + +#[test] +fn json_yaml_bin_and_text_agree_on_the_totals() { + let dir = mixed_tree(); + let (rc, json, _) = complexity(dir.path(), "json", &[]); + assert_eq!(rc, 0); + let (rc, yaml, _) = complexity(dir.path(), "yaml", &[]); + assert_eq!(rc, 0); + let (rc, bin, _) = complexity(dir.path(), "bin", &[]); + assert_eq!(rc, 0); + let (rc, text, _) = complexity(dir.path(), "text", &[]); + assert_eq!(rc, 0); + + let section = json_section(&json); + let yaml = String::from_utf8(yaml).expect("utf-8"); + let agg = bin_payload(&bin)["aggregate"].clone(); + let text = String::from_utf8(text).expect("utf-8"); + + for (label, key) in [ + ("Total Functions", "total_functions"), + ("Total Complexity", "total_complexity"), + ("Cognitive Total", "cognitive_complexity"), + ("Max Nesting", "nesting_depth"), + ("Decision Points", "decision_points"), + ("Max Complexity", "max_complexity"), + ] { + let from_json = metric_i64(§ion, label); + assert_eq!( + yaml_aggregate(&yaml, key), + from_json.to_string(), + "yaml {key}" + ); + assert_eq!(agg[key].as_i64().expect("int"), from_json, "bin {key}"); + assert_eq!(text_metric(&text, label), from_json, "text {label}"); + } + assert!( + text.contains("Top Issues"), + "text prints the ranked function rows:\n{text}" + ); + assert!(text.contains("decisions") && text.contains("CC=8 | Cog=7 | Nest=1")); +} + +#[test] +fn deep_nesting_exits_zero_in_every_format() { + let dir = deep_tree(); + let depth = i64::try_from(DEEP_DEPTH).expect("fits"); + let cognitive = depth * (depth + 1) / 2; + for format in ["json", "yaml", "bin", "text", "compact"] { + let (rc, stdout, stderr) = complexity(dir.path(), format, &[]); + assert_eq!(rc, 0, "--format {format} failed: {stderr}"); + assert!(!stdout.is_empty(), "--format {format}: empty output"); + } + let (_, json, _) = complexity(dir.path(), "json", &[]); + let rows = issue_rows(&json_section(&json)); + assert_eq!( + rows, + vec![( + "deep".to_string(), + "deep.go".to_string(), + depth + 1, + cognitive, + depth, + "poor".to_string() + )] + ); + let (_, yaml, _) = complexity(dir.path(), "yaml", &[]); + let yaml = String::from_utf8(yaml).expect("utf-8"); + assert_eq!(yaml_aggregate(&yaml, "nesting_depth"), depth.to_string()); +} + +#[test] +fn empty_directory_exits_zero_with_an_empty_report() { + let dir = tempfile::tempdir().expect("tempdir"); + let (rc, json, stderr) = complexity(dir.path(), "json", &[]); + assert_eq!(rc, 0, "json: {stderr}"); + let section = json_section(&json); + assert_eq!(metric(§ion, "Total Functions"), "0"); + assert_eq!(section["status"], "No complexity data available"); + assert!(section.get("distribution").is_none()); + assert!(section["issues"].as_array().expect("issues").is_empty()); + + let (rc, yaml, stderr) = complexity(dir.path(), "yaml", &[]); + assert_eq!(rc, 0, "yaml: {stderr}"); + let yaml = String::from_utf8(yaml).expect("utf-8"); + assert!(yaml.starts_with("function_complexity: []\n")); + assert_eq!(yaml_aggregate(&yaml, "total_functions"), "0"); + + for format in ["bin", "text", "compact"] { + let (rc, _, stderr) = complexity(dir.path(), format, &[]); + assert_eq!(rc, 0, "--format {format}: {stderr}"); + } +} + +#[test] +fn identically_named_functions_order_by_metrics_then_file() { + let dir = dup_tree(); + let (rc, json, _) = complexity(dir.path(), "json", &[]); + assert_eq!(rc, 0); + let rows = issue_rows(&json_section(&json)); + assert_eq!(rows.len(), 2); + assert_eq!( + (rows[0].0.as_str(), rows[0].1.as_str(), rows[0].2), + ("same", "b/x.go", 2) + ); + assert_eq!( + (rows[1].0.as_str(), rows[1].1.as_str(), rows[1].2), + ("same", "a/x.go", 1) + ); + + let (_, yaml, _) = complexity(dir.path(), "yaml", &[]); + let yaml = String::from_utf8(yaml).expect("utf-8"); + assert_eq!(yaml_aggregate(&yaml, "cognitive_complexity"), "1"); + assert_eq!(yaml_aggregate(&yaml, "nesting_depth"), "1"); +} + +#[test] +fn every_language_reports_its_real_functions() { + let dir = polyglot_tree(); + let (rc, json, stderr) = complexity(dir.path(), "json", &[]); + assert_eq!(rc, 0, "run failed: {stderr}"); + let section = json_section(&json); + // go 10 + py 3 + js 3 + rs 3 + java 2 + c 2 + gen.go 3 + assert_eq!(metric(§ion, "Total Functions"), "26"); + let rows = issue_rows(§ion); + let find = |name: &str, file: &str| { + rows.iter() + .find(|r| r.0 == name && r.1 == file) + .unwrap_or_else(|| panic!("{name} in {file} missing from {rows:?}")) + }; + let c = find("c_decisions", "cc.c"); + assert_eq!((c.2, c.3, c.4), (10, 10, 1)); + assert_eq!( + ( + find("jsDecisions", "cc.js").2, + find("jsDecisions", "cc.js").3 + ), + (11, 10) + ); + assert_eq!( + ( + find("py_decisions", "cc.py").2, + find("py_decisions", "cc.py").3 + ), + (8, 8) + ); + assert_eq!( + ( + find("rs_decisions", "cc.rs").2, + find("rs_decisions", "cc.rs").3 + ), + (9, 8) + ); + assert_eq!( + (find("nested", "Foo.java").2, find("nested", "Foo.java").3), + (4, 6) + ); + assert_eq!(find("outer", "gen.go").2, 3); + for row in &rows { + assert!( + !row.0.contains('\n') && !row.0.contains('(') && row.0.len() < 40, + "function name is an identifier, not source text: {:?}", + row.0 + ); + } +} + +#[test] +fn machine_formats_are_deterministic_across_runs() { + let dir = polyglot_tree(); + for format in ["json", "yaml", "bin"] { + let (rc, first, _) = complexity(dir.path(), format, &[]); + assert_eq!(rc, 0); + let (rc, second, _) = complexity(dir.path(), format, &[]); + assert_eq!(rc, 0); + assert_eq!(first, second, "--format {format} differs between runs"); + } +} diff --git a/bins/codefang/tests/composition_e2e.rs b/bins/codefang/tests/composition_e2e.rs new file mode 100644 index 00000000..ecbe659c --- /dev/null +++ b/bins/codefang/tests/composition_e2e.rs @@ -0,0 +1,276 @@ +//! End-to-end coverage of `static/composition`, driven through the **real +//! binary** over a hermetic on-disk fixture. +//! +//! The contract under test (see `site/analyzers/composition.md`): +//! +//! * every walked file is classified — vendored and generated files are +//! counted by default (only `--languages` narrows the walk); +//! * files that match no rule land in `other`, never in `source`; +//! * `--per-file` entries carry the file's category and language; +//! * the output is deterministic. + +mod common; + +use std::path::Path; + +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +/// Section title of the composition report. +const SECTION_TITLE: &str = "COMPOSITION"; + +/// Info-only score label. +const SCORE_INFO: &str = "Info"; + +/// Tolerance for the distribution-sum assertion. +const SUM_TOLERANCE: f64 = 1e-9; + +/// The fixture: one file per category, plus extra documentation, +/// configuration and source files so the percentages are not all equal +/// (`schema.sql` is source: enry lists `.sql` under PL/SQL first). +fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + let files: &[(&str, &[u8])] = &[ + (".editorconfig", b"[*]\nindent_style = tab\n"), + ("README.md", b"# Title\n"), + ("config.yaml", b"key: value\n"), + ("data.bin", b"\x00\x01\x02\xff\x00\x00\x00"), + ("docs/guide.md", b"# Guide\n"), + ("empty.go", b""), + ( + "gen/thing.pb.go", + b"// Code generated by protoc-gen-go. DO NOT EDIT.\npackage gen\n", + ), + ("logo.png", b""), + ("main.go", b"package main\n\nfunc main() {}\n"), + ("schema.sql", b"SELECT 1;\n"), + ("settings.json", b"{\"a\":1}"), + ("vendor/x/y.go", b"package x\n"), + ("data.xyz", b"hello world\n"), + ]; + for (rel, content) in files { + let path = dir.path().join(rel); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("mkdir"); + } + std::fs::write(path, content).expect("write fixture"); + } + dir +} + +/// Runs `codefang run -a static/composition ` with a pinned +/// environment and returns the raw stdout bytes. +fn run_composition(path: &Path, extra: &[&str]) -> Vec { + let out = common::command(BIN) + .arg("run") + .args(["-a", "static/composition"]) + .args(extra) + .arg(path) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary"); + assert!( + out.status.success(), + "run failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// The single composition section of a `--format json` run. +fn composition_section(path: &Path, extra: &[&str]) -> Value { + let stdout = run_composition(path, &[&["--format", "json"], extra].concat()); + let report: Value = serde_json::from_slice(&stdout).expect("valid JSON report"); + let sections = report["sections"].as_array().expect("sections array"); + assert_eq!(sections.len(), 1, "one composition section expected"); + assert_eq!(sections[0]["title"], SECTION_TITLE); + sections[0].clone() +} + +/// Looks up a `metrics[].value` by label. +fn metric(entry: &Value, label: &str) -> String { + entry["metrics"] + .as_array() + .expect("metrics array") + .iter() + .find(|m| m["label"] == label) + .unwrap_or_else(|| panic!("metric {label} missing in {entry}"))["value"] + .as_str() + .expect("metric value string") + .to_string() +} + +/// `(label, count)` of every distribution row, in emitted order. +fn distribution(entry: &Value) -> Vec<(String, i64)> { + entry["distribution"] + .as_array() + .map(|rows| { + rows.iter() + .map(|d| { + ( + d["label"].as_str().expect("label").to_string(), + d["count"].as_i64().expect("count"), + ) + }) + .collect() + }) + .unwrap_or_default() +} + +/// Every category is counted by default, vendor and generated included, and +/// an unknown extension lands in `other` rather than `source`. +#[test] +fn default_run_counts_every_category_including_vendor_and_generated() { + let dir = fixture(); + let section = composition_section(dir.path(), &[]); + + assert_eq!(metric(§ion, "Total Files"), "13"); + assert_eq!(metric(§ion, "Source Files"), "3"); + assert_eq!(metric(§ion, "Source %"), "23.1%"); + assert_eq!(section["status"], "File composition analysis completed"); + assert_eq!(section["score_label"], SCORE_INFO); + + assert_eq!( + distribution(§ion), + vec![ + ("source".to_string(), 3), + ("documentation".to_string(), 2), + ("configuration".to_string(), 2), + ("vendor".to_string(), 1), + ("generated".to_string(), 1), + ("dotfile".to_string(), 1), + ("image".to_string(), 1), + ("binary".to_string(), 1), + ("other".to_string(), 1), + ] + ); +} + +/// The distribution fractions of one run sum to 1 (they are `0..1` shares). +#[test] +fn distribution_fractions_sum_to_one() { + let dir = fixture(); + let section = composition_section(dir.path(), &[]); + let sum: f64 = section["distribution"] + .as_array() + .expect("distribution") + .iter() + .map(|d| d["percent"].as_f64().expect("percent")) + .sum(); + assert!((sum - 1.0).abs() < SUM_TOLERANCE, "fractions sum to {sum}"); +} + +/// `--per-file` entries carry each file's category and language. +#[test] +fn per_file_entries_carry_the_category() { + let dir = fixture(); + let section = composition_section(dir.path(), &["--per-file"]); + + let files = section["files"].as_array().expect("files array"); + let by_path = |rel: &str| -> &Value { + files + .iter() + .find(|f| f["file_path"] == rel) + .unwrap_or_else(|| panic!("no entry for {rel}")) + }; + + let vendored = by_path("vendor/x/y.go"); + assert_eq!(vendored["status"], "Classified as vendor"); + assert_eq!(vendored["score_label"], SCORE_INFO); + assert_eq!(metric(vendored, "Total Files"), "1"); + assert_eq!(metric(vendored, "Vendor Files"), "1"); + assert_eq!(metric(vendored, "Vendor %"), "100.0%"); + assert_eq!(metric(vendored, "Language"), "Go"); + assert_eq!(distribution(vendored), vec![("vendor".to_string(), 1)]); + + let generated = by_path("gen/thing.pb.go"); + assert_eq!(generated["status"], "Classified as generated"); + + let docs = by_path("docs/guide.md"); + assert_eq!(docs["status"], "Classified as documentation"); + assert_eq!(metric(docs, "Language"), "Markdown"); + + let other = by_path("data.xyz"); + assert_eq!(other["status"], "Classified as other"); + assert_eq!(metric(other, "Language"), "unknown"); + + let paths: Vec<&str> = files + .iter() + .map(|f| f["file_path"].as_str().expect("file_path")) + .collect(); + let mut sorted = paths.clone(); + sorted.sort_unstable(); + assert_eq!(paths, sorted, "entries follow the lexical walk order"); + assert_eq!(paths.len(), 13); +} + +/// Without `--per-file` the section carries no `files` key. +#[test] +fn no_per_file_flag_omits_files() { + let dir = fixture(); + let section = composition_section(dir.path(), &[]); + assert!(section.get("files").is_none(), "files must be omitted"); +} + +/// `--languages` is the only filter: it narrows the walk to the selected +/// language's files, which are still classified in every category. +#[test] +fn languages_flag_narrows_the_walk() { + let dir = fixture(); + let section = composition_section(dir.path(), &["--languages", "go"]); + + assert_eq!(metric(§ion, "Total Files"), "4"); + assert_eq!( + distribution(§ion), + vec![ + ("source".to_string(), 2), + ("vendor".to_string(), 1), + ("generated".to_string(), 1), + ] + ); +} + +/// An empty tree reports zero files with the empty status and no +/// distribution. +#[test] +fn empty_tree_reports_no_files() { + let dir = tempfile::tempdir().expect("tempdir"); + let section = composition_section(dir.path(), &[]); + assert_eq!(metric(§ion, "Total Files"), "0"); + assert_eq!(metric(§ion, "Source %"), "0.0%"); + assert_eq!(section["status"], "No files analyzed"); + assert!(section.get("distribution").is_none()); + assert_eq!(section["issues"].as_array().map(Vec::len), Some(0)); +} + +/// The YAML breakdown lists every category, `other` included. +#[test] +fn yaml_breakdown_lists_every_category() { + let dir = fixture(); + let yaml = String::from_utf8(run_composition(dir.path(), &["--format", "yaml"])).unwrap(); + assert!(yaml.contains("total_files: 13\n"), "{yaml}"); + for line in [ + " vendor: 1\n", + " generated: 1\n", + " other: 1\n", + " source: 3\n", + ] { + assert!(yaml.contains(line), "missing {line:?} in:\n{yaml}"); + } +} + +/// Two runs over the same tree produce identical bytes in every format. +#[test] +fn two_runs_are_byte_identical() { + let dir = fixture(); + for format in ["json", "yaml", "text", "compact"] { + let first = run_composition(dir.path(), &["--format", format]); + let second = run_composition(dir.path(), &["--format", format]); + assert_eq!(first, second, "--format {format} must be deterministic"); + } +} diff --git a/bins/codefang/tests/config_e2e.rs b/bins/codefang/tests/config_e2e.rs new file mode 100644 index 00000000..b097fbf2 --- /dev/null +++ b/bins/codefang/tests/config_e2e.rs @@ -0,0 +1,446 @@ +//! End-to-end coverage of the configuration file, driven through the **real +//! binary** (`specs/bugs/BUG-cli-inert-surface.md`, item 1). +//! +//! The contract (`site/guide/configuration.md`): `--config ` names the +//! file explicitly (it must be readable); without it `./.codefang.yaml` and +//! then `$HOME/.codefang.yaml` are discovered. Every supported key is the +//! default of one `run` flag, so the precedence is explicit flag > +//! `CODEFANG_*` environment > config file > compiled default. Unsupported +//! keys are ignored with ONE stderr warning naming them. +//! +//! Every run pins `HOME` and the working directory to empty temporary +//! directories, so a developer's own `~/.codefang.yaml` can never leak in. + +mod common; + +use std::path::{Path, PathBuf}; +use std::process::Output; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// The discovered configuration file name. +const CONFIG_NAME: &str = ".codefang.yaml"; +/// Base instant of the fixture history: 2021-01-01T12:00:00Z. +const T0: i64 = 1_609_502_400; +/// One day between quiet fixture commits. +const DAY: i64 = 86_400; +/// Quiet days before the busy day. +const QUIET_DAYS: i64 = 8; +/// New files written on the busy day. +const BURST_FILES: usize = 6; +/// A window shorter than the quiet history, so it changes every Z-score. +const SHORT_WINDOW: &str = "3"; +/// The compiled `--anomaly-window` default. +const DEFAULT_WINDOW: &str = "20"; +/// A config file that only shortens the anomaly window. +const SHORT_WINDOW_CONFIG: &str = "history:\n anomaly:\n window_size: 3\n"; + +/// One hermetic invocation: an empty `HOME`, an empty working directory, a +/// deterministic locale. +struct Sandbox { + home: tempfile::TempDir, + cwd: tempfile::TempDir, +} + +impl Sandbox { + fn new() -> Self { + Self { + home: tempfile::tempdir().expect("home tempdir"), + cwd: tempfile::tempdir().expect("cwd tempdir"), + } + } + + /// Writes `body` as a config file named `name` under `dir`. + fn write(dir: &Path, name: &str, body: &str) -> PathBuf { + let path = dir.join(name); + std::fs::write(&path, body).expect("write config"); + path + } + + /// Writes a config file in the sandbox's working directory under a + /// non-discovered name, for `--config`. + fn explicit(&self, body: &str) -> String { + Self::write(self.cwd.path(), "explicit.yaml", body) + .to_str() + .expect("utf-8 path") + .to_owned() + } + + /// Runs `codefang run ` with `envs` in the sandbox. + fn run(&self, args: &[&str], envs: &[(&str, &str)]) -> Output { + let mut cmd = common::command(BIN); + cmd.arg("run") + .args(args) + .current_dir(self.cwd.path()) + .env("HOME", self.home.path()) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C"); + for (k, v) in envs { + cmd.env(k, v); + } + cmd.output().expect("run the codefang binary") + } + + /// stdout of a successful run. + fn stdout_ok(&self, args: &[&str], envs: &[(&str, &str)]) -> String { + let out = self.run(args, envs); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") + } +} + +/// `n` numbered lines tagged with `tag`. +fn lines(tag: &str, n: usize) -> Vec { + (1..=n) + .map(|i| format!("line {i} of {tag}\n")) + .collect::() + .into_bytes() +} + +/// Eight quiet days (one file each, alternating 10 / 12 lines) and one busy +/// day writing six new files: the busy day is flagged, and its Z-scores +/// depend on the anomaly window. +fn burst_fixture() -> TestRepo { + let test = init_repo().expect("init"); + for d in 0..QUIET_DAYS { + let name = format!("q{d}.txt"); + let n = if d % 2 == 0 { 10 } else { 12 }; + commit_files( + &test, + "quiet", + T0 + d * DAY, + &[(name.as_str(), lines(&name, n).as_slice())], + ) + .expect("quiet commit"); + } + for k in 0..BURST_FILES { + let name = format!("burst{k}.txt"); + let when = T0 + QUIET_DAYS * DAY + i64::try_from(k).expect("small") * 60; + commit_files( + &test, + "burst", + when, + &[(name.as_str(), lines(&name, 10).as_slice())], + ) + .expect("burst commit"); + } + test +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// The anomaly report of `test` with the extra `args`, run with the compiled +/// defaults only (`--config /dev/null` disables discovery). +fn anomaly_with_flags(test: &TestRepo, args: &[&str]) -> String { + let sandbox = Sandbox::new(); + let mut all = vec!["-a", "history/anomaly", "--config", "/dev/null"]; + all.extend_from_slice(args); + all.push(path(test)); + sandbox.stdout_ok(&all, &[]) +} + +#[test] +fn config_window_size_changes_the_anomaly_report_like_the_flag() { + let test = burst_fixture(); + let by_flag = anomaly_with_flags(&test, &["--anomaly-window", SHORT_WINDOW]); + let by_default = anomaly_with_flags(&test, &[]); + assert_ne!(by_flag, by_default, "the fixture must be window-sensitive"); + + let sandbox = Sandbox::new(); + let config = sandbox.explicit(SHORT_WINDOW_CONFIG); + let by_config = sandbox.stdout_ok( + &["-a", "history/anomaly", "--config", &config, path(&test)], + &[], + ); + assert_eq!(by_config, by_flag); +} + +#[test] +fn an_explicit_flag_beats_the_config_file() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let config = sandbox.explicit(SHORT_WINDOW_CONFIG); + let out = sandbox.stdout_ok( + &[ + "-a", + "history/anomaly", + "--config", + &config, + "--anomaly-window", + DEFAULT_WINDOW, + path(&test), + ], + &[], + ); + assert_eq!(out, anomaly_with_flags(&test, &[])); +} + +#[test] +fn the_environment_beats_the_config_file_and_the_flag_beats_both() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let config = sandbox.explicit("history:\n anomaly:\n window_size: 20\n"); + let env = [("CODEFANG_HISTORY_ANOMALY_WINDOW_SIZE", SHORT_WINDOW)]; + let args = ["-a", "history/anomaly", "--config", &config, path(&test)]; + assert_eq!( + sandbox.stdout_ok(&args, &env), + anomaly_with_flags(&test, &["--anomaly-window", SHORT_WINDOW]) + ); + + let mut explicit = args.to_vec(); + explicit.extend_from_slice(&["--anomaly-window", DEFAULT_WINDOW]); + assert_eq!( + sandbox.stdout_ok(&explicit, &env), + anomaly_with_flags(&test, &[]) + ); +} + +#[test] +fn the_config_is_discovered_in_the_working_directory() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + Sandbox::write(sandbox.cwd.path(), CONFIG_NAME, SHORT_WINDOW_CONFIG); + // A HOME config must lose to the working-directory one. + Sandbox::write( + sandbox.home.path(), + CONFIG_NAME, + "history:\n anomaly:\n window_size: 20\n", + ); + let out = sandbox.stdout_ok(&["-a", "history/anomaly", path(&test)], &[]); + assert_eq!( + out, + anomaly_with_flags(&test, &["--anomaly-window", SHORT_WINDOW]) + ); +} + +#[test] +fn the_config_is_discovered_in_home() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + Sandbox::write(sandbox.home.path(), CONFIG_NAME, SHORT_WINDOW_CONFIG); + let out = sandbox.stdout_ok(&["-a", "history/anomaly", path(&test)], &[]); + assert_eq!( + out, + anomaly_with_flags(&test, &["--anomaly-window", SHORT_WINDOW]) + ); +} + +#[test] +fn config_analyzers_are_the_default_selection() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let config = sandbox.explicit("analyzers:\n - history/devs\n"); + let by_config = sandbox.stdout_ok(&["--config", &config, path(&test)], &[]); + let by_flag = sandbox.stdout_ok( + &["--config", "/dev/null", "-a", "history/devs", path(&test)], + &[], + ); + assert_eq!(by_config, by_flag); + + // `-a` on the command line replaces the configured selection. + let explicit = sandbox.stdout_ok( + &["--config", &config, "-a", "history/anomaly", path(&test)], + &[], + ); + assert_eq!(explicit, anomaly_with_flags(&test, &[])); +} + +#[test] +fn a_missing_explicit_config_is_an_error() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let missing = sandbox.cwd.path().join("missing.yaml"); + let missing = missing.to_str().expect("utf-8 path"); + let out = sandbox.run( + &["-a", "history/anomaly", "--config", missing, path(&test)], + &[], + ); + assert_eq!(out.status.code(), Some(1)); + assert!(out.stdout.is_empty(), "no report on a config error"); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains(&format!("Error: cannot read --config {missing}: ")), + "{err}" + ); +} + +#[test] +fn an_invalid_config_value_names_the_key() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let config = sandbox.explicit("history:\n anomaly:\n window_size: 1\n"); + let out = sandbox.run( + &["-a", "history/anomaly", "--config", &config, path(&test)], + &[], + ); + assert_eq!(out.status.code(), Some(1)); + assert!(out.stdout.is_empty()); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains(&format!( + "Error: invalid config {config}: history.anomaly.window_size must be >= 2, got 1" + )), + "{err}" + ); +} + +#[test] +fn unsupported_keys_warn_once_and_the_rest_applies() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let config = sandbox.explicit( + "pipeline:\n workers: 8\n max_changes_per_commit: 10000\n\ + checkpoint:\n enabled: false\n\ + history:\n burndown:\n goroutines: 4\n anomaly:\n window_size: 3\n", + ); + let out = sandbox.run( + &["-a", "history/anomaly", "--config", &config, path(&test)], + &[], + ); + assert!(out.status.success()); + let err = String::from_utf8_lossy(&out.stderr); + let warnings: Vec<&str> = err + .lines() + .filter(|l| l.starts_with("warning: ignoring unsupported config keys")) + .collect(); + assert_eq!( + warnings, + vec![ + "warning: ignoring unsupported config keys: pipeline.workers, \ + checkpoint.enabled, history.burndown.goroutines" + ], + "{err}" + ); + assert_eq!( + String::from_utf8(out.stdout).expect("utf-8"), + anomaly_with_flags(&test, &["--anomaly-window", SHORT_WINDOW]) + ); +} + +#[test] +fn the_repository_config_file_loads() { + let repo_config = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.codefang.yaml"); + let repo_config = repo_config.to_str().expect("utf-8 path"); + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let out = sandbox.run( + &[ + "-a", + "history/anomaly", + "--config", + repo_config, + path(&test), + ], + &[], + ); + let err = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "{err}"); + assert!(!err.contains("warning:"), "{err}"); +} + +#[test] +fn verbose_names_the_config_file() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let config = sandbox.explicit(SHORT_WINDOW_CONFIG); + let out = sandbox.run( + &[ + "-v", + "-a", + "history/anomaly", + "--config", + &config, + path(&test), + ], + &[], + ); + assert!(out.status.success()); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains(&format!("progress: config file: {config}")), + "{err}" + ); + + let out = sandbox.run(&["-v", "-a", "history/anomaly", path(&test)], &[]); + let err = String::from_utf8_lossy(&out.stderr); + assert!(err.contains("progress: config file: none"), "{err}"); +} + +/// `pipeline.chunk_size` and `pipeline.memory_budget` default their flags: +/// the configured chunk reaches the walk's plan (`-v`), the environment +/// beats the file, and an out-of-range value fails like the flag. +#[test] +fn the_pipeline_budget_keys_default_their_flags() { + let test = burst_fixture(); + let sandbox = Sandbox::new(); + let p = path(&test); + + let config = sandbox.explicit("pipeline:\n chunk_size: 3\n"); + let out = sandbox.run(&["-v", "-a", "history/devs", "--config", &config, p], &[]); + let err = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "{err}"); + assert!( + err.contains("progress: history/devs plan: 3 commits per chunk, "), + "{err}" + ); + + // The plan line names the walk's share of the budget: the environment's + // 1 GiB beats the file's 512 MiB exactly like the flag would. + let config = sandbox.explicit("pipeline:\n memory_budget: 512MiB\n"); + let share = |args: &[&str], envs: &[(&str, &str)]| { + let out = sandbox.run(args, envs); + let err = String::from_utf8_lossy(&out.stderr).into_owned(); + assert!(out.status.success(), "{err}"); + err.lines() + .find_map(|l| { + l.strip_suffix(" MiB)")? + .rsplit_once("(budget ")? + .1 + .parse::() + .ok() + }) + .unwrap_or_else(|| panic!("no plan line in {err}")) + }; + let from_file = share(&["-v", "-a", "history/devs", "--config", &config, p], &[]); + let from_env = share( + &["-v", "-a", "history/devs", "--config", &config, p], + &[("CODEFANG_PIPELINE_MEMORY_BUDGET", "1GiB")], + ); + let from_flag = share( + &["-v", "-a", "history/devs", "--memory-budget", "1GiB", p], + &[], + ); + assert_eq!(from_env, from_flag); + assert!(from_env > from_file, "{from_env} <= {from_file}"); + + let config = sandbox.explicit("pipeline:\n memory_budget: 1MB\n"); + let out = sandbox.run(&["-a", "history/devs", "--config", &config, p], &[]); + assert_eq!(out.status.code(), Some(1)); + assert!( + String::from_utf8_lossy(&out.stderr) + .contains("Error: invalid --memory-budget: \"1MB\" is below the minimum 256 MiB"), + "{}", + String::from_utf8_lossy(&out.stderr) + ); + + let config = sandbox.explicit("pipeline:\n chunk_size: 0\n"); + let out = sandbox.run(&["-a", "history/devs", "--config", &config, p], &[]); + assert_eq!(out.status.code(), Some(1)); + assert!( + String::from_utf8_lossy(&out.stderr).contains(&format!( + "Error: invalid config {config}: pipeline.chunk_size must be >= 1, got 0" + )), + "{}", + String::from_utf8_lossy(&out.stderr) + ); +} diff --git a/bins/codefang/tests/couples_e2e.rs b/bins/codefang/tests/couples_e2e.rs new file mode 100644 index 00000000..43a7d02c --- /dev/null +++ b/bins/codefang/tests/couples_e2e.rs @@ -0,0 +1,489 @@ +//! End-to-end coverage of `history/couples`, driven through the **real +//! binary** over real on-disk git fixtures +//! (`specs/bugs/BUG-history-couples-coupling.md`). +//! +//! Every number asserted here is computed by hand from the fixture per the +//! definitions in `site/explanation/couples.md`: a file pair's `co_changes` +//! is the number of walked commits in which BOTH files changed (symmetric, +//! the diagonal is the file's own change count); the developer matrix sums +//! `min(changes_i, changes_j)` over the files both developers changed; the +//! report lists the files present at the window head. + +mod common; + +use std::process::Command; + +use cf_gitlib::testutil::{commit_files_as, init_repo, CommitBuilder, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits (one default tick each). +const DAY: i64 = 24 * 3_600; +/// The fixture developers. +const ALICE: (&str, &str) = ("alice", "alice@example.com"); +const BOB: (&str, &str) = ("bob", "bob@example.com"); +/// The documented changeset cap: a commit touching MORE files than this is +/// excluded from the coupling matrix (but still counts for ownership). +const CAP: usize = cf_couples::COUPLES_MAXIMUM_MEANINGFUL_CONTEXT_SIZE; +/// Tolerance for the hand-computed coupling strengths. +const EPS: f64 = 1e-9; + +fn codefang_with(mut cmd: Command, args: &[&str]) -> std::process::Output { + cmd.arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + codefang_with(common::command(BIN), args) +} + +/// stdout of a successful run, as text. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// A `history/couples` run in `format` over `repo`, with `extra` flags. +fn couples(repo: &TestRepo, format: &str, extra: &[&str]) -> String { + let mut args = vec!["-a", "history/couples", "--format", format]; + args.extend_from_slice(extra); + args.push(path(repo)); + stdout_ok(&args) +} + +/// The couples JSON report over `repo`. +fn couples_json(repo: &TestRepo, extra: &[&str]) -> serde_json::Value { + let text = couples(repo, "json", extra); + serde_json::from_str(&text) + .unwrap_or_else(|e| panic!("stdout is not one JSON document ({e}):\n{text}")) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// The `file_coupling` entry for the unordered pair `{x, y}`, if any. +fn pair<'a>(report: &'a serde_json::Value, x: &str, y: &str) -> Option<&'a serde_json::Value> { + report["file_coupling"] + .as_array()? + .iter() + .find(|e| (e["file1"] == x && e["file2"] == y) || (e["file1"] == y && e["file2"] == x)) +} + +/// The `file_ownership` entry of `file`. +fn ownership<'a>(report: &'a serde_json::Value, file: &str) -> &'a serde_json::Value { + report["file_ownership"] + .as_array() + .expect("file_ownership") + .iter() + .find(|e| e["file"] == file) + .unwrap_or_else(|| panic!("no ownership entry for {file} in {report}")) +} + +/// The sorted file names listed under `file_ownership`. +fn owned_files(report: &serde_json::Value) -> Vec { + let mut names: Vec = report["file_ownership"] + .as_array() + .expect("file_ownership") + .iter() + .map(|e| e["file"].as_str().expect("file").to_string()) + .collect(); + names.sort(); + names +} + +fn assert_close(actual: f64, expected: f64, what: &str) { + assert!( + (actual - expected).abs() < EPS, + "{what}: expected {expected}, got {actual}" + ); +} + +/// Four linear commits by two developers: +/// `c1 alice {a,b}`, `c2 bob {a,b,c}`, `c3 alice {c}`, `c4 bob {a}`. +fn linear_fixture() -> TestRepo { + let test = init_repo().expect("init"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c1 {a,b}", + T0, + &[("a", b"a1\na2\n"), ("b", b"b1\n")], + ) + .expect("c1"); + commit_files_as( + &test, + BOB.0, + BOB.1, + "c2 {a,b,c}", + T0 + DAY, + &[("a", b"a1\na2\na3\n"), ("b", b"b1\nb2\n"), ("c", b"c1\n")], + ) + .expect("c2"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c3 {c}", + T0 + 2 * DAY, + &[("c", b"c1\nc2\n")], + ) + .expect("c3"); + commit_files_as( + &test, + BOB.0, + BOB.1, + "c4 {a}", + T0 + 3 * DAY, + &[("a", b"a1\na2\na3\na4\n")], + ) + .expect("c4"); + test +} + +/// The file matrix, the developer matrix and the ownership of the linear +/// fixture, by hand: a-b 2, a-c 1, b-c 1, diagonal a 3 / b 2 / c 2; alice +/// {a:1,b:1,c:1} vs bob {a:2,b:1,c:1} share 3; every file has 2 +/// contributors and its line count at the head. +#[test] +fn file_and_developer_matrices_follow_the_definition() { + let test = linear_fixture(); + let report = couples_json(&test, &[]); + + let ab = pair(&report, "a", "b").expect("a-b"); + assert_eq!(ab["co_changes"], 2, "{report}"); + assert_close(ab["coupling_strength"].as_f64().unwrap(), 2.0 / 2.5, "a-b"); + let ac = pair(&report, "a", "c").expect("a-c"); + assert_eq!(ac["co_changes"], 1, "{report}"); + assert_close(ac["coupling_strength"].as_f64().unwrap(), 1.0 / 2.5, "a-c"); + let bc = pair(&report, "b", "c").expect("b-c"); + assert_eq!(bc["co_changes"], 1, "{report}"); + assert_close(bc["coupling_strength"].as_f64().unwrap(), 1.0 / 2.0, "b-c"); + assert_eq!(report["file_coupling"].as_array().unwrap().len(), 3); + assert_eq!( + report["file_coupling"][0]["co_changes"], 2, + "pairs are co-change-descending: {report}" + ); + + let devs = report["developer_coupling"] + .as_array() + .expect("developer_coupling"); + assert_eq!(devs.len(), 1, "{report}"); + assert_eq!(devs[0]["developer1"], "alice"); + assert_eq!(devs[0]["developer2"], "bob"); + assert_eq!(devs[0]["shared_file_changes"], 3); + assert_close( + devs[0]["coupling_strength"].as_f64().unwrap(), + 3.0 / 3.5, + "alice-bob", + ); + + assert_eq!(owned_files(&report), ["a", "b", "c"]); + for (file, lines) in [("a", 4), ("b", 2), ("c", 2)] { + let own = ownership(&report, file); + assert_eq!(own["lines"], lines, "{file} lines: {report}"); + assert_eq!(own["contributors"], 2, "{file} contributors: {report}"); + } + + let agg = &report["aggregate"]; + assert_eq!(agg["total_files"], 3); + assert_eq!(agg["total_developers"], 2); + assert_eq!(agg["total_co_changes"], 4); + assert_eq!(agg["highly_coupled_pairs"], 0); + assert_close( + agg["avg_coupling_strength"].as_f64().unwrap(), + (0.8 + 0.4 + 0.5) / 3.0, + "avg_coupling_strength is the mean of the pair strengths", + ); +} + +/// A `git mv b d` carries b's history to d: one row `d` with b's co-changes +/// plus the rename commit, and no `b` row. A deleted file leaves the report. +#[test] +fn renames_merge_the_old_row_and_deleted_files_leave_the_report() { + let test = linear_fixture(); + CommitBuilder::new(&test, "c5 rename b->d", T0 + 4 * DAY) + .author(ALICE.0, ALICE.1) + .rename("b", "d") + .commit() + .expect("c5"); + CommitBuilder::new(&test, "c6 delete c", T0 + 5 * DAY) + .author(BOB.0, BOB.1) + .delete("c") + .commit() + .expect("c6"); + let report = couples_json(&test, &[]); + + assert_eq!( + owned_files(&report), + ["a", "d"], + "b merged into d, c deleted: {report}" + ); + assert_eq!(report["aggregate"]["total_files"], 2, "{report}"); + + // a: c1, c2, c4 = 3 changes; d: b's c1, c2 + the rename c5 = 3 changes; + // a-d: c1, c2 = 2. + let ad = pair(&report, "a", "d").expect("a-d"); + assert_eq!(ad["co_changes"], 2, "{report}"); + assert_close(ad["coupling_strength"].as_f64().unwrap(), 2.0 / 3.0, "a-d"); + assert_eq!(report["file_coupling"].as_array().unwrap().len(), 1); + + // Ownership follows the rename: alice (b in c1, rename c5) and bob (b in + // c2) both own d; d has b's 2 lines at the head. + let d = ownership(&report, "d"); + assert_eq!(d["contributors"], 2, "{report}"); + assert_eq!(d["lines"], 2, "{report}"); + + // Developer matrix over the head files: alice {a:1, d:2}, bob {a:2, d:1} + // share min(1,2) + min(2,1) = 2 out of 3 / 3 self changes. + let devs = &report["developer_coupling"][0]; + assert_eq!(devs["shared_file_changes"], 2, "{report}"); + assert_close( + devs["coupling_strength"].as_f64().unwrap(), + 2.0 / 3.0, + "alice-bob after rename", + ); + + // The rename is reported on its commit's ndjson record. + let ndjson = couples(&test, "ndjson", &[]); + assert!( + ndjson.contains(r#""Renames":[{"FromName":"b","ToName":"d"}]"#), + "{ndjson}" + ); +} + +/// A rename that also edits the file counts as one change of the new path. +#[test] +fn an_edited_rename_counts_the_modification_on_the_new_path() { + let body: Vec = (1..=100) + .map(|i| format!("line {i}\n")) + .collect::() + .into_bytes(); + let test = init_repo().expect("init"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c1", + T0, + &[("old.txt", body.as_slice()), ("peer.txt", b"p\n")], + ) + .expect("c1"); + let mut edited = body.clone(); + edited.extend_from_slice(b"line 101\n"); + CommitBuilder::new(&test, "c2 mv+edit", T0 + DAY) + .author(BOB.0, BOB.1) + .delete("old.txt") + .write("new.txt", &edited) + .write("peer.txt", b"p\nq\n") + .commit() + .expect("c2"); + let report = couples_json(&test, &[]); + + assert_eq!(owned_files(&report), ["new.txt", "peer.txt"], "{report}"); + let pair = pair(&report, "new.txt", "peer.txt").expect("new-peer"); + assert_eq!(pair["co_changes"], 2, "c1 (as old.txt) and c2: {report}"); + assert_eq!(ownership(&report, "new.txt")["contributors"], 2, "{report}"); + assert_eq!(ownership(&report, "new.txt")["lines"], 101, "{report}"); +} + +/// Alice's main line plus Bob's side line merged into it: +/// `c1 alice {a}` ← `side bob {b,c}` ; `c3 alice {a}` ; `merge alice`. +fn merge_fixture() -> TestRepo { + let test = init_repo().expect("init"); + let c1 = commit_files_as(&test, ALICE.0, ALICE.1, "c1", T0, &[("a", b"a1\n")]).expect("c1"); + let side = CommitBuilder::new(&test, "side", T0 + DAY) + .author(BOB.0, BOB.1) + .parent(c1) + .detached() + .write("b", b"b1\n") + .write("c", b"c1\n") + .commit() + .expect("side"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c3", + T0 + 2 * DAY, + &[("a", b"a1\na2\n")], + ) + .expect("c3"); + CommitBuilder::new(&test, "merge", T0 + 3 * DAY) + .author(ALICE.0, ALICE.1) + .merge_parent(side) + .write("b", b"b1\n") + .write("c", b"c1\n") + .commit() + .expect("merge"); + test +} + +/// A change merged in from a branch is counted once, at its own commit: the +/// merge neither re-couples b and c nor credits the merger with them. Under +/// `--first-parent` the side line is not walked, so the merge IS the one +/// commit that changes b and c. +#[test] +fn merged_in_changes_count_once() { + let test = merge_fixture(); + let report = couples_json(&test, &[]); + let bc = pair(&report, "b", "c").expect("b-c"); + assert_eq!(bc["co_changes"], 1, "{report}"); + assert_eq!( + ownership(&report, "b")["contributors"], + 1, + "bob only: {report}" + ); + assert_eq!( + ownership(&report, "c")["contributors"], + 1, + "bob only: {report}" + ); + assert_eq!( + ownership(&report, "a")["contributors"], + 1, + "alice only: {report}" + ); + assert!( + report["developer_coupling"].as_array().unwrap().is_empty(), + "alice and bob changed disjoint files: {report}" + ); + assert_eq!(report["aggregate"]["total_developers"], 2); + + let first_parent = couples_json(&test, &["--first-parent"]); + let bc = pair(&first_parent, "b", "c").expect("b-c first-parent"); + assert_eq!(bc["co_changes"], 1, "{first_parent}"); + assert_eq!( + first_parent["aggregate"]["total_developers"], 1, + "{first_parent}" + ); + assert_eq!( + ownership(&first_parent, "b")["contributors"], + 1, + "alice, via the merge: {first_parent}" + ); +} + +/// A commit touching more files than the cap stays out of the coupling +/// matrix but still counts for ownership, keeps its real hash in ndjson and +/// lists its author files there. +#[test] +fn oversized_commits_keep_ownership_and_their_hash() { + let test = init_repo().expect("init"); + commit_files_as(&test, ALICE.0, ALICE.1, "c1", T0, &[("x.txt", b"x\n")]).expect("c1"); + let mut big = CommitBuilder::new(&test, "c2 big", T0 + DAY).author(ALICE.0, ALICE.1); + let names: Vec = (1..=CAP + 1).map(|i| format!("f{i}.txt")).collect(); + for name in &names { + big = big.write(name, b"f\n"); + } + let big_hash = big.commit().expect("c2").to_string(); + commit_files_as( + &test, + BOB.0, + BOB.1, + "c3", + T0 + 2 * DAY, + &[("x.txt", b"x\ny\n"), ("f1.txt", b"f\nmod\n")], + ) + .expect("c3"); + let report = couples_json(&test, &[]); + + // Coupling: only c3's pair. + assert_eq!( + report["file_coupling"].as_array().unwrap().len(), + 1, + "{report}" + ); + assert_eq!(pair(&report, "f1.txt", "x.txt").unwrap()["co_changes"], 1); + // Ownership: every file at the head, with the oversized commit's author. + assert_eq!( + report["aggregate"]["total_files"].as_u64().unwrap() as usize, + CAP + 2, + "{}", + report["aggregate"] + ); + assert_eq!(ownership(&report, "f2.txt")["contributors"], 1); + assert_eq!(ownership(&report, "f1.txt")["contributors"], 2); + assert_eq!(ownership(&report, "x.txt")["contributors"], 2); + + let ndjson = couples(&test, "ndjson", &[]); + let line = ndjson + .lines() + .find(|l| l.contains(&big_hash)) + .unwrap_or_else(|| panic!("no record with the real hash of the big commit:\n{ndjson}")); + assert!(line.contains(r#""CouplingFiles":[]"#), "{line}"); + let record: serde_json::Value = serde_json::from_str(line).expect("ndjson line"); + assert_eq!( + record["data"]["AuthorFiles"].as_object().unwrap().len(), + CAP + 1, + "{line}" + ); + assert!( + !ndjson.contains("0000000000000000000000000000000000000000"), + "{ndjson}" + ); +} + +/// Every timeseries commit carries its own tick (daily commits ⇒ 0, 1, 2, 3) +/// and the number of files its author touched. +#[test] +fn timeseries_reports_the_commit_tick() { + let test = linear_fixture(); + let text = couples(&test, "timeseries", &[]); + let doc: serde_json::Value = serde_json::from_str(&text).expect("timeseries json"); + let ticks: Vec = doc["commits"] + .as_array() + .expect("commits") + .iter() + .map(|c| c["tick"].as_i64().expect("tick")) + .collect(); + assert_eq!(ticks, vec![0, 1, 2, 3], "{text}"); + let touched: Vec = doc["commits"] + .as_array() + .unwrap() + .iter() + .map(|c| { + c["couples"]["files_touched"] + .as_i64() + .expect("files_touched") + }) + .collect(); + assert_eq!(touched, vec![2, 3, 1, 1], "{text}"); +} + +/// Two runs are byte-identical; json and yaml carry the same numbers. +#[test] +fn output_is_deterministic_and_formats_agree() { + let test = linear_fixture(); + let first = couples(&test, "json", &[]); + let second = couples(&test, "json", &[]); + assert_eq!(first, second); + + let yaml = couples(&test, "yaml", &[]); + for line in [ + "co_changes: 2", + "shared_file_changes: 3", + "total_files: 3", + "total_co_changes: 4", + "lines: 4", + ] { + assert!(yaml.contains(line), "yaml lacks `{line}`:\n{yaml}"); + } + let text = couples(&test, "text", &[]); + assert!(text.contains("Total Co-Changes 4"), "{text}"); +} diff --git a/bins/codefang/tests/devs_e2e.rs b/bins/codefang/tests/devs_e2e.rs new file mode 100644 index 00000000..69a45053 --- /dev/null +++ b/bins/codefang/tests/devs_e2e.rs @@ -0,0 +1,410 @@ +//! End-to-end coverage of `history/devs` through the **real binary** over +//! on-disk git fixtures (`specs/bugs/BUG-history-devs-stats.md`). +//! +//! Every number is computed by hand from the fixture: commits and lines +//! added / removed / changed per developer with the per-language breakdown, +//! the bus factor, empty blobs, empty commits, the merged timeseries, and +//! determinism. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, CommitBuilder, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// 2021-01-01T12:00:00Z, the fixture's first commit. +const T0: i64 = 1_609_502_400; +/// One day. +const DAY: i64 = 24 * 3_600; +/// Days between the fixture's main-line commits. +const SPACING_DAYS: i64 = 31; +/// Fixture developers. +const ALICE: (&str, &str) = ("Alice", "alice@example.com"); +const BOB: (&str, &str) = ("Bob", "bob@example.com"); +const CAROL: (&str, &str) = ("Carol", "carol@example.com"); + +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +fn json_ok(args: &[&str]) -> serde_json::Value { + let text = stdout_ok(args); + serde_json::from_str(&text) + .unwrap_or_else(|e| panic!("stdout is not one JSON document ({e}):\n{text}")) +} + +fn devs_json(repo: &str, extra: &[&str]) -> serde_json::Value { + json_ok(&[&["-a", "history/devs", "--format", "json"], extra, &[repo]].concat()) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +fn lines(items: &[&str]) -> Vec { + let mut s = items.join("\n"); + s.push('\n'); + s.into_bytes() +} + +fn day(n: i64) -> i64 { + T0 + n * SPACING_DAYS * DAY +} + +/// The same fixture A as `burndown_e2e.rs` (see its table). The devs walk is +/// NOT first-parent, so Carol's side-line commit `cb1` is visited too. +fn fixture_a() -> TestRepo { + let test = init_repo().expect("init"); + let a10 = lines(&["l1", "l2", "l3", "l4", "l5", "l6", "l7", "l8", "l9", "l10"]); + let a12 = lines(&[ + "l1", "l3", "l4", "l6", "l7", "l9", "l10", "n1", "n2", "n3", "n4", "n5", + ]); + let commit = |msg: &str, n: i64, who: (&str, &str)| { + CommitBuilder::new(&test, msg, day(n)).author(who.0, who.1) + }; + commit("c1", 0, ALICE) + .write("a.go", &a10) + .commit() + .expect("c1"); + commit("c2", 1, BOB) + .write("a.go", &a12) + .commit() + .expect("c2"); + commit("c3", 2, ALICE) + .rename("a.go", "b.go") + .commit() + .expect("c3"); + commit("c4", 3, ALICE) + .write("img.bin", b"BIN\x00\x01\x02DATA\n") + .write("script.py", &lines(&["p1", "p2", "p3", "p4"])) + .commit() + .expect("c4"); + let c5 = commit("c5", 4, BOB) + .write("script.py", &lines(&["p1", "P2", "p3", "p4", "p5", "p6"])) + .commit() + .expect("c5"); + commit("c6", 5, BOB) + .write( + "script.py", + &lines(&["p1", "P2", "p3", "p4", "p5", "p6", "p7"]), + ) + .commit() + .expect("c6"); + let feat6 = lines(&["f1", "f2", "f3", "f4", "f5", "f6"]); + let cb1 = CommitBuilder::new(&test, "cb1", day(5) + 6 * 3_600) + .author(CAROL.0, CAROL.1) + .parent(c5) + .detached() + .write("feat.go", &feat6) + .delete("b.go") + .commit() + .expect("cb1"); + commit("merge", 6, ALICE) + .merge_parent(cb1) + .write("feat.go", &feat6) + .delete("b.go") + .commit() + .expect("merge"); + commit("c7", 7, ALICE) + .write("vendor/lib/x.go", &lines(&["v1", "v2", "v3", "v4", "v5"])) + .commit() + .expect("c7"); + commit("c8", 8, ("Alice Smith", "alice@example.com")) + .write( + "feat.go", + &lines(&["f1", "f2", "f3", "f4", "f5", "f6", "f7"]), + ) + .commit() + .expect("c8"); + commit("c9", 9, ALICE).commit().expect("c9"); + commit("c10", 10, ("Alice Smith", "asmith@other.com")) + .write( + "script.py", + &lines(&["p1", "P2", "p3", "p4", "p5", "p6", "p7", "p8"]), + ) + .commit() + .expect("c10"); + test +} + +/// `(name, commits, added, removed, changed, [(language, added, removed, changed)])`. +type DevRow = (String, i64, i64, i64, i64, Vec<(String, i64, i64, i64)>); + +fn developer_rows(report: &serde_json::Value) -> Vec { + report["developers"] + .as_array() + .unwrap() + .iter() + .map(|d| { + let langs = d["languages"] + .as_array() + .map(|ls| { + ls.iter() + .map(|l| { + ( + l["language"].as_str().unwrap().to_string(), + l["added"].as_i64().unwrap(), + l["removed"].as_i64().unwrap(), + l["changed"].as_i64().unwrap(), + ) + }) + .collect() + }) + .unwrap_or_default(); + ( + d["name"].as_str().unwrap().to_string(), + d["commits"].as_i64().unwrap(), + d["lines_added"].as_i64().unwrap(), + d["lines_removed"].as_i64().unwrap(), + d["lines_changed"].as_i64().unwrap(), + langs, + ) + }) + .collect() +} + +#[test] +fn per_developer_stats_match_the_hand_computed_fixture() { + // The loose identity detector lowercases names. + let test = fixture_a(); + let report = devs_json(path(&test), &[]); + let agg = &report["aggregate"]; + // c7 (vendored only) and c9 (empty) are not counted by default. + assert_eq!(agg["total_commits"], 10, "{agg}"); + assert_eq!(agg["total_developers"], 3); + assert_eq!(agg["total_lines_added"], 30); + assert_eq!(agg["total_lines_removed"], 15); + assert_eq!(agg["analysis_period_ticks"], 310); + assert_eq!(agg["total_languages"], 2); + assert_eq!(agg["estimated_total_developers"], 3); + + let rows = developer_rows(&report); + assert_eq!( + rows, + vec![ + ( + "alice".to_string(), + 6, + 16, + 0, + 0, + vec![ + ("Go".to_string(), 11, 0, 0), + ("Python".to_string(), 5, 0, 0), + ], + ), + ( + "bob".to_string(), + 3, + 8, + 3, + 1, + vec![("Go".to_string(), 5, 3, 0), ("Python".to_string(), 3, 0, 1),], + ), + ( + "carol".to_string(), + 1, + 6, + 12, + 0, + vec![("Go".to_string(), 6, 12, 0)], + ), + ] + ); + let alice = &report["developers"][0]; + assert_eq!(alice["email"], "alice@example.com"); + assert_eq!(alice["net_lines"], 16); + assert_eq!(alice["first_tick"], 0); + assert_eq!(alice["last_tick"], 310); + assert_eq!(alice["active_ticks"], 6); +} + +#[test] +fn bus_factor_emits_a_secondary_developer_with_id_zero() { + let test = fixture_a(); + let report = devs_json(path(&test), &[]); + let go = report["busfactor"] + .as_array() + .unwrap() + .iter() + .find(|b| b["language"] == "Go") + .expect("Go bus factor"); + // Go contributions (added + removed): Carol 18, Alice 11, Bob 8 of 37. + assert_eq!(go["bus_factor"], 2, "{go}"); + assert_eq!(go["total_contributors"], 3); + assert_eq!(go["primary_dev_id"], 2); + assert_eq!(go["primary_dev_name"], "carol"); + assert_eq!( + go["secondary_dev_id"], 0, + "Alice is id 0 and must be present" + ); + assert_eq!(go["secondary_dev_name"], "alice"); + assert_eq!(go["risk_level"], "LOW"); + let py = report["busfactor"] + .as_array() + .unwrap() + .iter() + .find(|b| b["language"] == "Python") + .expect("Python bus factor"); + // Python: Alice 5, Bob 3 of 8 → 62.5 %. + assert_eq!(py["bus_factor"], 1); + assert_eq!(py["primary_dev_id"], 0); + assert_eq!(py["secondary_dev_id"], 1); + assert_eq!(py["primary_percentage"], 62.5); + assert_eq!(py["risk_level"], "MEDIUM"); + assert_eq!(report["aggregate"]["project_bus_factor"], 2); +} + +#[test] +fn empty_commits_flag_counts_commits_without_changes() { + let test = fixture_a(); + let p = path(&test); + let report = devs_json(p, &["--empty-commits"]); + assert_eq!(report["aggregate"]["total_commits"], 12, "{report}"); + let alice = developer_rows(&report) + .into_iter() + .find(|r| r.0 == "alice") + .unwrap(); + assert_eq!(alice.1, 8, "c7 (vendored only) and c9 (empty) count"); + assert_eq!((alice.2, alice.3), (16, 0), "lines are unchanged"); + let ndjson = stdout_ok(&[ + "-a", + "history/devs", + "--format", + "ndjson", + "--empty-commits", + p, + ]); + assert_eq!(ndjson.lines().count(), 12); +} + +#[test] +fn zero_byte_blobs_are_ordinary_empty_text() { + let test = init_repo().expect("init"); + commit_files(&test, "c1", T0, &[("empty.txt", b""), ("a.txt", b"a1\n")]).expect("c1"); + commit_files(&test, "c2", T0 + DAY, &[("empty.txt", b"e1\ne2\n")]).expect("c2"); + commit_files(&test, "c3", T0 + 2 * DAY, &[("empty.txt", b"")]).expect("c3"); + let report = devs_json(path(&test), &[]); + let dev = &report["developers"][0]; + assert_eq!(dev["commits"], 3, "{report}"); + assert_eq!(dev["lines_added"], 3, "a.txt 1 + empty.txt 0→2"); + assert_eq!(dev["lines_removed"], 2, "empty.txt 2→0"); + assert_eq!(dev["lines_changed"], 0); +} + +#[test] +fn timeseries_lists_the_commits_in_tick_order() { + let test = fixture_a(); + let p = path(&test); + let ts = json_ok(&["-a", "history/devs", "--format", "timeseries", p]); + assert_eq!(ts["analyzers"], serde_json::json!(["devs"])); + let commits = ts["commits"].as_array().expect("commits"); + assert_eq!(commits.len(), 10, "{ts}"); + let ticks: Vec = commits + .iter() + .map(|c| c["tick"].as_i64().unwrap()) + .collect(); + assert_eq!(ticks, vec![0, 31, 62, 93, 124, 155, 155, 186, 248, 310]); + assert_eq!(commits[1]["devs"]["lines_removed"], 3); + assert_eq!(commits[1]["timestamp"], "2021-02-01T12:00:00Z"); + assert_eq!(commits[6]["devs"]["author_id"], 2, "cb1 is Carol's"); + + let ndjson = stdout_ok(&[ + "-a", + "history/devs", + "--format", + "timeseries", + "--ndjson", + p, + ]); + assert_eq!(ndjson.lines().count(), 10, "{ndjson}"); +} + +#[test] +fn json_and_yaml_agree_and_runs_are_deterministic() { + let test = fixture_a(); + let p = path(&test); + let a = stdout_ok(&["-a", "history/devs", "--format", "json", p]); + let b = stdout_ok(&["-a", "history/devs", "--format", "json", p]); + assert_eq!(a, b, "two runs must be byte-identical"); + let yaml = stdout_ok(&["-a", "history/devs", "--format", "yaml", p]); + assert!(yaml.contains(" total_commits: 10"), "{yaml}"); + assert!(yaml.contains("name: alice"), "{yaml}"); + assert!(yaml.contains(" secondary_dev_id: 0"), "{yaml}"); + assert!(yaml.contains(" estimated_total_developers: 3"), "{yaml}"); +} + +/// Commits dropped by `--max-changes-per-commit` must not shift devs' ndjson +/// records: every analyzer keys its records on the commit's window position, +/// so a multi-analyzer ndjson stream stays in commit order. +#[test] +fn capped_commits_keep_the_merged_ndjson_in_commit_order() { + let test = init_repo().expect("init fixture repo"); + let mut hashes = Vec::new(); + // c0 touches three files (dropped by a cap of 2); c1..c3 touch one each. + hashes.push( + commit_files( + &test, + "c0", + T0, + &[("a.txt", b"a\n"), ("b.txt", b"b\n"), ("c.txt", b"c\n")], + ) + .expect("c0"), + ); + for i in 1..4_i64 { + let body = format!("line {i}\n"); + hashes.push( + commit_files( + &test, + &format!("c{i}"), + T0 + i * DAY, + &[("a.txt", body.as_bytes())], + ) + .expect("commit"), + ); + } + let order: Vec = hashes.iter().map(ToString::to_string).collect(); + let out = stdout_ok(&[ + "-a", + "history/devs,history/burndown", + "--format", + "ndjson", + "--max-changes-per-commit", + "2", + path(&test), + ]); + let positions: Vec = out + .lines() + .map(|line| { + let v: serde_json::Value = serde_json::from_str(line).expect("ndjson line"); + let hash = v["hash"].as_str().expect("hash").to_string(); + order.iter().position(|h| *h == hash).expect("known hash") + }) + .collect(); + assert!( + !positions.contains(&0), + "the capped commit must be dropped: {out}" + ); + assert!( + positions.windows(2).all(|w| w[0] <= w[1]), + "records out of commit order {positions:?}:\n{out}" + ); +} diff --git a/bins/codefang/tests/file_history_e2e.rs b/bins/codefang/tests/file_history_e2e.rs new file mode 100644 index 00000000..173d3aa4 --- /dev/null +++ b/bins/codefang/tests/file_history_e2e.rs @@ -0,0 +1,426 @@ +//! End-to-end coverage of `history/file-history`, driven through the **real +//! binary** over real on-disk git fixtures +//! (`specs/bugs/BUG-history-file-history-lifecycle.md`). +//! +//! Every number asserted here is computed by hand from the fixture per +//! `site/explanation/file-history.md`: a file's commit list follows its +//! renames, every commit's line stats are attributed to its author, only the +//! files present at the window head are reported, the composition counts +//! distinct files (not path actions) by content-aware category, and the +//! output never depends on the host CPU count. + +mod common; + +use std::process::Command; + +use cf_gitlib::testutil::{commit_files_as, init_repo, CommitBuilder, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits (one default tick each). +const DAY: i64 = 24 * 3_600; +/// The fixture developers. +const ALICE: (&str, &str) = ("alice", "alice@example.com"); +const BOB: (&str, &str) = ("bob", "bob@example.com"); +/// Number of modifications of the single file in the composition fixture. +const SINGLE_FILE_EDITS: i64 = 8; +/// Tolerance for the hand-computed churn scores. +const EPS: f64 = 1e-9; + +fn codefang_with(mut cmd: Command, args: &[&str]) -> std::process::Output { + cmd.arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + codefang_with(common::command(BIN), args) +} + +/// Runs the binary under `taskset -c `. +fn codefang_pinned(cpus: &str, args: &[&str]) -> std::process::Output { + let mut cmd = common::command("taskset"); + cmd.args(["-c", cpus, BIN]); + codefang_with(cmd, args) +} + +/// stdout of a successful run, as text. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// The file-history arguments in `format` over `repo` with `extra` flags. +fn args<'a>(repo: &'a TestRepo, format: &'a str, extra: &[&'a str]) -> Vec<&'a str> { + let mut args = vec!["-a", "history/file-history", "--format", format]; + args.extend_from_slice(extra); + args.push(path(repo)); + args +} + +/// A `history/file-history` run in `format` over `repo`. +fn file_history(repo: &TestRepo, format: &str, extra: &[&str]) -> String { + stdout_ok(&args(repo, format, extra)) +} + +/// The file-history JSON report over `repo`. +fn file_history_json(repo: &TestRepo, extra: &[&str]) -> serde_json::Value { + let text = file_history(repo, "json", extra); + serde_json::from_str(&text) + .unwrap_or_else(|e| panic!("stdout is not one JSON document ({e}):\n{text}")) +} + +/// The `file_churn` entry of `path`. +fn churn<'a>(report: &'a serde_json::Value, path: &str) -> &'a serde_json::Value { + report["file_churn"] + .as_array() + .expect("file_churn") + .iter() + .find(|e| e["path"] == path) + .unwrap_or_else(|| panic!("no file_churn entry for {path} in {report}")) +} + +/// The sorted paths listed under `file_churn`. +fn churn_paths(report: &serde_json::Value) -> Vec { + let mut paths: Vec = report["file_churn"] + .as_array() + .expect("file_churn") + .iter() + .map(|e| e["path"].as_str().expect("path").to_string()) + .collect(); + paths.sort(); + paths +} + +/// The `contributors` entry of developer `dev_id` on `path`. +fn contributor<'a>( + report: &'a serde_json::Value, + path: &str, + dev_id: i64, +) -> &'a serde_json::Value { + report["file_contributors"] + .as_array() + .expect("file_contributors") + .iter() + .find(|e| e["path"] == path) + .and_then(|e| { + e["contributors"] + .as_array()? + .iter() + .find(|c| c["dev_id"] == dev_id) + }) + .unwrap_or_else(|| panic!("no contributor {dev_id} on {path} in {report}")) +} + +/// The ndjson record of commit `hash`. +fn record(ndjson: &str, hash: &str) -> serde_json::Value { + let line = ndjson + .lines() + .find(|l| l.contains(hash)) + .unwrap_or_else(|| panic!("no record for {hash} in:\n{ndjson}")); + serde_json::from_str(line).expect("ndjson line") +} + +/// The `(added, removed, changed)` of the single line-stat update in `record`. +fn line_stats(record: &serde_json::Value) -> (i64, i64, i64) { + let stats = &record["data"]["LineStatUpdates"][0]["Stats"]; + ( + stats["added"].as_i64().expect("added"), + stats["removed"].as_i64().expect("removed"), + stats["changed"].as_i64().expect("changed"), + ) +} + +/// The lifecycle of one file next to a `keep.txt` bystander: +/// `c1 alice create f (3 lines)`, `c2 alice change l2 + add l4`, +/// `c3 bob remove l1`, `c4 alice git mv f g`, `c5 bob add l5`. +fn lifecycle_fixture() -> (TestRepo, Vec) { + let test = init_repo().expect("init"); + let c1 = commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c1 create f", + T0, + &[("keep.txt", b"k\n"), ("f.txt", b"l1\nl2\nl3\n")], + ) + .expect("c1"); + let c2 = commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c2 change l2, add l4", + T0 + DAY, + &[("f.txt", b"l1\nl2x\nl3\nl4\n")], + ) + .expect("c2"); + let c3 = commit_files_as( + &test, + BOB.0, + BOB.1, + "c3 remove l1", + T0 + 2 * DAY, + &[("f.txt", b"l2x\nl3\nl4\n")], + ) + .expect("c3"); + let c4 = CommitBuilder::new(&test, "c4 rename f->g", T0 + 3 * DAY) + .author(ALICE.0, ALICE.1) + .rename("f.txt", "g.txt") + .commit() + .expect("c4"); + let c5 = commit_files_as( + &test, + BOB.0, + BOB.1, + "c5 add l5", + T0 + 4 * DAY, + &[("g.txt", b"l2x\nl3\nl4\nl5\n")], + ) + .expect("c5"); + let hashes = [c1, c2, c3, c4, c5] + .iter() + .map(ToString::to_string) + .collect(); + (test, hashes) +} + +/// g.txt's commit list follows the rename (5 commits), its line stats are +/// attributed per author, and every commit's ndjson record carries that +/// commit's own line stats. +#[test] +fn commit_list_and_line_stats_follow_the_rename() { + let (test, hashes) = lifecycle_fixture(); + let report = file_history_json(&test, &[]); + + assert_eq!(churn_paths(&report), ["g.txt", "keep.txt"], "{report}"); + let g = churn(&report, "g.txt"); + assert_eq!(g["commit_count"], 5, "{report}"); + assert_eq!(g["contributor_count"], 2); + assert_eq!(g["total_lines_added"], 5, "3 + 1 + 1"); + assert_eq!(g["total_lines_removed"], 1); + assert_eq!(g["total_lines_changed"], 1); + let score = g["churn_score"].as_f64().unwrap(); + assert!((score - 5.07).abs() < EPS, "5 + 7/100: {score}"); + // alice (dev 0): c1 +3, c2 +1 / ~1; bob (dev 1): c3 -1, c5 +1. + let alice = contributor(&report, "g.txt", 0); + assert_eq!( + ( + alice["added"].as_i64(), + alice["removed"].as_i64(), + alice["changed"].as_i64() + ), + (Some(4), Some(0), Some(1)), + "{report}" + ); + let bob = contributor(&report, "g.txt", 1); + assert_eq!( + ( + bob["added"].as_i64(), + bob["removed"].as_i64(), + bob["changed"].as_i64() + ), + (Some(1), Some(1), Some(0)), + "{report}" + ); + assert_eq!(churn(&report, "keep.txt")["commit_count"], 1); + assert_eq!(report["aggregate"]["total_files"], 2); + assert_eq!(report["aggregate"]["total_commits"], 6, "5 + 1"); + + let ndjson = file_history(&test, "ndjson", &[]); + let c2 = record(&ndjson, &hashes[1]); + assert_eq!(line_stats(&c2), (1, 0, 1), "{c2}"); + let c3 = record(&ndjson, &hashes[2]); + assert_eq!(line_stats(&c3), (0, 1, 0), "{c3}"); + let c4 = record(&ndjson, &hashes[3]); + assert_eq!(c4["data"]["PathActions"][0]["FromPath"], "f.txt", "{c4}"); + assert_eq!(c4["data"]["PathActions"][0]["ToPath"], "g.txt", "{c4}"); + assert_eq!(line_stats(&c4), (0, 0, 0), "a pure rename: {c4}"); + let c5 = record(&ndjson, &hashes[4]); + assert_eq!(line_stats(&c5), (1, 0, 0), "{c5}"); +} + +/// A file deleted at the window head is dropped from the report. +#[test] +fn deleted_files_leave_the_report() { + let (test, _) = lifecycle_fixture(); + CommitBuilder::new(&test, "c6 delete g", T0 + 5 * DAY) + .author(ALICE.0, ALICE.1) + .delete("g.txt") + .commit() + .expect("c6"); + let report = file_history_json(&test, &[]); + assert_eq!(churn_paths(&report), ["keep.txt"], "{report}"); + assert_eq!(report["aggregate"]["total_files"], 1); + assert_eq!(report["aggregate"]["total_commits"], 1); + + let yaml = file_history(&test, "yaml", &[]); + assert!(yaml.contains("- path: keep.txt"), "{yaml}"); + assert!(!yaml.contains("g.txt"), "{yaml}"); +} + +/// The composition counts FILES by content-aware category: a binary blob is +/// `binary` (not `source` by its path), and a single file edited eight times +/// is ONE source file in the aggregate while every tick lists it once; every +/// tick carries its time bounds. +#[test] +fn composition_counts_distinct_files_by_content_category() { + let test = init_repo().expect("init"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c0", + T0, + &[ + ("main.go", b"package main\n"), + ("bin.dat", b"\x00\x01\x02\xff\x00\x00"), + ], + ) + .expect("c0"); + let report = file_history_json(&test, &[]); + assert_eq!(report["composition"]["breakdown"]["binary"], 1, "{report}"); + assert_eq!(report["composition"]["breakdown"]["source"], 1, "{report}"); + assert_eq!( + report["composition"]["percentages"]["binary"], 50.0, + "{report}" + ); + + let single = init_repo().expect("init"); + for i in 0..SINGLE_FILE_EDITS { + let body = format!("package main\n// edit {i}\n"); + commit_files_as( + &single, + ALICE.0, + ALICE.1, + &format!("edit {i}"), + T0 + i * DAY, + &[("main.go", body.as_bytes())], + ) + .expect("edit"); + } + let report = file_history_json(&single, &[]); + assert_eq!(report["composition"]["breakdown"]["source"], 1, "{report}"); + assert_eq!( + report["composition"]["percentages"]["source"], 100.0, + "{report}" + ); + let ts = report["composition_ts"].as_array().expect("composition_ts"); + assert_eq!(ts.len() as i64, SINGLE_FILE_EDITS, "{report}"); + for (i, entry) in ts.iter().enumerate() { + assert_eq!(entry["tick"], i as i64, "{entry}"); + assert_eq!(entry["breakdown"]["source"], 1, "{entry}"); + let expected = format!("2021-01-{:02}T00:00:00Z", i + 1); + assert_eq!(entry["start_time"], expected.as_str(), "{entry}"); + assert_eq!(entry["end_time"], expected.as_str(), "{entry}"); + } + + let text = file_history(&single, "text", &[]); + assert!(text.contains("source 1 (100.0%)"), "{text}"); +} + +/// `h.txt` created, modified four times, deleted, then recreated after three +/// `other.txt` commits: the recreated h.txt has ONE commit, other.txt three +/// — independent of the host CPU count and of a window that drops the +/// oldest commit. +#[test] +fn head_files_and_consume_order_do_not_depend_on_the_cpu_count() { + let test = init_repo().expect("init"); + let mut when = (1..).map(|n| T0 + n * DAY); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c0", + when.next().unwrap(), + &[("h.txt", b"h0\n")], + ) + .expect("c0"); + let mut body = b"h0\n".to_vec(); + for i in 1..=4 { + body.extend_from_slice(format!("h{i}\n").as_bytes()); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "modify h", + when.next().unwrap(), + &[("h.txt", &body)], + ) + .expect("modify"); + } + CommitBuilder::new(&test, "c5 delete h", when.next().unwrap()) + .author(BOB.0, BOB.1) + .delete("h.txt") + .commit() + .expect("c5"); + let mut other = Vec::new(); + for i in 6..=8 { + other.extend_from_slice(format!("o{i}\n").as_bytes()); + commit_files_as( + &test, + BOB.0, + BOB.1, + "other", + when.next().unwrap(), + &[("other.txt", &other)], + ) + .expect("other"); + } + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c9 recreate h", + when.next().unwrap(), + &[("h.txt", b"new\n")], + ) + .expect("c9"); + + let full = file_history_json(&test, &[]); + assert_eq!(churn(&full, "h.txt")["commit_count"], 1, "{full}"); + assert_eq!(churn(&full, "other.txt")["commit_count"], 3, "{full}"); + assert_eq!(full["aggregate"]["total_files"], 2); + + let nine = file_history_json(&test, &["--limit", "9"]); + assert_eq!(churn(&nine, "h.txt")["commit_count"], 1, "{nine}"); + assert_eq!(churn(&nine, "other.txt")["commit_count"], 3, "{nine}"); + + let json_args = args(&test, "json", &[]); + let pinned = codefang_pinned("0-1", &json_args); + if !pinned.status.success() { + eprintln!( + "skipping the taskset comparison: {}", + String::from_utf8_lossy(&pinned.stderr) + ); + return; + } + let unpinned = codefang(&json_args); + assert_eq!( + pinned.stdout, unpinned.stdout, + "json must not depend on the CPU count" + ); + let ndjson_args = args(&test, "ndjson", &[]); + let pinned = codefang_pinned("0-1", &ndjson_args); + let unpinned = codefang(&ndjson_args); + assert_eq!( + pinned.stdout, unpinned.stdout, + "ndjson must not depend on the CPU count" + ); +} diff --git a/bins/codefang/tests/halstead_e2e.rs b/bins/codefang/tests/halstead_e2e.rs new file mode 100644 index 00000000..61049319 --- /dev/null +++ b/bins/codefang/tests/halstead_e2e.rs @@ -0,0 +1,454 @@ +//! End-to-end coverage of `static/halstead`, driven through the **real +//! binary** over an on-disk fixture, asserting exact Halstead numbers computed +//! by hand from the standard definitions (see `specs/bugs/BUG-static-halstead-operators.md`). +//! +//! Fixture (two Go files): +//! +//! * `hal.go` — `c := a + b; return c * 2`: operators `:=`, `+`, `*`, +//! `return` (n1 = 4, N1 = 4); operands `a, b, c, c, 2` (n2 = 4, N2 = 5); +//! V = 9·log2(8) = 27, D = (4/2)·(5/4) = 2.5, E = 67.5. +//! * `calls.go` — three `fmt.Println(...)` calls: operators `()`×3, `.`×3 +//! (n1 = 2, N1 = 6); operands `fmt`×3, `Println`×3, `"x"`×2, `"y"`×1 +//! (n2 = 4, N2 = 9); V = 15·log2(6), D = (2/2)·(9/4) = 2.25. +//! +//! Codebase aggregate = the UNION of both multisets: n1 = 6, N1 = 10, +//! n2 = 8, N2 = 14 → n = 14, N = 24, V = 24·log2(14), D = (6/2)·(14/8) = 5.25. + +mod common; + +use std::path::Path; + +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +const HAL_GO: &str = + "package main\n\nfunc hal(a int, b int) int {\n\tc := a + b\n\treturn c * 2\n}\n"; +const CALLS_GO: &str = "package main\n\nimport \"fmt\"\n\nfunc calls(x int) {\n\tfmt.Println(\"x\")\n\tfmt.Println(\"x\")\n\tfmt.Println(\"y\")\n}\n"; + +// Hand-computed expectations. +const HAL_VOLUME: f64 = 27.0; +const HAL_DIFFICULTY: f64 = 2.5; +const HAL_EFFORT: f64 = 67.5; +const CALLS_DIFFICULTY: f64 = 2.25; +const AGG_DISTINCT_OPERATORS: i64 = 6; +const AGG_DISTINCT_OPERANDS: i64 = 8; +const AGG_TOTAL_OPERATORS: i64 = 10; +const AGG_TOTAL_OPERANDS: i64 = 14; +const AGG_DIFFICULTY: f64 = 5.25; +const STATUS_LOW: &str = "Low Halstead complexity - well-structured code"; +const STATUS_EMPTY: &str = "No functions found"; +const FLOAT_TOLERANCE: f64 = 1e-9; + +fn calls_volume() -> f64 { + 15.0 * 6f64.log2() +} + +fn agg_volume() -> f64 { + 24.0 * 14f64.log2() +} + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// `codefang run -a static/halstead --format [extra] ` → +/// (exit code, stdout, stderr). +fn halstead(path: &str, format: &str, extra: &[&str]) -> (i32, String, String) { + let out = codefang( + &[ + &["-a", "static/halstead", "--format", format], + extra, + &[path], + ] + .concat(), + ); + ( + out.status.code().unwrap_or(-1), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("hal.go"), HAL_GO).unwrap(); + std::fs::write(dir.path().join("calls.go"), CALLS_GO).unwrap(); + dir +} + +fn json_report(path: &str, extra: &[&str]) -> Value { + let (rc, stdout, stderr) = halstead(path, "json", extra); + assert_eq!(rc, 0, "json run failed: {stderr}"); + serde_json::from_str(&stdout).unwrap_or_else(|e| panic!("invalid json ({e}):\n{stdout}")) +} + +/// The one `HALSTEAD` section of a json report. +fn section(report: &Value) -> &Value { + let sections = report["sections"].as_array().expect("sections"); + assert_eq!(sections.len(), 1); + assert_eq!(sections[0]["title"], "HALSTEAD"); + §ions[0] +} + +/// The `value` of the key metric labelled `label`. +fn metric<'a>(section: &'a Value, label: &str) -> &'a str { + section["metrics"] + .as_array() + .expect("metrics") + .iter() + .find(|m| m["label"] == label) + .unwrap_or_else(|| panic!("metric {label} missing in {section}"))["value"] + .as_str() + .expect("string value") +} + +/// One decimal, the same formatting the report uses. +fn one_decimal(v: f64) -> String { + format!("{v:.1}") +} + +/// The yaml scalar following `key:` on the first line that starts with it +/// (after indentation), parsed as f64. +fn yaml_number(yaml: &str, key: &str) -> f64 { + let prefix = format!("{key}: "); + yaml.lines() + .map(str::trim_start) + .find_map(|l| l.strip_prefix(&prefix)) + .unwrap_or_else(|| panic!("{key} missing in:\n{yaml}")) + .trim() + .parse() + .unwrap_or_else(|e| panic!("{key} is not a number ({e}) in:\n{yaml}")) +} + +fn close(a: f64, b: f64) -> bool { + (a - b).abs() < FLOAT_TOLERANCE +} + +/// `--format plot --output ` writes `report.json`, whose `halstead` +/// entry carries every function with its operator / operand maps. +fn plot_report(path: &str) -> Value { + let out_dir = tempfile::tempdir().expect("plot output dir"); + let out = codefang(&[ + "-a", + "static/halstead", + "--format", + "plot", + "--output", + out_dir.path().to_str().unwrap(), + path, + ]); + assert!( + out.status.success(), + "plot run failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + let raw = std::fs::read_to_string(out_dir.path().join("report.json")).expect("report.json"); + let report: Value = serde_json::from_str(&raw).expect("report.json is json"); + report["halstead"].clone() +} + +fn function<'a>(raw: &'a Value, name: &str) -> &'a Value { + raw["functions"] + .as_array() + .expect("functions") + .iter() + .find(|f| f["name"] == name) + .unwrap_or_else(|| panic!("function {name} missing in {raw}")) +} + +fn counts(v: &Value) -> Vec<(String, i64)> { + let mut out: Vec<(String, i64)> = v + .as_object() + .expect("map") + .iter() + .map(|(k, c)| (k.clone(), c.as_i64().expect("count"))) + .collect(); + out.sort(); + out +} + +fn pairs(items: &[(&str, i64)]) -> Vec<(String, i64)> { + let mut out: Vec<(String, i64)> = items.iter().map(|(k, c)| ((*k).to_string(), *c)).collect(); + out.sort(); + out +} + +/// The per-function operator / operand multisets are exactly the standard +/// Halstead tokens: no node type names, no statement text, no missing +/// literals, call parentheses and member access counted as operators. +#[test] +fn per_function_operators_and_operands_are_exact() { + let dir = fixture(); + let raw = plot_report(dir.path().to_str().unwrap()); + + let hal = function(&raw, "hal"); + assert_eq!( + counts(&hal["operators"]), + pairs(&[(":=", 1), ("+", 1), ("*", 1), ("return", 1)]) + ); + assert_eq!( + counts(&hal["operands"]), + pairs(&[("a", 1), ("b", 1), ("c", 2), ("2", 1)]) + ); + assert_eq!(hal["distinct_operators"], 4); + assert_eq!(hal["distinct_operands"], 4); + assert_eq!(hal["total_operators"], 4); + assert_eq!(hal["total_operands"], 5); + assert_eq!(hal["volume"], HAL_VOLUME); + assert_eq!(hal["difficulty"], HAL_DIFFICULTY); + assert_eq!(hal["effort"], HAL_EFFORT); + assert_eq!(hal["_source_file"], "hal.go"); + assert_eq!(hal["_language"], "go"); + + let calls = function(&raw, "calls"); + assert_eq!(counts(&calls["operators"]), pairs(&[("()", 3), (".", 3)])); + assert_eq!( + counts(&calls["operands"]), + pairs(&[("fmt", 3), ("Println", 3), ("\"x\"", 2), ("\"y\"", 1)]) + ); + assert!(close(calls["volume"].as_f64().unwrap(), calls_volume())); + assert_eq!(calls["difficulty"], CALLS_DIFFICULTY); + + // The raw aggregate is the union, with the per-function statistics. + assert_eq!(raw["total_functions"], 2); + assert_eq!(raw["distinct_operators"], AGG_DISTINCT_OPERATORS); + assert_eq!(raw["total_operands"], AGG_TOTAL_OPERANDS); + assert_eq!(raw["difficulty"], AGG_DIFFICULTY); + assert_eq!(raw["max_effort"], calls["effort"]); + assert_eq!(raw["high_effort_functions"], 0); + assert_eq!(raw["message"], STATUS_LOW); +} + +/// The json aggregate reports the UNION counts (never per-file averages), +/// and `--per-file` entries carry each file's own counts whose totals add up +/// to the aggregate totals. +#[test] +fn json_aggregate_is_the_union_and_per_file_entries_add_up() { + let dir = fixture(); + let report = json_report(dir.path().to_str().unwrap(), &["--per-file"]); + let sec = section(&report); + + assert_eq!(sec["status"], STATUS_LOW); + assert_eq!(metric(sec, "Total Functions"), "2"); + assert_eq!( + metric(sec, "Distinct Operators (n1)"), + AGG_DISTINCT_OPERATORS.to_string() + ); + assert_eq!( + metric(sec, "Distinct Operands (n2)"), + AGG_DISTINCT_OPERANDS.to_string() + ); + assert_eq!( + metric(sec, "Total Operators (N1)"), + AGG_TOTAL_OPERATORS.to_string() + ); + assert_eq!( + metric(sec, "Total Operands (N2)"), + AGG_TOTAL_OPERANDS.to_string() + ); + assert_eq!(metric(sec, "Vocabulary"), "14"); + assert_eq!(metric(sec, "Length"), "24"); + assert_eq!(metric(sec, "Volume"), one_decimal(agg_volume())); + assert_eq!(metric(sec, "Difficulty"), one_decimal(AGG_DIFFICULTY)); + assert_eq!( + metric(sec, "Effort"), + one_decimal(agg_volume() * AGG_DIFFICULTY) + ); + assert_eq!( + metric(sec, "Max Effort"), + one_decimal(calls_volume() * CALLS_DIFFICULTY) + ); + assert_eq!(metric(sec, "High-Effort Functions"), "0"); + assert_eq!(report["overall_score_label"], "10/10"); + assert_eq!(report["overall_score"], 1.0); + + // Distribution: both functions are Low (< 100). + let dist = sec["distribution"].as_array().expect("distribution"); + assert_eq!(dist[0]["label"], "Low (<100)"); + assert_eq!(dist[0]["count"], 2); + assert_eq!(dist[0]["percent"], 1.0); + assert_eq!(dist[3]["label"], "Very High (>=5000)"); + assert_eq!(dist[3]["count"], 0); + + // Issues: effort descending — calls (87.2) before hal (67.5). + let issues = sec["issues"].as_array().expect("issues"); + assert_eq!(issues[0]["name"], "calls"); + assert_eq!(issues[1]["name"], "hal"); + assert_eq!(issues[1]["value"], "effort=67.5 | vol=27.0 | bugs=0.0"); + assert_eq!(issues[1]["severity"], "good"); + + // --per-file: one entry per file, each with its own union counts. + let files = sec["files"].as_array().expect("files"); + assert_eq!(files.len(), 2); + let by_path = |p: &str| { + files + .iter() + .find(|f| f["file_path"] == p) + .unwrap_or_else(|| panic!("{p} entry missing")) + }; + let hal = by_path("hal.go"); + assert_eq!(metric(hal, "Distinct Operators (n1)"), "4"); + assert_eq!(metric(hal, "Total Operators (N1)"), "4"); + assert_eq!(metric(hal, "Total Operands (N2)"), "5"); + assert_eq!(metric(hal, "Volume"), one_decimal(HAL_VOLUME)); + let calls = by_path("calls.go"); + assert_eq!(metric(calls, "Distinct Operators (n1)"), "2"); + assert_eq!(metric(calls, "Total Operators (N1)"), "6"); + assert_eq!(metric(calls, "Total Operands (N2)"), "9"); + let sum = |label: &str| -> i64 { + files + .iter() + .map(|f| metric(f, label).parse::().unwrap()) + .sum() + }; + assert_eq!(sum("Total Operators (N1)"), AGG_TOTAL_OPERATORS); + assert_eq!(sum("Total Operands (N2)"), AGG_TOTAL_OPERANDS); + assert_eq!(sum("Total Functions"), 2); +} + +/// json, yaml and text agree on every number, and yaml's aggregate carries +/// the real integer counts. +#[test] +fn json_yaml_and_text_agree() { + let dir = fixture(); + let path = dir.path().to_str().unwrap(); + let report = json_report(path, &[]); + let sec = section(&report); + + let (rc, yaml, stderr) = halstead(path, "yaml", &[]); + assert_eq!(rc, 0, "yaml run failed: {stderr}"); + let agg = yaml.split("aggregate:").nth(1).expect("aggregate block"); + assert!(close(yaml_number(agg, "volume"), agg_volume()), "{yaml}"); + assert_eq!(yaml_number(agg, "difficulty"), AGG_DIFFICULTY); + assert_eq!( + yaml_number(agg, "distinct_operators") as i64, + AGG_DISTINCT_OPERATORS + ); + assert_eq!( + yaml_number(agg, "distinct_operands") as i64, + AGG_DISTINCT_OPERANDS + ); + assert_eq!( + yaml_number(agg, "total_operators") as i64, + AGG_TOTAL_OPERATORS + ); + assert_eq!( + yaml_number(agg, "total_operands") as i64, + AGG_TOTAL_OPERANDS + ); + assert_eq!(yaml_number(agg, "vocabulary") as i64, 14); + assert_eq!(yaml_number(agg, "length") as i64, 24); + assert_eq!(yaml_number(agg, "health_score"), 100.0); + assert!(agg.contains(&format!("message: {STATUS_LOW}")), "{yaml}"); + assert_eq!( + one_decimal(yaml_number(agg, "volume")), + metric(sec, "Volume"), + "yaml volume vs json Volume" + ); + assert_eq!( + one_decimal(yaml_number(agg, "effort")), + metric(sec, "Effort") + ); + // Same distribution buckets as json. + let dist = yaml + .split("distribution:") + .nth(1) + .expect("distribution block"); + assert_eq!(yaml_number(dist, "low") as i64, 2); + assert_eq!(yaml_number(dist, "very_high") as i64, 0); + // function_halstead: effort descending, calls first, with its bucket. + let funcs = yaml.split("function_halstead:").nth(1).unwrap(); + assert!(funcs.find("name: calls").unwrap() < funcs.find("name: hal").unwrap()); + assert!(funcs.contains("complexity_level: Low")); + assert!(yaml.contains("high_effort_functions: []"), "{yaml}"); + + let (rc, text, stderr) = halstead(path, "text", &[]); + assert_eq!(rc, 0, "text run failed: {stderr}"); + assert!(text.contains(STATUS_LOW), "{text}"); + for (label, value) in [ + ("Total Functions", "2".to_string()), + ("Total Operators (N1)", AGG_TOTAL_OPERATORS.to_string()), + ("Total Operands (N2)", AGG_TOTAL_OPERANDS.to_string()), + ("Volume", one_decimal(agg_volume())), + ("Effort", metric(sec, "Effort").to_string()), + ] { + let after = &text[text + .find(label) + .unwrap_or_else(|| panic!("{label} in:\n{text}")) + + label.len()..]; + let got: String = after + .trim_start() + .chars() + .take_while(|c| c.is_ascii_digit() || *c == '.') + .collect(); + assert_eq!(got, value, "text {label} vs json"); + } +} + +/// An empty folder, or one holding only unsupported files, produces the +/// empty report (zeros, `No functions found`) and exits 0 in every format. +#[test] +fn empty_folder_exits_zero_with_the_empty_report() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("notes.txt"), "this is not source\n").unwrap(); + let path = dir.path().to_str().unwrap(); + + let report = json_report(path, &["--per-file"]); + let sec = section(&report); + assert_eq!(sec["status"], STATUS_EMPTY); + assert_eq!(metric(sec, "Total Functions"), "0"); + assert_eq!(metric(sec, "Volume"), "0.0"); + assert!( + sec.get("distribution").is_none(), + "no buckets without functions" + ); + assert_eq!(sec["issues"].as_array().unwrap().len(), 0); + assert_eq!(sec["files"].as_array().unwrap().len(), 0); + + let (rc, yaml, stderr) = halstead(path, "yaml", &[]); + assert_eq!(rc, 0, "yaml: {stderr}"); + assert!(yaml.contains("total_functions: 0"), "{yaml}"); + assert!(yaml.contains(&format!("message: {STATUS_EMPTY}")), "{yaml}"); + + for format in ["text", "compact", "bin"] { + let (rc, _, stderr) = halstead(path, format, &[]); + assert_eq!(rc, 0, "{format}: {stderr}"); + } + + let empty = tempfile::tempdir().expect("empty"); + let (rc, stdout, stderr) = halstead(empty.path().to_str().unwrap(), "json", &[]); + assert_eq!(rc, 0, "empty dir: {stderr}"); + assert!(stdout.contains(STATUS_EMPTY), "{stdout}"); +} + +/// Two runs over the same fixture produce byte-identical reports. +#[test] +fn reports_are_deterministic() { + let dir = fixture(); + let path = dir.path().to_str().unwrap(); + for format in ["json", "yaml", "text"] { + let (rc1, a, _) = halstead(path, format, &["--per-file"]); + let (rc2, b, _) = halstead(path, format, &["--per-file"]); + assert_eq!((rc1, rc2), (0, 0)); + assert_eq!(a, b, "{format} output differs between runs"); + } +} + +/// Sanity: the fixture directory is what the tests assume. +#[test] +fn fixture_files_exist() { + let dir = fixture(); + assert!(Path::new(&dir.path().join("hal.go")).is_file()); + assert!(Path::new(&dir.path().join("calls.go")).is_file()); +} diff --git a/bins/codefang/tests/history_core_e2e.rs b/bins/codefang/tests/history_core_e2e.rs new file mode 100644 index 00000000..a07cde8d --- /dev/null +++ b/bins/codefang/tests/history_core_e2e.rs @@ -0,0 +1,671 @@ +//! End-to-end coverage of the SHARED history-analysis core, driven through the +//! **real binary** over real on-disk git fixtures +//! (`specs/bugs/BUG-history-core-pipeline.md`). +//! +//! Every assertion here is a contract from `site/guide/cli-reference.md` or +//! plain git semantics: the commit window (`--head`, `--limit`, `--since`), +//! first-parent isolation, rename identity, host-independence of the output, +//! the shared change filter, identity merging, tick sizing, the one-document +//! rule for multi-analyzer JSON, and the named error messages. + +mod common; + +use std::process::Command; + +use cf_gitlib::testutil::{ + commit_files, commit_files_as, init_repo, rename_file, CommitBuilder, TestRepo, +}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One hour. +const HOUR: i64 = 3_600; +/// One day between fixture commits. +const DAY: i64 = 24 * HOUR; +/// The name of the fixture's second developer. +const BOB: (&str, &str) = ("Bob", "bob@example.com"); +/// Number of files in the "big commit" fixture — above the 32-file spill +/// threshold the old pipeline silently applied. +const BIG_COMMIT_FILES: usize = 40; +/// The default commit-count a `--head` run must report. +const ONE_COMMIT: i64 = 1; + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + codefang_with(common::command(BIN), args) +} + +/// Runs the binary under `taskset -c ` (pins the CPU set the process +/// sees), with the same deterministic environment. +fn codefang_pinned(cpus: &str, args: &[&str]) -> std::process::Output { + let mut cmd = common::command("taskset"); + cmd.args(["-c", cpus, BIN]); + codefang_with(cmd, args) +} + +/// The pinned clock of every run: multi-analyzer documents carry the +/// analysis time (`metadata.analyzed_at`), so two runs a second apart would +/// otherwise differ. +const PINNED_NOW: &str = "2030-01-01T00:00:00Z"; + +fn codefang_with(mut cmd: Command, args: &[&str]) -> std::process::Output { + cmd.arg("run") + .args(args) + .env("CODEFANG_NOW", PINNED_NOW) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// stdout of a successful run, as text. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// stdout of a successful run, parsed as ONE JSON document. +fn json_ok(args: &[&str]) -> serde_json::Value { + let text = stdout_ok(args); + serde_json::from_str(&text) + .unwrap_or_else(|e| panic!("stdout is not one JSON document ({e}):\n{text}")) +} + +/// `(exit code, stderr)` of a run expected to fail. +fn fail(args: &[&str]) -> (i32, String, String) { + let out = codefang(args); + ( + out.status.code().unwrap_or(-1), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// A Go source body with `n` one-line functions. +fn go_source(n: usize) -> Vec { + let mut s = String::from("package main\n\n"); + for i in 0..n { + s.push_str(&format!("func f{i}() int {{ return {i} }}\n")); + } + s.into_bytes() +} + +/// A 100-line text body. +fn hundred_lines() -> Vec { + (1..=100) + .map(|i| format!("line {i}\n")) + .collect::() + .into_bytes() +} + +/// Alice's main line plus a side line by Bob merged into it: +/// `c1 (alice) ← side (bob) ; c3 (alice) ; merge (alice)`. +fn merge_fixture() -> TestRepo { + let test = init_repo().expect("init"); + let c1 = commit_files(&test, "c1", T0, &[("a.txt", b"a1\n")]).expect("c1"); + let side = CommitBuilder::new(&test, "side", T0 + DAY) + .author(BOB.0, BOB.1) + .parent(c1) + .detached() + .write("b.txt", b"b1\n") + .write("c.txt", b"c1\n") + .commit() + .expect("side"); + commit_files(&test, "c3", T0 + 2 * DAY, &[("a.txt", b"a1\na2\n")]).expect("c3"); + CommitBuilder::new(&test, "merge", T0 + 3 * DAY) + .merge_parent(side) + .write("b.txt", b"b1\n") + .write("c.txt", b"c1\n") + .commit() + .expect("merge"); + test +} + +/// The `developers` array of a devs report (standalone or inside the unified +/// document). +fn developers(report: &serde_json::Value) -> &Vec { + report["developers"] + .as_array() + .unwrap_or_else(|| panic!("no developers array in {report}")) +} + +/// The report of analyzer `id` inside a unified `codefang.run.v1` document. +fn analyzer_report<'a>(doc: &'a serde_json::Value, id: &str) -> &'a serde_json::Value { + doc["analyzers"] + .as_array() + .unwrap_or_else(|| panic!("no analyzers array in {doc}")) + .iter() + .find(|a| a["id"] == id) + .map(|a| &a["report"]) + .unwrap_or_else(|| panic!("no analyzer {id} in {doc}")) +} + +/// Burndown is the only analyzer that walks first-parent; selecting it next +/// to devs must NOT drop Bob's side-line commit from the devs report. +#[test] +fn burndown_coselection_does_not_force_first_parent_on_devs() { + let test = merge_fixture(); + let p = path(&test); + + let standalone = json_ok(&["-a", "history/devs", "--format", "json", p]); + assert_eq!( + developers(&standalone).len(), + 2, + "standalone devs: {standalone}" + ); + + let combined = json_ok(&["-a", "history/devs,history/burndown", "--format", "json", p]); + let devs = analyzer_report(&combined, "history/devs"); + assert_eq!( + developers(devs).len(), + 2, + "devs co-selected with burndown must still see Bob: {combined}" + ); + + let first_parent = json_ok(&[ + "-a", + "history/devs", + "--first-parent", + "--format", + "json", + p, + ]); + assert_eq!( + developers(&first_parent).len(), + 1, + "the user's --first-parent still applies: {first_parent}" + ); +} + +/// The report is a pure function of the repository and the flags: pinning the +/// process to two CPUs must give byte-identical output. +#[test] +fn output_does_not_depend_on_the_host_cpu_count() { + let test = merge_fixture(); + let p = path(&test); + let args_ndjson = [ + "-a", + "history/couples,history/file-history,history/typos,history/devs", + "--format", + "ndjson", + p, + ]; + let args_json = [ + "-a", + "history/couples,history/file-history", + "--format", + "json", + p, + ]; + + let pinned = codefang_pinned("0-1", &args_ndjson); + if !pinned.status.success() { + eprintln!( + "skipping: taskset -c 0-1 is not usable here: {}", + String::from_utf8_lossy(&pinned.stderr) + ); + return; + } + let unpinned = codefang(&args_ndjson); + assert!(unpinned.status.success()); + assert_eq!( + pinned.stdout, unpinned.stdout, + "ndjson must be byte-identical under taskset -c 0-1" + ); + + let pinned = codefang_pinned("0-1", &args_json); + let unpinned = codefang(&args_json); + assert!(pinned.status.success() && unpinned.status.success()); + assert_eq!( + pinned.stdout, unpinned.stdout, + "json must be byte-identical under taskset -c 0-1" + ); + + // A merged-in change couples b.txt and c.txt exactly once: they were + // co-changed in ONE commit (the side commit); the merge re-introduces + // them on the main line but must not double count. + let doc: serde_json::Value = serde_json::from_slice(&unpinned.stdout).expect("json"); + let couples = analyzer_report(&doc, "history/couples"); + let pair = couples["file_coupling"] + .as_array() + .expect("file_coupling") + .iter() + .find(|e| { + (e["file1"] == "b.txt" && e["file2"] == "c.txt") + || (e["file1"] == "c.txt" && e["file2"] == "b.txt") + }) + .unwrap_or_else(|| panic!("no b.txt/c.txt coupling in {couples}")); + assert_eq!( + pair["co_changes"], 1, + "merge double-counted the pair: {pair}" + ); +} + +/// A pure `git mv` is a rename, not 100 deleted + 100 added lines. +#[test] +fn pure_rename_adds_and_removes_zero_lines_in_devs() { + let test = init_repo().expect("init"); + let body = hundred_lines(); + commit_files(&test, "add", T0, &[("a.txt", &body)]).expect("add"); + let mv = rename_file(&test, "mv", T0 + DAY, "a.txt", "b.txt").expect("mv"); + let p = path(&test); + + let ndjson = stdout_ok(&["-a", "history/devs", "--format", "ndjson", p]); + let line = ndjson + .lines() + .find(|l| l.contains(&mv.to_string())) + .unwrap_or_else(|| panic!("no devs record for the rename commit in:\n{ndjson}")); + assert!( + line.contains(r#""lines_added":0,"lines_removed":0"#), + "a pure rename must add/remove zero lines: {line}" + ); + + let json = json_ok(&["-a", "history/devs", "--format", "json", p]); + assert_eq!(json["aggregate"]["total_lines_removed"], 0, "{json}"); + assert_eq!(json["aggregate"]["total_lines_added"], 100, "{json}"); +} + +/// `--since` is an author-time filter: a commit authored after the cutoff but +/// committed before it is included, one authored before the cutoff but +/// committed after it is excluded — regardless of walk order. +#[test] +fn since_keeps_every_commit_authored_after_the_cutoff() { + let test = init_repo().expect("init"); + let cutoff = T0 + 10 * DAY; + let a = CommitBuilder::new(&test, "a", cutoff + DAY) + .committer_time(cutoff - 9 * DAY) + .write("a.txt", b"a\n") + .commit() + .expect("a"); + let b = CommitBuilder::new(&test, "b", cutoff - DAY) + .committer_time(cutoff + DAY) + .write("b.txt", b"b\n") + .commit() + .expect("b"); + let c = commit_files(&test, "c", cutoff + 5 * DAY, &[("c.txt", b"c\n")]).expect("c"); + let p = path(&test); + + let ndjson = stdout_ok(&[ + "-a", + "history/burndown", + "--format", + "ndjson", + "--since", + "2021-01-11T00:00:00Z", + p, + ]); + let hashes: Vec<&str> = ndjson + .lines() + .filter_map(|l| l.find("\"hash\":\"").map(|i| &l[i + 8..i + 48])) + .collect(); + assert_eq!( + hashes, + vec![a.to_string(), c.to_string()], + "a (authored after, committed before) must be kept and b dropped; got {hashes:?}, b={b}" + ); +} + +/// `--head` is the single-commit window `[HEAD]` for every analyzer and +/// format — including a non-merge HEAD for devs, and burndown's streaming +/// formats. +#[test] +fn head_is_a_single_commit_window_for_every_format() { + let test = init_repo().expect("init"); + for i in 0..3_i64 { + commit_files( + &test, + &format!("c{i}"), + T0 + i * DAY, + &[("f.go", go_source(i as usize + 1).as_slice())], + ) + .expect("commit"); + } + let head = test.repo.head().expect("head").to_string(); + let p = path(&test); + + let ndjson = stdout_ok(&["-a", "history/burndown", "--format", "ndjson", "--head", p]); + let lines: Vec<&str> = ndjson.lines().collect(); + assert_eq!( + lines.len(), + 1, + "burndown ndjson --head must emit one record:\n{ndjson}" + ); + assert!(lines[0].contains(&head)); + + let ts = json_ok(&[ + "-a", + "history/burndown", + "--format", + "timeseries", + "--head", + p, + ]); + assert_eq!(ts["commits"].as_array().map(Vec::len), Some(1), "{ts}"); + assert_eq!(ts["commits"][0]["hash"], head.as_str()); + + let head_raw = stdout_ok(&["-a", "history/devs", "--format", "json", "--head", p]); + let devs: serde_json::Value = serde_json::from_str(&head_raw).expect("json"); + assert_eq!(devs["aggregate"]["total_commits"], ONE_COMMIT, "{devs}"); + assert_eq!(developers(&devs).len(), 1, "{devs}"); + + // `--head` ≡ `--limit 1`, byte for byte. + let limit_one = stdout_ok(&["-a", "history/devs", "--format", "json", "--limit", "1", p]); + assert_eq!(limit_one, head_raw); +} + +/// Several history analyzers in `--format json` emit ONE document. +#[test] +fn multi_analyzer_history_json_is_one_document() { + let test = merge_fixture(); + let p = path(&test); + let doc = json_ok(&["-a", "history/devs,history/burndown", "--format", "json", p]); + assert_eq!(doc["version"], "codefang.run.v1"); + let ids: Vec<&str> = doc["analyzers"] + .as_array() + .expect("analyzers") + .iter() + .filter_map(|a| a["id"].as_str()) + .collect(); + assert_eq!(ids, vec!["history/devs", "history/burndown"]); + assert!(analyzer_report(&doc, "history/burndown")["aggregate"].is_object()); +} + +/// Every user error names its cause and exits 1 with empty stdout. +#[test] +fn user_errors_are_named() { + let (rc, out, err) = fail(&["-a", "history/devs", "--format", "json", "/no/such/path"]); + assert_eq!((rc, out.as_str()), (1, "")); + assert!( + err.contains("Error: path not found: /no/such/path"), + "{err}" + ); + + let dir = tempfile::tempdir().expect("tempdir"); + let d = dir.path().to_str().unwrap(); + let (rc, out, err) = fail(&["-a", "history/devs", "--format", "json", d]); + assert_eq!((rc, out.as_str()), (1, "")); + assert!( + err.contains(&format!("Error: cannot open repository: {d}")), + "{err}" + ); + + let test = merge_fixture(); + let p = path(&test); + let (rc, _out, err) = fail(&["-a", "history/devs", "--format", "bogus", p]); + assert_eq!(rc, 1); + assert!(err.contains("Error: unsupported format: bogus"), "{err}"); + + let (rc, _out, err) = fail(&["-a", "static/complexity", "--format", "ndjson", p]); + assert_eq!(rc, 1); + assert!( + err.contains("Error: format ndjson is not supported for static/complexity"), + "{err}" + ); + + let (rc, _out, err) = fail(&["-a", "history/devs", "--format", "json", "--limit=-5", p]); + assert_eq!(rc, 1); + assert!( + err.contains("Error: invalid --limit: must be >= 0, got -5"), + "{err}" + ); + + let (rc, _out, err) = fail(&["-a", "history/devs", "--tick-size", "0", p]); + assert_eq!(rc, 1); + assert!( + err.contains("Error: invalid --tick-size: must be > 0, got 0"), + "{err}" + ); + + for (_, _, err) in [ + fail(&["-a", "history/devs", "/no/such/path"]), + fail(&["-a", "history/devs", "--format", "bogus", p]), + ] { + assert!(!err.contains("dispatch is blocked"), "{err}"); + } +} + +/// `--tick-size 1` buckets commits by the hour: the documented +/// `[0, 1, 11, 31, 73]` over the ticks fixture, and the timeseries document +/// reports the configured size. +#[test] +fn tick_size_one_hour_follows_the_documented_fixture() { + // 2020-01-01T00:00:00Z. + const START_2020: i64 = 1_577_836_800; + let times = [ + START_2020 + 23 * HOUR + 59 * 60, // 2020-01-01T23:59:00Z + START_2020 + DAY + 60, // 2020-01-02T00:01:00Z + START_2020 + DAY + 10 * HOUR, // 2020-01-02T12:00:00+02:00 + START_2020 + 2 * DAY + 6 * HOUR, // 2020-01-03T01:00:00-05:00 + START_2020 + 4 * DAY, // 2020-01-05T00:00:00Z + ]; + let test = init_repo().expect("init"); + for (i, t) in times.iter().enumerate() { + commit_files( + &test, + &format!("c{i}"), + *t, + &[(&format!("{i}.go"), go_source(1).as_slice())], + ) + .expect("commit"); + } + let p = path(&test); + + let ndjson = stdout_ok(&[ + "-a", + "history/burndown", + "--format", + "ndjson", + "--tick-size", + "1", + p, + ]); + let ticks: Vec = ndjson + .lines() + .filter_map(|l| { + let i = l.find("\"tick\":")? + 7; + l[i..].split(',').next()?.parse().ok() + }) + .collect(); + assert_eq!(ticks, vec![0, 1, 11, 31, 73], "{ndjson}"); + + let ts = json_ok(&[ + "-a", + "history/burndown", + "--format", + "timeseries", + "--tick-size", + "1", + p, + ]); + assert_eq!(ts["tick_size_hours"], 1, "{ts}"); + let daily = json_ok(&["-a", "history/burndown", "--format", "timeseries", p]); + assert_eq!(daily["tick_size_hours"], 24, "{daily}"); +} + +/// `--languages ""` means no filter in the history phase, like the static +/// phase. +#[test] +fn empty_languages_value_is_no_filter_in_history() { + let test = merge_fixture(); + let p = path(&test); + let unfiltered = stdout_ok(&["-a", "history/devs", "--format", "json", p]); + let empty = stdout_ok(&[ + "-a", + "history/devs", + "--format", + "json", + "--languages", + "", + p, + ]); + assert_eq!(unfiltered, empty); + let json: serde_json::Value = serde_json::from_str(&empty).expect("json"); + assert_eq!(json["aggregate"]["total_commits"], 4, "{json}"); +} + +/// A file whose header carries a generated-file marker is excluded from the +/// history by default and included with `--include-generated`. +#[test] +fn generated_marker_is_applied_in_history() { + let test = init_repo().expect("init"); + let marked = b"// Code generated by protoc-gen-go. DO NOT EDIT.\npackage main\n\nfunc g() {}\n"; + commit_files( + &test, + "init", + T0, + &[ + ("a.go", go_source(2).as_slice()), + ("gen.go", marked.as_slice()), + ], + ) + .expect("init"); + let p = path(&test); + + let default = json_ok(&["-a", "history/devs", "--format", "json", p]); + assert_eq!( + default["aggregate"]["total_lines_added"], 4, + "a.go only: {default}" + ); + + let with_generated = json_ok(&[ + "-a", + "history/devs", + "--format", + "json", + "--include-generated", + p, + ]); + assert_eq!( + with_generated["aggregate"]["total_lines_added"], 8, + "a.go + gen.go: {with_generated}" + ); +} + +/// Loose identity merges every alias that shares a token with a known +/// person; `--exact-signatures` keeps them apart; `--anonymize` hides names. +#[test] +fn alias_identities_are_merged_and_flags_shape_names() { + let test = init_repo().expect("init"); + commit_files_as( + &test, + "alice", + "alice@x.com", + "c1", + T0, + &[("a.txt", b"1\n")], + ) + .expect("c1"); + commit_files_as( + &test, + "Alice Smith", + "alice@x.com", + "c2", + T0 + DAY, + &[("a.txt", b"1\n2\n")], + ) + .expect("c2"); + commit_files_as( + &test, + "Alice Smith", + "alice@other.org", + "c3", + T0 + 2 * DAY, + &[("a.txt", b"1\n2\n3\n")], + ) + .expect("c3"); + let p = path(&test); + + let loose = json_ok(&["-a", "history/devs", "--format", "json", p]); + assert_eq!( + developers(&loose).len(), + 1, + "three aliases, one person: {loose}" + ); + assert_eq!(developers(&loose)[0]["commits"], 3); + + let exact = json_ok(&[ + "-a", + "history/devs", + "--format", + "json", + "--exact-signatures", + p, + ]); + assert_eq!(developers(&exact).len(), 3, "{exact}"); + + let anon = json_ok(&["-a", "history/devs", "--format", "json", "--anonymize", p]); + assert_eq!(developers(&anon)[0]["name"], "Developer-A", "{anon}"); + + // Burndown and devs agree on the author ids (one identity detector). + let burndown = stdout_ok(&["-a", "history/burndown", "--format", "ndjson", p]); + assert_eq!( + burndown.matches(r#""author_id":0"#).count(), + 3, + "every commit resolves to author 0: {burndown}" + ); + + let dict = test.path().join("people.txt"); + std::fs::write(&dict, "Alice|alice@x.com|alice@other.org\n").expect("dict"); + let with_dict = json_ok(&[ + "-a", + "history/devs", + "--format", + "json", + "--people-dict", + dict.to_str().unwrap(), + p, + ]); + assert_eq!(developers(&with_dict)[0]["name"], "Alice", "{with_dict}"); +} + +/// A commit touching more than 32 files contributes every file: no spill +/// gate. +#[test] +fn large_commits_are_fully_analyzed_by_quality() { + let test = init_repo().expect("init"); + let files: Vec<(String, Vec)> = (0..BIG_COMMIT_FILES) + .map(|i| (format!("f{i}.go"), go_source(1))) + .collect(); + let refs: Vec<(&str, &[u8])> = files + .iter() + .map(|(n, b)| (n.as_str(), b.as_slice())) + .collect(); + commit_files(&test, "big", T0, &refs).expect("big"); + let p = path(&test); + + let quality = json_ok(&["-a", "history/quality", "--format", "json", p]); + assert_eq!( + quality["aggregate"]["total_files_analyzed"], BIG_COMMIT_FILES as i64, + "{quality}" + ); +} + +/// The whole-selection history `--format compact` renders one section per +/// analyzer. +#[test] +fn history_compact_renders_one_section_per_analyzer() { + let test = merge_fixture(); + let p = path(&test); + let out = stdout_ok(&[ + "-a", + "history/devs,history/burndown", + "--format", + "compact", + p, + ]); + assert!(out.contains("history/devs"), "{out}"); + assert!(out.contains("history/burndown"), "{out}"); +} diff --git a/bins/codefang/tests/history_formats_e2e.rs b/bins/codefang/tests/history_formats_e2e.rs new file mode 100644 index 00000000..d3b4de0e --- /dev/null +++ b/bins/codefang/tests/history_formats_e2e.rs @@ -0,0 +1,435 @@ +//! End-to-end coverage of the history OUTPUT layer, driven through the **real +//! binary** over a real on-disk git fixture +//! (`specs/bugs/BUG-history-text-format.md`). +//! +//! The contract: every `history/*` analyzer renders in every format +//! `codefang run --help` advertises — `json`, `yaml`, `bin`, `timeseries`, +//! `timeseries --ndjson`, `ndjson`, `text`, `compact`, `plot` — with exit 0 +//! and a non-empty result, and a multi-analyzer selection renders ONE output +//! (one text section, one compact line, one timeseries line per commit) per +//! analyzer. A failing run never writes a partial report to stdout. + +mod common; + +use std::path::Path; + +use cf_gitlib::testutil::{commit_files_as, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture history: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits (one default tick each). +const DAY: i64 = 24 * 3_600; +/// The fixture developers. +const ALICE: (&str, &str) = ("alice", "alice@example.com"); +const BOB: (&str, &str) = ("bob", "bob@example.com"); +/// Number of commits in [`fixture`]. +const FIXTURE_COMMITS: usize = 3; + +/// Every history analyzer id with its plot page name (`.html`). +const ANALYZERS: [(&str, &str); 10] = [ + ("history/burndown", "burndown"), + ("history/devs", "devs"), + ("history/couples", "couples"), + ("history/file-history", "file-history"), + ("history/shotness", "shotness"), + ("history/quality", "quality"), + ("history/sentiment", "sentiment"), + ("history/typos", "typos"), + ("history/anomaly", "anomaly"), + ("history/imports", "imports-per-dev"), +]; + +/// The `--format` values `codefang run --help` advertises. +const ADVERTISED_FORMATS: [&str; 8] = [ + "json", + "yaml", + "plot", + "bin", + "timeseries", + "ndjson", + "text", + "compact", +]; + +/// One requested output: a label and the flags that select it. The +/// `--ndjson` modifier turns `timeseries` into one JSON line per commit. +const FORMATS: [(&str, &[&str]); 9] = [ + ("json", &["--format", "json"]), + ("yaml", &["--format", "yaml"]), + ("bin", &["--format", "bin"]), + ("timeseries", &["--format", "timeseries"]), + ("timeseries+ndjson", &["--format", "timeseries", "--ndjson"]), + ("ndjson", &["--format", "ndjson"]), + ("text", &["--format", "text"]), + ("compact", &["--format", "compact"]), + ("plot", &["--format", "plot"]), +]; + +/// Go source before the typo fix: one documented function, one import. +const MAIN_GO_V1: &[u8] = b"package main + +import \"fmt\" + +// great work, this is a wonderful helper +func recieve(x int) int { +\tif x > 0 { +\t\treturn x +\t} +\treturn -x +} + +func main() { fmt.Println(recieve(1)) } +"; + +/// Go source after Bob's commit: the typo `recieve` → `receive` is fixed on +/// the same lines, a second import and a negative comment appear. +const MAIN_GO_V2: &[u8] = b"package main + +import ( +\t\"fmt\" +\t\"os\" +) + +// great work, this is a wonderful helper +func receive(x int) int { +\tif x > 0 { +\t\treturn x +\t} +\treturn -x +} + +// terrible awful broken hack, I hate it +func main() { fmt.Println(receive(1)); os.Exit(0) } +"; + +/// Python helper with an import and a comment. +const UTIL_PY_V2: &[u8] = b"import os +# nice and happy code +def helper(a): + if a: + return os.getcwd() + return None +"; + +/// The Python helper with a second function. +const UTIL_PY_V3: &[u8] = b"import os +# nice and happy code +def helper(a): + if a: + return os.getcwd() + return None +def other(): return 1 +"; + +/// Three commits by two developers over Go and Python sources: a typo fix, +/// imports, positive and negative comments, functions, co-changed files. +fn fixture() -> TestRepo { + let test = init_repo().expect("init"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c1", + T0, + &[("main.go", MAIN_GO_V1), ("util.py", b"lib\n")], + ) + .expect("c1"); + commit_files_as( + &test, + BOB.0, + BOB.1, + "c2", + T0 + 4 * DAY, + &[("main.go", MAIN_GO_V2), ("util.py", UTIL_PY_V2)], + ) + .expect("c2"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "c3", + T0 + 9 * DAY, + &[("util.py", UTIL_PY_V3), ("notes.txt", b"x\n")], + ) + .expect("c3"); + test +} + +/// Runs `codefang run ` with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// stdout of a successful run, as text. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// A file under `dir` that exists and is non-empty. +fn non_empty_file(dir: &Path, name: &str) -> bool { + std::fs::metadata(dir.join(name)).is_ok_and(|m| m.len() > 0) +} + +/// What is wrong with one `(analyzer, format)` run, or `None` when it +/// rendered: exit 0, no `Error:` on stderr, and a non-empty result that +/// parses as the format promises (plot: the page and the index on disk). +fn format_defect(id: &str, page: &str, label: &str, flags: &[&str], repo: &str) -> Option { + let plot_dir = tempfile::tempdir().expect("plot dir"); + let plot_out = plot_dir.path().to_str().expect("utf-8 dir"); + let mut args = vec!["-a", id]; + args.extend_from_slice(flags); + if label == "plot" { + args.extend_from_slice(&["--output", plot_out]); + } + args.push(repo); + let out = codefang(&args); + let stderr = String::from_utf8_lossy(&out.stderr); + if !out.status.success() || stderr.contains("Error:") { + return Some(format!( + "{id} {label}: exit {:?}, stdout {} bytes, stderr: {stderr}", + out.status.code(), + out.stdout.len() + )); + } + let stdout = String::from_utf8_lossy(&out.stdout); + let ok = match label { + "plot" => { + non_empty_file(plot_dir.path(), "index.html") + && non_empty_file(plot_dir.path(), &format!("{page}.html")) + } + "json" | "timeseries" => serde_json::from_str::(&stdout).is_ok(), + "ndjson" | "timeseries+ndjson" => { + !stdout.is_empty() + && stdout + .lines() + .all(|l| serde_json::from_str::(l).is_ok()) + } + "text" => stdout.contains(&format!("{id}:\n")), + "compact" => stdout.lines().count() == 1 && stdout.starts_with(id), + _ => !out.stdout.is_empty(), + }; + (!ok).then(|| format!("{id} {label}: unexpected output:\n{stdout}")) +} + +/// Runs `id` in every advertised format over the fixture and fails with the +/// full list of defects. +fn assert_every_format(id: &str) { + let test = fixture(); + let page = ANALYZERS + .iter() + .find(|(a, _)| *a == id) + .map(|(_, p)| *p) + .expect("known analyzer"); + let defects: Vec = FORMATS + .iter() + .filter_map(|(label, flags)| format_defect(id, page, label, flags, path(&test))) + .collect(); + assert!(defects.is_empty(), "{}", defects.join("\n")); +} + +#[test] +fn burndown_renders_every_advertised_format() { + assert_every_format("history/burndown"); +} + +#[test] +fn devs_renders_every_advertised_format() { + assert_every_format("history/devs"); +} + +#[test] +fn couples_renders_every_advertised_format() { + assert_every_format("history/couples"); +} + +#[test] +fn file_history_renders_every_advertised_format() { + assert_every_format("history/file-history"); +} + +#[test] +fn shotness_renders_every_advertised_format() { + assert_every_format("history/shotness"); +} + +#[test] +fn quality_renders_every_advertised_format() { + assert_every_format("history/quality"); +} + +#[test] +fn sentiment_renders_every_advertised_format() { + assert_every_format("history/sentiment"); +} + +#[test] +fn typos_renders_every_advertised_format() { + assert_every_format("history/typos"); +} + +#[test] +fn anomaly_renders_every_advertised_format() { + assert_every_format("history/anomaly"); +} + +#[test] +fn imports_renders_every_advertised_format() { + assert_every_format("history/imports"); +} + +/// The matrix above covers exactly the formats `--help` advertises (plus the +/// `--ndjson` modifier), so a newly advertised format cannot ship untested. +#[test] +fn the_matrix_covers_every_format_the_help_advertises() { + let out = common::command(BIN) + .args(["run", "--help"]) + .output() + .expect("run --help"); + let help = String::from_utf8_lossy(&out.stdout); + let line = help + .lines() + .find_map(|l| l.trim().strip_prefix("Output format: ")) + .unwrap_or_else(|| panic!("no `Output format:` line in:\n{help}")); + let mut advertised: Vec<&str> = line.split(',').map(str::trim).collect(); + advertised.sort_unstable(); + let mut covered = ADVERTISED_FORMATS.to_vec(); + covered.sort_unstable(); + assert_eq!(advertised, covered, "{help}"); + for format in ADVERTISED_FORMATS { + assert!( + FORMATS.iter().any(|(label, _)| *label == format), + "{format} is not in the matrix" + ); + } + assert!(help.contains("--ndjson"), "{help}"); +} + +/// The text body of each analyzer is a human summary of ITS report: the +/// four analyzers that had no text renderer name their key findings. +#[test] +fn text_summaries_name_the_key_findings() { + let test = fixture(); + let p = path(&test); + let text = |id: &str| stdout_ok(&["-a", id, "--format", "text", p]); + + let typos = text("history/typos"); + assert!( + typos.contains("recieve") && typos.contains("receive"), + "{typos}" + ); + assert!(typos.contains("main.go:9"), "{typos}"); + + let imports = text("history/imports"); + for needle in ["fmt", "os", "alice", "bob"] { + assert!( + imports.contains(needle), + "imports lacks {needle}:\n{imports}" + ); + } + + let quality = text("history/quality"); + assert!(quality.contains("Files Analyzed"), "{quality}"); + assert!(quality.contains("Complexity"), "{quality}"); + + let anomaly = text("history/anomaly"); + assert!(anomaly.contains("Ticks Analyzed"), "{anomaly}"); + assert!(anomaly.contains("Anomalies"), "{anomaly}"); +} + +/// `-a 'history/*' --format text`: the header once, then one section per +/// analyzer, exit 0. +#[test] +fn multi_analyzer_text_renders_one_section_per_analyzer() { + let test = fixture(); + let out = stdout_ok(&["-a", "history/*", "--format", "text", path(&test)]); + assert_eq!(out.matches("codefang (v2):").count(), 1, "{out}"); + assert!(out.starts_with("codefang (v2):\n"), "{out}"); + for (id, _) in ANALYZERS { + assert_eq!( + out.lines().filter(|l| *l == format!("{id}:")).count(), + 1, + "{id} section:\n{out}" + ); + } +} + +/// `-a 'history/*' --format compact`: exactly one line per analyzer, each +/// led by the analyzer id. +#[test] +fn multi_analyzer_compact_is_one_line_per_analyzer() { + let test = fixture(); + let out = stdout_ok(&["-a", "history/*", "--format", "compact", path(&test)]); + let lines: Vec<&str> = out.lines().collect(); + assert_eq!(lines.len(), ANALYZERS.len(), "{out}"); + for (id, _) in ANALYZERS { + assert_eq!( + lines.iter().filter(|l| l.starts_with(id)).count(), + 1, + "{id}:\n{out}" + ); + } +} + +/// `--format timeseries --ndjson` is the timeseries document's `commits` +/// array, one line per commit with every selected analyzer merged into it. +#[test] +fn timeseries_ndjson_is_the_merged_commit_array() { + let test = fixture(); + let p = path(&test); + let selection = "history/devs,history/couples,history/anomaly"; + let doc: serde_json::Value = + serde_json::from_str(&stdout_ok(&["-a", selection, "--format", "timeseries", p])) + .expect("timeseries json"); + let lines: Vec = + stdout_ok(&["-a", selection, "--format", "timeseries", "--ndjson", p]) + .lines() + .map(|l| serde_json::from_str(l).expect("ndjson line")) + .collect(); + assert_eq!(lines.len(), FIXTURE_COMMITS, "{lines:?}"); + assert_eq!(&serde_json::Value::Array(lines.clone()), &doc["commits"]); + for line in &lines { + for key in ["hash", "tick", "timestamp", "devs", "couples", "anomaly"] { + assert!(line.get(key).is_some(), "{key} missing in {line}"); + } + } +} + +/// A run that cannot produce its report writes NOTHING to stdout. +#[test] +fn a_failing_text_run_writes_nothing_to_stdout() { + let dir = tempfile::tempdir().expect("tempdir"); + let out = codefang(&[ + "-a", + "history/*", + "--format", + "text", + dir.path().to_str().expect("utf-8"), + ]); + assert_eq!(out.status.code(), Some(1)); + assert!(out.stdout.is_empty(), "{:?}", out.stdout); + assert!( + String::from_utf8_lossy(&out.stderr).contains("Error: cannot open repository"), + "{}", + String::from_utf8_lossy(&out.stderr) + ); +} diff --git a/bins/codefang/tests/history_window.rs b/bins/codefang/tests/history_window.rs index 205ecb6e..a2e27971 100644 --- a/bins/codefang/tests/history_window.rs +++ b/bins/codefang/tests/history_window.rs @@ -7,6 +7,8 @@ //! Reports are asserted in machine formats only (`ndjson` / `json`), which are //! the byte-stable formats the golden harness also treats as binding. +mod common; + use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; use cf_gitlib::Hash; @@ -30,8 +32,8 @@ const NEEDLE_HASH: &str = "\"hash\":\""; /// Runs the binary with a deterministic environment; state is disabled so the /// run is self-contained (same discipline as the golden captures). fn codefang(args: &[&str]) -> std::process::Output { - Command::new(BIN) - .args(["run", "--checkpoint=false", "--resume=false", "--no-cache"]) + common::command(BIN) + .arg("run") .args(args) .env("TZ", "UTC") .env("NO_COLOR", "1") @@ -41,8 +43,6 @@ fn codefang(args: &[&str]) -> std::process::Output { .expect("run the codefang binary") } -use std::process::Command; - /// Builds the five-commit fixture; returns it with its hashes, oldest-first. fn fixture() -> (TestRepo, Vec) { let test = init_repo().expect("init fixture repo"); @@ -65,14 +65,7 @@ fn fixture() -> (TestRepo, Vec) { fn ndjson_window(path: &str, extra: &[&str]) -> (i32, Vec, String, String) { let out = codefang( &[ - &[ - "-a", - "history/burndown", - "--workers", - "1", - "--format", - "ndjson", - ], + &["-a", "history/burndown", "--format", "ndjson"], extra, &[path], ] diff --git a/bins/codefang/tests/imports_e2e.rs b/bins/codefang/tests/imports_e2e.rs new file mode 100644 index 00000000..d95b34f0 --- /dev/null +++ b/bins/codefang/tests/imports_e2e.rs @@ -0,0 +1,301 @@ +//! End-to-end coverage of `static/imports`, driven through the **real binary** +//! over an on-disk fixture tree. +//! +//! The contract (see `specs/bugs/BUG-static-imports-extraction.md` and +//! `site/analyzers/imports.md`): one entry per imported module/package/file, +//! aliases and specifier lists never count, the aggregate value of an import +//! is the number of files importing it, an empty root yields the empty report +//! with exit 0, a file root analyzes that one file, and output is +//! deterministic. + +mod common; + +use serde_json::Value; +use std::path::Path; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +/// Go: plain, aliased, dot, blank, duplicate, external and relative specs. +const GO_SRC: &str = "package main\n\nimport \"fmt\"\n\nimport (\n\t\"os\"\n\tstr \"strings\"\n\t. \"math\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"github.com/example/lib/v2\"\n\t\"github.com/example/lib/v2\"\n\t\"../relative/pkg\"\n)\n\nfunc main() {\n\tfmt.Println(os.Args, str.ToUpper(\"x\"), Pi, lib.X)\n}\n"; +/// Python: comma list, dotted module, `from` form, aliases, relative forms. +const PY_SRC: &str = + "import os, sys\nimport os.path\nfrom typing import List, Dict\nfrom x import y as z\nfrom . import sibling\nfrom ..pkg.sub import thing\nimport os\nimport numpy as np\n"; +/// JavaScript: `require`, default/named/namespace/side-effect/mixed ES imports, +/// and a dynamic `import()`. +const JS_SRC: &str = "const fs = require('fs');\nconst path = require(\"path\");\nimport React from 'react';\nimport { useState, useEffect } from 'react';\nimport * as ns from 'ns-lib';\nimport './side-effect.css';\nimport def, { named } from './local/mod';\nasync function f() { const m = await import('dyn-mod'); return m; }\n"; +/// Go stdlib packages that the old 17-name table classified as external. +const GO_STDLIB_SRC: &str = "package main\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc f() { _ = errors.New(\"x\"); log.Println(strconv.Itoa(1)); var _ testing.T }\n"; +/// Python stdlib modules that the old 7-name table classified as external. +const PY_STDLIB_SRC: &str = "import datetime\nimport subprocess\nimport math\n"; + +/// The expected `(import, files importing it)` pairs for [`fixture`], in the +/// report's order: count descending, then name ascending. +const EXPECTED_AGGREGATE: &[(&str, &str)] = &[ + ("os", "2"), + ("../relative/pkg", "1"), + ("..pkg.sub", "1"), + ("./local/mod", "1"), + ("./side-effect.css", "1"), + (".sibling", "1"), + ("dyn-mod", "1"), + ("embed", "1"), + ("fmt", "1"), + ("fs", "1"), + ("github.com/example/lib/v2", "1"), + ("math", "1"), + ("ns-lib", "1"), + ("numpy", "1"), + ("os.path", "1"), + ("path", "1"), + ("react", "1"), + ("strings", "1"), + ("sys", "1"), + ("typing", "1"), + ("x", "1"), +]; + +/// Per-file expectations for [`fixture`] (`--per-file` entries list a file's +/// imports alphabetically, each valued `1`). +const EXPECTED_PER_FILE: &[(&str, &[&str])] = &[ + ( + "a.go", + &[ + "../relative/pkg", + "embed", + "fmt", + "github.com/example/lib/v2", + "math", + "os", + "strings", + ], + ), + ( + "b.py", + &[ + "..pkg.sub", + ".sibling", + "numpy", + "os", + "os.path", + "sys", + "typing", + "x", + ], + ), + ( + "c.js", + &[ + "./local/mod", + "./side-effect.css", + "dyn-mod", + "fs", + "ns-lib", + "path", + "react", + ], + ), +]; + +/// Runs `static/imports` with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .args(["run", "-a", "static/imports"]) + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// Runs the json format over `path` (+ `extra` flags) and returns +/// `(exit code, parsed stdout, stderr)`. +fn run_json(path: &Path, extra: &[&str]) -> (i32, Value, String) { + let path = path.to_str().expect("utf-8 path"); + let out = codefang(&[&["--format", "json"], extra, &[path]].concat()); + let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); + let stdout = String::from_utf8_lossy(&out.stdout); + let json: Value = serde_json::from_str(&stdout) + .unwrap_or_else(|e| panic!("stdout is not JSON ({e}): {stdout}\nstderr: {stderr}")); + (out.status.code().unwrap_or(-1), json, stderr) +} + +/// Writes the three-language fixture into a fresh tempdir. +fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("a.go"), GO_SRC).expect("write a.go"); + std::fs::write(dir.path().join("b.py"), PY_SRC).expect("write b.py"); + std::fs::write(dir.path().join("c.js"), JS_SRC).expect("write c.js"); + dir +} + +/// The single IMPORTS section of a `static/imports` json report. +fn section(report: &Value) -> &Value { + let sections = report["sections"].as_array().expect("sections array"); + assert_eq!(sections.len(), 1, "one section expected: {report}"); + §ions[0] +} + +/// The `(name, value)` pairs of a section's or file entry's `issues`. +fn issues(node: &Value) -> Vec<(String, String)> { + node["issues"] + .as_array() + .expect("issues array") + .iter() + .map(|i| { + ( + i["name"].as_str().expect("issue name").to_owned(), + i["value"].as_str().expect("issue value").to_owned(), + ) + }) + .collect() +} + +/// The value of the metric labelled `label`. +fn metric(node: &Value, label: &str) -> String { + node["metrics"] + .as_array() + .expect("metrics array") + .iter() + .find(|m| m["label"] == label) + .unwrap_or_else(|| panic!("metric {label} missing: {node}"))["value"] + .as_str() + .expect("metric value") + .to_owned() +} + +/// Each module/package/file is listed exactly once with the number of files +/// importing it; aliases, specifier lists and keywords never appear. +#[test] +fn aggregate_lists_each_module_once_with_file_counts() { + let dir = fixture(); + let (rc, report, stderr) = run_json(dir.path(), &[]); + assert_eq!(rc, 0, "run failed: {stderr}"); + let sec = section(&report); + + let expected: Vec<(String, String)> = EXPECTED_AGGREGATE + .iter() + .map(|(n, v)| ((*n).to_owned(), (*v).to_owned())) + .collect(); + assert_eq!(issues(sec), expected, "issues differ: {sec}"); + assert_eq!( + metric(sec, "Unique Imports"), + EXPECTED_AGGREGATE.len().to_string() + ); + assert_eq!(metric(sec, "Total Files"), "3"); + assert_eq!( + sec["status"], + format!("Found {} unique imports", EXPECTED_AGGREGATE.len()) + ); +} + +/// An empty root (or one with no supported files) is an empty report, not an +/// error: exit 0, zero metrics, no issues. +#[test] +fn empty_root_yields_the_empty_report_with_exit_zero() { + let empty = tempfile::tempdir().expect("tempdir"); + let unsupported = tempfile::tempdir().expect("tempdir"); + std::fs::write(unsupported.path().join("notes.txt"), "no code here\n").expect("write"); + + for dir in [empty.path(), unsupported.path()] { + let (rc, report, stderr) = run_json(dir, &[]); + assert_eq!(rc, 0, "empty root must exit 0: {stderr}"); + let sec = section(&report); + assert_eq!(sec["status"], "No import data available"); + assert_eq!(metric(sec, "Unique Imports"), "0"); + assert_eq!(metric(sec, "Total Files"), "0"); + assert!(issues(sec).is_empty(), "no issues expected: {sec}"); + assert_eq!(report["overall_score_label"], "Info"); + } +} + +/// A file given as the root analyzes that single file. +#[test] +fn a_file_root_analyzes_that_single_file() { + let dir = fixture(); + let (rc, report, stderr) = run_json(&dir.path().join("c.js"), &["--per-file"]); + assert_eq!(rc, 0, "file root must exit 0: {stderr}"); + let sec = section(&report); + assert_eq!(metric(sec, "Total Files"), "1"); + let names: Vec = issues(sec).into_iter().map(|(n, _)| n).collect(); + assert_eq!( + names, EXPECTED_PER_FILE[2].1, + "file-root imports differ: {sec}" + ); + let files = sec["files"].as_array().expect("files array"); + assert_eq!(files.len(), 1); + assert_eq!(files[0]["file_path"], "c.js"); +} + +/// `--per-file` adds one entry per analyzed file listing exactly that file's +/// imports, each located at the file and counted once per file. +#[test] +fn per_file_entries_list_exactly_each_files_imports() { + let dir = fixture(); + let (rc, report, stderr) = run_json(dir.path(), &["--per-file"]); + assert_eq!(rc, 0, "run failed: {stderr}"); + let files = section(&report)["files"].as_array().expect("files array"); + assert_eq!(files.len(), EXPECTED_PER_FILE.len(), "{files:?}"); + + for (entry, (path, imports)) in files.iter().zip(EXPECTED_PER_FILE) { + assert_eq!(entry["file_path"], *path); + assert_eq!(metric(entry, "Total Files"), "1", "per-file entry: {entry}"); + assert_eq!(metric(entry, "Unique Imports"), imports.len().to_string()); + let got = issues(entry); + let want: Vec<(String, String)> = imports + .iter() + .map(|i| ((*i).to_owned(), "1".to_owned())) + .collect(); + assert_eq!(got, want, "imports of {path} differ: {entry}"); + for issue in entry["issues"].as_array().expect("issues") { + assert_eq!(issue["location"], *path); + } + } +} + +/// The yaml metrics classify stdlib packages by the importing file's language. +#[test] +fn yaml_classifies_stdlib_per_language() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("a.go"), GO_STDLIB_SRC).expect("write a.go"); + std::fs::write(dir.path().join("b.py"), PY_STDLIB_SRC).expect("write b.py"); + let path = dir.path().to_str().expect("utf-8 path"); + + let out = codefang(&["--format", "yaml", path]); + assert_eq!(out.status.code(), Some(0)); + let yaml = String::from_utf8_lossy(&out.stdout); + + for name in [ + "errors", + "log", + "strconv", + "testing", + "datetime", + "subprocess", + "math", + ] { + assert!( + yaml.contains(&format!("- path: {name}\n category: stdlib\n")), + "{name} must be stdlib:\n{yaml}" + ); + } + assert!(yaml.contains("external_imports: 0\n"), "{yaml}"); + assert!(yaml.contains("internal_imports: 7\n"), "{yaml}"); + assert!(yaml.contains("external_ratio: 0\n"), "{yaml}"); +} + +/// Two runs over the same tree emit identical bytes. +#[test] +fn two_runs_emit_identical_bytes() { + let dir = fixture(); + let path = dir.path().to_str().expect("utf-8 path"); + let first = codefang(&["--format", "json", "--per-file", path]); + let second = codefang(&["--format", "json", "--per-file", path]); + assert_eq!(first.status.code(), Some(0)); + assert_eq!( + first.stdout, second.stdout, + "json output must be deterministic" + ); +} diff --git a/bins/codefang/tests/imports_history_e2e.rs b/bins/codefang/tests/imports_history_e2e.rs new file mode 100644 index 00000000..70cfc7c7 --- /dev/null +++ b/bins/codefang/tests/imports_history_e2e.rs @@ -0,0 +1,247 @@ +//! End-to-end coverage of `history/imports`, driven through the **real +//! binary** over on-disk git fixtures +//! (`specs/bugs/BUG-history-imports-report.md`). +//! +//! The contract (`site/analyzers/imports.md`, History section): the json / +//! yaml / bin report is `tick_size` (seconds), `author_index` (author id → +//! name) and `imports`: developer → language → import → tick → count, with +//! the parser's language names and the per-commit extraction the ndjson +//! records show; two runs emit identical bytes. + +mod common; + +use cf_gitlib::testutil::{commit_files_as, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T12:00:00Z. +const T0: i64 = 1_609_502_400; +/// One day between fixture commits. +const DAY: i64 = 24 * 3_600; +/// The default tick size in seconds. +const DEFAULT_TICK_SECS: i64 = 86_400; +/// Size of the CFB1 envelope header. +const BIN_HEADER: usize = 8; + +const ALICE: (&str, &str) = ("Alice", "alice@x.io"); +const BOB: (&str, &str) = ("Bob", "bob@x.io"); + +const A_GO_V1: &[u8] = b"package main + +import ( + \"fmt\" + \"os\" +) + +func main() { fmt.Println(os.Args) } +"; +const B_GO: &[u8] = b"package main + +import ( + \"encoding/json\" + \"fmt\" +) + +func b() { fmt.Println(json.Valid(nil)) } +"; +const C_PY: &[u8] = b"import os\nimport sys\n\nprint(os.getcwd(), sys.argv)\n"; +const A_GO_V2: &[u8] = b"package main + +import ( + \"fmt\" + \"net/http\" +) + +func main() { fmt.Println(http.StatusOK) } +"; + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// stdout of a successful run. +fn stdout_ok(args: &[&str]) -> Vec { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// stdout of a successful run, parsed as ONE JSON document. +fn json_ok(args: &[&str]) -> serde_json::Value { + let bytes = stdout_ok(args); + serde_json::from_slice(&bytes).unwrap_or_else(|e| { + panic!( + "stdout is not one JSON document ({e}):\n{}", + String::from_utf8_lossy(&bytes) + ) + }) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// Alice adds `a.go` (fmt, os) on day 0; Bob adds `b.go` (encoding/json, +/// fmt) and `c.py` (os, sys) on day 1; Alice rewrites `a.go` (fmt, +/// net/http) on day 2. +fn fixture() -> TestRepo { + let test = init_repo().expect("init"); + commit_files_as(&test, ALICE.0, ALICE.1, "i1", T0, &[("a.go", A_GO_V1)]).expect("i1"); + commit_files_as( + &test, + BOB.0, + BOB.1, + "i2", + T0 + DAY, + &[("b.go", B_GO), ("c.py", C_PY)], + ) + .expect("i2"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "i3", + T0 + 2 * DAY, + &[("a.go", A_GO_V2)], + ) + .expect("i3"); + test +} + +/// Whether the identity detector's `names|emails` label names `who`. +fn is_developer(label: &str, who: &str) -> bool { + label.to_lowercase().starts_with(&who.to_lowercase()) +} + +/// The developer entry of `report["imports"]` labelled `who`. +fn developer<'a>(report: &'a serde_json::Value, who: &str) -> &'a serde_json::Value { + let imports = report["imports"].as_object().expect("imports object"); + imports + .iter() + .find(|(name, _)| is_developer(name, who)) + .map(|(_, v)| v) + .unwrap_or_else(|| panic!("no developer {who} in {report}")) +} + +/// `--format json` is the documented 4-level map with real language names +/// and per-tick counts, plus `tick_size` and `author_index`. +#[test] +fn json_carries_developer_language_import_tick_counts() { + let test = fixture(); + let report = json_ok(&["-a", "history/imports", "--format", "json", path(&test)]); + assert_eq!(report["tick_size"], DEFAULT_TICK_SECS, "{report}"); + + let index = report["author_index"].as_array().expect("author_index"); + assert_eq!(index.len(), 2, "{report}"); + assert!( + is_developer(index[0].as_str().unwrap(), ALICE.0), + "{report}" + ); + assert!(is_developer(index[1].as_str().unwrap(), BOB.0), "{report}"); + + let alice = developer(&report, ALICE.0); + assert_eq!(alice["go"]["fmt"]["0"], 1, "{report}"); + assert_eq!(alice["go"]["fmt"]["2"], 1, "{report}"); + assert_eq!(alice["go"]["os"]["0"], 1, "{report}"); + assert_eq!(alice["go"]["net/http"]["2"], 1, "{report}"); + assert!(alice["go"]["os"].get("2").is_none(), "{report}"); + + let bob = developer(&report, BOB.0); + assert_eq!(bob["go"]["encoding/json"]["1"], 1, "{report}"); + assert_eq!(bob["go"]["fmt"]["1"], 1, "{report}"); + assert_eq!(bob["python"]["os"]["1"], 1, "{report}"); + assert_eq!(bob["python"]["sys"]["1"], 1, "{report}"); + assert!( + bob.get("uast").is_none(), + "no placeholder language: {report}" + ); +} + +/// yaml renders the same map under the analyzer header; bin carries the json +/// value; two runs are byte-identical. +#[test] +fn yaml_and_bin_carry_the_same_report_deterministically() { + let test = fixture(); + let p = path(&test); + let yaml = String::from_utf8(stdout_ok(&["-a", "history/imports", "--format", "yaml", p])) + .expect("utf-8 yaml"); + assert!(yaml.starts_with("codefang (v2):\n"), "{yaml}"); + assert!(yaml.contains("ImportsPerDeveloper:\n"), "{yaml}"); + assert!(yaml.contains("tick_size: 86400"), "{yaml}"); + assert!(yaml.contains("net/http:"), "{yaml}"); + assert!(yaml.contains("python:"), "{yaml}"); + assert!(yaml.contains("author_index:"), "{yaml}"); + + let json = stdout_ok(&["-a", "history/imports", "--format", "json", p]); + let bin = stdout_ok(&["-a", "history/imports", "--format", "bin", p]); + assert_eq!(&bin[..4], b"CFB1"); + assert_eq!( + &bin[BIN_HEADER..], + &json[..], + "bin payload is the json value" + ); + + let again = stdout_ok(&["-a", "history/imports", "--format", "json", p]); + assert_eq!(json, again, "two runs are byte-identical"); + let yaml_again = stdout_ok(&["-a", "history/imports", "--format", "yaml", p]); + assert_eq!(yaml.as_bytes(), &yaml_again[..]); +} + +/// The ndjson records carry the parser's language names. +#[test] +fn ndjson_entries_carry_real_language_names() { + let test = fixture(); + let ndjson = String::from_utf8(stdout_ok(&[ + "-a", + "history/imports", + "--format", + "ndjson", + path(&test), + ])) + .expect("utf-8"); + assert!( + ndjson.contains(r#"{"Lang":"go","Import":"fmt"}"#), + "{ndjson}" + ); + assert!( + ndjson.contains(r#"{"Lang":"python","Import":"sys"}"#), + "{ndjson}" + ); + assert!(!ndjson.contains(r#""Lang":"uast""#), "{ndjson}"); +} + +/// A history without any supported file is the empty report, not an error. +#[test] +fn no_imports_is_the_empty_report() { + let test = init_repo().expect("init"); + commit_files_as( + &test, + ALICE.0, + ALICE.1, + "txt", + T0, + &[("notes.txt", b"hello\n")], + ) + .expect("txt"); + let report = json_ok(&["-a", "history/imports", "--format", "json", path(&test)]); + assert_eq!(report["tick_size"], DEFAULT_TICK_SECS, "{report}"); + assert_eq!(report["imports"], serde_json::json!({}), "{report}"); + assert_eq!( + report["author_index"].as_array().map(Vec::len), + Some(1), + "{report}" + ); +} diff --git a/bins/codefang/tests/mcp_e2e.rs b/bins/codefang/tests/mcp_e2e.rs new file mode 100644 index 00000000..41f06f4a --- /dev/null +++ b/bins/codefang/tests/mcp_e2e.rs @@ -0,0 +1,236 @@ +//! End-to-end coverage of `codefang mcp`, the Model Context Protocol server, +//! driven over its stdio transport through the **real binary** +//! (`site/integrations/mcp.md`): the client handshake, the tool list, and +//! one call of each tool, whose results are the CLI's own reports. + +mod common; + +use std::collections::BTreeMap; +use std::io::Write as _; +use std::process::Stdio; + +use cf_gitlib::testutil::{commit_files_as, init_repo, TestRepo}; +use serde_json::{json, Value}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day. +const DAY: i64 = 86_400; +/// A Go function with one branch. +const GO_CODE: &str = + "package main\n\nfunc sign(x int) int {\n\tif x < 0 {\n\t\treturn -1\n\t}\n\treturn 1\n}\n"; + +/// A two-author history. +fn history() -> TestRepo { + let repo = init_repo().expect("init"); + commit_files_as( + &repo, + "alice", + "alice@x", + "add", + T0, + &[("a.go", GO_CODE.as_bytes())], + ) + .expect("commit"); + let edited = format!("{GO_CODE}\nfunc two() int {{\n\treturn 2\n}}\n"); + commit_files_as( + &repo, + "bob", + "bob@x", + "edit", + T0 + DAY, + &[("a.go", edited.as_bytes())], + ) + .expect("commit"); + repo +} + +/// Sends `requests` (one JSON-RPC message per line) to `codefang mcp`, closes +/// its stdin and returns every response keyed by request id. +fn session(requests: &[Value]) -> BTreeMap { + let mut child = common::command(BIN) + .arg("mcp") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn codefang mcp"); + { + let mut stdin = child.stdin.take().expect("stdin"); + for request in requests { + writeln!(stdin, "{request}").expect("write request"); + } + } + let out = child.wait_with_output().expect("wait"); + assert!( + out.status.success(), + "mcp exited {:?}: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout) + .expect("utf-8") + .lines() + .map(|line| { + let response: Value = + serde_json::from_str(line).expect("one JSON-RPC response per line"); + (response["id"].as_i64().expect("response id"), response) + }) + .collect() +} + +/// A `tools/call` request. +fn call(id: i64, tool: &str, arguments: Value) -> Value { + json!({"jsonrpc": "2.0", "id": id, "method": "tools/call", + "params": {"name": tool, "arguments": arguments}}) +} + +/// The JSON document a successful tool result carries. +fn tool_json(response: &Value) -> Value { + let result = &response["result"]; + assert_eq!(result["isError"], false, "{response}"); + let text = result["content"][0]["text"].as_str().expect("text content"); + serde_json::from_str(text).unwrap_or_else(|e| panic!("tool text is not JSON ({e}): {text}")) +} + +/// The handshake, the tool list, and one call of every tool: the parsed +/// tree, the static report and the history report the CLI renders. +#[test] +fn an_mcp_client_lists_and_calls_every_tool() { + let repo = history(); + let repo_path = repo.path().to_str().expect("utf-8 path"); + let responses = session(&[ + json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2024-11-05", "capabilities": {}, + "clientInfo": {"name": "e2e", "version": "1"}}}), + json!({"jsonrpc": "2.0", "method": "notifications/initialized"}), + json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}), + call(3, "uast_parse", json!({"code": GO_CODE, "language": "go"})), + call( + 4, + "codefang_analyze", + json!({"code": GO_CODE, "language": "go", "analyzers": ["complexity"]}), + ), + call( + 5, + "codefang_history", + json!({"repo_path": repo_path, "analyzers": ["devs"]}), + ), + call(6, "codefang_analyze", json!({"code": "", "language": "go"})), + ]); + + assert!( + responses[&1]["result"]["serverInfo"]["name"].is_string(), + "{}", + responses[&1] + ); + let mut tools: Vec<&str> = responses[&2]["result"]["tools"] + .as_array() + .expect("tools") + .iter() + .map(|t| t["name"].as_str().expect("tool name")) + .collect(); + tools.sort_unstable(); + assert_eq!( + tools, + ["codefang_analyze", "codefang_history", "uast_parse"] + ); + + let tree = serde_json::to_string(&tool_json(&responses[&3])).expect("tree"); + assert!( + tree.contains("sign"), + "the parsed tree names the function: {tree}" + ); + + let analyzed = tool_json(&responses[&4]); + let cli = common::command(BIN) + .args(["run", "-a", "static/complexity", "--format", "json"]) + .arg(write_snippet(GO_CODE).path()) + .output() + .expect("run"); + let cli: Value = serde_json::from_slice(&cli.stdout).expect("cli json"); + assert_eq!( + analyzed["complexity"], cli, + "the analyze tool returns the CLI report" + ); + + let history = tool_json(&responses[&5]); + let cli = common::command(BIN) + .args([ + "run", + "-a", + "history/devs", + "--format", + "json", + "--limit", + "1000", + repo_path, + ]) + .output() + .expect("run"); + let cli: Value = serde_json::from_slice(&cli.stdout).expect("cli json"); + assert_eq!( + history["devs"], cli, + "the history tool returns the CLI report" + ); + + assert_eq!( + responses[&6]["result"]["isError"], true, + "{}", + responses[&6] + ); +} + +/// A directory holding `code` as `code.go`, the file the analyze tool reads. +fn write_snippet(code: &str) -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("dir"); + std::fs::write(dir.path().join("code.go"), code).expect("write"); + dir +} + +/// The tools take Linguist language names (and their aliases), not only +/// file extensions. +#[test] +fn tools_take_language_names() { + let responses = session(&[ + call( + 1, + "uast_parse", + json!({"code": "def hello():\n return 1\n", "language": "python"}), + ), + call( + 2, + "uast_parse", + json!({"code": "function hello() { return 1 }\n", "language": "JavaScript"}), + ), + call( + 3, + "uast_parse", + json!({"code": "def hello():\n return 1\n", "language": "py"}), + ), + call( + 4, + "codefang_analyze", + json!({"code": "def hello():\n return 1\n", "language": "python", "analyzers": ["complexity"]}), + ), + call( + 5, + "uast_parse", + json!({"code": "x", "language": "notalang"}), + ), + ]); + for id in 1..=4 { + let tree = serde_json::to_string(&tool_json(&responses[&id])).expect("tree"); + assert!(tree.contains("hello"), "{id}: {tree}"); + } + let unknown = &responses[&5]["result"]; + assert_eq!(unknown["isError"], true, "{unknown}"); + assert!( + unknown["content"][0]["text"] + .as_str() + .is_some_and(|t| t.contains("unsupported language: notalang")), + "{unknown}" + ); +} diff --git a/bins/codefang/tests/mixed_selection_e2e.rs b/bins/codefang/tests/mixed_selection_e2e.rs new file mode 100644 index 00000000..49c2391b --- /dev/null +++ b/bins/codefang/tests/mixed_selection_e2e.rs @@ -0,0 +1,118 @@ +//! End-to-end coverage of a MIXED static+history selection +//! (`-a static/complexity,history/devs`) in the formats whose content is +//! per-phase: `timeseries`, `timeseries --ndjson`, `text` and `compact`. +//! +//! A mixed run must carry each phase's own rendering: the history time +//! series (static analyzers have no per-commit data), the static text +//! sections followed by the history text, and one compact line per analyzer. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; + +/// The binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture history: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits. +const DAY: i64 = 86_400; +/// Number of fixture commits. +const N_COMMITS: usize = 3; +/// The mixed selection under test. +const MIXED: &str = "static/complexity,history/devs"; + +/// Three daily commits, each adding one Go function with one branch. +fn fixture() -> TestRepo { + let test = init_repo().expect("init fixture repo"); + for i in 0..N_COMMITS { + let source = + format!("package main\n\nfunc f{i}(a int) int {{\n\tif a > {i} {{\n\t\treturn a\n\t}}\n\treturn 0\n}}\n"); + commit_files( + &test, + &format!("commit {i}"), + T0 + i64::try_from(i).expect("index fits i64") * DAY, + &[(format!("f{i}.go").as_str(), source.as_bytes())], + ) + .expect("fixture commit"); + } + test +} + +/// Runs `codefang run ` and returns `(exit code, stdout, stderr)`. +fn run(args: &[&str]) -> (i32, String, String) { + let out = common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .output() + .expect("run the codefang binary"); + ( + out.status.code().unwrap_or(-1), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +/// Runs a selection in a format and asserts a clean exit. +fn stdout_of(selection: &str, format_args: &[&str], path: &str) -> (String, String) { + let args = [&["-a", selection], format_args, &[path]].concat(); + let (rc, stdout, stderr) = run(&args); + assert_eq!(rc, 0, "{selection} {format_args:?} failed: {stderr}"); + (stdout, stderr) +} + +/// The mixed time series is the history phase's time series; the static +/// analyzers, which have no per-commit data, are named on stderr. +#[test] +fn mixed_timeseries_is_the_history_time_series() { + let test = fixture(); + let p = test.path().to_str().expect("utf-8 path"); + let (history, _) = stdout_of("history/devs", &["--format", "timeseries"], p); + let (mixed, stderr) = stdout_of(MIXED, &["--format", "timeseries"], p); + assert_eq!( + mixed, history, + "mixed timeseries must carry the history commits" + ); + let doc: serde_json::Value = serde_json::from_str(&mixed).expect("timeseries json"); + assert_eq!(doc["commits"].as_array().map(Vec::len), Some(N_COMMITS)); + assert!(stderr.contains("static/complexity"), "{stderr}"); +} + +/// `--format timeseries --ndjson` in a mixed run: one line per commit, equal +/// to the history-only lines. +#[test] +fn mixed_timeseries_ndjson_is_the_history_lines() { + let test = fixture(); + let p = test.path().to_str().expect("utf-8 path"); + let ts = ["--format", "timeseries", "--ndjson"]; + let (history, _) = stdout_of("history/devs", &ts, p); + let (mixed, _) = stdout_of(MIXED, &ts, p); + assert_eq!(mixed, history); + assert_eq!(mixed.lines().count(), N_COMMITS); +} + +/// Mixed text: the static sections, then the history text. +#[test] +fn mixed_text_renders_both_phases() { + let test = fixture(); + let p = test.path().to_str().expect("utf-8 path"); + let (history, _) = stdout_of("history/devs", &["--format", "text"], p); + let (mixed, _) = stdout_of(MIXED, &["--format", "text"], p); + assert!( + mixed.contains("COMPLEXITY"), + "static section missing:\n{mixed}" + ); + assert!(mixed.ends_with(&history), "history text missing:\n{mixed}"); +} + +/// Mixed compact: each phase's compact output, static first. +#[test] +fn mixed_compact_renders_both_phases() { + let test = fixture(); + let p = test.path().to_str().expect("utf-8 path"); + let (static_only, _) = stdout_of("static/complexity", &["--format", "compact"], p); + let (history, _) = stdout_of("history/devs", &["--format", "compact"], p); + let (mixed, _) = stdout_of(MIXED, &["--format", "compact"], p); + assert_eq!(mixed, format!("{static_only}{history}")); +} diff --git a/bins/codefang/tests/quality_e2e.rs b/bins/codefang/tests/quality_e2e.rs new file mode 100644 index 00000000..703241f8 --- /dev/null +++ b/bins/codefang/tests/quality_e2e.rs @@ -0,0 +1,342 @@ +//! End-to-end coverage of `history/quality`, driven through the **real +//! binary** over real on-disk git fixtures +//! (`specs/bugs/BUG-history-quality-ticks.md`). +//! +//! Every assertion is a contract from `site/analyzers/quality.md`: a tick's +//! statistics are the static analyzers' numbers over the files changed in +//! that tick, ticks without analyzed files are absent, the aggregate is +//! internally consistent (`min <= mean`), and a large commit is fully counted. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T12:00:00Z. +const T0: i64 = 1_609_502_400; +/// One day between fixture commits (one tick at the default tick size). +const DAY: i64 = 24 * 3_600; +/// Number of files in the "big commit" fixture. +const BIG_COMMIT_FILES: usize = 40; +/// Float tolerance for numbers that cross a YAML/JSON round trip. +const EPS: f64 = 1e-9; + +/// `Add` with one `if`: cyclomatic 2. +const ADD_CC2_GO: &[u8] = b"package main + +// Add returns the sum of a and b. +func Add(a, b int) int { +\tif a > b { +\t\treturn a + b +\t} +\treturn b +} +"; + +/// `Add` with three `if`s: cyclomatic 4. +const ADD_CC4_GO: &[u8] = b"package main + +// Add returns the sum of a and b. +func Add(a, b int) int { +\tif a > b { +\t\treturn a + b +\t} +\tif a == 0 { +\t\treturn b +\t} +\tif b == 0 { +\t\treturn a +\t} +\treturn b +} +"; + +/// A type declaration only: no functions, no cohesion data. +const NO_FUNCTIONS_GO: &[u8] = + b"package main\n\n// Config holds settings.\ntype Config struct {\n\tName string\n}\n"; + +/// The complexity crate's Python fixture: token-less operator nodes, so the +/// numbers depend on source recovery. +const PY_FIXTURE: &str = include_str!("../../../crates/cf-complexity/tests/fixtures/cc.py"); + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// stdout of a successful run, as text. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// The quality report of `repo` as JSON. +fn quality_json(repo: &str) -> Value { + let text = stdout_ok(&["-a", "history/quality", "--format", "json", repo]); + serde_json::from_str(&text).unwrap_or_else(|e| panic!("stdout is not JSON ({e}):\n{text}")) +} + +/// A top-level `key: value` scalar of a YAML document, as `f64`. +fn yaml_scalar(yaml: &str, key: &str) -> f64 { + let prefix = format!("{key}: "); + yaml.lines() + .find_map(|line| line.strip_prefix(&prefix)) + .unwrap_or_else(|| panic!("no top-level {key:?} in:\n{yaml}")) + .trim() + .parse() + .unwrap_or_else(|e| panic!("{key} is not a number ({e}) in:\n{yaml}")) +} + +/// `aggregate.` of the yaml report of static analyzer `id` over `path`. +fn static_aggregate(id: &str, path: &str, key: &str) -> f64 { + let yaml = stdout_ok(&["-a", id, "--format", "yaml", path]); + let aggregate = yaml + .split_once("\naggregate:\n") + .map(|(_, rest)| rest) + .unwrap_or_else(|| panic!("no aggregate in {id} yaml:\n{yaml}")); + let unindented: String = aggregate + .lines() + .map(|l| l.trim_start()) + .collect::>() + .join("\n"); + yaml_scalar(&unindented, key) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +fn series(report: &Value) -> &Vec { + report["time_series"] + .as_array() + .unwrap_or_else(|| panic!("no time_series in {report}")) +} + +fn stat(entry: &Value, key: &str) -> f64 { + entry["stats"][key] + .as_f64() + .unwrap_or_else(|| panic!("no numeric stats.{key} in {entry}")) +} + +/// A Go source body with `n` one-line functions. +fn go_source(n: usize) -> Vec { + let mut s = String::from("package main\n\n"); + for i in 0..n { + s.push_str(&format!("func f{i}() int {{ return {i} }}\n")); + } + s.into_bytes() +} + +/// Per-tick statistics follow the files changed in that tick: complexity +/// 2 → 4 over two ticks separated by an empty day; the gap day has no tick. +#[test] +fn per_tick_complexity_follows_the_changed_file() { + let test = init_repo().expect("init"); + commit_files(&test, "cc2", T0, &[("a.go", ADD_CC2_GO)]).expect("cc2"); + commit_files(&test, "cc4", T0 + 2 * DAY, &[("a.go", ADD_CC4_GO)]).expect("cc4"); + let report = quality_json(path(&test)); + + let ticks: Vec = series(&report) + .iter() + .map(|e| e["tick"].as_i64().unwrap()) + .collect(); + assert_eq!(ticks, vec![0, 2], "{report}"); + let (first, second) = (&series(&report)[0], &series(&report)[1]); + assert_eq!(stat(first, "complexity_mean"), 2.0, "{first}"); + assert_eq!(stat(first, "files_analyzed"), 1.0); + assert_eq!(stat(first, "total_functions"), 1.0); + assert_eq!(stat(second, "complexity_mean"), 4.0, "{second}"); + assert_eq!(stat(second, "max_complexity"), 4.0); + assert_eq!(first["start_time"], "2021-01-01T12:00:00Z"); + assert_eq!(second["end_time"], "2021-01-03T12:00:00Z"); + assert_eq!(report["aggregate"]["total_ticks"], 2); + assert_eq!(report["aggregate"]["total_files_analyzed"], 2); + assert_eq!(report["aggregate"]["complexity_median_mean"], 3.0); +} + +/// A commit without analyzable files (a text file) and a commit whose only +/// Go file has no functions do not drag the aggregate means below the +/// aggregate minimums: `min <= mean` holds for cohesion and comment score, +/// and a tick without analyzed files is absent. +#[test] +fn aggregate_minimums_never_exceed_the_means() { + let test = init_repo().expect("init"); + commit_files(&test, "code", T0, &[("a.go", ADD_CC2_GO)]).expect("code"); + commit_files(&test, "notes", T0 + DAY, &[("notes.txt", b"not source\n")]).expect("notes"); + commit_files(&test, "types", T0 + 2 * DAY, &[("c.go", NO_FUNCTIONS_GO)]).expect("types"); + let report = quality_json(path(&test)); + + let ticks: Vec = series(&report) + .iter() + .map(|e| e["tick"].as_i64().unwrap()) + .collect(); + assert_eq!( + ticks, + vec![0, 2], + "the text-only tick has no entry: {report}" + ); + let agg = &report["aggregate"]; + let f = |k: &str| agg[k].as_f64().unwrap_or_else(|| panic!("no {k} in {agg}")); + assert_eq!(agg["total_ticks"], 2, "{agg}"); + assert_eq!(agg["total_files_analyzed"], 2, "{agg}"); + assert!(f("min_cohesion") <= f("cohesion_mean_mean"), "{agg}"); + assert!( + f("min_comment_score") <= f("comment_score_mean_mean"), + "{agg}" + ); + // The function-free file carries no cohesion data: both ticks' cohesion + // aggregates come from the one function-bearing file. + assert_eq!(f("min_cohesion"), 1.0, "{agg}"); + assert_eq!(f("cohesion_mean_mean"), 1.0, "{agg}"); + let types_tick = &series(&report)[1]; + assert_eq!(stat(types_tick, "files_analyzed"), 1.0, "{types_tick}"); + assert_eq!(stat(types_tick, "total_functions"), 0.0, "{types_tick}"); + assert_eq!( + stat(types_tick, "cohesion_mean"), + 0.0, + "no cohesion data: {types_tick}" + ); +} + +/// A 40-file commit contributes every file. +#[test] +fn a_large_commit_is_fully_counted() { + let test = init_repo().expect("init"); + let files: Vec<(String, Vec)> = (0..BIG_COMMIT_FILES) + .map(|i| (format!("f{i}.go"), go_source(1))) + .collect(); + let refs: Vec<(&str, &[u8])> = files + .iter() + .map(|(n, b)| (n.as_str(), b.as_slice())) + .collect(); + commit_files(&test, "big", T0, &refs).expect("big"); + let report = quality_json(path(&test)); + assert_eq!( + report["aggregate"]["total_files_analyzed"], BIG_COMMIT_FILES as i64, + "{report}" + ); + assert_eq!( + stat(&series(&report)[0], "files_analyzed"), + BIG_COMMIT_FILES as f64 + ); + assert_eq!( + stat(&series(&report)[0], "total_functions"), + BIG_COMMIT_FILES as f64 + ); +} + +/// The per-file numbers of a one-commit, one-file repository are the static +/// analyzers' numbers for that file — for a Go file and for a Python file +/// whose Halstead counts depend on source recovery. +#[test] +fn quality_numbers_equal_the_static_analyzers() { + for (name, body) in [("a.go", ADD_CC2_GO), ("cc.py", PY_FIXTURE.as_bytes())] { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join(name), body).expect("write"); + let dir_path = dir.path().to_str().unwrap(); + + let test = init_repo().expect("init"); + commit_files(&test, "one", T0, &[(name, body)]).expect("one"); + let report = quality_json(path(&test)); + let entry = &series(&report)[0]; + assert_eq!(stat(entry, "files_analyzed"), 1.0, "{name}: {entry}"); + + let cx = |k: &str| static_aggregate("static/complexity", dir_path, k); + assert_eq!( + stat(entry, "complexity_mean"), + cx("total_complexity"), + "{name}" + ); + assert_eq!( + stat(entry, "max_complexity"), + cx("max_complexity"), + "{name}" + ); + assert_eq!( + stat(entry, "total_functions"), + cx("total_functions"), + "{name}" + ); + + let hal = |k: &str| static_aggregate("static/halstead", dir_path, k); + assert!( + (stat(entry, "halstead_vol_mean") - hal("volume")).abs() < EPS, + "{name}: halstead volume {} vs static {}", + stat(entry, "halstead_vol_mean"), + hal("volume") + ); + assert!( + (stat(entry, "delivered_bugs_sum") - hal("delivered_bugs")).abs() < EPS, + "{name}: delivered bugs" + ); + + let com = |k: &str| static_aggregate("static/comments", dir_path, k); + assert_eq!( + stat(entry, "comment_score_mean"), + com("overall_score"), + "{name}" + ); + assert_eq!( + stat(entry, "doc_coverage_mean"), + com("documentation_coverage"), + "{name}" + ); + + let coh = static_aggregate("static/cohesion", dir_path, "cohesion_score"); + assert_eq!(stat(entry, "cohesion_mean"), coh, "{name}"); + } +} + +/// `json` and `yaml` carry the same numbers, and a run is deterministic. +#[test] +fn json_and_yaml_agree_and_runs_are_deterministic() { + let test = init_repo().expect("init"); + commit_files(&test, "cc2", T0, &[("a.go", ADD_CC2_GO)]).expect("cc2"); + commit_files(&test, "cc4", T0 + DAY, &[("b.go", ADD_CC4_GO)]).expect("cc4"); + let p = path(&test); + + let first = stdout_ok(&["-a", "history/quality", "--format", "json", p]); + let second = stdout_ok(&["-a", "history/quality", "--format", "json", p]); + assert_eq!(first, second, "json output differs between runs"); + + let json: Value = serde_json::from_str(&first).expect("json"); + let yaml = stdout_ok(&["-a", "history/quality", "--format", "yaml", p]); + let agg = yaml + .split_once("\naggregate:\n") + .map(|(_, rest)| rest) + .unwrap_or_else(|| panic!("no aggregate in yaml:\n{yaml}")); + let unindented: String = agg + .lines() + .map(|l| l.trim_start()) + .collect::>() + .join("\n"); + for key in [ + "total_ticks", + "total_files_analyzed", + "complexity_median_mean", + "min_cohesion", + "cohesion_mean_mean", + ] { + let want = json["aggregate"][key].as_f64().expect("number"); + assert!( + (yaml_scalar(&unindented, key) - want).abs() < EPS, + "{key}: yaml {} vs json {want}", + yaml_scalar(&unindented, key) + ); + } +} diff --git a/bins/codefang/tests/sentiment_e2e.rs b/bins/codefang/tests/sentiment_e2e.rs new file mode 100644 index 00000000..041cb990 --- /dev/null +++ b/bins/codefang/tests/sentiment_e2e.rs @@ -0,0 +1,272 @@ +//! End-to-end coverage of `history/sentiment`, driven through the **real +//! binary** over real on-disk git fixtures +//! (`specs/bugs/BUG-history-sentiment-scoring.md`). +//! +//! Every assertion is a contract from `site/analyzers/sentiment.md`: only +//! the comments a commit introduced or changed are scored, only ticks with +//! such comments carry a score, scores and trend endpoints lie in `[0, 1]`, +//! Javadoc blocks count, `--min-comment-len` is applied, and the output is +//! deterministic across runs and formats. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T12:00:00Z. +const T0: i64 = 1_609_502_400; +/// One day between fixture commits (one tick at the default tick size). +const DAY: i64 = 24 * 3_600; +/// The documented positive / negative classification thresholds. +const POSITIVE_THRESHOLD: f64 = 0.6; +const NEGATIVE_THRESHOLD: f64 = 0.4; +/// A `--min-comment-len` longer than every fixture comment. +const LONGER_THAN_EVERY_COMMENT: &str = "200"; + +const POSITIVE_GO: &[u8] = + b"package main\n\n// This is great, excellent work and really wonderful code\nfunc A() {}\n"; +const POSITIVE_PLUS_CODE_GO: &[u8] = + b"package main\n\n// This is great, excellent work and really wonderful code\nfunc A() {}\n\nfunc B() {}\n"; +const NEGATIVE_GO: &[u8] = + b"package main\n\n// This is a terrible awful hack, horrible nightmare mess\nfunc N() {}\n"; +const JAVADOC_GO: &[u8] = b"package main + +/** + * This function is absolutely wonderful and great + * and it works beautifully every single time + */ +func J() {} +"; +const NEUTRAL_PY: &[u8] = b"# a python file whose comment describes the input validation\ndef receive(x):\n return x\n"; + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// stdout of a successful run, as text. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// The sentiment report of `repo` as JSON, with extra flags. +fn sentiment_json(repo: &str, extra: &[&str]) -> Value { + let args = [ + &["-a", "history/sentiment", "--format", "json"], + extra, + &[repo], + ] + .concat(); + let text = stdout_ok(&args); + serde_json::from_str(&text).unwrap_or_else(|e| panic!("stdout is not JSON ({e}):\n{text}")) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +fn series(report: &Value) -> &Vec { + report["time_series"] + .as_array() + .unwrap_or_else(|| panic!("no time_series in {report}")) +} + +fn ticks(report: &Value) -> Vec { + series(report) + .iter() + .map(|e| e["tick"].as_i64().unwrap()) + .collect() +} + +fn sentiment(entry: &Value) -> f64 { + entry["sentiment"] + .as_f64() + .unwrap_or_else(|| panic!("no numeric sentiment in {entry}")) +} + +/// The audit fixture: positive comment, code-only commit, negative comment, +/// Javadoc block, Python comment — one commit per day. +fn audit_fixture() -> TestRepo { + let test = init_repo().expect("init"); + commit_files(&test, "positive", T0, &[("s.go", POSITIVE_GO)]).expect("s1"); + commit_files( + &test, + "code only", + T0 + DAY, + &[("s.go", POSITIVE_PLUS_CODE_GO)], + ) + .expect("s2"); + commit_files(&test, "negative", T0 + 2 * DAY, &[("n.go", NEGATIVE_GO)]).expect("s3"); + commit_files(&test, "javadoc", T0 + 3 * DAY, &[("j.go", JAVADOC_GO)]).expect("s4"); + commit_files(&test, "python", T0 + 4 * DAY, &[("x.py", NEUTRAL_PY)]).expect("s5"); + test +} + +/// Positive and negative comments give ordered scores in `[0, 1]` with the +/// documented classifications; the code-only commit's tick carries no score +/// and is not a low-sentiment period; the Javadoc block is scored. +#[test] +fn scores_are_ordered_and_only_commented_ticks_are_scored() { + let test = audit_fixture(); + let report = sentiment_json(path(&test), &[]); + + assert_eq!(ticks(&report), vec![0, 2, 3, 4], "{report}"); + let by_tick = |t: i64| { + series(&report) + .iter() + .find(|e| e["tick"] == t) + .unwrap_or_else(|| panic!("no tick {t} in {report}")) + }; + let positive = sentiment(by_tick(0)); + let negative = sentiment(by_tick(2)); + let javadoc = sentiment(by_tick(3)); + for s in [positive, negative, javadoc, sentiment(by_tick(4))] { + assert!((0.0..=1.0).contains(&s), "score out of range: {report}"); + } + assert!(positive > negative, "{report}"); + assert!(positive >= POSITIVE_THRESHOLD, "{report}"); + assert!(negative <= NEGATIVE_THRESHOLD, "{report}"); + assert_eq!(by_tick(0)["classification"], "positive"); + assert_eq!(by_tick(2)["classification"], "negative"); + assert_eq!(by_tick(3)["comment_count"], 1, "javadoc counted: {report}"); + assert!(javadoc >= POSITIVE_THRESHOLD, "{report}"); + + let low = report["low_sentiment_periods"] + .as_array() + .unwrap_or_else(|| panic!("no low_sentiment_periods in {report}")); + assert_eq!(low.len(), 1, "{report}"); + assert_eq!(low[0]["tick"], 2); + assert_eq!(report["aggregate"]["total_ticks"], 4); + assert_eq!(report["aggregate"]["total_commits"], 5); + assert_eq!(report["aggregate"]["total_comments"], 4); +} + +/// Unchanged comments are not re-counted: a commit that only adds code to a +/// commented file contributes zero comments, in the per-commit stream too. +#[test] +fn unchanged_comments_are_not_recounted() { + let test = audit_fixture(); + let p = path(&test); + let ndjson = stdout_ok(&["-a", "history/sentiment", "--format", "ndjson", p]); + let lines: Vec<&str> = ndjson.lines().collect(); + assert_eq!(lines.len(), 5, "{ndjson}"); + assert!( + lines[0].contains("This is great, excellent work"), + "{}", + lines[0] + ); + assert!( + lines[1].contains(r#""Comments":[]"#), + "the code-only commit must carry no comment: {}", + lines[1] + ); + + // Editing the comment counts it once as new text. + let edited = b"package main\n\n// This is still great, excellent work and really wonderful code\nfunc A() {}\n\nfunc B() {}\n"; + commit_files(&test, "edit comment", T0 + 5 * DAY, &[("s.go", edited)]).expect("s6"); + let ndjson = stdout_ok(&["-a", "history/sentiment", "--format", "ndjson", p]); + let last = ndjson.lines().last().expect("a record"); + assert!(last.contains("still great"), "{last}"); + assert_eq!(last.matches("excellent work").count(), 1, "{last}"); +} + +/// Trend endpoints lie in `[0, 1]` even when the fitted line overshoots. +#[test] +fn trend_endpoints_stay_in_the_unit_interval() { + let test = init_repo().expect("init"); + commit_files(&test, "p1", T0, &[("a.go", POSITIVE_GO)]).expect("p1"); + commit_files(&test, "p2", T0 + DAY, &[("b.go", JAVADOC_GO)]).expect("p2"); + commit_files(&test, "n", T0 + 2 * DAY, &[("c.go", NEGATIVE_GO)]).expect("n"); + let report = sentiment_json(path(&test), &[]); + let trend = &report["trend"]; + let start = trend["start_sentiment"].as_f64().unwrap(); + let end = trend["end_sentiment"].as_f64().unwrap(); + assert!((0.0..=1.0).contains(&start), "{trend}"); + assert!((0.0..=1.0).contains(&end), "{trend}"); + assert_eq!(trend["trend_direction"], "declining", "{trend}"); + let change = trend["change_percent"].as_f64().unwrap(); + assert!(change >= -100.0, "{trend}"); + assert!( + ((end - start) / start * 100.0 - change).abs() < 1e-3, + "{trend}" + ); +} + +/// `--min-comment-len` is applied: a threshold above every comment length +/// leaves no scored tick; a negative value is a named error. +#[test] +fn min_comment_len_flag_is_applied_and_validated() { + let test = audit_fixture(); + let p = path(&test); + let default = sentiment_json(p, &[]); + assert_eq!(default["aggregate"]["total_comments"], 4, "{default}"); + + let strict = sentiment_json(p, &["--min-comment-len", LONGER_THAN_EVERY_COMMENT]); + assert_eq!(strict["aggregate"]["total_comments"], 0, "{strict}"); + assert_eq!(strict["aggregate"]["total_ticks"], 0, "{strict}"); + assert_eq!(strict["aggregate"]["total_commits"], 5, "{strict}"); + assert!(ticks(&strict).is_empty(), "{strict}"); + + let out = codefang(&[ + "-a", + "history/sentiment", + "--format", + "json", + "--min-comment-len=-1", + p, + ]); + assert_eq!(out.status.code(), Some(1)); + assert!(out.stdout.is_empty()); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains("Error: invalid --min-comment-len: must be >= 0, got -1"), + "{err}" + ); +} + +/// Runs are deterministic and `json` / `yaml` agree. +#[test] +fn runs_are_deterministic_and_formats_agree() { + let test = audit_fixture(); + let p = path(&test); + let first = stdout_ok(&["-a", "history/sentiment", "--format", "json", p]); + let second = stdout_ok(&["-a", "history/sentiment", "--format", "json", p]); + assert_eq!(first, second, "json output differs between runs"); + + let json: Value = serde_json::from_str(&first).expect("json"); + let yaml = stdout_ok(&["-a", "history/sentiment", "--format", "yaml", p]); + let agg = &json["aggregate"]; + for key in [ + "total_ticks", + "total_comments", + "total_commits", + "positive_ticks", + ] { + let line = format!("{key}: {}", agg[key]); + assert!(yaml.contains(&line), "yaml lacks {line:?}:\n{yaml}"); + } + assert!( + yaml.contains(&format!("average_sentiment: {}", agg["average_sentiment"])), + "{yaml}" + ); + for entry in series(&json) { + let line = format!("sentiment: {}", entry["sentiment"]); + assert!(yaml.contains(&line), "yaml lacks {line:?}:\n{yaml}"); + } +} diff --git a/bins/codefang/tests/shotness_e2e.rs b/bins/codefang/tests/shotness_e2e.rs new file mode 100644 index 00000000..345ef70d --- /dev/null +++ b/bins/codefang/tests/shotness_e2e.rs @@ -0,0 +1,736 @@ +//! End-to-end coverage of `history/shotness` through the **real binary** over +//! on-disk git fixtures (`specs/bugs/BUG-history-shotness-nested-entities.md`). +//! +//! The contract under test is the one `site/analyzers/shotness.md` documents: +//! an entity is a DSL-selected declaration anywhere in the tree (class +//! methods included), identified by its qualified name (`Class.method`, +//! `Receiver.method`) and file; a commit counts once per entity whose span +//! its hunks overlap; the entities a commit touches are coupled pairwise; a +//! file rename moves the live entities; a removed declaration or a deleted +//! file ends them (dropped by default; kept with their history and marked +//! `ended` under `--shotness-keep-ended`); merge commits are not re-counted; +//! the output is deterministic. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, rename_file, CommitBuilder, TestRepo}; +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits. +const DAY: i64 = 24 * 3_600; +/// The analyzer under test. +const SHOTNESS: &str = "history/shotness"; + +/// `m.go` with two one-body functions. +const GO_F_G: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 1\n}\n\nfunc g() int {\n\treturn 2\n}\n"; +/// `GO_F_G` with `f`'s body changed. +const GO_F_CHANGED: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 11\n}\n\nfunc g() int {\n\treturn 2\n}\n"; +/// `GO_F_CHANGED` with `g`'s body changed too. +const GO_G_CHANGED: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 11\n}\n\nfunc g() int {\n\treturn 22\n}\n"; +/// Both bodies changed again. +const GO_BOTH_CHANGED: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 111\n}\n\nfunc g() int {\n\treturn 222\n}\n"; +/// `f` renamed to `f2` in place (its declaration line edited). +const GO_F_RENAMED: &[u8] = + b"package m\n\nfunc f2() int {\n\treturn 111\n}\n\nfunc g() int {\n\treturn 222\n}\n"; +/// `g` removed. +const GO_G_DELETED: &[u8] = b"package m\n\nfunc f2() int {\n\treturn 111\n}\n"; +/// `h` added. +const GO_H_ADDED: &[u8] = + b"package m\n\nfunc f2() int {\n\treturn 111\n}\n\nfunc h() int {\n\treturn 3\n}\n"; + +/// A Java class with two methods. +const JAVA_A: &[u8] = b"public class A {\n public int f() {\n return 1;\n }\n\n public int g() {\n return 2;\n }\n}\n"; +/// `JAVA_A` with `f` changed. +const JAVA_A_CHANGED: &[u8] = b"public class A {\n public int f() {\n return 11;\n }\n\n public int g() {\n return 2;\n }\n}\n"; +/// Two Python functions and a class with a method. +const PY_P: &[u8] = + b"def pf():\n return 1\n\ndef pg():\n return 2\n\nclass K:\n def km(self):\n return 3\n"; +/// `PY_P` with `K.km` changed. +const PY_P_CHANGED: &[u8] = + b"def pf():\n return 1\n\ndef pg():\n return 2\n\nclass K:\n def km(self):\n return 33\n"; +/// A TypeScript class with two methods and a top-level function. +const TS_T: &[u8] = b"class C {\n m1(): number { return 1; }\n m2(): number { return 2; }\n}\nfunction top(): number { return 3; }\n"; +/// A C# namespace with a class with two methods. +const CS_P: &[u8] = b"namespace N {\n public class C {\n public int M1() { return 1; }\n public int M2() { return 2; }\n }\n}\n"; +/// Nested Python declarations. +const PY_NESTED: &[u8] = b"def outer():\n def inner():\n return 1\n return inner\n\nclass K:\n def km(self):\n return 3\n class Inner:\n def im(self):\n return 4\n"; +/// Two Go receivers with a same-named method. +const GO_RECEIVERS: &[u8] = b"package r\n\ntype A struct{}\ntype B struct{}\n\nfunc (a *A) String() string { return \"a\" }\n\nfunc (b *B) String() string { return \"b\" }\n"; +/// `GO_RECEIVERS` with `A.String` changed. +const GO_RECEIVERS_CHANGED: &[u8] = b"package r\n\ntype A struct{}\ntype B struct{}\n\nfunc (a *A) String() string { return \"aa\" }\n\nfunc (b *B) String() string { return \"b\" }\n"; + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// stdout of a successful run, as text. +fn stdout_ok(args: &[&str]) -> String { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// The shotness JSON report of `repo` with `extra` flags. +fn shotness(repo: &TestRepo, extra: &[&str]) -> Value { + let mut args = vec!["-a", SHOTNESS, "--format", "json"]; + args.extend_from_slice(extra); + args.push(path(repo)); + let text = stdout_ok(&args); + serde_json::from_str(&text) + .unwrap_or_else(|e| panic!("stdout is not one JSON document ({e}):\n{text}")) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// Commits `files` at `T0 + day * DAY`. +fn commit(repo: &TestRepo, day: i64, files: &[(&str, &[u8])]) { + commit_files(repo, &format!("day {day}"), T0 + day * DAY, files).expect("commit"); +} + +/// `(name, type, file)` of every entity in the report, key-sorted. +fn entities(report: &Value) -> Vec<(String, String, String)> { + let mut out: Vec<(String, String, String)> = report["node_hotness"] + .as_array() + .unwrap_or_else(|| panic!("no node_hotness in {report}")) + .iter() + .map(|n| { + ( + n["name"].as_str().unwrap().to_string(), + n["type"].as_str().unwrap().to_string(), + n["file"].as_str().unwrap().to_string(), + ) + }) + .collect(); + out.sort(); + out +} + +/// The entity row for `(name, file)`. +fn row<'a>(report: &'a Value, name: &str, file: &str) -> &'a Value { + report["node_hotness"] + .as_array() + .expect("node_hotness") + .iter() + .find(|n| n["name"] == name && n["file"] == file) + .unwrap_or_else(|| panic!("no entity {name} in {file}: {report}")) +} + +fn change_count(report: &Value, name: &str, file: &str) -> i64 { + row(report, name, file)["change_count"].as_i64().unwrap() +} + +fn coupled_nodes(report: &Value, name: &str, file: &str) -> i64 { + row(report, name, file)["coupled_nodes"].as_i64().unwrap() +} + +/// The co-change count of the pair, 0 when the pair is not listed. +fn co_changes(report: &Value, a: &str, b: &str) -> i64 { + report["node_coupling"] + .as_array() + .map(|rows| { + rows.iter() + .filter(|r| { + (r["node1_name"] == a && r["node2_name"] == b) + || (r["node1_name"] == b && r["node2_name"] == a) + }) + .map(|r| r["co_changes"].as_i64().unwrap()) + .sum() + }) + .unwrap_or(0) +} + +fn total_nodes(report: &Value) -> i64 { + report["aggregate"]["total_nodes"].as_i64().unwrap() +} + +/// The sorted entity names of the report. +fn names(report: &Value) -> Vec { + entities(report).into_iter().map(|(n, _, _)| n).collect() +} + +/// The sorted names of the entities the report marks ended. +fn ended(report: &Value) -> Vec { + let mut out: Vec = report["node_hotness"] + .as_array() + .unwrap_or_else(|| panic!("no node_hotness in {report}")) + .iter() + .filter(|n| n["ended"].as_bool().expect("ended flag")) + .map(|n| n["name"].as_str().unwrap().to_string()) + .collect(); + out.sort(); + out +} + +/// One repository, asserted after every commit: counts accrue per touched +/// entity, an in-place rename ends the old name and starts the new one, a +/// removed declaration ends its entity (dropped with its couplings), and a +/// `git mv` moves the live entities under the new path with their counts. +#[test] +fn go_entity_lifecycle_counts_renames_and_deletions() { + let repo = init_repo().expect("init"); + const M: &str = "m.go"; + + commit(&repo, 0, &[(M, GO_F_G)]); + let r = shotness(&repo, &[]); + assert_eq!(names(&r), vec!["f", "g"], "{r}"); + assert_eq!(change_count(&r, "f", M), 1); + assert_eq!(change_count(&r, "g", M), 1); + assert_eq!(co_changes(&r, "f", "g"), 1, "created together: {r}"); + + commit(&repo, 1, &[(M, GO_F_CHANGED)]); + let r = shotness(&repo, &[]); + assert_eq!(change_count(&r, "f", M), 2, "{r}"); + assert_eq!(change_count(&r, "g", M), 1); + assert_eq!(co_changes(&r, "f", "g"), 1); + + commit(&repo, 2, &[(M, GO_G_CHANGED)]); + let r = shotness(&repo, &[]); + assert_eq!(change_count(&r, "f", M), 2); + assert_eq!(change_count(&r, "g", M), 2, "{r}"); + + commit(&repo, 3, &[(M, GO_BOTH_CHANGED)]); + let r = shotness(&repo, &[]); + assert_eq!(change_count(&r, "f", M), 3); + assert_eq!(change_count(&r, "g", M), 3); + assert_eq!(co_changes(&r, "f", "g"), 2, "{r}"); + assert_eq!(coupled_nodes(&r, "f", M), 1); + + commit(&repo, 4, &[(M, GO_F_RENAMED)]); + let r = shotness(&repo, &[]); + assert_eq!(names(&r), vec!["f2", "g"], "f ended, f2 born: {r}"); + assert_eq!(change_count(&r, "f2", M), 1); + assert_eq!(change_count(&r, "g", M), 3); + assert_eq!(co_changes(&r, "f2", "g"), 0); + assert_eq!(coupled_nodes(&r, "g", M), 0, "f's coupling is gone: {r}"); + assert!( + r["node_hotness"][0].get("ended").is_none(), + "no ended field by default: {r}" + ); + assert!(r["aggregate"].get("ended_nodes").is_none(), "{r}"); + + commit(&repo, 5, &[(M, GO_G_DELETED)]); + let r = shotness(&repo, &[]); + assert_eq!(names(&r), vec!["f2"], "g ended: {r}"); + assert_eq!(change_count(&r, "f2", M), 1); + + commit(&repo, 6, &[(M, GO_H_ADDED)]); + let r = shotness(&repo, &[]); + assert_eq!(names(&r), vec!["f2", "h"], "{r}"); + assert_eq!(change_count(&r, "h", M), 1); + assert_eq!(change_count(&r, "f2", M), 1); + assert_eq!(co_changes(&r, "f2", "h"), 0); + + rename_file(&repo, "mv", T0 + 7 * DAY, M, "n.go").expect("git mv"); + let r = shotness(&repo, &[]); + assert_eq!( + entities(&r), + vec![ + ("f2".to_string(), "Function".to_string(), "n.go".to_string()), + ("h".to_string(), "Function".to_string(), "n.go".to_string()), + ], + "a git mv moves the entities, it does not duplicate them: {r}" + ); + assert_eq!(change_count(&r, "f2", "n.go"), 1); + assert_eq!(change_count(&r, "h", "n.go"), 1); + assert_eq!(total_nodes(&r), 2); +} + +/// The same history under `--shotness-keep-ended`: ended entities stay, +/// marked, with their counts and couplings; a `git mv` moves only the live +/// ones. +#[test] +fn keep_ended_reports_ended_entities_with_their_history() { + let repo = init_repo().expect("init"); + const M: &str = "m.go"; + const KEEP: &[&str] = &["--shotness-keep-ended"]; + for (day, file) in [GO_F_G, GO_F_CHANGED, GO_G_CHANGED, GO_BOTH_CHANGED] + .into_iter() + .enumerate() + { + commit(&repo, day as i64, &[(M, file)]); + } + + commit(&repo, 4, &[(M, GO_F_RENAMED)]); + let r = shotness(&repo, KEEP); + assert_eq!(names(&r), vec!["f", "f2", "g"], "f ended, f2 born: {r}"); + assert_eq!(ended(&r), vec!["f"], "{r}"); + assert_eq!( + change_count(&r, "f", M), + 4, + "the rename touched f once more" + ); + assert_eq!(co_changes(&r, "f", "f2"), 1, "renamed in one commit: {r}"); + assert_eq!( + coupled_nodes(&r, "g", M), + 1, + "g keeps its history with f: {r}" + ); + + commit(&repo, 5, &[(M, GO_G_DELETED)]); + commit(&repo, 6, &[(M, GO_H_ADDED)]); + let r = shotness(&repo, KEEP); + assert_eq!(names(&r), vec!["f", "f2", "g", "h"], "{r}"); + assert_eq!(ended(&r), vec!["f", "g"], "{r}"); + + rename_file(&repo, "mv", T0 + 7 * DAY, M, "n.go").expect("git mv"); + let r = shotness(&repo, KEEP); + assert_eq!( + entities(&r), + vec![ + ("f".to_string(), "Function".to_string(), "m.go".to_string()), + ("f2".to_string(), "Function".to_string(), "n.go".to_string()), + ("g".to_string(), "Function".to_string(), "m.go".to_string()), + ("h".to_string(), "Function".to_string(), "n.go".to_string()), + ], + "the ended entities keep the path they ended under: {r}" + ); + assert_eq!(total_nodes(&r), 4); + assert_eq!(r["aggregate"]["ended_nodes"], 2, "{r}"); +} + +/// A file rename keeps every live entity's count and coupling under the new +/// path: three entities stay three (the old keys never linger). +#[test] +fn file_rename_keeps_counts_under_the_new_path() { + let repo = init_repo().expect("init"); + const SRC: &[u8] = b"package m\n\nfunc f() int {\n\treturn 1\n}\n\nfunc g() int {\n\treturn 2\n}\n\nfunc h() int {\n\treturn 3\n}\n"; + const F_CHANGED: &[u8] = b"package m\n\nfunc f() int {\n\treturn 11\n}\n\nfunc g() int {\n\treturn 2\n}\n\nfunc h() int {\n\treturn 3\n}\n"; + const F_CHANGED_AGAIN: &[u8] = b"package m\n\nfunc f() int {\n\treturn 111\n}\n\nfunc g() int {\n\treturn 2\n}\n\nfunc h() int {\n\treturn 3\n}\n"; + commit(&repo, 0, &[("m.go", SRC)]); + commit(&repo, 1, &[("m.go", F_CHANGED)]); + commit(&repo, 2, &[("m.go", F_CHANGED_AGAIN)]); + rename_file(&repo, "mv", T0 + 3 * DAY, "m.go", "pkg/n.go").expect("git mv"); + + let r = shotness(&repo, &[]); + assert_eq!(total_nodes(&r), 3, "{r}"); + assert!( + entities(&r).iter().all(|(_, _, file)| file == "pkg/n.go"), + "{r}" + ); + assert_eq!(change_count(&r, "f", "pkg/n.go"), 3); + assert_eq!(change_count(&r, "g", "pkg/n.go"), 1); + assert_eq!(change_count(&r, "h", "pkg/n.go"), 1); + assert_eq!(co_changes(&r, "f", "g"), 1); + assert_eq!(co_changes(&r, "g", "h"), 1); +} + +/// Class methods are entities, named by their class: Java, Python, TypeScript +/// and C# methods are all tracked, and a change inside a method counts for +/// that method only. +#[test] +fn class_methods_are_tracked_with_qualified_names() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[ + ("A.java", JAVA_A), + ("p.py", PY_P), + ("t.ts", TS_T), + ("p.cs", CS_P), + ], + ); + commit( + &repo, + 1, + &[("A.java", JAVA_A_CHANGED), ("p.py", PY_P_CHANGED)], + ); + + let r = shotness(&repo, &[]); + let expected: Vec<(String, String, String)> = [ + ("A.f", "Method", "A.java"), + ("A.g", "Method", "A.java"), + ("C.M1", "Method", "p.cs"), + ("C.M2", "Method", "p.cs"), + ("C.m1", "Method", "t.ts"), + ("C.m2", "Method", "t.ts"), + ("K.km", "Function", "p.py"), + ("pf", "Function", "p.py"), + ("pg", "Function", "p.py"), + ("top", "Function", "t.ts"), + ] + .iter() + .map(|(n, t, f)| ((*n).to_string(), (*t).to_string(), (*f).to_string())) + .collect(); + assert_eq!(entities(&r), expected, "{r}"); + assert_eq!(change_count(&r, "A.f", "A.java"), 2); + assert_eq!(change_count(&r, "A.g", "A.java"), 1); + assert_eq!(change_count(&r, "K.km", "p.py"), 2); + assert_eq!(change_count(&r, "pf", "p.py"), 1); + assert_eq!(co_changes(&r, "A.f", "K.km"), 2, "{r}"); +} + +/// Nested declarations carry the whole enclosing chain. +#[test] +fn nested_declarations_are_qualified_by_every_enclosing_declaration() { + let repo = init_repo().expect("init"); + commit(&repo, 0, &[("n.py", PY_NESTED)]); + let r = shotness(&repo, &[]); + assert_eq!( + names(&r), + vec!["K.Inner.im", "K.km", "outer", "outer.inner"], + "{r}" + ); +} + +/// Go methods are qualified by their receiver type: two `String()` methods +/// are two entities, and only the changed one counts. +#[test] +fn same_name_methods_on_two_receivers_are_distinct() { + let repo = init_repo().expect("init"); + commit(&repo, 0, &[("r.go", GO_RECEIVERS)]); + commit(&repo, 1, &[("r.go", GO_RECEIVERS_CHANGED)]); + let r = shotness(&repo, &[]); + assert_eq!( + entities(&r), + vec![ + ( + "A.String".to_string(), + "Method".to_string(), + "r.go".to_string() + ), + ( + "B.String".to_string(), + "Method".to_string(), + "r.go".to_string() + ), + ], + "{r}" + ); + assert_eq!(change_count(&r, "A.String", "r.go"), 2); + assert_eq!(change_count(&r, "B.String", "r.go"), 1); +} + +/// A merge commit re-shows the side line's changes against its first +/// parent; counting it would double every side-line touch. Under +/// `--first-parent` the merge is the only sight of the side line and counts +/// as one commit. +#[test] +fn merge_commits_are_not_counted_twice() { + let repo = merge_fixture(); + + let r = shotness(&repo, &[]); + assert_eq!( + change_count(&r, "f", "m.go"), + 3, + "c1 + two side commits: {r}" + ); + assert_eq!(change_count(&r, "g", "m.go"), 2, "c1 + c3: {r}"); + + let r = shotness(&repo, &["--first-parent"]); + assert_eq!(change_count(&r, "f", "m.go"), 2, "c1 + the merge: {r}"); + assert_eq!(change_count(&r, "g", "m.go"), 2); +} + +/// `c1 ; side 1 ; side 2 (f changed twice on a side line) ; c3 (g changed) ; +/// merge` — every side-line change reaches the main line through the merge. +fn merge_fixture() -> TestRepo { + let repo = init_repo().expect("init"); + const SIDE_1: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 11\n}\n\nfunc g() int {\n\treturn 2\n}\n"; + const SIDE_2: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 111\n}\n\nfunc g() int {\n\treturn 2\n}\n"; + const MAIN: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 1\n}\n\nfunc g() int {\n\treturn 22\n}\n"; + const MERGED: &[u8] = + b"package m\n\nfunc f() int {\n\treturn 111\n}\n\nfunc g() int {\n\treturn 22\n}\n"; + let c1 = commit_files(&repo, "c1", T0, &[("m.go", GO_F_G)]).expect("c1"); + let side_1 = CommitBuilder::new(&repo, "side 1", T0 + DAY) + .parent(c1) + .detached() + .write("m.go", SIDE_1) + .commit() + .expect("side 1"); + let side_2 = CommitBuilder::new(&repo, "side 2", T0 + 2 * DAY) + .parent(side_1) + .detached() + .write("m.go", SIDE_2) + .commit() + .expect("side 2"); + commit(&repo, 3, &[("m.go", MAIN)]); + CommitBuilder::new(&repo, "merge", T0 + 4 * DAY) + .merge_parent(side_2) + .write("m.go", MERGED) + .commit() + .expect("merge"); + repo +} + +/// The shotness report is the same alone and co-selected with another UAST +/// analyzer (the shared multi-analyzer walk): `--first-parent` and both DSL +/// flags reach either walk. +#[test] +fn co_selection_honors_first_parent_and_the_dsl_flags() { + let merges = merge_fixture(); + let classes = init_repo().expect("init"); + commit(&classes, 0, &[("A.java", JAVA_A), ("p.py", PY_P)]); + commit( + &classes, + 1, + &[("A.java", JAVA_A_CHANGED), ("p.py", PY_P_CHANGED)], + ); + let selection = format!("{SHOTNESS},history/quality"); + let cases: [(&TestRepo, &[&str]); 3] = [ + (&merges, &["--first-parent"]), + ( + &classes, + &["--shotness-dsl-struct", "rfilter(.roles has \"Class\")"], + ), + (&classes, &["--shotness-dsl-name", ".type"]), + ]; + let mut defects = Vec::new(); + for (repo, extra) in cases { + let alone = shotness(repo, extra); + let mut args = vec!["-a", selection.as_str(), "--format", "json"]; + args.extend_from_slice(extra); + args.push(path(repo)); + let doc: Value = serde_json::from_str(&stdout_ok(&args)).expect("one JSON document"); + let co_selected = doc["analyzers"] + .as_array() + .expect("analyzers array") + .iter() + .find(|a| a["id"] == SHOTNESS) + .map(|a| &a["report"]) + .unwrap_or_else(|| panic!("no {SHOTNESS} in {doc}")); + if &alone != co_selected { + defects.push(format!( + "{extra:?}:\n alone {alone}\n co-selected {co_selected}" + )); + } + } + assert!(defects.is_empty(), "{}", defects.join("\n")); +} + +/// `--languages` restricts the tracked entities to the named languages; +/// binary and unparsable files never fail the run. +#[test] +fn languages_filter_is_respected_and_odd_files_are_skipped() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[ + ("m.go", GO_F_G), + ("p.py", PY_P), + ("bin.dat", b"bin\x00ary\n"), + ("broken.go", b"package z\n\nfunc (((( {{{{ broken\n"), + ], + ); + let all = shotness(&repo, &[]); + assert_eq!(names(&all), vec!["K.km", "f", "g", "pf", "pg"], "{all}"); + let py = shotness(&repo, &["--languages", "python"]); + assert_eq!(names(&py), vec!["K.km", "pf", "pg"], "{py}"); + let go = shotness(&repo, &["--languages", "go"]); + assert_eq!(names(&go), vec!["f", "g"], "{go}"); +} + +/// Two runs are byte-identical; json and yaml carry the same rows; the +/// multi-analyzer (shared) walk gives the same shotness report as the direct +/// walk; ndjson and timeseries list the node-touching commits. +#[test] +fn output_is_deterministic_and_every_format_agrees() { + let repo = init_repo().expect("init"); + commit(&repo, 0, &[("A.java", JAVA_A)]); + commit(&repo, 1, &[("A.java", JAVA_A_CHANGED)]); + let p = path(&repo); + + let first = stdout_ok(&["-a", SHOTNESS, "--format", "json", p]); + let second = stdout_ok(&["-a", SHOTNESS, "--format", "json", p]); + assert_eq!(first, second, "two runs must be byte-identical"); + + let yaml = stdout_ok(&["-a", SHOTNESS, "--format", "yaml", p]); + assert!(yaml.contains("name: A.f"), "{yaml}"); + assert!(yaml.contains("change_count: 2"), "{yaml}"); + assert!(yaml.contains("name: A.g"), "{yaml}"); + + let combined: Value = serde_json::from_str(&stdout_ok(&[ + "-a", + "history/shotness,history/typos", + "--format", + "json", + p, + ])) + .expect("one JSON document"); + let shared = combined["analyzers"] + .as_array() + .expect("analyzers") + .iter() + .find(|a| a["id"] == SHOTNESS) + .map(|a| a["report"].clone()) + .expect("shotness in the combined document"); + let direct: Value = serde_json::from_str(&first).expect("json"); + assert_eq!(shared, direct, "the shared walk must match the direct walk"); + + let ndjson = stdout_ok(&["-a", SHOTNESS, "--format", "ndjson", p]); + let lines: Vec = ndjson + .lines() + .map(|l| serde_json::from_str(l).expect("ndjson line")) + .collect(); + assert_eq!( + lines.len(), + 2, + "one line per node-touching commit: {ndjson}" + ); + let touched = lines[1]["data"]["NodesTouched"] + .as_object() + .expect("NodesTouched"); + assert_eq!(touched.len(), 1, "the second commit touched A.f only"); + assert!(touched.contains_key("Method_A.f_A.java"), "{ndjson}"); + + let ts: Value = + serde_json::from_str(&stdout_ok(&["-a", SHOTNESS, "--format", "timeseries", p])) + .expect("timeseries"); + let commits = ts["commits"].as_array().expect("commits"); + assert_eq!(commits[0]["shotness"]["nodes_touched"], 2, "{ts}"); + assert_eq!(commits[0]["shotness"]["coupling_pairs"], 1); + assert_eq!(commits[1]["shotness"]["nodes_touched"], 1); + assert_eq!(commits[1]["shotness"]["coupling_pairs"], 0); +} + +/// `--shotness-dsl-struct` selects the tracked entities; an invalid +/// expression is a named flag error before any walk. +#[test] +fn dsl_flags_are_honored_and_validated() { + let repo = init_repo().expect("init"); + commit(&repo, 0, &[("A.java", JAVA_A), ("p.py", PY_P)]); + + let classes = shotness( + &repo, + &["--shotness-dsl-struct", "rfilter(.roles has \"Class\")"], + ); + assert_eq!( + entities(&classes), + vec![ + ("A".to_string(), "Class".to_string(), "A.java".to_string()), + ("K".to_string(), "Class".to_string(), "p.py".to_string()), + ], + "{classes}" + ); + + let shallow = shotness( + &repo, + &["--shotness-dsl-struct", "filter(.roles has \"Function\")"], + ); + assert_eq!( + names(&shallow), + vec!["pf", "pg"], + "top-level only: {shallow}" + ); + + // The name query names the selected nodes AND their enclosing + // declarations alike. + let typed = shotness(&repo, &["--shotness-dsl-name", ".type"]); + assert_eq!( + names(&typed), + vec!["Class.Function", "Class.Method", "Function"], + "{typed}" + ); + + for (flag, value) in [ + ("--shotness-dsl-struct", "rfilter("), + ("--shotness-dsl-name", "'unterminated"), + ] { + let out = codefang(&["-a", SHOTNESS, "--format", "json", flag, value, path(&repo)]); + assert!(!out.status.success(), "{flag} {value} must fail"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains(flag), "stderr must name the flag: {stderr}"); + assert!(out.stdout.is_empty(), "no report on a flag error"); + } +} + +/// Two functions with names distinctive enough to search rendered pages for. +const KEPT_AND_REMOVED: &[u8] = + b"package m\n\nfunc keptfunction() int {\n\treturn 1\n}\n\nfunc removedfunction() int {\n\treturn 2\n}\n"; +/// The same file after `removedfunction` is deleted. +const ONLY_KEPT: &[u8] = b"package m\n\nfunc keptfunction() int {\n\treturn 1\n}\n"; + +/// Every file under `dir`, concatenated. +fn rendered_pages(dir: &std::path::Path) -> String { + let mut out = String::new(); + let mut pending = vec![dir.to_path_buf()]; + while let Some(next) = pending.pop() { + for entry in std::fs::read_dir(&next).expect("read plot dir") { + let path = entry.expect("dir entry").path(); + if path.is_dir() { + pending.push(path); + } else { + out.push_str(&String::from_utf8_lossy( + &std::fs::read(&path).expect("read"), + )); + } + } + } + out +} + +/// Under `--shotness-keep-ended` a removed declaration stays in the machine +/// report, marked ended, and the text report marks it; the plot pages, a +/// view of the code that exists, leave it out. +#[test] +fn ended_entities_stay_in_the_report_and_leave_the_plot() { + let repo = init_repo().expect("init"); + commit(&repo, 0, &[("m.go", KEPT_AND_REMOVED)]); + commit(&repo, 1, &[("m.go", ONLY_KEPT)]); + let p = path(&repo); + + let r = shotness(&repo, &["--shotness-keep-ended"]); + assert_eq!(ended(&r), vec!["removedfunction"], "{r}"); + assert_eq!(r["aggregate"]["ended_nodes"], 1, "{r}"); + + let text = stdout_ok(&[ + "-a", + SHOTNESS, + "--shotness-keep-ended", + "--format", + "text", + p, + ]); + let removed_row = text + .lines() + .find(|l| l.contains("removedfunction")) + .unwrap_or_else(|| panic!("no row for the ended entity:\n{text}")); + assert!(removed_row.contains("ended"), "{text}"); + assert!(text.contains("Ended Nodes"), "{text}"); + + let dir = tempfile::tempdir().expect("plot dir"); + let out = dir.path().to_str().expect("utf-8 dir"); + stdout_ok(&[ + "-a", + SHOTNESS, + "--shotness-keep-ended", + "--format", + "plot", + "--output", + out, + p, + ]); + let pages = rendered_pages(dir.path()); + assert!(pages.contains("keptfunction"), "the live entity is plotted"); + assert!( + !pages.contains("removedfunction"), + "the ended entity is not plotted" + ); +} diff --git a/bins/codefang/tests/static_walk_e2e.rs b/bins/codefang/tests/static_walk_e2e.rs new file mode 100644 index 00000000..d9a9d0f2 --- /dev/null +++ b/bins/codefang/tests/static_walk_e2e.rs @@ -0,0 +1,311 @@ +//! End-to-end coverage of the ONE static file walk every `static/*` +//! analyzer shares, driven through the **real binary** over hermetic trees +//! written into a tempdir. +//! +//! The walk contract under test (`cf_commands::handlers::walk_source_files`): +//! +//! * the generated-content marker (`Code generated … DO NOT EDIT` in the +//! first 512 bytes) excludes a file from EVERY analyzer by default and +//! `--include-generated` re-admits it — composition is the designed +//! exception and classifies the file in both runs; +//! * `-a 'static/*'` reports the same per-analyzer numbers as the single +//! runs, in json, yaml and bin; +//! * `--per-file` applies to `--format json` only and says so on stderr for +//! any other format; +//! * an unreadable file is reported ONCE on stderr for the whole run; +//! * the output is a pure function of the tree and the flags. + +mod common; + +use std::fs; +use std::path::Path; + +use serde_json::Value; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); + +/// Every static analyzer, in the registry (merged-report section) order. +const STATIC_ANALYZERS: [&str; 7] = [ + "static/clones", + "static/complexity", + "static/comments", + "static/halstead", + "static/cohesion", + "static/imports", + "static/composition", +]; + +/// The one analyzer that classifies generated files instead of skipping them. +const COMPOSITION: &str = "static/composition"; + +/// The generated-file marker in the header of the fixture's `gen.go`. +const GENERATED_HEADER: &str = "// Code generated by protoc-gen-go. DO NOT EDIT.\n"; + +/// The stderr note printed when `--per-file` meets a non-json format. +const PER_FILE_NOTE_PREFIX: &str = "--per-file applies to --format json only; ignored for "; + +/// The stderr line prefix of a skipped (unreadable / oversized) file. +const SKIPPED_PREFIX: &str = "warning: skipped "; + +/// A plain Go file with one function (an `if`, so every analyzer has data). +const PLAIN_GO: &str = + "package main\n\nimport \"fmt\"\n\n// plain is documented.\nfunc plain(a int) int {\n\tif a > 0 {\n\t\tfmt.Println(a)\n\t\treturn a\n\t}\n\treturn -a\n}\n"; +/// The same function under a different name, in a nested directory. +const NESTED_GO: &str = + "package sub\n\nfunc nested(b int) int {\n\tif b > 0 {\n\t\treturn b\n\t}\n\treturn -b\n}\n"; +/// A vendored function. +const VENDOR_GO: &str = + "package dep\n\nfunc vendored(c int) int {\n\tif c > 0 {\n\t\treturn c\n\t}\n\treturn -c\n}\n"; +/// The function body of the generated file (the header is prepended). +const GEN_BODY: &str = + "package main\n\nfunc generated(d int) int {\n\tif d > 0 {\n\t\treturn d\n\t}\n\treturn -d\n}\n"; + +fn write(dir: &Path, rel: &str, content: &str) { + let path = dir.join(rel); + fs::create_dir_all(path.parent().expect("parent")).expect("mkdir"); + fs::write(path, content).expect("write fixture"); +} + +/// The fixture: a plain file, a nested file, a vendored file, a file whose +/// HEADER carries the generated marker (its name is not a generated +/// pattern), a `.git` entry and an unsupported text file. +fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + write(dir.path(), "a.go", PLAIN_GO); + write(dir.path(), "sub/b.go", NESTED_GO); + write(dir.path(), "vendor/dep.go", VENDOR_GO); + write( + dir.path(), + "gen.go", + &format!("{GENERATED_HEADER}{GEN_BODY}"), + ); + write(dir.path(), ".git/x.go", PLAIN_GO); + write(dir.path(), "notes.txt", "not source\n"); + dir +} + +/// Runs `codefang run ` with a pinned environment. +fn codefang(args: &[&str]) -> std::process::Output { + common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// `(exit code, stdout, stderr)` of `-a --format `. +fn run(analyzer: &str, format: &str, extra: &[&str], path: &Path) -> (i32, Vec, String) { + let p = path.to_str().expect("utf-8 path"); + let out = codefang(&[&["-a", analyzer, "--format", format], extra, &[p]].concat()); + ( + out.status.code().unwrap_or(-1), + out.stdout, + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +/// The `--per-file` json report of one analyzer; exit 0 is asserted. +fn per_file_json(analyzer: &str, extra: &[&str], path: &Path) -> Value { + let (code, stdout, stderr) = run(analyzer, "json", &[&["--per-file"], extra].concat(), path); + assert_eq!(code, 0, "{analyzer} failed:\n{stderr}"); + serde_json::from_slice(&stdout).expect("json report parses") +} + +/// The `file_path` values of the first section's `files` array. +fn file_paths(report: &Value) -> Vec { + report["sections"][0]["files"] + .as_array() + .expect("files array") + .iter() + .map(|f| f["file_path"].as_str().expect("file_path").to_string()) + .collect() +} + +/// Every analyzer's `--per-file` list excludes the marker-generated file by +/// default and lists it with `--include-generated`; composition (which +/// exists to report generated shares) lists it both times. The `.git` +/// entry, the vendored file and the unsupported file never appear. +#[test] +fn generated_marker_is_applied_by_every_static_analyzer() { + let dir = fixture(); + for analyzer in STATIC_ANALYZERS { + let default = file_paths(&per_file_json(analyzer, &[], dir.path())); + let included = file_paths(&per_file_json( + analyzer, + &["--include-generated"], + dir.path(), + )); + assert!( + default.iter().any(|p| p == "a.go") && default.iter().any(|p| p == "sub/b.go"), + "{analyzer}: plain files missing from {default:?}" + ); + assert!( + !default.iter().any(|p| p.contains(".git")), + "{analyzer}: .git walked: {default:?}" + ); + if analyzer == COMPOSITION { + assert!( + default.iter().any(|p| p == "gen.go") && included.iter().any(|p| p == "gen.go"), + "{analyzer}: composition must classify gen.go ({default:?} / {included:?})" + ); + continue; + } + assert!( + !default.iter().any(|p| p == "gen.go"), + "{analyzer}: the generated marker was not applied: {default:?}" + ); + assert!( + included.iter().any(|p| p == "gen.go"), + "{analyzer}: --include-generated did not re-admit gen.go: {included:?}" + ); + assert!( + !default.iter().any(|p| p.starts_with("vendor/")), + "{analyzer}: vendored file analyzed: {default:?}" + ); + assert!( + !default.iter().any(|p| p == "notes.txt"), + "{analyzer}: unsupported file analyzed: {default:?}" + ); + } +} + +/// The merged `static/*` json report carries each analyzer's section EXACTLY +/// as the single run renders it (registry order). +#[test] +fn multi_analyzer_json_sections_equal_the_single_runs() { + let dir = fixture(); + let (code, stdout, stderr) = run("static/*", "json", &["--per-file"], dir.path()); + assert_eq!(code, 0, "merged run failed:\n{stderr}"); + let merged: Value = serde_json::from_slice(&stdout).expect("merged json parses"); + let sections = merged["sections"].as_array().expect("sections"); + assert_eq!(sections.len(), STATIC_ANALYZERS.len()); + for (section, analyzer) in sections.iter().zip(STATIC_ANALYZERS) { + let single = per_file_json(analyzer, &[], dir.path()); + assert_eq!( + section, &single["sections"][0], + "{analyzer}: merged section differs from the single run" + ); + } +} + +/// `-a 'static/*'` in yaml and bin is the concatenation of the single runs. +#[test] +fn multi_analyzer_yaml_and_bin_equal_the_single_runs() { + let dir = fixture(); + for format in ["yaml", "bin"] { + let (code, merged, stderr) = run("static/*", format, &[], dir.path()); + assert_eq!(code, 0, "merged {format} run failed:\n{stderr}"); + let mut expected = Vec::new(); + for analyzer in STATIC_ANALYZERS { + let (code, stdout, stderr) = run(analyzer, format, &[], dir.path()); + assert_eq!(code, 0, "{analyzer} {format} failed:\n{stderr}"); + expected.extend_from_slice(&stdout); + } + assert!( + merged == expected, + "{format}: merged output differs from the concatenated single runs" + ); + } +} + +/// `--per-file` is a json-only enrichment: every other format prints ONE +/// stderr note naming the format; json prints none. +#[test] +fn per_file_note_is_printed_once_for_non_json_formats() { + let dir = fixture(); + for format in ["yaml", "bin"] { + let (code, _stdout, stderr) = run("static/complexity", format, &["--per-file"], dir.path()); + assert_eq!(code, 0, "{format} run failed:\n{stderr}"); + let expected = format!("{PER_FILE_NOTE_PREFIX}{format}"); + assert_eq!( + stderr.lines().filter(|l| *l == expected).count(), + 1, + "{format}: expected one note {expected:?} in:\n{stderr}" + ); + } + // A multi-analyzer bin run prints the note once, not once per analyzer. + let (code, _stdout, stderr) = run("static/*", "bin", &["--per-file"], dir.path()); + assert_eq!(code, 0, "{stderr}"); + assert_eq!( + stderr + .lines() + .filter(|l| l.starts_with(PER_FILE_NOTE_PREFIX)) + .count(), + 1, + "static/* bin: one note expected in:\n{stderr}" + ); + let (code, _stdout, stderr) = run("static/complexity", "json", &["--per-file"], dir.path()); + assert_eq!(code, 0, "{stderr}"); + assert!( + !stderr.contains(PER_FILE_NOTE_PREFIX), + "json must not print the note:\n{stderr}" + ); +} + +/// An unreadable file is skipped with ONE stderr warning for the whole +/// multi-analyzer run and counted once in the end-of-run summary. +#[test] +#[cfg(unix)] +fn unreadable_file_is_reported_once_per_run() { + use std::os::unix::fs::PermissionsExt; + + let dir = fixture(); + let secret = dir.path().join("secret.go"); + fs::write(&secret, PLAIN_GO).expect("write secret"); + fs::set_permissions(&secret, fs::Permissions::from_mode(0o000)).expect("chmod"); + if fs::read(&secret).is_ok() { + // Running as root: the file is readable regardless of its mode. + return; + } + let (code, stdout, stderr) = run("static/*", "json", &["--per-file"], dir.path()); + assert_eq!(code, 0, "{stderr}"); + let warning = format!("{SKIPPED_PREFIX}{}: unreadable", secret.display()); + assert_eq!( + stderr.lines().filter(|l| *l == warning).count(), + 1, + "expected one warning {warning:?} in:\n{stderr}" + ); + assert!( + stderr.contains("progress: skipped 1 unreadable/oversized/unparseable files"), + "end-of-run summary missing or wrong:\n{stderr}" + ); + let merged: Value = serde_json::from_slice(&stdout).expect("json"); + let sections = merged["sections"].as_array().expect("sections"); + for (section, analyzer) in sections.iter().zip(STATIC_ANALYZERS) { + let paths: Vec<&str> = section["files"] + .as_array() + .expect("files") + .iter() + .filter_map(|f| f["file_path"].as_str()) + .collect(); + // Composition classifies every file the walk reaches; an unreadable + // one is classified on its path alone. Every source analyzer skips it. + let listed = paths.contains(&"secret.go"); + assert_eq!( + listed, + analyzer == COMPOSITION, + "{analyzer}: unexpected secret.go handling: {paths:?}" + ); + } +} + +/// The walk output is a pure function of the tree and the flags. +#[test] +fn static_walk_output_is_deterministic() { + let dir = fixture(); + let first = run("static/*", "json", &["--per-file"], dir.path()); + let second = run("static/*", "json", &["--per-file"], dir.path()); + assert_eq!(first.0, 0, "{}", first.2); + assert!( + first.1 == second.1, + "two identical runs produced different bytes" + ); + let yaml_first = run("static/*", "yaml", &[], dir.path()); + let yaml_second = run("static/*", "yaml", &[], dir.path()); + assert!(yaml_first.1 == yaml_second.1, "yaml runs differ"); +} diff --git a/bins/codefang/tests/typos_e2e.rs b/bins/codefang/tests/typos_e2e.rs new file mode 100644 index 00000000..d532e5db --- /dev/null +++ b/bins/codefang/tests/typos_e2e.rs @@ -0,0 +1,317 @@ +//! End-to-end coverage of `history/typos`, driven through the **real binary** +//! over on-disk git fixtures (`specs/bugs/BUG-history-typos-false-positives.md`). +//! +//! The contract (`site/analyzers/typos.md`): a typo is ONE identifier that a +//! commit changed into a DIFFERENT, Levenshtein-close identifier on the same +//! line; `typo_list` carries every distinct `wrong → correct` pair once (the +//! chronologically first fix wins), `patterns` counts every occurrence of a +//! pair, `line` is 1-based, the report is a pure function of the repository +//! and the flags, and `bin` carries the json value. + +mod common; + +use std::process::Command; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// Base instant of the fixture histories: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits. +const DAY: i64 = 24 * 3_600; +/// Size of the CFB1 envelope header (`magic:4` + `length:4`). +const BIN_HEADER: usize = 8; +/// The 1-based line of `func receive()` in [`FIXED_GO`]. +const RECEIVE_DECL_LINE: i64 = 3; + +/// Before the fix: a misspelled function used on two lines, a function that +/// is about to be renamed to something unrelated, and a literal constant. +const TYPO_GO: &[u8] = b"package main + +func recieve() int { return 1 } + +func main() { + v := recieve() + _ = v + foo() +} + +func foo() {} + +const numConTimes = 4 +"; + +/// After the fix: `recieve` → `receive` on both lines, `foo` → +/// `completelyDifferent` (a rename, not a typo) and `4` → `17` (a literal +/// change that leaves the identifier untouched). +const FIXED_GO: &[u8] = b"package main + +func receive() int { return 1 } + +func main() { + v := receive() + _ = v + completelyDifferent() +} + +func completelyDifferent() {} + +const numConTimes = 17 +"; + +/// Runs the binary with a deterministic environment. +fn codefang(args: &[&str]) -> std::process::Output { + codefang_with(common::command(BIN), args) +} + +/// Runs the binary under `taskset -c `. +fn codefang_pinned(cpus: &str, args: &[&str]) -> std::process::Output { + let mut cmd = common::command("taskset"); + cmd.args(["-c", cpus, BIN]); + codefang_with(cmd, args) +} + +fn codefang_with(mut cmd: Command, args: &[&str]) -> std::process::Output { + cmd.arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary") +} + +/// stdout of a successful run. +fn stdout_ok(args: &[&str]) -> Vec { + let out = codefang(args); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// stdout of a successful run, parsed as ONE JSON document. +fn json_ok(args: &[&str]) -> serde_json::Value { + let bytes = stdout_ok(args); + serde_json::from_slice(&bytes).unwrap_or_else(|e| { + panic!( + "stdout is not one JSON document ({e}):\n{}", + String::from_utf8_lossy(&bytes) + ) + }) +} + +fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") +} + +/// The typos report of `test` as JSON. +fn typos_json(test: &TestRepo) -> serde_json::Value { + json_ok(&["-a", "history/typos", "--format", "json", path(test)]) +} + +/// A single-file history: the typo'd source, then the fixing commit. +fn fix_fixture() -> TestRepo { + let test = init_repo().expect("init"); + commit_files( + &test, + "typo", + T0, + &[("a.go", TYPO_GO), ("README.md", b"# Hello\n")], + ) + .expect("typo"); + commit_files( + &test, + "fix", + T0 + DAY, + &[("a.go", FIXED_GO), ("README.md", b"# Hallo\n")], + ) + .expect("fix"); + test +} + +/// The same pair fixed in two files, five commits apart, with filler commits +/// between them: `b.go` (walk index 1) is the chronologically first fix. +fn duplicate_pair_fixture() -> (TestRepo, String) { + let test = init_repo().expect("init"); + commit_files( + &test, + "idx0 add", + T0, + &[ + ("b.go", b"package main\n\nfunc main() {\n\trecieve()\n}\n"), + ("e.go", b"package main\n\nfunc other() {\n\trecieve()\n}\n"), + ("z.go", b"package main\n\nfunc filler() {}\n"), + ], + ) + .expect("idx0"); + let first_fix = commit_files( + &test, + "idx1 fix b.go", + T0 + DAY, + &[("b.go", b"package main\n\nfunc main() {\n\treceive()\n}\n")], + ) + .expect("idx1"); + for i in 2..4_i64 { + commit_files( + &test, + &format!("idx{i}"), + T0 + i * DAY, + &[( + "z.go", + format!("package main\n\nfunc filler() {{}}\n// {i}\n").as_bytes(), + )], + ) + .expect("filler"); + } + commit_files( + &test, + "idx4 fix e.go", + T0 + 4 * DAY, + &[("e.go", b"package main\n\nfunc other() {\n\treceive()\n}\n")], + ) + .expect("idx4"); + commit_files( + &test, + "idx5", + T0 + 5 * DAY, + &[("z.go", b"package main\n\nfunc filler() {}\n// 5\n")], + ) + .expect("idx5"); + (test, first_fix.to_string()) +} + +/// `recieve → receive` fixed on two lines is ONE `typo_list` entry (1-based +/// line of the first fix) and ONE pattern with frequency 2; the rename and +/// the literal change are not typos; `file_typos` and `aggregate` count +/// occurrences. +#[test] +fn a_pair_fixed_on_two_lines_is_reported_once_with_frequency_two() { + let test = fix_fixture(); + let fix = test.repo.head().expect("head").to_string(); + let report = typos_json(&test); + + let list = report["typo_list"].as_array().expect("typo_list"); + assert_eq!(list.len(), 1, "{report}"); + assert_eq!(list[0]["wrong"], "recieve", "{report}"); + assert_eq!(list[0]["correct"], "receive", "{report}"); + assert_eq!(list[0]["file"], "a.go", "{report}"); + assert_eq!( + list[0]["line"], RECEIVE_DECL_LINE, + "line is 1-based: {report}" + ); + assert_eq!(list[0]["commit"], fix.as_str(), "{report}"); + + let patterns = report["patterns"].as_array().expect("patterns is an array"); + assert_eq!(patterns.len(), 1, "{report}"); + assert_eq!(patterns[0]["wrong"], "recieve"); + assert_eq!(patterns[0]["correct"], "receive"); + assert_eq!(patterns[0]["frequency"], 2, "{report}"); + + let files = report["file_typos"].as_array().expect("file_typos"); + assert_eq!(files.len(), 1, "{report}"); + assert_eq!(files[0]["file"], "a.go"); + assert_eq!(files[0]["typo_count"], 2, "{report}"); + + assert_eq!(report["aggregate"]["total_typos"], 2, "{report}"); + assert_eq!(report["aggregate"]["unique_patterns"], 1, "{report}"); + assert_eq!(report["aggregate"]["affected_files"], 1, "{report}"); + assert_eq!(report["aggregate"]["affected_commits"], 1, "{report}"); +} + +/// Neither an unrelated rename, nor a literal change next to an unchanged +/// identifier, nor a Markdown heading edit is a typo — and no reported pair +/// is ever empty or self-identical. +#[test] +fn renames_literal_changes_and_prose_are_not_typos() { + let test = fix_fixture(); + let report = typos_json(&test); + let list = report["typo_list"].as_array().expect("typo_list"); + for typo in list { + let wrong = typo["wrong"].as_str().expect("wrong"); + let correct = typo["correct"].as_str().expect("correct"); + assert!(!wrong.is_empty() && !correct.is_empty(), "{report}"); + assert_ne!(wrong, correct, "{report}"); + assert_eq!(typo["file"], "a.go", "{report}"); + } + assert!( + !list.iter().any(|t| t["correct"] == "completelyDifferent"), + "a rename is not a typo: {report}" + ); + assert!( + !list.iter().any(|t| t["wrong"] == "numConTimes"), + "a literal change is not a typo: {report}" + ); +} + +/// The chronologically first fix wins the `typo_list` slot, and the bytes do +/// not depend on the host CPU count. +#[test] +fn first_fix_wins_regardless_of_the_cpu_count() { + let (test, first_fix) = duplicate_pair_fixture(); + let p = path(&test); + let args = ["-a", "history/typos", "--format", "json", p]; + + let unpinned = codefang(&args); + assert!(unpinned.status.success()); + let report: serde_json::Value = serde_json::from_slice(&unpinned.stdout).expect("json"); + let list = report["typo_list"].as_array().expect("typo_list"); + assert_eq!(list.len(), 1, "{report}"); + assert_eq!(list[0]["file"], "b.go", "the first fix wins: {report}"); + assert_eq!(list[0]["commit"], first_fix.as_str(), "{report}"); + assert_eq!(report["patterns"][0]["frequency"], 2, "{report}"); + assert_eq!(report["aggregate"]["affected_commits"], 2, "{report}"); + + let pinned = codefang_pinned("0-1", &args); + if !pinned.status.success() { + eprintln!( + "skipping the pinned run: taskset -c 0-1 is not usable here: {}", + String::from_utf8_lossy(&pinned.stderr) + ); + return; + } + assert_eq!( + pinned.stdout, unpinned.stdout, + "json must be byte-identical under taskset -c 0-1" + ); +} + +/// `--format bin` carries exactly the json value; `--format yaml` renders the +/// same report under the analyzer header. +#[test] +fn bin_and_yaml_carry_the_json_value() { + let test = fix_fixture(); + let p = path(&test); + let json = stdout_ok(&["-a", "history/typos", "--format", "json", p]); + let bin = stdout_ok(&["-a", "history/typos", "--format", "bin", p]); + assert_eq!(&bin[..4], b"CFB1"); + assert_eq!( + &bin[BIN_HEADER..], + &json[..], + "bin payload is the json value" + ); + + let yaml = String::from_utf8(stdout_ok(&["-a", "history/typos", "--format", "yaml", p])) + .expect("utf-8 yaml"); + assert!(yaml.starts_with("codefang (v2):\n"), "{yaml}"); + assert!(yaml.contains("history/typos:\n"), "{yaml}"); + assert!(yaml.contains("wrong: recieve"), "{yaml}"); + assert!(yaml.contains("frequency: 2"), "{yaml}"); +} + +/// A history without any fix yields the empty report with every list +/// present as `[]`. +#[test] +fn no_fix_is_the_empty_report() { + let test = init_repo().expect("init"); + commit_files(&test, "only", T0, &[("a.go", TYPO_GO)]).expect("only"); + let report = typos_json(&test); + assert_eq!(report["typo_list"], serde_json::json!([]), "{report}"); + assert_eq!(report["patterns"], serde_json::json!([]), "{report}"); + assert_eq!(report["file_typos"], serde_json::json!([]), "{report}"); + assert_eq!(report["aggregate"]["total_typos"], 0, "{report}"); +} diff --git a/bins/uast/Cargo.toml b/bins/uast/Cargo.toml index 8f470fe7..75503240 100644 --- a/bins/uast/Cargo.toml +++ b/bins/uast/Cargo.toml @@ -22,6 +22,10 @@ cf-uast = { path = "../../crates/cf-uast" } cf-uast-node = { path = "../../crates/cf-uast-node" } # Mapping DSL / grammar analysis (`uast mapping`). cf-uast-mapping = { path = "../../crates/cf-uast-mapping" } +# Raw tree-sitter parse for `uast mapping --show-treesitter`: the grammar +# tables come from `cf_uast::languages::get_language`, the parser and node +# walk from tree-sitter itself (the version cf-uast pins). +tree-sitter = { workspace = true } # Embedded UAST JSON Schema (`uast validate` default schema). cf-uast-spec = { path = "../../crates/cf-uast-spec" } @@ -41,3 +45,7 @@ serde_json = { workspace = true } # JSON-Schema validation engine for `uast validate` (replaces Go # xeipuuv/gojsonschema). Exit codes 0/1/2 are driven from its results. jsonschema = "0.18" + +[dev-dependencies] +# Scratch fixtures for the real-binary CLI tests (`tests/cli_surface_e2e.rs`). +tempfile = { workspace = true } diff --git a/bins/uast/src/completion.rs b/bins/uast/src/completion.rs index 2dc5801f..65ad6ac3 100644 --- a/bins/uast/src/completion.rs +++ b/bins/uast/src/completion.rs @@ -1,13 +1,11 @@ //! `uast completion [shell]` — shell completion scripts (port of //! `cmd/uast/completion.go`). //! -//! Go builds a *separate* root command (Short "Unified AST - Parse, analyze, and -//! transform code across 100+ languages") registering only parse, query, -//! analyze, diff, explore, completion, version, then calls cobra's per-shell -//! generator. The script bytes are clap-vs-cobra cosmetic (Layer-D informational, -//! DESIGN §6), so we generate best-effort via `clap_complete` against a command -//! that mirrors the same subset; the shell set and the `unsupported shell` error -//! wording are reproduced exactly. +//! The script is generated by `clap_complete` from the real `uast` command +//! tree, so every subcommand and flag `uast --help` lists is completed (the Go +//! reference completed a hand-picked subset that omitted `validate` and +//! `mapping`). The shell set and the `unsupported shell` error wording are +//! kept. use std::io; @@ -41,25 +39,11 @@ fn run_for_shell(shell: &str) -> Result<(), String> { other => return Err(format!("unsupported shell: {other}")), }; - let mut root = completion_root(); + let mut root = crate::build_cli(); generate(sh, &mut root, "uast", &mut io::stdout()); Ok(()) } -/// The dedicated completion root (completion.go `runCompletion` registers this -/// subset with a different Short string). -fn completion_root() -> Command { - Command::new("uast") - .about("Unified AST - Parse, analyze, and transform code across 100+ languages") - .subcommand(crate::parse::command()) - .subcommand(crate::query::command()) - .subcommand(crate::analyze::command()) - .subcommand(crate::diff::command()) - .subcommand(crate::explore::command()) - .subcommand(command()) - .subcommand(Command::new("version").about("Show version information")) -} - #[cfg(test)] mod tests { use super::*; diff --git a/bins/uast/src/main.rs b/bins/uast/src/main.rs index 9c9856aa..97074541 100644 --- a/bins/uast/src/main.rs +++ b/bins/uast/src/main.rs @@ -2,12 +2,15 @@ //! //! A clap **builder**-API mirror of the cobra CLI in `cmd/uast/*.go` //! (DESIGN §4.2). Root: `Use "uast"`, Short "UAST (Universal Abstract Syntax -//! Tree) parser and analyzer". Persistent flags `--config`, `--verbose`/`-v`, -//! `--quiet`/`-q`. The 9 subcommands are: parse, diff, query, explore, -//! analyze, completion, version, validate, mapping. Every -//! subcommand's `Use`/`Short`, flag long/short/default/help string, and valid -//! format-value list is copied verbatim from the Go source so help/usage and -//! error wording match cobra (the Layer-D CLI golden, DESIGN §6). +//! Tree) parser and analyzer". The root carries no flags of its own. The 9 +//! subcommands are: parse, diff, query, explore, analyze, completion, +//! version, validate, mapping. +//! +//! The help text is a contract: every flag a subcommand lists is read by that +//! subcommand. The Go-era persistent `--config` / `--verbose` / `--quiet`, the +//! `parse --progress` / `--workers`, `validate --color` / `--no-color` and +//! `mapping --mapping` / `--coverage` flags had no reader and are not +//! accepted (`specs/bugs/BUG-cli-inert-surface.md`). //! //! ## Error-handling asymmetry (DESIGN §4) //! @@ -41,7 +44,7 @@ mod validate; use std::process::exit; -use clap::{Arg, ArgAction, Command}; +use clap::Command; /// The `"json"` output-format constant (Go `formatJSON`, main.go:14). pub const FORMAT_JSON: &str = "json"; @@ -50,47 +53,40 @@ pub const FORMAT_COMPACT: &str = "compact"; /// The `"none"` output-format constant (Go `formatNone`, parse.go:28). pub const FORMAT_NONE: &str = "none"; -/// The percent multiplier used by `analyze`/`mapping` (Go `coveragePercent`). +/// The percent multiplier used by `analyze` (Go `coveragePercent`). pub const COVERAGE_PERCENT: f64 = 100.0; +/// The file argument that means "read stdin" for `parse` and `query`. +pub const STDIN_ARG: &str = "-"; + +/// Opens the `-o/--output` destination of `parse` / `query` ONCE per run: +/// stdout for an empty path, else the file (created or truncated). Every +/// input's document is appended to the same writer, so a multi-file run keeps +/// every input instead of only the last one. +/// +/// # Errors +/// +/// `failed to create output file: ` when the file cannot be created. +pub fn open_output(path: &str) -> Result, String> { + if path.is_empty() { + return Ok(Box::new(std::io::stdout())); + } + std::fs::File::create(path) + .map(|f| Box::new(f) as Box) + .map_err(|e| format!("failed to create output file: {e}")) +} + /// Builds the root `uast` command with all 9 subcommands. /// /// NOTE: the Go reference also shipped `lsp` (a `.uastmap`/query-DSL language /// server) and `server` (the UAST development HTTP server + web playground). /// Both, along with the `.uastmap` DSL data they edited, were removed from the /// Rust product — analysis runs off the native `cf-uast-mappings` tables, so -/// the DSL editor tooling had no remaining consumer. This is a deliberate -/// divergence from the frozen Go oracle's CLI surface (see the cli-surface -/// gate's intentional-divergence allowlist). +/// the DSL editor tooling had no remaining consumer. fn build_cli() -> Command { Command::new("uast") .about("UAST (Universal Abstract Syntax Tree) parser and analyzer") .long_about("UAST is a tool for parsing source code into Universal Abstract Syntax Trees.") - // Persistent flags (main.go:29-31). - .arg( - Arg::new("config") - .long("config") - .help("config file (default is $HOME/.uast.yaml)") - .global(true) - .default_value("") - .action(ArgAction::Set), - ) - .arg( - Arg::new("verbose") - .long("verbose") - .short('v') - .help("verbose output") - .global(true) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("quiet") - .long("quiet") - .short('q') - .help("suppress output") - .global(true) - .action(ArgAction::SetTrue), - ) // Subcommands in main.go AddCommand order. .subcommand(parse::command()) .subcommand(diff::command()) diff --git a/bins/uast/src/mapping.rs b/bins/uast/src/mapping.rs index 20604b12..8fe5371e 100644 --- a/bins/uast/src/mapping.rs +++ b/bins/uast/src/mapping.rs @@ -1,43 +1,45 @@ //! `uast mapping` — UAST mapping helpers (port of `cmd/uast/mapping.go`). //! -//! Analyzes `node-types.json`, classifies nodes, computes mapping coverage, -//! generates `.uastmap` DSL, or shows the raw tree-sitter JSON structure of an -//! input file. JSON output (`{node_count, categories, nodes[, coverage]}`) routes -//! through [`cf_textutil::write_json`] (DESIGN rule 1); `text` is human output. +//! Analyzes and classifies `node-types.json`, generates `.uastmap` DSL from +//! it, or shows the raw tree-sitter JSON structure of input files. JSON output +//! (`{node_count, categories, nodes}`, the tree-sitter dump) routes through +//! [`cf_textutil::write_json`] (DESIGN rule 1); `text` is human output. +//! +//! The Go-era `--mapping` / `--coverage` pair is gone: the reference parsed +//! the mapping file and discarded the rules, so the coverage was never +//! computed (`specs/bugs/BUG-cli-inert-surface.md`). use std::io::{self, Write}; use cf_textutil::{GoMap, GoValue}; use cf_uast_mapping::grammar_analysis::{ - apply_heuristic_classification, coverage_analysis, generate_mapping_dsl, parse_node_types, + apply_heuristic_classification, generate_mapping_dsl, parse_node_types, }; -use cf_uast_mapping::{NodeTypeInfo, Parser as MappingParser, Rule}; +use cf_uast_mapping::NodeTypeInfo; use clap::{Arg, ArgAction, ArgMatches, Command}; -use crate::{COVERAGE_PERCENT, FORMAT_JSON}; +use crate::FORMAT_JSON; /// Builds the `mapping` subcommand (mapping.go:38-69). pub fn command() -> Command { Command::new("mapping") - .about("UAST mapping helpers: grammar analysis, classification, coverage") + .about("UAST mapping helpers: grammar analysis, classification, DSL generation") .long_about( - "Analyze node-types.json, classify nodes, compute mapping coverage, and show tree-sitter JSON structure.", + "Analyze and classify node-types.json, generate .uastmap DSL from it, and show the tree-sitter JSON structure of input files.", + ) + // The positional files are the inputs of `--show-treesitter`. + .arg( + Arg::new("files") + .num_args(0..) + .index(1) + .help("Input source files for --show-treesitter"), ) - // Go's `mapping` command sets no cobra `Args`, so its `--help` Usage line - // is `uast mapping [flags]` with NO positional placeholder (the - // cli-surface positional shape is `[]`). The body still consumes free - // arguments as input files (`--show-treesitter`), so we keep the - // positional but `.hide(true)` it: clap then omits it from the Usage - // line (matching Go's empty positional shape) while still parsing it. - .arg(Arg::new("files").num_args(0..).index(1).hide(true)) .arg(long_opt("node-types", "", "Path to node-types.json (required for non-treesitter operations)")) - .arg(long_opt("mapping", "", "Path to mapping DSL file (optional)")) - .arg(long_opt("format", "text", "Output format: text or json")) - .arg(long_flag("coverage", "Compute mapping coverage if mapping is provided")) + .arg(long_opt("format", "text", "Output format of the node-types summary: text or json")) .arg(long_flag("generate", "Generate .uastmap DSL from node-types.json")) .arg(long_flag("show-treesitter", "Show original tree-sitter JSON structure for input files")) - .arg(long_opt("language", "", "Language for tree-sitter parsing (language name or grammar file path)")) - .arg(long_opt("extensions", "", "Comma-separated list of file extensions for language declaration")) + .arg(long_opt("language", "", "Tree-sitter language name (e.g. go) for --show-treesitter and the --generate header")) + .arg(long_opt("extensions", "", "Comma-separated file extensions for the --generate language header")) } /// Runs `mapping` (mapping.go `runMappingHelper`). @@ -46,15 +48,10 @@ pub fn run(m: &ArgMatches) -> Result<(), String> { .get_one::("node-types") .map(String::as_str) .unwrap_or(""); - let mapping_path = m - .get_one::("mapping") - .map(String::as_str) - .unwrap_or(""); let format = m .get_one::("format") .map(String::as_str) .unwrap_or("text"); - let coverage = m.get_flag("coverage"); let generate = m.get_flag("generate"); let show_treesitter = m.get_flag("show-treesitter"); let language = m @@ -90,12 +87,10 @@ pub fn run(m: &ArgMatches) -> Result<(), String> { return Ok(()); } - let rules = load_mapping_rules(mapping_path)?; - if format == FORMAT_JSON { - return output_json(&nodes, &rules, coverage); + return output_json(&nodes); } - output_text(&nodes, &rules, coverage) + output_text(&nodes) } /// Loads + classifies `node-types.json` (mapping.go `loadNodeTypes`). Errors @@ -111,34 +106,14 @@ fn load_node_types(path: &str) -> Result, String> { Ok(apply_heuristic_classification(nodes)) } -/// Loads mapping rules from a DSL file (mapping.go `loadMappingRules`). The Go -/// code parses the DSL only for validation and returns an empty rule list, which -/// is reproduced here. -fn load_mapping_rules(path: &str) -> Result, String> { - if path.is_empty() { - return Ok(Vec::new()); - } - let data = - std::fs::read_to_string(path).map_err(|e| format!("failed to open mapping DSL: {e}"))?; - MappingParser::new() - .parse_mapping(&data) - .map_err(|e| format!("failed to load mapping DSL: {e}"))?; - Ok(Vec::new()) -} - /// Outputs the JSON summary (mapping.go `outputMappingJSON`). -fn output_json(nodes: &[NodeTypeInfo], rules: &[Rule], coverage: bool) -> Result<(), String> { - let mut entries: Vec<(String, GoValue)> = vec![ +fn output_json(nodes: &[NodeTypeInfo]) -> Result<(), String> { + let entries: Vec<(String, GoValue)> = vec![ ("node_count".to_string(), GoValue::Int(nodes.len() as i64)), ("categories".to_string(), categories_value(nodes)), ("nodes".to_string(), nodes_value(nodes)), ]; - if coverage && !rules.is_empty() { - let cov = coverage_analysis(rules, nodes).map_err(|e| e.to_string())?; - entries.push(("coverage".to_string(), GoValue::Float(cov))); - } - // Go builds `map[string]any{...}` (map-origin, byte-sorted keys). let value = GoValue::object(GoMap::from_map(entries)); let mut out = io::stdout(); @@ -146,17 +121,13 @@ fn output_json(nodes: &[NodeTypeInfo], rules: &[Rule], coverage: bool) -> Result } /// Outputs the text summary (mapping.go `outputMappingText`). -fn output_text(nodes: &[NodeTypeInfo], rules: &[Rule], coverage: bool) -> Result<(), String> { +fn output_text(nodes: &[NodeTypeInfo]) -> Result<(), String> { let out = io::stdout(); let mut out = out.lock(); let _ = writeln!(out, "Node types: {}", nodes.len()); for (cat, count) in summarize_categories(nodes) { let _ = writeln!(out, " {cat}: {count}"); } - if coverage && !rules.is_empty() { - let cov = coverage_analysis(rules, nodes).map_err(|e| e.to_string())?; - let _ = writeln!(out, "Coverage: {:.2}%", cov * COVERAGE_PERCENT); - } Ok(()) } @@ -244,36 +215,96 @@ fn nodes_value(nodes: &[NodeTypeInfo]) -> GoValue { } /// Shows the raw tree-sitter JSON for input files (mapping.go -/// `showTreeSitterJSON`). The grammar wiring lives in `cf-uast` (DESIGN §5); this -/// reproduces the control flow and sentinel errors. Until grammars are vendored -/// the parse step is unavailable, so an unset/unknown language yields the same -/// `unsupported language` sentinel. +/// `showTreeSitterJSON`): for each file, a `=== Tree-sitter JSON for +/// (language: ) ===` header, the pretty-printed tree, and a blank line. +/// The first failing file aborts with `failed to process : `. fn show_tree_sitter(files: &[String], language: &str) -> Result<(), String> { if files.is_empty() { return Err("no input files provided".to_string()); } - // The reference loops over every input file, but every path below returns, - // so only the first file is ever reached while grammar wiring is pending in - // cf-uast (DESIGN §5). Stated as an explicit first-file bail instead of a - // loop that cannot iterate twice; the sentinel strings are unchanged. - if let Some(filename) = files.first() { - let _ = std::fs::read(filename) - .map_err(|e| format!("failed to process {filename}: failed to read file: {e}"))?; - // Language resolution mirrors mapping.go: an unknown/empty language is an - // `unsupported language` error (the grammar dispatch is owned by cf-uast - // and not yet wired; see cf-uast todos / DESIGN §5). - if language.is_empty() { - return Err(format!( - "failed to process {filename}: tree-sitter parsing requires a language to be set" - )); - } - return Err(format!( - "failed to process {filename}: unsupported language: {language}" - )); + let stdout = io::stdout(); + let mut out = stdout.lock(); + for filename in files { + write_tree_sitter_json(&mut out, filename, language) + .map_err(|e| format!("failed to process {filename}: {e}"))?; } Ok(()) } +/// Parses `filename` with the vendored `language` grammar and writes its +/// header + tree JSON (mapping.go `processFileForTreeSitterJSON`). +fn write_tree_sitter_json( + out: &mut dyn Write, + filename: &str, + language: &str, +) -> Result<(), String> { + let content = std::fs::read(filename).map_err(|e| format!("failed to read file: {e}"))?; + if language.is_empty() { + return Err( + "tree-sitter parsing requires a language to be set; use --language ".to_string(), + ); + } + let grammar = cf_uast::languages::get_language(language) + .ok_or_else(|| format!("unsupported language: {language}"))?; + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&grammar) + .map_err(|e| format!("failed to load the {language} grammar: {e}"))?; + let tree = parser + .parse(&content, None) + .ok_or_else(|| "failed to parse with tree-sitter".to_string())?; + let value = tree_sitter_node_value(tree.root_node(), &content); + writeln!( + out, + "=== Tree-sitter JSON for {filename} (language: {language}) ===" + ) + .map_err(|e| e.to_string())?; + cf_textutil::write_json(&mut *out, &value, true).map_err(|e| e.to_string())?; + writeln!(out).map_err(|e| e.to_string()) +} + +/// One tree-sitter node as JSON (mapping.go `convertTreeSitterNodeToJSON`): +/// `type`, `named`, `start_pos` / `end_pos` (`row`, `column`), `start_byte` / +/// `end_byte`, `text` when non-empty, and `children` (the NAMED children) when +/// there are any. Keys are map-origin (byte-sorted), like the Go `map`. +fn tree_sitter_node_value(node: tree_sitter::Node<'_>, source: &[u8]) -> GoValue { + let position = |p: tree_sitter::Point| { + GoValue::object(GoMap::from_map(vec![ + ("row".to_string(), usize_value(p.row)), + ("column".to_string(), usize_value(p.column)), + ])) + }; + let mut entries = vec![ + ("type".to_string(), GoValue::Str(node.kind().to_string())), + ("named".to_string(), GoValue::Bool(node.is_named())), + ("start_pos".to_string(), position(node.start_position())), + ("end_pos".to_string(), position(node.end_position())), + ("start_byte".to_string(), usize_value(node.start_byte())), + ("end_byte".to_string(), usize_value(node.end_byte())), + ]; + let text = source + .get(node.start_byte()..node.end_byte()) + .map(String::from_utf8_lossy) + .unwrap_or_default(); + if !text.is_empty() { + entries.push(("text".to_string(), GoValue::Str(text.into_owned()))); + } + let mut cursor = node.walk(); + let children: Vec = node + .named_children(&mut cursor) + .map(|child| tree_sitter_node_value(child, source)) + .collect(); + if !children.is_empty() { + entries.push(("children".to_string(), GoValue::Array(children))); + } + GoValue::object(GoMap::from_map(entries)) +} + +/// A tree-sitter offset as a JSON integer. +fn usize_value(v: usize) -> GoValue { + GoValue::Int(i64::try_from(v).unwrap_or(i64::MAX)) +} + fn long_opt(name: &'static str, default: &'static str, help: &'static str) -> Arg { Arg::new(name) .long(name) @@ -307,9 +338,4 @@ mod tests { let err = show_tree_sitter(&[], "go").unwrap_err(); assert_eq!(err, "no input files provided"); } - - #[test] - fn empty_mapping_path_is_empty_rules() { - assert!(load_mapping_rules("").unwrap().is_empty()); - } } diff --git a/bins/uast/src/parse.rs b/bins/uast/src/parse.rs index 8f6fa3bd..0ad76bca 100644 --- a/bins/uast/src/parse.rs +++ b/bins/uast/src/parse.rs @@ -3,15 +3,14 @@ //! //! Resolves files (`--all` walks `.` skipping hidden dirs), parses each into a //! UAST, and serializes via [`cf_textutil::write_json`] (DESIGN rule 1). With no -//! files it reads stdin (`stdin.go`, or `stdin.` when `--language` is set) -//! and assigns stable IDs before output. +//! files, or for a `-` file argument, it reads stdin (`stdin.go`, or +//! `stdin.` when `--language` is set) and assigns stable IDs before +//! output. //! //! Output formats (parse.go `outputNode`): `json` (pretty), `compact`, `none` -//! (parse only, no serialization). Note: the flag help lists `tree`, but the Go -//! `switch` has no `tree` arm, so `tree` (and any other value) hits the default -//! and yields `unsupported format: ` — reproduced exactly here. +//! (parse only, no serialization). Any other value (the Go help also listed +//! `tree`, which no code path renders) is `unsupported format: `. -use std::fs::File; use std::io::{self, Read, Write}; use std::path::Path; @@ -19,7 +18,7 @@ use cf_uast::Parser; use clap::{Arg, ArgAction, ArgMatches, Command}; use crate::govalue_bridge::node_to_value; -use crate::{FORMAT_COMPACT, FORMAT_JSON, FORMAT_NONE}; +use crate::{FORMAT_COMPACT, FORMAT_JSON, FORMAT_NONE, STDIN_ARG}; /// Builds the `parse` subcommand (parse.go:39-66). pub fn command() -> Command { @@ -32,28 +31,13 @@ pub fn command() -> Command { .arg(opt( "format", 'f', - "json", - "output format (json, compact, tree, none)", - )) - .arg(flag( - "progress", - Some('p'), - "show progress for multiple files", + FORMAT_JSON, + "output format (json, compact, none)", )) .arg(flag( "all", - None, "parse all source files in the codebase recursively", )) - .arg( - Arg::new("workers") - .long("workers") - .short('w') - .help("number of parallel workers (default: number of CPUs)") - .default_value("0") - .value_parser(clap::value_parser!(i64)) - .action(ArgAction::Set), - ) } /// Runs `parse` (parse.go `runParse`). @@ -80,14 +64,19 @@ pub fn run(m: &ArgMatches) -> Result<(), String> { let files = resolve_files(files, all, &parser)?; + let mut writer = crate::open_output(output)?; if files.is_empty() { - return parse_stdin(lang, output, format); + return parse_stdin(lang, &mut *writer, format); } // Go has a parallel path for `>1 file && format==none`; pooling is // behavioral-only (DESIGN), so the sequential path is byte-identical. for file in &files { - parse_one(&parser, file, lang, output, format) + if file == STDIN_ARG { + parse_stdin(lang, &mut *writer, format)?; + continue; + } + parse_one(&parser, file, lang, &mut *writer, format) .map_err(|e| format!("failed to parse {file}: {e}"))?; } Ok(()) @@ -139,7 +128,7 @@ fn is_hidden_dir(name: &str) -> bool { } /// Parses stdin into UAST and outputs it (parse.go `parseStdin`). -fn parse_stdin(lang: &str, output: &str, format: &str) -> Result<(), String> { +fn parse_stdin(lang: &str, writer: &mut dyn Write, format: &str) -> Result<(), String> { let mut code = Vec::new(); io::stdin() .read_to_end(&mut code) @@ -156,7 +145,7 @@ fn parse_stdin(lang: &str, output: &str, format: &str) -> Result<(), String> { .parse(&filename, &code) .map_err(|e| format!("parse error: {e}"))?; node.assign_stable_ids(); - output_node(&node, output, format) + output_node(&node, writer, format) } /// Parses a single file and outputs it (parse.go `parseFileWithParser`). @@ -164,7 +153,7 @@ fn parse_one( parser: &Parser, file: &str, lang: &str, - output: &str, + writer: &mut dyn Write, format: &str, ) -> Result<(), String> { let mut node = parser.parse_file(file, lang).map_err(|e| e.to_string())?; @@ -173,20 +162,14 @@ fn parse_one( return Ok(()); } node.assign_stable_ids(); - output_node(&node, output, format) + output_node(&node, writer, format) } -/// Serializes a node to the chosen writer and format (parse.go `outputNode`). -fn output_node(node: &cf_uast::Node, output: &str, format: &str) -> Result<(), String> { - let mut writer: Box = if output.is_empty() { - Box::new(io::stdout()) - } else { - Box::new(File::create(output).map_err(|e| format!("failed to create output file: {e}"))?) - }; - +/// Serializes a node to `writer` in `format` (parse.go `outputNode`). +fn output_node(node: &cf_uast::Node, writer: &mut dyn Write, format: &str) -> Result<(), String> { match format { - FORMAT_JSON => write_node(&mut writer, node, true), - FORMAT_COMPACT => write_node(&mut writer, node, false), + FORMAT_JSON => write_node(writer, node, true), + FORMAT_COMPACT => write_node(writer, node, false), FORMAT_NONE => Ok(()), // Go: default => `unsupported format: ` (ErrUnsupportedParseFmt). other => Err(format!("unsupported format: {other}")), @@ -218,15 +201,11 @@ fn opt(name: &'static str, short: char, default: &'static str, help: &'static st .action(ArgAction::Set) } -fn flag(name: &'static str, short: Option, help: &'static str) -> Arg { - let mut a = Arg::new(name) +fn flag(name: &'static str, help: &'static str) -> Arg { + Arg::new(name) .long(name) .help(help) - .action(ArgAction::SetTrue); - if let Some(s) = short { - a = a.short(s); - } - a + .action(ArgAction::SetTrue) } #[cfg(test)] @@ -245,7 +224,7 @@ mod tests { fn tree_format_is_unsupported() { // parse.go's switch has no `tree` arm, so `tree` hits the default error. let node = cf_uast::Node::with_token("File", ""); - let err = output_node(&node, "", "tree").unwrap_err(); + let err = output_node(&node, &mut io::sink(), "tree").unwrap_err(); assert_eq!(err, "unsupported format: tree"); } @@ -253,6 +232,6 @@ mod tests { fn none_format_writes_nothing() { let node = cf_uast::Node::with_token("File", ""); // `none` returns Ok without touching the writer. - assert!(output_node(&node, "", "none").is_ok()); + assert!(output_node(&node, &mut io::sink(), "none").is_ok()); } } diff --git a/bins/uast/src/query.rs b/bins/uast/src/query.rs index 69c85692..fdb74dd8 100644 --- a/bins/uast/src/query.rs +++ b/bins/uast/src/query.rs @@ -11,7 +11,6 @@ //! number of results). `interactive` opens a REPL (non-binding human output). //! Input UAST JSON decoding uses `serde_json` (DESIGN §2 permits input decode). -use std::fs::File; use std::io::{self, Read, Write}; use cf_textutil::{GoMap, GoValue}; @@ -19,7 +18,7 @@ use cf_uast::{Node, Parser}; use clap::{Arg, ArgAction, ArgMatches, Command}; use crate::govalue_bridge::node_to_value; -use crate::{FORMAT_COMPACT, FORMAT_JSON}; +use crate::{FORMAT_COMPACT, FORMAT_JSON, STDIN_ARG}; /// Builds the `query` subcommand (query.go:33-61). pub fn command() -> Command { @@ -31,7 +30,7 @@ pub fn command() -> Command { "input", 'i', "", - "input file (UAST JSON or source code)", + "input file (UAST JSON or source code), queried before any positional files", )) .arg(opt("output", 'o', "", "output file (default: stdout)")) .arg(opt( @@ -64,7 +63,16 @@ pub fn run(m: &ArgMatches) -> Result<(), String> { let interactive = m.get_flag("interactive"); if interactive { - return run_interactive(input); + // The REPL explores ONE tree: the `--input` file, else the single + // positional file, else source read from stdin. + let targets = query_targets(input, &args); + if targets.len() > 1 { + return Err(format!( + "interactive mode queries one file, got {}", + targets.len() + )); + } + return run_interactive(targets.first().map_or("", String::as_str)); } // Go: `if len(args) == 0 { return ErrQueryExprRequired }`. @@ -72,21 +80,37 @@ pub fn run(m: &ArgMatches) -> Result<(), String> { return Err("query expression required".to_string()); } let query = &args[0]; - let files = &args[1..]; + let files = query_targets(input, &args[1..]); - if files.is_empty() && input.is_empty() { - return query_stdin(query, output, format); + let mut writer = crate::open_output(output)?; + if files.is_empty() { + return query_stdin(query, &mut *writer, format); } - for file in files { - query_file(file, query, output, format) + for file in &files { + if file == STDIN_ARG { + query_stdin(query, &mut *writer, format)?; + continue; + } + query_file(file, query, &mut *writer, format) .map_err(|e| format!("failed to query {file}: {e}"))?; } Ok(()) } +/// The files a query runs over: the `--input` file (when set) first, then the +/// positional files. Empty means the query reads a UAST tree from stdin, as +/// does a `-` target. +fn query_targets(input: &str, files: &[String]) -> Vec { + std::iter::once(input) + .filter(|i| !i.is_empty()) + .map(str::to_owned) + .chain(files.iter().cloned()) + .collect() +} + /// Queries a UAST tree decoded from stdin (query.go `queryStdin`). -fn query_stdin(query: &str, output: &str, format: &str) -> Result<(), String> { +fn query_stdin(query: &str, writer: &mut dyn Write, format: &str) -> Result<(), String> { let mut buf = Vec::new(); io::stdin() .read_to_end(&mut buf) @@ -95,16 +119,16 @@ fn query_stdin(query: &str, output: &str, format: &str) -> Result<(), String> { let results = node .find_dsl(query) .map_err(|e| format!("query error: {e}"))?; - output_results(&results, output, format) + output_results(&results, writer, format) } /// Queries a single file (query.go `queryFile` / `parseFileForQuery`). -fn query_file(file: &str, query: &str, output: &str, format: &str) -> Result<(), String> { +fn query_file(file: &str, query: &str, writer: &mut dyn Write, format: &str) -> Result<(), String> { let node = load_query_node(file)?; let results = node .find_dsl(query) .map_err(|e| format!("query error: {e}"))?; - output_results(&results, output, format) + output_results(&results, writer, format) } /// Loads a UAST node from a file, auto-detecting JSON vs source (query.go @@ -133,7 +157,8 @@ fn load_uast_from_json(file: &str) -> Result { decode_uast(&bytes).map_err(|e| format!("failed to decode UAST from {file}: {e}")) } -/// The interactive REPL (query.go `runInteractiveQuery`). Human/non-binding. +/// The interactive REPL (query.go `runInteractiveQuery`) over `input` (a +/// UAST JSON or source file; empty reads source from stdin). Human/non-binding. fn run_interactive(input: &str) -> Result<(), String> { let node = if input.is_empty() { let mut buf = Vec::new(); @@ -197,22 +222,16 @@ fn run_interactive(input: &str) -> Result<(), String> { Ok(()) } -/// Serializes results (query.go `outputResults`). -fn output_results(results: &[Node], output: &str, format: &str) -> Result<(), String> { - let mut writer: Box = if output.is_empty() { - Box::new(io::stdout()) - } else { - Box::new(File::create(output).map_err(|e| format!("failed to create output file: {e}"))?) - }; - +/// Serializes results to `writer` (query.go `outputResults`). +fn output_results(results: &[Node], writer: &mut dyn Write, format: &str) -> Result<(), String> { let mapped = nodes_to_value(results); match format { FORMAT_JSON => { - cf_textutil::write_json(&mut writer, &mapped, true).map_err(|e| e.to_string()) + cf_textutil::write_json(&mut *writer, &mapped, true).map_err(|e| e.to_string()) } FORMAT_COMPACT => { - cf_textutil::write_json(&mut writer, &mapped, false).map_err(|e| e.to_string()) + cf_textutil::write_json(&mut *writer, &mapped, false).map_err(|e| e.to_string()) } "count" => { let count = result_count(&mapped); @@ -409,6 +428,14 @@ mod tests { ); } + #[test] + fn query_targets_put_the_input_file_first() { + let files = vec!["b.go".to_string()]; + assert_eq!(query_targets("a.go", &files), ["a.go", "b.go"]); + assert_eq!(query_targets("", &files), ["b.go"]); + assert!(query_targets("", &[]).is_empty(), "no target reads stdin"); + } + #[test] fn is_json_file_case_insensitive() { assert!(is_json_file("tree.json")); diff --git a/bins/uast/src/validate.rs b/bins/uast/src/validate.rs index 969c9786..e85da1d6 100644 --- a/bins/uast/src/validate.rs +++ b/bins/uast/src/validate.rs @@ -7,10 +7,9 @@ //! * `1` — validation FAILED (document is well-formed JSON but violates schema); //! * `2` — error (bad JSON, schema read failure, open failure, engine error). //! -//! `--no-color` wins over `--color` (cosmetic). The schema defaults to the -//! embedded `cf-uast-spec` schema when `--schema` is empty or the Go default -//! path string; otherwise the path is read from disk. The compliance percentage -//! mirrors `calculateCompliance`/`countNodes`. +//! The schema is the embedded `cf-uast-spec` schema unless `--schema` names a +//! file to read instead. The report is plain text (no color toggles). The +//! compliance percentage mirrors `calculateCompliance`/`countNodes`. use std::io::{self, Read}; use std::process::exit; @@ -24,8 +23,8 @@ const EXIT_INVALID: i32 = 1; const EXIT_ERROR: i32 = 2; /// Maximum compliance percentage (validate.go `complianceMax`). const COMPLIANCE_MAX: i64 = 100; -/// The Go default `--schema` value, which selects the embedded schema. -const DEFAULT_SCHEMA_PATH: &str = "pkg/uast/spec/uast-schema.json"; +/// The `--schema` default: empty selects the embedded schema. +const EMBEDDED_SCHEMA: &str = ""; /// Builds the `validate` subcommand (validate.go:24-43). pub fn command() -> Command { @@ -36,22 +35,10 @@ pub fn command() -> Command { .arg( Arg::new("schema") .long("schema") - .help("path to UAST JSON schema") - .default_value(DEFAULT_SCHEMA_PATH) + .help("path to a UAST JSON schema (default: the embedded UAST schema)") + .default_value(EMBEDDED_SCHEMA) .action(ArgAction::Set), ) - .arg( - Arg::new("color") - .long("color") - .help("force colored output") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("no-color") - .long("no-color") - .help("disable colored output") - .action(ArgAction::SetTrue), - ) } /// Runs `validate`. Always exits via [`exit`] with code 0/1/2 (validate.go uses @@ -65,9 +52,7 @@ pub fn run(m: &ArgMatches) -> Result<(), String> { let schema_path = m .get_one::("schema") .map(String::as_str) - .unwrap_or(DEFAULT_SCHEMA_PATH); - // --no-color wins over --color (validate.go); both are cosmetic. - let _color = !m.get_flag("no-color") && m.get_flag("color"); + .unwrap_or(EMBEDDED_SCHEMA); let (input_bytes, label) = load_input(file); let input: Value = match serde_json::from_slice(&input_bytes) { @@ -145,11 +130,10 @@ fn load_input(path: &str) -> (Vec, String) { } } -/// Loads the schema bytes (validate.go `loadSchema`). An empty path or the Go -/// default path selects the embedded `cf-uast-spec` schema; otherwise the file -/// is read (failure exits 2). +/// Loads the schema bytes (validate.go `loadSchema`). An empty path selects the +/// embedded `cf-uast-spec` schema; otherwise the file is read (failure exits 2). fn load_schema(path: &str) -> Vec { - if path.is_empty() || path == DEFAULT_SCHEMA_PATH { + if path == EMBEDDED_SCHEMA { return cf_uast_spec::schema().as_bytes().to_vec(); } match std::fs::read(path) { @@ -230,7 +214,7 @@ mod tests { #[test] fn embedded_schema_is_loaded_for_default_path() { - let bytes = load_schema(DEFAULT_SCHEMA_PATH); + let bytes = load_schema(EMBEDDED_SCHEMA); assert!(!bytes.is_empty()); assert!(String::from_utf8_lossy(&bytes).contains("$schema")); } diff --git a/bins/uast/tests/cli_surface_e2e.rs b/bins/uast/tests/cli_surface_e2e.rs new file mode 100644 index 00000000..b5ec2f10 --- /dev/null +++ b/bins/uast/tests/cli_surface_e2e.rs @@ -0,0 +1,317 @@ +//! The `uast` CLI surface contract, through the **real binary**: every flag +//! `uast --help` lists does what its help says, and the flags that +//! never had a reader are rejected by the argument parser instead of being +//! accepted and ignored (`specs/bugs/BUG-cli-inert-surface.md`). + +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +/// Path to the freshly built `uast` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_uast"); + +/// A Go fixture with two function declarations. +const GO_SOURCE: &str = "package p\n\nfunc a() {}\n\nfunc b() {}\n"; + +/// The query every `--input` / positional comparison runs. +const FUNCTION_QUERY: &str = r#"filter(.type == "Function")"#; + +/// The clap usage-error marker for an argument the command does not declare. +const UNEXPECTED_ARGUMENT: &str = "unexpected argument"; + +/// Runs the binary with `args` in `cwd`. +fn uast(cwd: &Path, args: &[&str]) -> Output { + Command::new(BIN) + .args(args) + .current_dir(cwd) + .env("NO_COLOR", "1") + .output() + .expect("run the uast binary") +} + +/// Runs the binary and returns stdout, asserting a zero exit. +fn stdout_ok(cwd: &Path, args: &[&str]) -> String { + let out = uast(cwd, args); + assert!( + out.status.success(), + "uast {args:?} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// A scratch directory holding `m.go` (the Go fixture). +fn go_fixture() -> (tempfile::TempDir, PathBuf) { + let dir = tempfile::tempdir().expect("scratch dir"); + let file = dir.path().join("m.go"); + std::fs::write(&file, GO_SOURCE).expect("write fixture"); + (dir, file) +} + +#[test] +fn inert_flags_are_rejected() { + let (dir, file) = go_fixture(); + let f = file.to_str().unwrap(); + let cases: &[&[&str]] = &[ + // Global flags with no reader. + &["--config", "x.yaml", "version"], + &["--verbose", "version"], + &["-v", "version"], + &["--quiet", "version"], + &["-q", "version"], + // `parse` flags with no reader. + &["parse", "--progress", f], + &["parse", "-p", f], + &["parse", "--workers", "2", f], + &["parse", "-w", "2", f], + // `validate` color toggles (the output is plain text either way). + &["validate", "--color", f], + &["validate", "--no-color", f], + // `mapping` coverage: the mapping rules were never loaded, so the + // coverage was never computed. + &["mapping", "--coverage", "--node-types", f], + &["mapping", "--mapping", f, "--node-types", f], + ]; + for args in cases { + let out = uast(dir.path(), args); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + !out.status.success() && stderr.contains(UNEXPECTED_ARGUMENT), + "uast {args:?} must be rejected by the parser, got rc {:?}: {stderr}", + out.status.code() + ); + } +} + +#[test] +fn query_input_flag_queries_the_file() { + let (dir, file) = go_fixture(); + let f = file.to_str().unwrap(); + let positional = stdout_ok(dir.path(), &["query", FUNCTION_QUERY, f, "-f", "compact"]); + let via_input = stdout_ok( + dir.path(), + &["query", FUNCTION_QUERY, "-i", f, "-f", "compact"], + ); + assert!( + positional.contains("\"results\":[{"), + "the fixture has functions: {positional}" + ); + assert_eq!(via_input, positional, "--input queries the same file"); +} + +#[test] +fn parse_format_help_lists_only_accepted_formats() { + let (dir, file) = go_fixture(); + let help = stdout_ok(dir.path(), &["parse", "--help"]); + assert!(help.contains("json, compact, none"), "{help}"); + assert!(!help.contains("tree"), "`tree` is rejected: {help}"); + let f = file.to_str().unwrap(); + for format in ["json", "compact", "none"] { + stdout_ok(dir.path(), &["parse", "-f", format, f]); + } +} + +#[test] +fn show_treesitter_prints_the_raw_tree() { + let (dir, file) = go_fixture(); + let f = file.to_str().unwrap(); + let out = stdout_ok( + dir.path(), + &["mapping", "--show-treesitter", "--language", "go", f], + ); + let header = format!("=== Tree-sitter JSON for {f} (language: go) ===\n"); + let body = out + .strip_prefix(header.as_str()) + .unwrap_or_else(|| panic!("header missing: {out}")); + let tree: serde_json::Value = serde_json::from_str(body).expect("one JSON document"); + assert_eq!(tree["type"], "source_file"); + assert_eq!(tree["named"], true); + assert_eq!(tree["start_byte"], 0); + assert_eq!(tree["end_byte"], GO_SOURCE.len()); + let kinds: Vec<&str> = tree["children"] + .as_array() + .expect("named children") + .iter() + .filter_map(|c| c["type"].as_str()) + .collect(); + assert_eq!( + kinds, + [ + "package_clause", + "function_declaration", + "function_declaration" + ] + ); + assert_eq!(tree["children"][1]["text"], "func a() {}"); + assert_eq!(tree["children"][1]["start_pos"]["row"], 2); +} + +#[test] +fn show_treesitter_names_a_missing_or_unknown_language() { + let (dir, file) = go_fixture(); + let f = file.to_str().unwrap(); + let missing = uast(dir.path(), &["mapping", "--show-treesitter", f]); + assert_eq!(missing.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&missing.stderr) + .contains("tree-sitter parsing requires a language to be set")); + let unknown = uast( + dir.path(), + &["mapping", "--show-treesitter", "--language", "cobol", f], + ); + assert_eq!(unknown.status.code(), Some(1)); + assert!( + String::from_utf8_lossy(&unknown.stderr).contains("unsupported language: cobol"), + "{}", + String::from_utf8_lossy(&unknown.stderr) + ); +} + +#[test] +fn completion_scripts_cover_every_subcommand_and_parse() { + let (dir, _file) = go_fixture(); + let bash = stdout_ok(dir.path(), &["completion", "bash"]); + for sub in [ + "parse", "diff", "query", "explore", "analyze", "version", "validate", "mapping", + ] { + assert!(bash.contains(sub), "bash completion lacks `{sub}`"); + } + assert!( + !bash.contains("--progress"), + "removed flags are not completed" + ); + let script = dir.path().join("uast.bash"); + std::fs::write(&script, &bash).expect("write script"); + let syntax = Command::new("bash") + .arg("-n") + .arg(&script) + .output() + .expect("run bash -n"); + assert!( + syntax.status.success(), + "bash -n: {}", + String::from_utf8_lossy(&syntax.stderr) + ); + let zsh = stdout_ok(dir.path(), &["completion", "zsh"]); + assert!(zsh.starts_with("#compdef uast"), "{zsh}"); + let fish = stdout_ok(dir.path(), &["completion", "fish"]); + assert!(fish.contains("complete -c uast"), "{fish}"); +} + +#[test] +fn validate_uses_the_embedded_schema_by_default() { + let (dir, file) = go_fixture(); + let help = stdout_ok(dir.path(), &["validate", "--help"]); + assert!( + !help.contains("pkg/uast/spec"), + "the default is the embedded schema, not a Go-tree path: {help}" + ); + let tree = stdout_ok(dir.path(), &["parse", "-f", "json", file.to_str().unwrap()]); + let doc = dir.path().join("m.uast.json"); + std::fs::write(&doc, tree).expect("write UAST document"); + let report = stdout_ok(dir.path(), &["validate", doc.to_str().unwrap()]); + assert!(report.starts_with("UAST is valid"), "{report}"); +} + +#[test] +fn output_file_keeps_every_input() { + let (dir, a) = go_fixture(); + let b = dir.path().join("n.go"); + std::fs::write(&b, GO_SOURCE).expect("write second fixture"); + let (a, b) = (a.to_str().unwrap(), b.to_str().unwrap()); + let parsed = dir.path().join("parsed.jsonl"); + stdout_ok( + dir.path(), + &[ + "parse", + "-f", + "compact", + "-o", + parsed.to_str().unwrap(), + a, + b, + ], + ); + let parsed = std::fs::read_to_string(&parsed).expect("parse output file"); + assert_eq!( + parsed.lines().count(), + 2, + "one document per input: {parsed}" + ); + let counted = dir.path().join("counts.txt"); + stdout_ok( + dir.path(), + &[ + "query", + FUNCTION_QUERY, + a, + b, + "-f", + "count", + "-o", + counted.to_str().unwrap(), + ], + ); + let counts = std::fs::read_to_string(&counted).expect("query output file"); + assert_eq!(counts, "2\n2\n", "one count per input"); +} + +/// Runs the binary with `stdin` piped in and returns stdout (zero exit). +fn stdout_with_stdin(cwd: &Path, args: &[&str], stdin: &str) -> String { + use std::io::Write as _; + use std::process::Stdio; + let mut child = Command::new(BIN) + .args(args) + .current_dir(cwd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn the uast binary"); + child + .stdin + .take() + .expect("stdin pipe") + .write_all(stdin.as_bytes()) + .expect("write stdin"); + let out = child.wait_with_output().expect("wait for uast"); + assert!( + out.status.success(), + "uast {args:?} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +#[test] +fn interactive_query_explores_the_named_file() { + let (dir, file) = go_fixture(); + let f = file.to_str().unwrap(); + let session = format!("{FUNCTION_QUERY}\nquit\n"); + let positional: &[&str] = &["query", "-t", f]; + let via_input: &[&str] = &["query", "-t", "-i", f]; + for args in [positional, via_input] { + let out = stdout_with_stdin(dir.path(), args, &session); + assert!(out.contains("Found 2 results:"), "uast {args:?}: {out}"); + } + let two = uast(dir.path(), &["query", "-t", f, f]); + assert_eq!( + two.status.code(), + Some(1), + "interactive mode takes one file" + ); +} + +#[test] +fn dash_reads_stdin() { + let (dir, file) = go_fixture(); + let f = file.to_str().unwrap(); + let from_stdin = stdout_with_stdin(dir.path(), &["parse", "-f", "compact", "-"], GO_SOURCE); + let from_file = stdout_ok(dir.path(), &["parse", "-f", "compact", f]); + assert_eq!(from_stdin, from_file, "`-` parses stdin as the source"); + let tree = stdout_ok(dir.path(), &["parse", "-f", "json", f]); + let piped = stdout_with_stdin( + dir.path(), + &["query", FUNCTION_QUERY, "-", "-f", "count"], + &tree, + ); + assert_eq!(piped, "2\n", "`-` queries the UAST JSON on stdin"); +} diff --git a/bins/uast/tests/parse_shapes.rs b/bins/uast/tests/parse_shapes.rs new file mode 100644 index 00000000..7119d067 --- /dev/null +++ b/bins/uast/tests/parse_shapes.rs @@ -0,0 +1,201 @@ +//! End-to-end coverage of the UAST shape contract through the **real +//! binary**: `uast parse --format json` over two on-disk fixtures, asserting +//! the JSON an analyzer would consume (`site/architecture/uast.md`, "Shape +//! contract"; `specs/bugs/BUG-uast-mapping-shapes.md`). + +use std::path::PathBuf; +use std::process::Command; + +use serde_json::Value; + +/// Path to the freshly built `uast` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_uast"); + +/// A C fixture: two definitions (one pointer-returning), a prototype, a +/// ternary and an else-if chain. +const C_SOURCE: &str = "int f(int a) { return a > 0 && a < 9 ? 1 : 2; }\n\ +char *g(void);\n\ +char *g(void) { if (x) { y(); } else if (z) { w(); } else { v(); } return 0; }\n"; + +/// A Go fixture: a named struct, a func-typed parameter, a three-clause for, +/// literals and operators. +const GO_SOURCE: &str = "package p\n\n\ +type Config struct {\n\tN int\n}\n\n\ +func apply(f func(int) int, n int) int {\n\ +\tr := 0\n\ +\tfor i := 0; i < n; i++ {\n\ +\t\tr += f(i) * 2\n\ +\t}\n\ +\treturn r\n\ +}\n"; + +/// Runs `uast parse --format json` on `path` and decodes the document. +fn parse_json(path: &PathBuf) -> Value { + let out = Command::new(BIN) + .args(["parse", "--format", "json"]) + .arg(path) + .env("NO_COLOR", "1") + .output() + .expect("run the uast binary"); + assert!( + out.status.success(), + "uast parse failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + serde_json::from_slice(&out.stdout).expect("stdout is one JSON document") +} + +/// Writes `source` to a unique scratch file with the given extension. +fn fixture(name: &str, source: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("uast-parse-shapes-{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("scratch dir"); + let path = dir.join(name); + std::fs::write(&path, source).expect("write fixture"); + path +} + +/// All nodes (pre-order) whose `type` equals `node_type`. +fn of_type<'a>(node: &'a Value, node_type: &str, out: &mut Vec<&'a Value>) { + if node["type"] == node_type { + out.push(node); + } + if let Some(children) = node["children"].as_array() { + for c in children { + of_type(c, node_type, out); + } + } +} + +fn find<'a>(root: &'a Value, node_type: &str) -> Vec<&'a Value> { + let mut out = Vec::new(); + of_type(root, node_type, &mut out); + out +} + +fn prop<'a>(node: &'a Value, key: &str) -> &'a str { + node["props"][key].as_str().unwrap_or("") +} + +fn has_role(node: &Value, role: &str) -> bool { + node["roles"] + .as_array() + .is_some_and(|rs| rs.iter().any(|r| r == role)) +} + +#[test] +fn c_functions_are_named_once_with_operators_and_else_chain() { + let root = parse_json(&fixture("shapes.c", C_SOURCE)); + + let fns = find(&root, "Function"); + let names: Vec<&str> = fns.iter().map(|f| prop(f, "name")).collect(); + assert_eq!( + names, + ["f", "g"], + "one Function per definition, none for the prototype" + ); + for f in &fns { + assert_eq!( + f["token"], + prop(f, "name"), + "token is the name, not the source" + ); + assert_eq!(find(f, "Function").len(), 1, "no nested Function"); + } + + let ops: Vec<&str> = find(&root, "BinaryOp") + .iter() + .map(|n| prop(n, "operator")) + .collect(); + assert_eq!(ops, ["&&", ">", "<"]); + let ternary = find(&root, "If"); + assert_eq!(ternary.len(), 3, "ternary + if + else-if"); + assert_eq!(prop(ternary[0], "operator"), "?:"); + + let outer = ternary[1]; + let link = outer["children"] + .as_array() + .expect("children") + .iter() + .find(|c| c["type"] == "If") + .expect("else-if is a direct If child"); + assert!(has_role(link, "Else")); + let else_block = link["children"] + .as_array() + .expect("children") + .iter() + .find(|c| c["type"] == "Block" && has_role(c, "Else")) + .expect("else is a direct Block child with Else"); + assert!(has_role(else_block, "Body")); + + assert!(find(&root, "Literal") + .iter() + .all(|n| !n["token"].as_str().unwrap_or("").is_empty())); +} + +#[test] +fn go_struct_name_single_loop_literal_tokens_and_operators() { + let root = parse_json(&fixture("shapes.go", GO_SOURCE)); + + let structs = find(&root, "Struct"); + assert_eq!(structs.len(), 1); + assert_eq!(prop(structs[0], "name"), "Config"); + assert_eq!(find(structs[0], "Field").len(), 1); + + let fns = find(&root, "Function"); + assert_eq!(fns.len(), 1, "func(int) int is a type, not a Function"); + assert_eq!(prop(fns[0], "name"), "apply"); + + assert_eq!( + find(&root, "Loop").len(), + 1, + "a three-clause for is one Loop" + ); + + let lits: Vec<&str> = find(&root, "Literal") + .iter() + .map(|n| n["token"].as_str().unwrap_or("")) + .collect(); + assert_eq!(lits, ["0", "0", "2"]); + + let ops: Vec<&str> = find(&root, "BinaryOp") + .iter() + .map(|n| prop(n, "operator")) + .collect(); + assert_eq!(ops, ["<", "*"]); + let assigns: Vec<&str> = find(&root, "Assignment") + .iter() + .map(|n| prop(n, "operator")) + .collect(); + assert_eq!(assigns, ["+="]); + let unary: Vec<&str> = find(&root, "UnaryOp") + .iter() + .map(|n| prop(n, "operator")) + .collect(); + assert_eq!(unary, ["++"]); +} + +/// Runs `uast validate` on `json` written to a scratch file; the embedded +/// schema must accept everything `uast parse` emits. +fn assert_validates(name: &str, json: &Value) { + let path = fixture(name, &serde_json::to_string(json).expect("re-encode")); + let out = Command::new(BIN) + .arg("validate") + .arg(&path) + .env("NO_COLOR", "1") + .output() + .expect("run the uast binary"); + assert!( + out.status.success(), + "uast validate rejected {name}: {}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); +} + +#[test] +fn parsed_shapes_pass_uast_validate() { + for (name, source) in [("valid.c", C_SOURCE), ("valid.go", GO_SOURCE)] { + let root = parse_json(&fixture(name, source)); + assert_validates(&format!("{name}.json"), &root); + } +} diff --git a/bins/uast/tests/query_dsl.rs b/bins/uast/tests/query_dsl.rs new file mode 100644 index 00000000..a92b0472 --- /dev/null +++ b/bins/uast/tests/query_dsl.rs @@ -0,0 +1,78 @@ +//! End-to-end coverage of the `uast query` DSL through the **real binary**: +//! `filter` looks at the root's direct children, `rfilter` at the whole +//! tree, `rmap` applies an expression to every node, and `.props.name` +//! reads the declaration name the mapping contract guarantees. + +use std::path::PathBuf; +use std::process::Command; + +use serde_json::Value; + +/// Path to the freshly built `uast` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_uast"); + +/// A Java class with two methods: no function-role node is a direct child +/// of the file. +const JAVA_SOURCE: &str = + "public class A {\n public int f() { return 1; }\n public int g() { return 2; }\n}\n"; + +/// Runs `uast query -f ` and returns stdout. +fn query(q: &str, file: &PathBuf, format: &str) -> String { + let out = Command::new(BIN) + .args(["query", q]) + .arg(file) + .args(["-f", format]) + .env("NO_COLOR", "1") + .output() + .expect("run the uast binary"); + assert!( + out.status.success(), + "uast query failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf-8 stdout") +} + +/// The `results` array of a JSON query output. +fn results(q: &str, file: &PathBuf) -> Vec { + let doc: Value = serde_json::from_str(&query(q, file, "json")).expect("one JSON document"); + doc["results"].as_array().expect("results").clone() +} + +/// Writes `source` to a unique scratch file. +fn fixture(name: &str, source: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("uast-query-dsl-{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("scratch dir"); + let path = dir.join(name); + std::fs::write(&path, source).expect("write fixture"); + path +} + +#[test] +fn filter_is_shallow_and_rfilter_is_recursive() { + let file = fixture("A.java", JAVA_SOURCE); + assert_eq!( + query(r#"filter(.roles has "Function")"#, &file, "count").trim(), + "0" + ); + let methods = results(r#"rfilter(.roles has "Function")"#, &file); + let names: Vec<&str> = methods + .iter() + .map(|n| n["props"]["name"].as_str().unwrap_or("")) + .collect(); + assert_eq!(names, vec!["f", "g"]); + assert!(methods.iter().all(|n| n["type"] == "Method")); +} + +/// Literal result nodes (field values) print as bare strings. +#[test] +fn rmap_and_props_name_read_every_declaration_name() { + let file = fixture("A.java", JAVA_SOURCE); + let names = results("rmap(.props.name)", &file); + assert_eq!( + names, + vec![Value::from("A"), Value::from("f"), Value::from("g")] + ); + let piped = results(r#"rfilter(.type == "Class") | map(.props.name)"#, &file); + assert_eq!(piped, vec![Value::from("A")]); +} diff --git a/crates/cf-alg-bloom/src/lib.rs b/crates/cf-alg-bloom/src/lib.rs index b51aae53..1cf3960b 100644 --- a/crates/cf-alg-bloom/src/lib.rs +++ b/crates/cf-alg-bloom/src/lib.rs @@ -9,7 +9,7 @@ //! `h(i) = h1 + i*h2 mod m`, avoiding `k` independent hash functions. //! //! Compatibility: filter behavior is pinned to the reference implementation -//! (differential gate: `tests/compat`). Concretely: +//! (the report e2e suites, `bins/codefang/tests`). Concretely: //! //! * `optimal_m` / `optimal_k` use the reference sizing formulas, including //! the `f64`-to-integer truncation, so [`Filter::bit_count`] and diff --git a/crates/cf-alg-cms/src/lib.rs b/crates/cf-alg-cms/src/lib.rs index a26d6128..b9df0b55 100644 --- a/crates/cf-alg-cms/src/lib.rs +++ b/crates/cf-alg-cms/src/lib.rs @@ -14,7 +14,7 @@ //! The column indices, per-row seeds and resulting frequency estimates are a //! frozen compatibility contract: they flow into the Halstead analyzer's //! machine-format reports, whose bytes are pinned against the reference -//! implementation by `tests/compat`. The hash machinery (`mix64`, +//! implementation by the report e2e suites (`bins/codefang/tests`). The hash machinery (`mix64`, //! `generate_seeds`, `fnv64a`) is provided by the [`cf_alg_hashutil`] crate, //! whose fixed seeds and finalizers are part of the same contract. //! diff --git a/crates/cf-alg-hashutil/src/lib.rs b/crates/cf-alg-hashutil/src/lib.rs index 87b439e7..84ee7e0d 100644 --- a/crates/cf-alg-hashutil/src/lib.rs +++ b/crates/cf-alg-hashutil/src/lib.rs @@ -7,7 +7,7 @@ //! The seeds and hash values produced here are a frozen compatibility //! contract: sketch estimates derived from them appear in machine-format //! reports whose bytes are pinned against the reference implementation by -//! `tests/compat`. Do not change the constants, seed schedule, or mixing +//! the report e2e suites (`bins/codefang/tests`). Do not change the constants, seed schedule, or mixing //! algebra. //! //! # Wrapping arithmetic diff --git a/crates/cf-alg-hll/Cargo.toml b/crates/cf-alg-hll/Cargo.toml index 49899ce6..7f9fdd3f 100644 --- a/crates/cf-alg-hll/Cargo.toml +++ b/crates/cf-alg-hll/Cargo.toml @@ -4,7 +4,7 @@ # # This crate is INTERNAL numeric behavior: per DESIGN.md §2.6 ("Sketch/hash # determinism") its estimates are a frozen contract because they appear in -# MACHINE-format reports pinned by the differential gate (tests/compat). +# MACHINE-format reports pinned by the report e2e suites (bins/codefang/tests). # The hashing kernel is the shared cf-alg-hashutil (FNV-1a -> Mix64), so the # same byte input always maps to the same register. This crate emits no report # serialization itself, so it does NOT depend on cf-gojson / cf-goyaml. Its diff --git a/crates/cf-alg-hll/src/lib.rs b/crates/cf-alg-hll/src/lib.rs index f84490e1..7b947596 100644 --- a/crates/cf-alg-hll/src/lib.rs +++ b/crates/cf-alg-hll/src/lib.rs @@ -15,7 +15,7 @@ //! LogLog-Beta polynomial and `alpha` constants are fixed, and the final //! estimate is rounded half away from zero (`f64::round`) before truncation to //! `u64`. Sketch estimates appear in machine-format reports whose bytes are -//! pinned against the reference implementation by `tests/compat`. +//! pinned by the report e2e suites (`bins/codefang/tests`). //! //! # Thread safety //! diff --git a/crates/cf-alg-lsh/src/lib.rs b/crates/cf-alg-lsh/src/lib.rs index f1e70e50..0fa4cea8 100644 --- a/crates/cf-alg-lsh/src/lib.rs +++ b/crates/cf-alg-lsh/src/lib.rs @@ -3,50 +3,61 @@ //! //! LSH groups similar MinHash signatures into the same buckets by hashing //! *bands* of consecutive hash values. This enables `O(N)` indexing and -//! sublinear query time, replacing the naive `O(N^2)` pairwise comparison. +//! sublinear candidate generation, replacing the naive `O(N^2)` pairwise +//! comparison. //! //! The index is parameterized by `num_bands` and `num_rows`, where //! `num_bands * num_rows` must equal the number of hash functions in every -//! inserted/queried signature. A higher `num_bands` lowers the similarity -//! threshold for candidate retrieval. +//! inserted/queried signature. Two signatures with Jaccard similarity `s` +//! share at least one band bucket with probability +//! `1 - (1 - s^num_rows)^num_bands` — see [`candidate_probability`]. More bands +//! (fewer rows each) move that S-curve to the left, i.e. lower the +//! similarity at which a pair becomes a candidate. +//! +//! Items are identified by dense `usize` ids assigned by the caller in +//! insertion order (strictly increasing), so the index stores only bucket +//! membership — no per-item strings and no signature copies. Querying with +//! [`Index::query_after`] yields only candidates with a larger id than the +//! item being queried, which lets a caller enumerate every unordered pair +//! exactly once without a `seen` set. //! //! Determinism is exact: each band hash is computed with FNV-1a (64-bit, as //! in [`cf_alg_hashutil::fnv64a`]) over the band index (8 bytes, big-endian, //! for domain separation) followed by that band's slice of the signature's //! big-endian serialized minimums (skipping the 4-byte header). Because both //! the signature bytes ([`cf_alg_minhash`]) and the FNV hashing are -//! bit-stable, candidate sets are reproducible across runs and platforms -//! (reference-implementation behavior, pinned by golden tests below). +//! bit-stable, candidate sets are reproducible across runs and platforms. //! -//! This crate produces **no** MACHINE-format report bytes — its outputs are -//! in-memory candidate ID lists consumed internally by clone detection — so it -//! does not route through `cf-gojson` / `cf-goyaml` and depends on no -//! serialization machinery. +//! This crate produces **no** machine-format report bytes — its outputs are +//! in-memory candidate id lists consumed by clone detection — so it does not +//! depend on any serialization machinery. //! //! # Thread safety //! -//! Mutation (`insert`) takes `&mut self` and reads (`query`) take `&self`; -//! the borrow checker proves exclusivity at compile time, so no runtime lock -//! is needed. To share an [`Index`] for concurrent reads, wrap it in an -//! [`std::sync::Arc`]; for concurrent mutation, wrap it in a `Mutex`/`RwLock`. -//! Numeric results are identical regardless of strategy. +//! Mutation (`insert`) takes `&mut self` and reads (`query*`) take `&self`; +//! each query needs its own [`Scratch`]. To share an [`Index`] for concurrent +//! reads, wrap it in an [`std::sync::Arc`] and give every thread a `Scratch`. //! //! # Examples //! //! ``` -//! use cf_alg_lsh::Index; +//! use cf_alg_lsh::{Index, Scratch}; //! use cf_alg_minhash::Signature; //! -//! let mut idx = Index::new(16, 8).unwrap(); // 16 bands * 8 rows = 128 hashes +//! let mut idx = Index::new(32, 4).unwrap(); // 32 bands * 4 rows = 128 hashes //! let mut a = Signature::new(128).unwrap(); //! let mut b = Signature::new(128).unwrap(); //! for tok in ["func", "main", "return"] { //! a.add(tok.as_bytes()); //! b.add(tok.as_bytes()); //! } -//! idx.insert("funcA".to_string(), &a).unwrap(); -//! let candidates = idx.query(&b).unwrap(); -//! assert!(candidates.iter().any(|id| id == "funcA")); +//! idx.insert(0, &a).unwrap(); +//! idx.insert(1, &b).unwrap(); +//! +//! // Every candidate of `b`, and only those strictly after item 0. +//! assert_eq!(idx.query(&b).unwrap(), vec![0, 1]); +//! let mut scratch = Scratch::default(); +//! assert_eq!(idx.query_after(&a, 0, &mut scratch).unwrap(), vec![1]); //! ``` #![forbid(unsafe_code)] @@ -67,43 +78,88 @@ const FNV64A_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; const FNV64A_PRIME: u64 = 0x0000_0100_0000_01b3; /// Errors returned by [`Index`] operations. -/// -/// The message strings are part of the CLI/log compatibility contract and -/// must not change. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum LshError { /// `num_bands` or `num_rows` was not positive. #[error("lsh: numBands and numRows must be positive")] InvalidParams, - /// A nil signature was provided. - /// - /// The type system makes "nil" unrepresentable for `&Signature`, so this - /// variant exists only to keep the error surface complete. - #[error("lsh: signature must not be nil")] - NilSignature, /// Signature size did not equal `num_bands * num_rows`. #[error("lsh: signature size must equal numBands * numRows")] SizeMismatch, + /// An id was inserted that is not larger than every id already stored. + #[error("lsh: ids must be inserted in strictly increasing order")] + IdOrder, +} + +/// Probability that two items with Jaccard similarity `similarity` share at +/// least one band bucket in a `num_bands` × `num_rows` index: +/// `1 - (1 - s^rows)^bands`. This is the recall of the candidate stage at +/// that similarity; its 50% point is roughly `(1/bands)^(1/rows)`. +/// +/// ``` +/// use cf_alg_lsh::candidate_probability; +/// +/// // 32 x 4: pairs at the 0.5 Jaccard threshold are candidates ~87% of the +/// // time, pairs at 0.64 practically always, unrelated pairs rarely. +/// assert!(candidate_probability(0.5, 32, 4) > 0.85); +/// assert!(candidate_probability(0.64, 32, 4) > 0.99); +/// assert!(candidate_probability(0.2, 32, 4) < 0.06); +/// ``` +#[must_use] +#[allow(clippy::cast_precision_loss)] // band/row counts are tiny +pub fn candidate_probability(similarity: f64, num_bands: usize, num_rows: usize) -> f64 { + let band_match = similarity.powf(num_rows as f64); + 1.0 - (1.0 - band_match).powf(num_bands as f64) +} + +/// Reusable per-query workspace for [`Index::query_after`]: a +/// generation-stamped mark per item id, so deduplicating the candidates of a +/// query costs one array write per bucket member instead of a hash-set +/// insert, and no allocation after the first query. +#[derive(Debug, Default)] +pub struct Scratch { + marks: Vec, + generation: u32, +} + +impl Scratch { + /// Starts a new query generation; returns the stamp to mark seen ids with. + fn next_generation(&mut self) -> u32 { + if self.generation == u32::MAX { + self.marks.iter_mut().for_each(|m| *m = 0); + self.generation = 0; + } + self.generation += 1; + self.generation + } + + /// Marks `id` as seen in the current generation; `true` the first time. + fn mark(&mut self, id: usize, generation: u32) -> bool { + if id >= self.marks.len() { + self.marks.resize(id + 1, 0); + } + if self.marks[id] == generation { + return false; + } + self.marks[id] = generation; + true + } } /// A deterministic LSH index for approximate nearest-neighbor retrieval over -/// MinHash signatures. +/// MinHash signatures, keyed by dense `usize` item ids. /// /// Construct one with [`Index::new`]. The index stores, per band, a map from -/// band-hash to the set of item IDs in that bucket, plus the full signatures -/// (needed by [`Index::query_threshold`]). Inserting an existing ID replaces -/// the prior entry. +/// band-hash to the (ascending) list of item ids in that bucket. pub struct Index { num_bands: usize, num_rows: usize, - /// One bucket map per band: band-hash -> set of item IDs. - /// - /// The inner map is keyed for membership; iteration order is not - /// observable to callers ([`Index::query`] returns IDs in unspecified map - /// order). - bands: Vec>>, - /// All stored signatures by ID. - sigs: HashMap, + /// One bucket map per band: band-hash -> ascending item ids. + bands: Vec>>, + /// The largest id inserted so far (ids are strictly increasing). + last_id: Option, + /// Number of inserted items. + size: usize, } impl Index { @@ -120,131 +176,105 @@ impl Index { if num_bands == 0 || num_rows == 0 { return Err(LshError::InvalidParams); } - - let mut bands = Vec::with_capacity(num_bands); - for _ in 0..num_bands { - bands.push(HashMap::new()); - } - Ok(Self { num_bands, num_rows, - bands, - sigs: HashMap::new(), + bands: (0..num_bands).map(|_| HashMap::new()).collect(), + last_id: None, + size: 0, }) } - /// Inserts a signature into the index under the given `id`. + /// Inserts a signature under item `id`. /// - /// If `id` already exists, its previous signature is removed from all band - /// buckets before the new one is inserted (an update, not a duplicate). + /// Ids must be inserted in strictly increasing order; this keeps every + /// bucket sorted, which [`Index::query_after`] relies on. /// /// # Errors /// - /// - [`LshError::SizeMismatch`] if `sig.len() != num_bands * num_rows`. - /// - /// ([`LshError::NilSignature`] is unreachable here because `sig` is a - /// non-null reference.) - pub fn insert(&mut self, id: String, sig: &Signature) -> Result<(), LshError> { - let expected_size = self.num_bands * self.num_rows; - if sig.len() != expected_size { - return Err(LshError::SizeMismatch); + /// - [`LshError::SizeMismatch`] if `sig.len() != num_bands * num_rows`; + /// - [`LshError::IdOrder`] if `id` is not larger than the last inserted id. + pub fn insert(&mut self, id: usize, sig: &Signature) -> Result<(), LshError> { + self.check_size(sig)?; + if self.last_id.is_some_and(|last| id <= last) { + return Err(LshError::IdOrder); } - let band_hashes = self.compute_band_hashes(sig); - - // Remove the old entry if the ID already exists. - if let Some(old_sig) = self.sigs.get(&id) { - let old_hashes = self.compute_band_hashes(old_sig); - self.remove(&id, &old_hashes); - } - - self.sigs.insert(id.clone(), sig.clone()); - - for (b, &h) in band_hashes.iter().enumerate() { - self.bands[b].entry(h).or_default().insert(id.clone(), true); + for (band, hash) in self.bands.iter_mut().zip(band_hashes) { + band.entry(hash).or_default().push(id); } - + self.last_id = Some(id); + self.size += 1; Ok(()) } - /// Returns deduplicated candidate IDs whose signatures share at least one - /// band hash with the query signature. - /// - /// The order of returned IDs is unspecified (it follows hash-map iteration - /// order). + /// Returns every item id whose signature shares at least one band bucket + /// with `sig`, ascending and deduplicated. /// /// # Errors /// /// [`LshError::SizeMismatch`] if `sig.len() != num_bands * num_rows`. - pub fn query(&self, sig: &Signature) -> Result, LshError> { - let expected_size = self.num_bands * self.num_rows; - if sig.len() != expected_size { - return Err(LshError::SizeMismatch); - } - - let band_hashes = self.compute_band_hashes(sig); - - // Deduplicate via a membership map. - let mut seen: HashMap = HashMap::new(); - for (b, &h) in band_hashes.iter().enumerate() { - if let Some(bucket) = self.bands[b].get(&h) { - for id in bucket.keys() { - seen.insert(id.clone(), true); - } - } - } - - Ok(seen.into_keys().collect()) + pub fn query(&self, sig: &Signature) -> Result, LshError> { + let mut scratch = Scratch::default(); + let mut ids = self.collect_candidates(sig, 0, &mut scratch)?; + ids.sort_unstable(); + Ok(ids) } - /// Returns candidate IDs whose exact MinHash similarity with the query - /// signature is at or above `threshold`. + /// Returns the deduplicated candidate ids of `sig` that are **greater + /// than** `id` — the candidates of item `id` that come after it. /// - /// This first retrieves LSH candidates via [`Index::query`], then filters - /// them by computing the exact MinHash similarity against each stored - /// signature, keeping those `>= threshold`. Candidates whose similarity - /// computation fails (e.g. size mismatch) are skipped. + /// Querying every item in id order with its own id yields each unordered + /// candidate pair exactly once (bucket sharing is symmetric, so the pair + /// `(i, j)`, `i < j`, surfaces when `i` is queried and is skipped when + /// `j` is). The order of the returned ids is deterministic but + /// unspecified. /// /// # Errors /// - /// [`LshError::SizeMismatch`] if `sig.len() != num_bands * num_rows` - /// (propagated from the inner [`Index::query`]). - pub fn query_threshold( + /// [`LshError::SizeMismatch`] if `sig.len() != num_bands * num_rows`. + pub fn query_after( &self, sig: &Signature, - threshold: f64, - ) -> Result, LshError> { - let candidates = self.query(sig)?; + id: usize, + scratch: &mut Scratch, + ) -> Result, LshError> { + self.collect_candidates(sig, id.saturating_add(1), scratch) + } - let mut result = Vec::new(); - for id in candidates { - let Some(stored) = self.sigs.get(&id) else { + /// Collects the unique ids `>= lower_bound` across all band buckets of + /// `sig`, in band/bucket order. + fn collect_candidates( + &self, + sig: &Signature, + lower_bound: usize, + scratch: &mut Scratch, + ) -> Result, LshError> { + self.check_size(sig)?; + let generation = scratch.next_generation(); + let mut out = Vec::new(); + for (band, hash) in self.bands.iter().zip(self.compute_band_hashes(sig)) { + let Some(bucket) = band.get(&hash) else { continue; }; - // Entries whose similarity computation errors are skipped. - if let Ok(sim) = sig.similarity(stored) { - if sim >= threshold { - result.push(id); + // Buckets are ascending (ids are inserted in increasing order), so + // the candidates above the bound are a suffix. + let start = bucket.partition_point(|&member| member < lower_bound); + for &member in &bucket[start..] { + if scratch.mark(member, generation) { + out.push(member); } } } - - Ok(result) + Ok(out) } - /// Removes a signature's ID from all band buckets given its precomputed - /// band hashes, dropping now-empty buckets and the stored signature. - fn remove(&mut self, id: &str, band_hashes: &[u64]) { - for (b, &h) in band_hashes.iter().enumerate() { - if let Some(bucket) = self.bands[b].get_mut(&h) { - bucket.remove(id); - if bucket.is_empty() { - self.bands[b].remove(&h); - } - } + fn check_size(&self, sig: &Signature) -> Result<(), LshError> { + if sig.len() == self.num_bands * self.num_rows { + Ok(()) + } else { + Err(LshError::SizeMismatch) } - self.sigs.remove(id); } /// Computes the FNV-1a (64-bit) hash for each band of the signature. @@ -282,25 +312,26 @@ impl Index { hashes } - /// Returns the number of signatures currently stored in the index. + /// Returns the number of items currently stored in the index. #[must_use] pub fn size(&self) -> usize { - self.sigs.len() + self.size } - /// Returns `true` if the index contains no signatures. + /// Returns `true` if the index contains no items. #[must_use] pub fn is_empty(&self) -> bool { - self.sigs.is_empty() + self.size == 0 } - /// Removes all signatures and empties every band bucket, preserving the + /// Removes every item and empties every band bucket, preserving the /// `num_bands` / `num_rows` configuration. pub fn clear(&mut self) { for band in &mut self.bands { band.clear(); } - self.sigs.clear(); + self.last_id = None; + self.size = 0; } /// Returns the configured number of bands. @@ -321,7 +352,7 @@ impl fmt::Debug for Index { f.debug_struct("Index") .field("num_bands", &self.num_bands) .field("num_rows", &self.num_rows) - .field("size", &self.sigs.len()) + .field("size", &self.size) .finish() } } @@ -332,338 +363,239 @@ mod tests { use cf_alg_hashutil::fnv64a; // Test constants. - const TEST_BANDS: usize = 16; - const TEST_ROWS: usize = 8; + const TEST_BANDS: usize = 32; + const TEST_ROWS: usize = 4; const TEST_NUM_HASHES: usize = TEST_BANDS * TEST_ROWS; const TEST_LARGE_INDEX_SIZE: usize = 1000; - const TEST_HIGH_THRESHOLD: f64 = 0.8; - const TEST_LOW_THRESHOLD: f64 = 0.0; - const TEST_CONCURRENT_THREADS: usize = 50; - const TEST_CONCURRENT_OPS_PER_THREAD: usize = 20; + const TEST_CONCURRENT_THREADS: usize = 8; + const TEST_CONCURRENT_QUERIES_PER_THREAD: usize = 20; fn new_sig(n: usize) -> Signature { Signature::new(n).expect("valid signature") } + /// A signature over `count` tokens drawn from `prefix`. + fn sig_of(prefix: &str, count: usize) -> Signature { + let mut sig = new_sig(TEST_NUM_HASHES); + for i in 0..count { + sig.add(format!("{prefix}_{i}").as_bytes()); + } + sig + } + // --- Constructor Tests --- #[test] - fn test_new_valid() { + fn new_valid() { let idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); assert_eq!(idx.num_bands(), TEST_BANDS); assert_eq!(idx.num_rows(), TEST_ROWS); assert_eq!(idx.size(), 0); + assert!(idx.is_empty()); } #[test] - fn test_new_zero_bands() { - let err = Index::new(0, TEST_ROWS).unwrap_err(); - assert_eq!(err, LshError::InvalidParams); - } - - #[test] - fn test_new_zero_rows() { - let err = Index::new(TEST_BANDS, 0).unwrap_err(); - assert_eq!(err, LshError::InvalidParams); + fn new_rejects_zero_bands_or_rows() { + assert_eq!( + Index::new(0, TEST_ROWS).unwrap_err(), + LshError::InvalidParams + ); + assert_eq!( + Index::new(TEST_BANDS, 0).unwrap_err(), + LshError::InvalidParams + ); } // --- Insert and Query Tests --- #[test] - fn test_insert_query_duplicate() { + fn identical_signatures_are_candidates() { let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig_a = new_sig(TEST_NUM_HASHES); - let mut sig_b = new_sig(TEST_NUM_HASHES); - - let tokens = [ - "func", "main", "return", "if", "else", "for", "range", "var", "int", "string", - ]; - for tok in tokens { - sig_a.add(tok.as_bytes()); - sig_b.add(tok.as_bytes()); - } - - idx.insert("funcA".to_string(), &sig_a).expect("insert ok"); - - let candidates = idx.query(&sig_b).expect("query ok"); - assert!( - candidates.iter().any(|c| c == "funcA"), - "expected funcA in {candidates:?}" - ); - } - - #[test] - fn test_insert_query_dissimilar() { - let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig_a = new_sig(TEST_NUM_HASHES); - let mut sig_b = new_sig(TEST_NUM_HASHES); - - for i in 0..TEST_LARGE_INDEX_SIZE { - sig_a.add(format!("tokenA_{i}").as_bytes()); - sig_b.add(format!("tokenB_{i}").as_bytes()); - } - - idx.insert("funcA".to_string(), &sig_a).expect("insert ok"); - - let candidates = idx.query(&sig_b).expect("query ok"); - assert!( - !candidates.iter().any(|c| c == "funcA"), - "dissimilar signatures should not collide: {candidates:?}" - ); + let sig = sig_of("tok", 10); + idx.insert(0, &sig).expect("insert ok"); + assert_eq!(idx.query(&sig).expect("query ok"), vec![0]); } #[test] - fn test_insert_query_similar_pair() { + fn dissimilar_signatures_are_not_candidates() { let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig_a = new_sig(TEST_NUM_HASHES); - let mut sig_b = new_sig(TEST_NUM_HASHES); - - let shared_count = 900; - let unique_count = 100; - for i in 0..shared_count { - let shared = format!("shared_{i}"); - sig_a.add(shared.as_bytes()); - sig_b.add(shared.as_bytes()); - } - for i in 0..unique_count { - sig_a.add(format!("uniqueA_{i}").as_bytes()); - sig_b.add(format!("uniqueB_{i}").as_bytes()); - } - - idx.insert("funcA".to_string(), &sig_a).expect("insert ok"); - - let candidates = idx.query(&sig_b).expect("query ok"); - assert!( - candidates.iter().any(|c| c == "funcA"), - "similar signatures should be candidates: {candidates:?}" - ); + idx.insert(0, &sig_of("a", TEST_LARGE_INDEX_SIZE)) + .expect("insert ok"); + let candidates = idx + .query(&sig_of("b", TEST_LARGE_INDEX_SIZE)) + .expect("query ok"); + assert!(candidates.is_empty(), "{candidates:?}"); } - // --- QueryThreshold Tests --- - #[test] - fn test_query_threshold_filters_correctly() { + fn similar_signatures_are_candidates() { let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig_similar = new_sig(TEST_NUM_HASHES); - let mut sig_different = new_sig(TEST_NUM_HASHES); - let mut sig_query = new_sig(TEST_NUM_HASHES); - - for i in 0..900 { - let shared = format!("shared_{i}"); - sig_similar.add(shared.as_bytes()); - sig_query.add(shared.as_bytes()); - } + let mut a = sig_of("shared", 900); + let mut b = sig_of("shared", 900); for i in 0..100 { - sig_similar.add(format!("simUnique_{i}").as_bytes()); - sig_query.add(format!("queryUnique_{i}").as_bytes()); - } - for i in 0..TEST_LARGE_INDEX_SIZE { - sig_different.add(format!("different_{i}").as_bytes()); + a.add(format!("uniqueA_{i}").as_bytes()); + b.add(format!("uniqueB_{i}").as_bytes()); } - - idx.insert("similar".to_string(), &sig_similar).expect("ok"); - idx.insert("different".to_string(), &sig_different) - .expect("ok"); - - let results = idx - .query_threshold(&sig_query, TEST_HIGH_THRESHOLD) - .expect("ok"); - assert!(results.iter().any(|c| c == "similar"), "{results:?}"); - assert!(!results.iter().any(|c| c == "different"), "{results:?}"); + idx.insert(0, &a).expect("insert ok"); + assert_eq!(idx.query(&b).expect("query ok"), vec![0]); } #[test] - fn test_query_threshold_zero_threshold() { + fn query_after_returns_only_larger_ids() { let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig = new_sig(TEST_NUM_HASHES); - sig.add(b"token"); - idx.insert("funcA".to_string(), &sig).expect("ok"); - - let results = idx.query_threshold(&sig, TEST_LOW_THRESHOLD).expect("ok"); - assert!(results.iter().any(|c| c == "funcA"), "{results:?}"); - } - - // --- Empty Index Tests --- - - #[test] - fn test_query_empty_index() { - let idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig = new_sig(TEST_NUM_HASHES); - sig.add(b"token"); - - let candidates = idx.query(&sig).expect("ok"); - assert!(candidates.is_empty(), "{candidates:?}"); + let sig = sig_of("tok", 10); + for id in 0..5 { + idx.insert(id, &sig).expect("insert ok"); + } + let mut scratch = Scratch::default(); + let mut after_two = idx.query_after(&sig, 2, &mut scratch).expect("ok"); + after_two.sort_unstable(); + assert_eq!(after_two, vec![3, 4]); + assert!(idx + .query_after(&sig, 4, &mut scratch) + .expect("ok") + .is_empty()); + // The scratch is reusable across queries. + let mut all = idx.query_after(&sig, 0, &mut scratch).expect("ok"); + all.sort_unstable(); + assert_eq!(all, vec![1, 2, 3, 4]); } - // --- Size Mismatch Tests --- - #[test] - fn test_insert_size_mismatch() { + fn candidates_are_deduplicated_across_bands() { + // Identical signatures collide in EVERY band; the id must still + // surface once. let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let wrong_sig = new_sig(TEST_NUM_HASHES + 1); - let err = idx.insert("funcA".to_string(), &wrong_sig).unwrap_err(); - assert_eq!(err, LshError::SizeMismatch); + let sig = sig_of("tok", 3); + idx.insert(7, &sig).expect("insert ok"); + let mut scratch = Scratch::default(); + assert_eq!(idx.query_after(&sig, 0, &mut scratch).expect("ok"), vec![7]); + assert_eq!(idx.query(&sig).expect("ok"), vec![7]); } #[test] - fn test_query_size_mismatch() { - let idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let wrong_sig = new_sig(TEST_NUM_HASHES + 1); - let err = idx.query(&wrong_sig).unwrap_err(); - assert_eq!(err, LshError::SizeMismatch); - } - - // --- Size and Clear Tests --- - - #[test] - fn test_size() { + fn insert_rejects_out_of_order_ids() { let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - assert_eq!(idx.size(), 0); - - let mut sig = new_sig(TEST_NUM_HASHES); - sig.add(b"token"); - - idx.insert("funcA".to_string(), &sig).expect("ok"); + let sig = sig_of("tok", 3); + idx.insert(5, &sig).expect("insert ok"); + assert_eq!(idx.insert(5, &sig).unwrap_err(), LshError::IdOrder); + assert_eq!(idx.insert(4, &sig).unwrap_err(), LshError::IdOrder); assert_eq!(idx.size(), 1); - - idx.insert("funcB".to_string(), &sig).expect("ok"); + idx.insert(6, &sig).expect("larger id ok"); assert_eq!(idx.size(), 2); } #[test] - fn test_clear() { - let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig = new_sig(TEST_NUM_HASHES); - sig.add(b"token"); - - idx.insert("funcA".to_string(), &sig).expect("ok"); - idx.clear(); - - assert_eq!(idx.size(), 0); - let candidates = idx.query(&sig).expect("ok"); - assert!(candidates.is_empty(), "{candidates:?}"); - } - - // --- Duplicate Insert Test --- - - #[test] - fn test_insert_duplicate_id() { + fn size_mismatch_is_rejected() { let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig = new_sig(TEST_NUM_HASHES); - sig.add(b"token"); - - idx.insert("funcA".to_string(), &sig).expect("ok"); - // Insert same ID again — should update, not duplicate. - idx.insert("funcA".to_string(), &sig).expect("ok"); - - assert_eq!(idx.size(), 1); - - let candidates = idx.query(&sig).expect("ok"); - let count = candidates.iter().filter(|&c| c == "funcA").count(); - assert_eq!(count, 1, "duplicate ID should appear once: {candidates:?}"); + let wrong = new_sig(TEST_NUM_HASHES + 1); + assert_eq!(idx.insert(0, &wrong).unwrap_err(), LshError::SizeMismatch); + assert_eq!(idx.query(&wrong).unwrap_err(), LshError::SizeMismatch); + let mut scratch = Scratch::default(); + assert_eq!( + idx.query_after(&wrong, 0, &mut scratch).unwrap_err(), + LshError::SizeMismatch + ); } - // --- NumBands / NumRows Tests --- - #[test] - fn test_num_bands() { + fn query_empty_index() { let idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - assert_eq!(idx.num_bands(), TEST_BANDS); + assert!(idx.query(&sig_of("tok", 1)).expect("ok").is_empty()); } #[test] - fn test_num_rows() { - let idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - assert_eq!(idx.num_rows(), TEST_ROWS); + fn clear_empties_the_index_and_resets_id_order() { + let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); + let sig = sig_of("tok", 1); + idx.insert(3, &sig).expect("ok"); + idx.clear(); + assert_eq!(idx.size(), 0); + assert!(idx.query(&sig).expect("ok").is_empty()); + idx.insert(0, &sig).expect("ids restart after clear"); } - // --- Large Index Test --- - #[test] - fn test_insert_query_large_index() { + fn large_index_finds_the_matching_item() { let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - for i in 0..TEST_LARGE_INDEX_SIZE { - let mut sig = new_sig(TEST_NUM_HASHES); - for j in 0..10 { - sig.add(format!("sig_{i}_tok_{j}").as_bytes()); - } - idx.insert(format!("func_{i}"), &sig).expect("ok"); + idx.insert(i, &sig_of(&format!("sig_{i}"), 10)).expect("ok"); } - assert_eq!(idx.size(), TEST_LARGE_INDEX_SIZE); + let candidates = idx.query(&sig_of("sig_0", 10)).expect("ok"); + assert!(candidates.contains(&0), "{candidates:?}"); + } - let mut query_sig = new_sig(TEST_NUM_HASHES); - for j in 0..10 { - query_sig.add(format!("sig_{}_tok_{j}", 0).as_bytes()); - } + #[test] + fn scratch_generation_wrap_is_safe() { + let mut scratch = Scratch { + marks: vec![u32::MAX - 1, 0], + generation: u32::MAX - 1, + }; + let g = scratch.next_generation(); + assert_eq!(g, u32::MAX); + assert!(scratch.mark(0, g)); + let g2 = scratch.next_generation(); + assert_eq!(g2, 1, "generation wraps to 1 after clearing the marks"); + assert!(scratch.mark(0, g2), "stale marks never count as seen"); + assert!(!scratch.mark(0, g2)); + } + + // --- S-curve --- - let candidates = idx.query(&query_sig).expect("ok"); - assert!(candidates.iter().any(|c| c == "func_0"), "{candidates:?}"); + #[test] + fn candidate_probability_matches_the_banded_formula() { + // One band, one row: p == s. + assert!((candidate_probability(0.3, 1, 1) - 0.3).abs() < 1e-12); + // 32 x 4: the published operating points of the clone engine. + assert!(candidate_probability(0.5, 32, 4) > 0.85); + assert!(candidate_probability(0.641, 32, 4) > 0.99); + assert!(candidate_probability(0.2, 32, 4) < 0.06); + // The old 16 x 8 curve missed most pairs at the 0.5 threshold. + assert!(candidate_probability(0.5, 16, 8) < 0.07); } // --- Determinism Test (band-hash bit-identity) --- - /// Two indexes built identically must return the same candidate set, - /// confirming deterministic band hashing. #[test] - fn test_deterministic_hashing() { + fn deterministic_hashing() { let mut a = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); let mut b = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); - let mut sig = new_sig(TEST_NUM_HASHES); - for tok in ["alpha", "beta", "gamma", "delta"] { - sig.add(tok.as_bytes()); - } - a.insert("x".to_string(), &sig).expect("ok"); - b.insert("x".to_string(), &sig).expect("ok"); - - let mut ca = a.query(&sig).expect("ok"); - let mut cb = b.query(&sig).expect("ok"); - ca.sort(); - cb.sort(); - assert_eq!(ca, cb, "non-deterministic candidate sets"); + let sig = sig_of("alpha", 4); + a.insert(0, &sig).expect("ok"); + b.insert(0, &sig).expect("ok"); + assert_eq!(a.query(&sig).expect("ok"), b.query(&sig).expect("ok")); } - // --- Concurrent Access Test --- + // --- Concurrent read test --- - /// Concurrent mutation requires a lock; wrap the index in a `Mutex` and - /// exercise mixed insert/query from many threads, then assert the index is - /// non-empty. #[test] - fn test_concurrent_insert_query() { - use std::sync::{Arc, Mutex}; + fn concurrent_queries_share_the_index() { + use std::sync::Arc; use std::thread; - let idx = Arc::new(Mutex::new( - Index::new(TEST_BANDS, TEST_ROWS).expect("valid"), - )); - + let mut idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); + for i in 0..TEST_CONCURRENT_THREADS { + idx.insert(i, &sig_of(&format!("t{i}"), 5)).expect("ok"); + } + let idx = Arc::new(idx); let handles: Vec<_> = (0..TEST_CONCURRENT_THREADS) - .map(|g| { + .map(|t| { let idx = Arc::clone(&idx); thread::spawn(move || { - for i in 0..TEST_CONCURRENT_OPS_PER_THREAD { - let mut sig = match Signature::new(TEST_NUM_HASHES) { - Ok(s) => s, - Err(_) => continue, - }; - sig.add(format!("thread_{g}_token_{i}").as_bytes()); - - if g % 2 == 0 { - let _ = idx.lock().unwrap().insert(format!("func_{g}_{i}"), &sig); - } else { - let _ = idx.lock().unwrap().query(&sig); - } + let mut scratch = Scratch::default(); + let sig = sig_of(&format!("t{t}"), 5); + for _ in 0..TEST_CONCURRENT_QUERIES_PER_THREAD { + assert!(idx.query(&sig).expect("ok").contains(&t)); + let after = idx.query_after(&sig, t, &mut scratch).expect("ok"); + assert!(!after.contains(&t)); } }) }) .collect(); - for h in handles { h.join().expect("worker thread panicked"); } - - assert!(idx.lock().unwrap().size() > 0, "index should not be empty"); } // --- Band-hash byte-parity cross-check --- @@ -671,7 +603,7 @@ mod tests { // Confirms the accumulating FNV-1a path matches a reference one-shot FNV // over the concatenated [band-index BE u64] ++ [band bytes]. #[test] - fn test_compute_band_hashes_matches_reference_fnv() { + fn compute_band_hashes_matches_reference_fnv() { let idx = Index::new(TEST_BANDS, TEST_ROWS).expect("valid"); let mut sig = new_sig(TEST_NUM_HASHES); for tok in ["a", "b", "c"] { @@ -684,7 +616,6 @@ mod tests { assert_eq!(computed.len(), TEST_BANDS); for (b, &got) in computed.iter().enumerate() { - // Reference: build the exact byte stream and run fnv64a once. let mut buf: Vec = Vec::new(); buf.extend_from_slice(&(b as u64).to_be_bytes()); let start = b * TEST_ROWS * BYTES_PER_UINT64; @@ -695,12 +626,12 @@ mod tests { } /// Hard golden: the band hashes for signature `["a","b","c"]` with `16` - /// bands × `8` rows must equal the exact `u64` values captured from the - /// reference implementation. This proves byte-for-byte determinism - /// end-to-end: MinHash seeds + FNV-1a base hash + mix + big-endian - /// serialization + band-index domain separation + band FNV-1a. + /// bands × `8` rows must equal the exact `u64` values captured when the + /// scheme was frozen. This proves bit-determinism end-to-end: MinHash + /// seeds + FNV-1a base hash + mix + big-endian serialization + band-index + /// domain separation + band FNV-1a. #[test] - fn test_band_hashes_reference_golden() { + fn band_hashes_reference_golden() { const REFERENCE_BAND_HASHES: [u64; 16] = [ 5_179_986_799_449_527_917, 2_684_347_889_088_141_715, @@ -728,7 +659,7 @@ mod tests { let got = idx.compute_band_hashes(&sig); assert_eq!( got, REFERENCE_BAND_HASHES, - "band hashes diverged from the reference golden" + "band hashes diverged from the golden" ); } @@ -736,12 +667,10 @@ mod tests { /// signature to different values (no two bands collide on identical zero /// rows), confirming the band index is folded into the hash. #[test] - fn test_band_index_domain_separation() { + fn band_index_domain_separation() { let idx = Index::new(4, TEST_ROWS).expect("valid"); let sig = new_sig(4 * TEST_ROWS); // all rows = u64::MAX, identical per band let hashes = idx.compute_band_hashes(&sig); - // With identical row bytes, only the band index differs; all four hashes - // must still be distinct because the index is part of the hash input. for i in 0..hashes.len() { for j in (i + 1)..hashes.len() { assert_ne!(hashes[i], hashes[j], "bands {i} and {j} collided"); diff --git a/crates/cf-alg-minhash/src/lib.rs b/crates/cf-alg-minhash/src/lib.rs index fd24733d..626dc245 100644 --- a/crates/cf-alg-minhash/src/lib.rs +++ b/crates/cf-alg-minhash/src/lib.rs @@ -110,7 +110,28 @@ impl Signature { /// each function the running minimum is lowered if the mixed value is /// smaller. An empty token is valid and still updates the minimums. pub fn add(&mut self, token: &[u8]) { - let base_hash = fnv64a(token); + self.add_hash(fnv64a(token)); + } + + /// Updates all hash function minimums with a pre-computed FNV-1a base + /// hash — the value [`cf_alg_hashutil::fnv64a`] returns for a token. + /// + /// [`Signature::add`] is exactly `add_hash(fnv64a(token))`; callers that + /// already hash every token once for their own bookkeeping (e.g. an exact + /// shingle set kept next to the signature) use this to avoid hashing + /// twice. + /// + /// ``` + /// use cf_alg_hashutil::fnv64a; + /// use cf_alg_minhash::Signature; + /// + /// let mut by_token = Signature::new(16).unwrap(); + /// let mut by_hash = Signature::new(16).unwrap(); + /// by_token.add(b"shingle"); + /// by_hash.add_hash(fnv64a(b"shingle")); + /// assert_eq!(by_token.bytes(), by_hash.bytes()); + /// ``` + pub fn add_hash(&mut self, base_hash: u64) { for (min, &seed) in self.mins.iter_mut().zip(self.seeds.iter()) { let h = mix_hash(base_hash, seed); if h < *min { diff --git a/crates/cf-analyze/Cargo.toml b/crates/cf-analyze/Cargo.toml index f89ee88c..aafcba0c 100644 --- a/crates/cf-analyze/Cargo.toml +++ b/crates/cf-analyze/Cargo.toml @@ -9,7 +9,7 @@ publish.workspace = true description = "Analyzer registry/dispatch hub + analyzer interfaces, BaseHistoryAnalyzer, and cross-format report conversion (json/yaml/ndjson/timeseries/binary). Stays BELOW the framework layer. See specs/rust-rewrite/DESIGN.md §3." [dependencies] -# Tier-0 report-format serialization crates (byte-pinned by tests/compat). +# Tier-0 report-format serialization crates (byte-pinned by the report e2e suites in bins/codefang/tests). # ALL machine-format report # bytes (json, yaml, ndjson, timeseries, timeseries+ndjson, compact, bin) flow # through these — never serde_json/serde_yaml for output (DESIGN §2). diff --git a/crates/cf-analyze/src/conversion.rs b/crates/cf-analyze/src/conversion.rs index 1b6619c9..6513fc5b 100644 --- a/crates/cf-analyze/src/conversion.rs +++ b/crates/cf-analyze/src/conversion.rs @@ -123,6 +123,21 @@ impl AnalyzerResult { m.insert("report", GoValue::Map(self.report.clone())); GoValue::Map(m) } + + /// [`Self::to_go_value`], moving the report instead of copying it. + #[must_use] + pub fn into_go_value(self) -> GoValue { + let mut m = GoMap::new(MapOrigin::Struct); + m.insert("id", GoValue::Str(self.id)); + m.insert("mode", GoValue::Str(self.mode.as_str().to_string())); + if let Some(schema) = &self.schema { + if !schema.is_empty() { + m.insert("schema", schema_to_go_value(schema)); + } + } + m.insert("report", GoValue::Map(self.report)); + GoValue::Map(m) + } } /// Converts an [`AnalyzerSchema`] into a byte-sorted @@ -181,6 +196,26 @@ impl UnifiedModel { GoValue::Map(m) } + /// [`Self::to_go_value`], moving the reports instead of copying them. + #[must_use] + pub fn into_go_value(self) -> GoValue { + let mut m = GoMap::new(MapOrigin::Struct); + m.insert("version", GoValue::Str(self.version)); + if let Some(meta) = &self.metadata { + m.insert("metadata", meta.to_go_value()); + } + m.insert( + "analyzers", + GoValue::Array( + self.analyzers + .into_iter() + .map(AnalyzerResult::into_go_value) + .collect(), + ), + ); + GoValue::Map(m) + } + /// Ensures canonical model constraints are satisfied. /// /// The version must equal [`UNIFIED_MODEL_VERSION`]; each analyzer must @@ -438,25 +473,16 @@ pub fn write_converted_output( plot_renderer: Option<&PlotRenderer>, ) -> Result<(), ConversionError> { match output_format { - FORMAT_JSON => { - let enc = Encoder::indented(" ").with_trailing_newline(true); - let bytes = enc.encode(&model.to_go_value()); - writer - .write_all(&bytes) - .map_err(|e| ConversionError::Encode(format!("encode converted json: {e}"))) - } - FORMAT_YAML => { - let bytes = cf_goyaml::marshal(&model.to_go_value()); - writer - .write_all(&bytes) - .map_err(|e| ConversionError::Encode(format!("converted yaml write: {e}"))) - } - FORMAT_BINARY => { - let bytes = cf_reportutil::binary::encode_binary_envelope(&model.to_go_value()) - .map_err(|e| ConversionError::Encode(format!("encode converted binary: {e}")))?; - writer - .write_all(&bytes) - .map_err(|e| ConversionError::Encode(format!("encode converted binary: {e}"))) + FORMAT_JSON | FORMAT_YAML | FORMAT_BINARY => { + let bytes = encode_document(&model.to_go_value(), output_format)?; + writer.write_all(&bytes).map_err(|e| { + let context = match output_format { + FORMAT_JSON => "encode converted json", + FORMAT_YAML => "converted yaml write", + _ => "encode converted binary", + }; + ConversionError::Encode(format!("{context}: {e}")) + }) } FORMAT_TIMESERIES => write_converted_time_series(model, false, writer), FORMAT_TIMESERIES_NDJSON => write_converted_time_series(model, true, writer), @@ -477,6 +503,46 @@ pub fn write_converted_output( } } +/// The bytes of a whole-document format (json, yaml, binary) for `value`. +fn encode_document(value: &GoValue, output_format: &str) -> Result, ConversionError> { + match output_format { + FORMAT_JSON => Ok(Encoder::indented(" ") + .with_trailing_newline(true) + .encode(value)), + FORMAT_YAML => Ok(cf_goyaml::marshal(value)), + FORMAT_BINARY => cf_reportutil::binary::encode_binary_envelope(value) + .map_err(|e| ConversionError::Encode(format!("encode converted binary: {e}"))), + other => Err(ConversionError::UnsupportedFormat( + FormatError::Unsupported { + format: other.to_string(), + }, + )), + } +} + +/// [`write_converted_output`] for a model the caller no longer needs, +/// returning the bytes. The whole-document formats (json, yaml, binary) +/// encode the reports in place instead of from a copy and hand back the +/// encoder's buffer, so a run holds one copy of its reports and one of its +/// output, never two of each (a 31,923-commit kubernetes `history/*` report +/// is 538 MB of json). +/// +/// # Errors +/// +/// As [`write_converted_output`]. +pub fn render_converted_output( + model: UnifiedModel, + output_format: &str, + plot_renderer: Option<&PlotRenderer>, +) -> Result, ConversionError> { + if matches!(output_format, FORMAT_JSON | FORMAT_YAML | FORMAT_BINARY) { + return encode_document(&model.into_go_value(), output_format); + } + let mut out = Vec::new(); + write_converted_output(&model, output_format, &mut out, plot_renderer)?; + Ok(out) +} + /// Writes one compact JSON line per analyzer result, with a leading /// `{version, metadata}` line when metadata is present. /// @@ -675,6 +741,47 @@ mod tests { assert!(s.ends_with('\n')); } + /// Rendering a model the caller gives up yields exactly the bytes the + /// borrowing writer produces, in every format both accept. + #[test] + fn rendering_an_owned_model_matches_the_writer() { + let model = UnifiedModel { + version: UNIFIED_MODEL_VERSION.to_string(), + metadata: Some(AnalysisMetadata::with_clock( + "/home/user/sources/kubernetes", + &FixedClock, + )), + analyzers: vec![ + AnalyzerResult { + id: "history/devs".into(), + mode: AnalyzerMode::history(), + schema: schema_for_analyzer("history/devs"), + report: report(&[("total", GoValue::Int(3))]), + }, + AnalyzerResult { + id: "static/test".into(), + mode: AnalyzerMode::static_mode(), + schema: None, + report: report(&[("files", GoValue::Array(vec![GoValue::Str("a".into())]))]), + }, + ], + }; + for format in [ + FORMAT_JSON, + FORMAT_YAML, + FORMAT_BINARY, + FORMAT_NDJSON, + FORMAT_TIMESERIES, + FORMAT_TIMESERIES_NDJSON, + ] { + let mut written = Vec::new(); + write_converted_output(&model, format, &mut written, None).expect("write"); + let rendered = render_converted_output(model.clone(), format, None).expect("render"); + assert_eq!(rendered, written, "{format}"); + } + assert!(render_converted_output(model, "html", None).is_err()); + } + #[test] fn validate_rejects_bad_version() { let model = UnifiedModel { diff --git a/crates/cf-analyze/src/metadata.rs b/crates/cf-analyze/src/metadata.rs index 26fa55fb..13fd3fce 100644 --- a/crates/cf-analyze/src/metadata.rs +++ b/crates/cf-analyze/src/metadata.rs @@ -35,7 +35,7 @@ pub struct AnalysisMetadata { } /// An injectable wall clock. The default ([`SystemClock`]) reads -/// `time.Now().UTC().Format(time.RFC3339)`; tests/golden runs substitute a fixed +/// `time.Now().UTC().Format(time.RFC3339)`; tests substitute a fixed /// clock so the `analyzed_at` envelope field is reproducible (DESIGN §2.8). pub trait Clock { /// Returns the current time as contract RFC3339 UTC @@ -58,7 +58,7 @@ impl Clock for SystemClock { impl AnalysisMetadata { /// Creates metadata for `repo_path`, stamping `analyzed_at` from the supplied /// [`Clock`]. Equivalent to [`new_analysis_metadata`] but with the time - /// source injected (used by tests/golden runs for determinism). + /// source injected (used by tests for determinism). pub fn with_clock(repo_path: &str, clock: &dyn Clock) -> Self { Self { repo_path: repo_path.to_string(), @@ -239,6 +239,14 @@ fn parse_rfc3339_utc_secs(s: &str) -> Option { #[cfg(test)] mod tests { use super::*; + + /// Report metadata names the same version `codefang version` prints: + /// the workspace crate version unless a build injects another. + #[test] + fn metadata_names_the_crate_version() { + let meta = new_analysis_metadata("/tmp/repo"); + assert_eq!(meta.codefang_version, env!("CARGO_PKG_VERSION")); + } use std::sync::Mutex; // Serialize tests that mutate the global pinned clock. diff --git a/crates/cf-analyzer-burndown/Cargo.toml b/crates/cf-analyzer-burndown/Cargo.toml index 48ef96f4..64261f8d 100644 --- a/crates/cf-analyzer-burndown/Cargo.toml +++ b/crates/cf-analyzer-burndown/Cargo.toml @@ -10,6 +10,6 @@ publish.workspace = true [dependencies] # Report serialization routes exclusively through cf-gojson (JSON/bin) and # cf-goyaml (YAML); the burndown ComputedMetrics output bytes are pinned -# against the reference binary by tests/compat. +# by the report e2e suites in bins/codefang/tests. cf-gojson = { path = "../cf-gojson" } cf-goyaml = { path = "../cf-goyaml" } diff --git a/crates/cf-analyzer-burndown/src/lib.rs b/crates/cf-analyzer-burndown/src/lib.rs index bf42924d..7bf4410d 100644 --- a/crates/cf-analyzer-burndown/src/lib.rs +++ b/crates/cf-analyzer-burndown/src/lib.rs @@ -1,18 +1,21 @@ -//! `cf-analyzer-burndown` — burndown analyzer metrics + serialization. +//! `cf-analyzer-burndown` — burndown report model, survival arithmetic and +//! serialization. //! -//! The full history walk lives in the command layer; [`metrics`] owns the -//! `ComputedMetrics` model, the sparse→dense history densification, and the -//! serialization emitted for the `json` / `yaml` / `bin` machine formats. -//! -//! Compatibility: output bytes are pinned against the reference implementation -//! by `tests/compat`. +//! The history walk (line-survival treaps per file, identity, ticks) lives in +//! the command layer; this crate turns the walk's sparse histories into the +//! report: [`metrics`] owns the [`ComputedMetrics`] model and its machine +//! encoding, [`survival`] the sparse → dense densification and every figure +//! derived from it (global, per-file and per-developer survival, the +//! developer interaction matrix). pub mod metrics; +pub mod survival; pub use metrics::{ - compute_global_metrics, group_sparse_history, AggregateData, ComputedMetrics, DenseHistory, - SparseHistory, SurvivalData, + AggregateData, ComputedMetrics, DeveloperSurvival, FileOwnership, FileSurvival, Interaction, + SurvivalData, +}; +pub use survival::{ + compute_metrics, compute_survival_sample, find_peak_lines, group_sparse_history, DenseHistory, + Grid, MetricsInput, SparseHistory, }; - -/// Crate name, used by smoke tests to confirm the module links. -pub const CRATE_NAME: &str = "cf-analyzer-burndown"; diff --git a/crates/cf-analyzer-burndown/src/metrics.rs b/crates/cf-analyzer-burndown/src/metrics.rs index e7102cd1..9098ce42 100644 --- a/crates/cf-analyzer-burndown/src/metrics.rs +++ b/crates/cf-analyzer-burndown/src/metrics.rs @@ -1,16 +1,15 @@ -//! Burndown `ComputedMetrics` model and report serialization. +//! The burndown report model and its serialization. //! -//! The per-analyzer `ComputedMetrics` struct that the history serializer -//! marshals for the `json` / `yaml` / `bin` machine formats. Field/key order -//! follows the declaration order (`aggregate`, `global_survival`, -//! `file_survival`, `developer_survival`, `interactions`), which the -//! struct-origin [`GoMap`] preserves (report-format contract; pinned by -//! `tests/compat`). +//! [`ComputedMetrics`] is the value every machine format encodes; the +//! json / bin / yaml encoders all take the same [`ComputedMetrics::to_go_value`] +//! tree, so the formats agree field for field (an empty list is `[]` +//! everywhere). Field order follows the declaration order (`aggregate`, +//! `global_survival`, `file_survival`, `developer_survival`, +//! `interactions`), which the struct-origin [`GoMap`] preserves. //! -//! Nil-slice asymmetry (report-format contract): an absent list renders as -//! `null` in JSON but as `[]` in YAML. The two builders -//! [`ComputedMetrics::to_go_value`] (JSON/bin) and -//! [`ComputedMetrics::to_go_value_yaml`] (YAML) differ only on this point. +//! The figures are computed by [`crate::survival::compute_metrics`]. + +use std::collections::BTreeMap; use cf_gojson::{GoMap, GoValue}; @@ -33,15 +32,7 @@ impl SurvivalData { m.push("sample_index", GoValue::Int(self.sample_index)); m.push("total_lines", GoValue::Int(self.total_lines)); m.push("survival_rate", GoValue::Float(self.survival_rate)); - m.push( - "band_breakdown", - GoValue::Array( - self.band_breakdown - .iter() - .map(|&v| GoValue::Int(v)) - .collect(), - ), - ); + m.push("band_breakdown", int_array(&self.band_breakdown)); GoValue::Map(m) } } @@ -91,263 +82,191 @@ impl AggregateData { } } -/// All computed burndown metric results. -/// -/// `file_survival`, `developer_survival`, and `interactions` are modeled as -/// optional vectors; `None` reproduces an absent list (`null` in JSON, `[]` in -/// YAML), `Some` an explicit (possibly empty) slice (`[]` in both). +/// Who owns the live lines of one file at the end of the window +/// (`--burndown-files` with `--burndown-people`). #[derive(Clone, Debug, Default, PartialEq)] -pub struct ComputedMetrics { - /// `aggregate`. - pub aggregate: AggregateData, - /// `global_survival`. - pub global_survival: Vec, - /// `file_survival`: `None` = absent list. - pub file_survival: Option>, - /// `developer_survival`: `None` = absent list. - pub developer_survival: Option>, - /// `interactions`: `None` = absent list. - pub interactions: Option>, +pub struct FileOwnership { + /// Live lines per person id (persons with zero lines are left out). + pub lines_by_person: BTreeMap, + /// `(id, name, percentage of the file's live lines)` of the person with + /// the most lines (the smaller id on a tie); `None` for a file with no + /// live lines. + pub top_owner: Option<(i64, String, f64)>, } -impl ComputedMetrics { - fn build(&self, nil_as_empty: bool) -> GoValue { - let nil = |opt: &Option>| -> GoValue { - match opt { - Some(v) => GoValue::Array(v.clone()), - None => { - if nil_as_empty { - GoValue::Array(Vec::new()) - } else { - GoValue::Null - } - } - } - }; - - let mut m = GoMap::new_struct(); - m.push("aggregate", self.aggregate.to_go_value()); - // Reference: `global_survival` is a `var`-declared append target — an - // empty-walk run leaves it nil (JSON `null`, YAML `[]`), never `[]`. - m.push( - "global_survival", - if self.global_survival.is_empty() { - GoValue::NilSlice - } else { - GoValue::Array( - self.global_survival - .iter() - .map(SurvivalData::to_go_value) - .collect(), - ) - }, - ); - m.push("file_survival", nil(&self.file_survival)); - m.push("developer_survival", nil(&self.developer_survival)); - m.push("interactions", nil(&self.interactions)); - GoValue::Map(m) - } - - /// Build the JSON/bin value tree (absent list → `null`). - /// - /// ``` - /// use cf_analyzer_burndown::ComputedMetrics; - /// - /// // A default report leaves `interactions` absent (`None`). - /// let m = ComputedMetrics::default(); - /// let json = String::from_utf8(cf_gojson::marshal(&m.to_go_value())).unwrap(); - /// // The JSON builder renders an absent list as `null`... - /// assert!(json.contains(r#""interactions":null"#)); - /// // ...while the YAML builder renders it as `[]` (report contract). - /// let yaml = String::from_utf8(cf_goyaml::marshal(&m.to_go_value_yaml())).unwrap(); - /// assert!(yaml.contains("interactions: []")); - /// ``` - #[must_use] - pub fn to_go_value(&self) -> GoValue { - self.build(false) - } - - /// Build the YAML value tree (absent list → `[]`). - #[must_use] - pub fn to_go_value_yaml(&self) -> GoValue { - self.build(true) - } +/// Survival of one file (`--burndown-files`). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FileSurvival { + /// The file's path at the end of the window (renames followed). + pub path: String, + /// Live lines at the last sample. + pub current_lines: i64, + /// Lines ever written to the file that lived until a sample boundary. + pub peak_lines: i64, + /// `current_lines / peak_lines`. + pub survival_rate: f64, + /// Ownership; `None` without `--burndown-people`. + pub ownership: Option, } -/// Sparse global history accumulated across the commit walk: -/// `curTick -> prevTick -> lineCountDelta`. The burndown aggregator builds this -/// by additively merging every per-commit set of global deltas. -pub type SparseHistory = std::collections::BTreeMap>; - -/// Dense burndown matrix: rows are samples, columns are cohort bands. -pub type DenseHistory = Vec>; - -/// Densify a sparse tick history into a `samples x bands` matrix. -/// -/// `sampling` and `granularity` are the band/sample sizes (both 30 by -/// default); `last_tick` is the maximum tick observed across the whole walk -/// and determines the matrix dimensions. -/// -/// ``` -/// use cf_analyzer_burndown::{group_sparse_history, SparseHistory}; -/// use std::collections::BTreeMap; -/// -/// // One commit at tick 0 adds 100 lines authored at tick 0. -/// let mut history: SparseHistory = BTreeMap::new(); -/// history.insert(0, BTreeMap::from([(0i64, 100i64)])); -/// -/// // sampling = granularity = 10, last_tick = 0 → a 1x1 matrix. -/// let dense = group_sparse_history(&history, 10, 10, 0); -/// assert_eq!(dense.len(), 1); // 1 sample row -/// assert_eq!(dense[0].len(), 1); // 1 cohort band -/// assert_eq!(dense[0][0], 100); -/// -/// // An empty history densifies to an empty matrix. -/// assert!(group_sparse_history(&SparseHistory::new(), 10, 10, 0).is_empty()); -/// ``` -#[must_use] -pub fn group_sparse_history( - history: &SparseHistory, - sampling: i64, - granularity: i64, - last_tick: i64, -) -> DenseHistory { - if history.is_empty() { - return Vec::new(); - } - - // Sorted tick keys (BTreeMap is already sorted); append last_tick when it - // exceeds the largest key so the carry-forward loop fills the tail rows. - let mut ticks: Vec = history.keys().copied().collect(); - if ticks.last().is_some_and(|&max_key| max_key < last_tick) { - ticks.push(last_tick); - } - - let samples = (last_tick / sampling + 1) as usize; - let bands = (last_tick / granularity + 1) as usize; - - let mut result: DenseHistory = vec![vec![0i64; bands]; samples]; - - // Carry forward state across empty sample rows, then add each tick's - // per-band deltas into its sample row. - let mut prevsi: usize = 0; - for &tick in &ticks { - let si = (tick / sampling) as usize; - if si > prevsi { - let state = result[prevsi].clone(); - for row in result.iter_mut().take(si + 1).skip(prevsi + 1) { - row.copy_from_slice(&state); +impl FileSurvival { + fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("path", GoValue::Str(self.path.clone())); + m.push("current_lines", GoValue::Int(self.current_lines)); + m.push("peak_lines", GoValue::Int(self.peak_lines)); + m.push("survival_rate", GoValue::Float(self.survival_rate)); + if let Some(own) = &self.ownership { + let mut lines = GoMap::new_int_map(); + for (id, n) in &own.lines_by_person { + lines.insert(id.to_string(), GoValue::Int(*n)); } - prevsi = si; - } - if let Some(row) = history.get(&tick) { - for (&t, &value) in row { - let band = (t / granularity) as usize; - if band < bands { - result[si][band] += value; - } + m.push("ownership", GoValue::Map(lines)); + if let Some((id, name, pct)) = &own.top_owner { + m.push("top_owner_id", GoValue::Int(*id)); + m.push("top_owner_name", GoValue::Str(name.clone())); + m.push("top_owner_percentage", GoValue::Float(*pct)); } } + GoValue::Map(m) } +} - result +/// Survival of the lines one developer wrote (`--burndown-people`). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct DeveloperSurvival { + /// Person id (index into the people dictionary). + pub id: i64, + /// Display name. + pub name: String, + /// The developer's lines alive at the last sample. + pub current_lines: i64, + /// Lines the developer wrote that lived until a sample boundary. + pub peak_lines: i64, + /// `current_lines / peak_lines`. + pub survival_rate: f64, } -/// Sum, over every band, of that band's maximum value across all samples — the -/// total lines ever written. -fn find_peak_lines(history: &DenseHistory) -> i64 { - if history.is_empty() { - return 0; - } - let num_bands = history[0].len(); - let mut band_peaks = vec![0i64; num_bands]; - for sample in history { - for band in 0..sample.len().min(num_bands) { - if sample[band] > band_peaks[band] { - band_peaks[band] = sample[band]; - } - } +impl DeveloperSurvival { + fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("id", GoValue::Int(self.id)); + m.push("name", GoValue::Str(self.name.clone())); + m.push("current_lines", GoValue::Int(self.current_lines)); + m.push("peak_lines", GoValue::Int(self.peak_lines)); + m.push("survival_rate", GoValue::Float(self.survival_rate)); + GoValue::Map(m) } - band_peaks.iter().sum() } -/// Sum of the positive values in a band row. -fn sum_positive(values: &[i64]) -> i64 { - values.iter().filter(|&&v| v > 0).sum() +/// One cell of the developer interaction matrix (`--burndown-people`): lines +/// written by `author` that `modifier` deleted or replaced within the window. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Interaction { + /// The author of the affected lines. + pub author_id: i64, + /// The author's display name. + pub author_name: String, + /// The developer who deleted or replaced them. + pub modifier_id: i64, + /// The modifier's display name. + pub modifier_name: String, + /// Number of lines. + pub lines_modified: i64, + /// `author_id == modifier_id`. + pub is_self_modify: bool, } -/// Survival statistics for one sample row. -fn compute_survival_sample(index: i64, sample: &[i64], peak_lines: i64) -> SurvivalData { - let mut breakdown = vec![0i64; sample.len()]; - let mut total = 0i64; - for (j, &v) in sample.iter().enumerate() { - if v > 0 { - total += v; - breakdown[j] = v; - } - } - let survival_rate = if peak_lines > 0 { - total as f64 / peak_lines as f64 - } else { - 0.0 - }; - SurvivalData { - sample_index: index, - total_lines: total, - survival_rate, - band_breakdown: breakdown, +impl Interaction { + fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("author_id", GoValue::Int(self.author_id)); + m.push("author_name", GoValue::Str(self.author_name.clone())); + m.push("modifier_id", GoValue::Int(self.modifier_id)); + m.push("modifier_name", GoValue::Str(self.modifier_name.clone())); + m.push("lines_modified", GoValue::Int(self.lines_modified)); + m.push("is_self_modify", GoValue::Bool(self.is_self_modify)); + GoValue::Map(m) } } -/// Computes all metrics for the default report shape (no per-people and no -/// per-file tracking): only the global history, `sampling`, `granularity`, and -/// the tick size feed the output, so `file_survival` / `developer_survival` -/// stay empty and `interactions` stays absent. +/// All computed burndown metric results. /// -/// `tick_size_hours` is the configured tick size in hours (24 by default); -/// `analysis_period_days = (num_samples-1) * sampling * tick_size_hours / 24`, -/// integer-truncated (report contract). -#[must_use] -pub fn compute_global_metrics( - global_dense: &DenseHistory, - sampling: i64, - tick_size_hours: i64, -) -> ComputedMetrics { - // Global survival per sample row. - let peak = find_peak_lines(global_dense); - let global_survival: Vec = global_dense - .iter() - .enumerate() - .map(|(i, sample)| compute_survival_sample(i as i64, sample, peak)) - .collect(); +/// `file_survival`, `developer_survival` and `interactions` are empty when +/// the corresponding tracking flag is off. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ComputedMetrics { + /// `aggregate`. + pub aggregate: AggregateData, + /// `global_survival`, one entry per sample. + pub global_survival: Vec, + /// `file_survival` (`--burndown-files`). + pub file_survival: Vec, + /// `developer_survival` (`--burndown-people`). + pub developer_survival: Vec, + /// `interactions` (`--burndown-people`). + pub interactions: Vec, +} - // Aggregate (tracked_files/tracked_developers are 0: no per-file / - // per-people histories in the default report). - let mut aggregate = AggregateData::default(); - if !global_dense.is_empty() { - let num_samples = global_dense.len() as i64; - aggregate.num_samples = num_samples; - aggregate.num_bands = global_dense[0].len() as i64; - let total_ticks = (num_samples - 1) * sampling; - aggregate.analysis_period_days = total_ticks * tick_size_hours / 24; - aggregate.total_peak_lines = peak; - aggregate.total_current_lines = sum_positive(&global_dense[num_samples as usize - 1]); - if aggregate.total_peak_lines > 0 { - aggregate.overall_survival_rate = - aggregate.total_current_lines as f64 / aggregate.total_peak_lines as f64; - } +impl ComputedMetrics { + /// Build the value tree every machine format (json, bin, yaml) encodes. + /// + /// ``` + /// use cf_analyzer_burndown::ComputedMetrics; + /// + /// let m = ComputedMetrics::default(); + /// let json = String::from_utf8(cf_gojson::marshal(&m.to_go_value())).unwrap(); + /// assert!(json.contains(r#""interactions":[]"#)); + /// let yaml = String::from_utf8(cf_goyaml::marshal(&m.to_go_value())).unwrap(); + /// assert!(yaml.contains("interactions: []")); + /// ``` + #[must_use] + pub fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("aggregate", self.aggregate.to_go_value()); + m.push( + "global_survival", + GoValue::Array( + self.global_survival + .iter() + .map(SurvivalData::to_go_value) + .collect(), + ), + ); + m.push( + "file_survival", + GoValue::Array( + self.file_survival + .iter() + .map(FileSurvival::to_go_value) + .collect(), + ), + ); + m.push( + "developer_survival", + GoValue::Array( + self.developer_survival + .iter() + .map(DeveloperSurvival::to_go_value) + .collect(), + ), + ); + m.push( + "interactions", + GoValue::Array( + self.interactions + .iter() + .map(Interaction::to_go_value) + .collect(), + ), + ); + GoValue::Map(m) } +} - ComputedMetrics { - aggregate, - global_survival, - // File/developer survival over empty ownership maps → explicit empty - // slices; interactions over an empty people matrix → absent list - // (report contract). - file_survival: Some(Vec::new()), - developer_survival: Some(Vec::new()), - interactions: None, - } +/// An integer list value. +fn int_array(values: &[i64]) -> GoValue { + GoValue::Array(values.iter().map(|&v| GoValue::Int(v)).collect()) } #[cfg(test)] @@ -372,29 +291,95 @@ mod tests { survival_rate: 1.0, band_breakdown: vec![n], }], - file_survival: Some(Vec::new()), - developer_survival: Some(Vec::new()), - interactions: None, + file_survival: Vec::new(), + developer_survival: Vec::new(), + interactions: Vec::new(), } } #[test] - fn json_matches_golden_shape() { + fn json_matches_the_documented_shape() { let bytes = cf_gojson::marshal(&head_metrics(112_539).to_go_value()); let s = String::from_utf8(bytes).unwrap(); assert_eq!( s, - r#"{"aggregate":{"total_current_lines":112539,"total_peak_lines":112539,"overall_survival_rate":1,"analysis_period_days":0,"num_bands":1,"num_samples":1,"tracked_files":0,"tracked_developers":0},"global_survival":[{"sample_index":0,"total_lines":112539,"survival_rate":1,"band_breakdown":[112539]}],"file_survival":[],"developer_survival":[],"interactions":null}"# + r#"{"aggregate":{"total_current_lines":112539,"total_peak_lines":112539,"overall_survival_rate":1,"analysis_period_days":0,"num_bands":1,"num_samples":1,"tracked_files":0,"tracked_developers":0},"global_survival":[{"sample_index":0,"total_lines":112539,"survival_rate":1,"band_breakdown":[112539]}],"file_survival":[],"developer_survival":[],"interactions":[]}"# ); } #[test] - fn yaml_renders_nil_slice_as_empty() { - let bytes = cf_goyaml::marshal(&head_metrics(112_539).to_go_value_yaml()); + fn yaml_and_json_agree_on_empty_lists() { + let bytes = cf_goyaml::marshal(&head_metrics(112_539).to_go_value()); let s = String::from_utf8(bytes).unwrap(); assert!(s.contains("interactions: []"), "{s}"); assert!(s.contains("file_survival: []"), "{s}"); + assert!(s.contains("developer_survival: []"), "{s}"); assert!(s.contains(" total_current_lines: 112539"), "{s}"); assert!(s.contains(" - 112539"), "{s}"); } + + #[test] + fn file_survival_emits_ownership_only_when_present() { + let plain = FileSurvival { + path: "a.go".into(), + current_lines: 3, + peak_lines: 4, + survival_rate: 0.75, + ownership: None, + }; + let s = String::from_utf8(cf_gojson::marshal(&plain.to_go_value())).unwrap(); + assert_eq!( + s, + r#"{"path":"a.go","current_lines":3,"peak_lines":4,"survival_rate":0.75}"# + ); + + let owned = FileSurvival { + ownership: Some(FileOwnership { + lines_by_person: BTreeMap::from([(0, 2), (10, 1)]), + top_owner: Some((0, "alice".into(), 66.5)), + }), + ..plain.clone() + }; + let s = String::from_utf8(cf_gojson::marshal(&owned.to_go_value())).unwrap(); + assert_eq!( + s, + r#"{"path":"a.go","current_lines":3,"peak_lines":4,"survival_rate":0.75,"ownership":{"0":2,"10":1},"top_owner_id":0,"top_owner_name":"alice","top_owner_percentage":66.5}"# + ); + + let dead = FileSurvival { + ownership: Some(FileOwnership::default()), + ..plain + }; + let s = String::from_utf8(cf_gojson::marshal(&dead.to_go_value())).unwrap(); + assert!(s.ends_with(r#""ownership":{}}"#), "{s}"); + } + + #[test] + fn developer_and_interaction_rows_keep_field_order() { + let d = DeveloperSurvival { + id: 1, + name: "bob".into(), + current_lines: 4, + peak_lines: 9, + survival_rate: 0.5, + }; + let s = String::from_utf8(cf_gojson::marshal(&d.to_go_value())).unwrap(); + assert_eq!( + s, + r#"{"id":1,"name":"bob","current_lines":4,"peak_lines":9,"survival_rate":0.5}"# + ); + let i = Interaction { + author_id: 0, + author_name: "alice".into(), + modifier_id: 0, + modifier_name: "alice".into(), + lines_modified: 7, + is_self_modify: true, + }; + let s = String::from_utf8(cf_gojson::marshal(&i.to_go_value())).unwrap(); + assert_eq!( + s, + r#"{"author_id":0,"author_name":"alice","modifier_id":0,"modifier_name":"alice","lines_modified":7,"is_self_modify":true}"# + ); + } } diff --git a/crates/cf-analyzer-burndown/src/survival.rs b/crates/cf-analyzer-burndown/src/survival.rs new file mode 100644 index 00000000..5969360b --- /dev/null +++ b/crates/cf-analyzer-burndown/src/survival.rs @@ -0,0 +1,747 @@ +//! Survival arithmetic: the sparse → dense densification and every report +//! figure derived from the dense matrices (global, per file, per developer) +//! plus the developer interaction matrix. +//! +//! All inputs are pure data accumulated by the history walk; nothing here +//! reads the repository or a clock, so a report is a pure function of the +//! walk products and the sampling grid. + +use std::collections::BTreeMap; + +use crate::metrics::{ + AggregateData, ComputedMetrics, DeveloperSurvival, FileOwnership, FileSurvival, Interaction, + SurvivalData, +}; + +/// Sparse history accumulated across the commit walk: +/// `curTick -> prevTick -> lineCountDelta` — how many lines written at +/// `prevTick` were added (positive) or deleted (negative) at `curTick`. +pub type SparseHistory = BTreeMap>; + +/// Dense burndown matrix: rows are samples, columns are age bands. +pub type DenseHistory = Vec>; + +/// Hours per day, for the period figures. +const HOURS_PER_DAY: i64 = 24; + +/// Percent scale of ownership shares. +const PERCENT: f64 = 100.0; + +/// The sampling grid every dense matrix of one report is laid out on. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Grid { + /// Ticks per sample row (`--sampling`). + pub sampling: i64, + /// Ticks per age band (`--granularity`). + pub granularity: i64, + /// The tick of the newest commit in the window; fixes the matrix size. + pub last_tick: i64, + /// `--tick-size` in hours. + pub tick_size_hours: i64, +} + +impl Grid { + /// Number of sample rows. + #[must_use] + pub const fn samples(&self) -> usize { + (self.last_tick / self.sampling + 1) as usize + } + + /// Number of age bands. + #[must_use] + pub const fn bands(&self) -> usize { + (self.last_tick / self.granularity + 1) as usize + } + + /// Length of the analysis period in days: the span from the window's + /// first tick to its last, not quantized to the sampling grid. + #[must_use] + pub const fn period_days(&self) -> i64 { + self.last_tick * self.tick_size_hours / HOURS_PER_DAY + } + + /// Densifies one sparse history on this grid. + #[must_use] + pub fn densify(&self, history: &SparseHistory) -> DenseHistory { + group_sparse_history(history, self.sampling, self.granularity, self.last_tick) + } +} + +/// Densify a sparse tick history into a `samples x bands` matrix. +/// +/// Sample `i` is the state after the last tick of the `i`-th sampling period +/// (`[i·sampling, (i+1)·sampling)`); band `j` holds the lines written in +/// ticks `[j·granularity, (j+1)·granularity)`. A sample with no recorded +/// tick repeats the previous row, so every row is a complete snapshot; a +/// `last_tick` beyond the newest recorded tick extends the carry-forward to +/// the end of the window. +/// +/// ``` +/// use cf_analyzer_burndown::{group_sparse_history, SparseHistory}; +/// use std::collections::BTreeMap; +/// +/// // One commit at tick 0 adds 100 lines authored at tick 0. +/// let mut history: SparseHistory = BTreeMap::new(); +/// history.insert(0, BTreeMap::from([(0i64, 100i64)])); +/// +/// // sampling = granularity = 10, last_tick = 0 → a 1x1 matrix. +/// let dense = group_sparse_history(&history, 10, 10, 0); +/// assert_eq!(dense, vec![vec![100]]); +/// +/// // An empty history densifies to an empty matrix. +/// assert!(group_sparse_history(&SparseHistory::new(), 10, 10, 0).is_empty()); +/// ``` +#[must_use] +pub fn group_sparse_history( + history: &SparseHistory, + sampling: i64, + granularity: i64, + last_tick: i64, +) -> DenseHistory { + if history.is_empty() { + return Vec::new(); + } + let grid = Grid { + sampling, + granularity, + last_tick, + tick_size_hours: HOURS_PER_DAY, + }; + let (samples, bands) = (grid.samples(), grid.bands()); + let mut result: DenseHistory = vec![vec![0i64; bands]; samples]; + + // Carry forward across empty sample rows, then add each tick's per-band + // deltas into its sample row; the trailing carry fills up to last_tick. + let mut prev_sample = 0usize; + let mut carry_to = |result: &mut DenseHistory, sample: usize| { + for row in prev_sample + 1..=sample.min(samples - 1) { + let state = result[row - 1].clone(); + result[row].copy_from_slice(&state); + } + prev_sample = prev_sample.max(sample.min(samples - 1)); + }; + for (&tick, row) in history { + let sample = (tick / sampling) as usize; + if sample >= samples { + break; + } + carry_to(&mut result, sample); + for (&born, &delta) in row { + let band = (born / granularity) as usize; + if band < bands { + result[sample][band] += delta; + } + } + } + carry_to(&mut result, samples - 1); + result +} + +/// Sum, over every band, of that band's maximum value across all samples — +/// the lines ever written that lived until a sample boundary. +#[must_use] +pub fn find_peak_lines(history: &DenseHistory) -> i64 { + let Some(first) = history.first() else { + return 0; + }; + let mut band_peaks = vec![0i64; first.len()]; + for sample in history { + for (peak, &v) in band_peaks.iter_mut().zip(sample) { + *peak = (*peak).max(v); + } + } + band_peaks.iter().sum() +} + +/// Sum of the positive values in a band row. +fn sum_positive(values: &[i64]) -> i64 { + values.iter().filter(|&&v| v > 0).sum() +} + +/// Survival statistics for one sample row. A negative cell (a deletion of +/// lines no earlier sample ever credited, e.g. lines written and deleted +/// within one sampling period) counts as zero. +#[must_use] +pub fn compute_survival_sample(index: i64, sample: &[i64], peak_lines: i64) -> SurvivalData { + let breakdown: Vec = sample.iter().map(|&v| v.max(0)).collect(); + let total: i64 = breakdown.iter().sum(); + SurvivalData { + sample_index: index, + total_lines: total, + survival_rate: rate(total, peak_lines), + band_breakdown: breakdown, + } +} + +/// `current / peak`, or 0 when nothing was ever written. +fn rate(current: i64, peak: i64) -> f64 { + if peak > 0 { + current as f64 / peak as f64 + } else { + 0.0 + } +} + +/// The `(current, peak)` lines of one dense history. +fn current_and_peak(dense: &DenseHistory) -> (i64, i64) { + let current = dense.last().map_or(0, |row| sum_positive(row)); + (current, find_peak_lines(dense)) +} + +/// The walk products one report is computed from. +pub struct MetricsInput<'a> { + /// The global sparse history. + pub global: &'a SparseHistory, + /// Per-file sparse histories keyed by the file's final path; `None` + /// when per-file tracking is off. + pub files: Option<&'a BTreeMap>, + /// Per-developer sparse histories keyed by person id; `None` when + /// per-developer tracking is off. + pub people: Option<&'a BTreeMap>, + /// Final line ownership per file (`path -> person -> lines`); only + /// populated when both trackings are on. + pub ownership: &'a BTreeMap>, + /// The interaction matrix (`author -> modifier -> lines of the author + /// deleted or replaced by the modifier`). + pub matrix: &'a BTreeMap>, + /// Display name per person id (index = id). + pub names: &'a [String], + /// The sampling grid. + pub grid: Grid, +} + +/// Display name of a person id, `dev_` past the end of the dictionary. +fn person_name(id: i64, names: &[String]) -> String { + usize::try_from(id) + .ok() + .and_then(|i| names.get(i)) + .cloned() + .unwrap_or_else(|| format!("dev_{id}")) +} + +/// Computes the whole burndown report from the walk products. +/// +/// ``` +/// use cf_analyzer_burndown::{compute_metrics, Grid, MetricsInput, SparseHistory}; +/// use std::collections::BTreeMap; +/// +/// let mut global: SparseHistory = BTreeMap::new(); +/// global.insert(0, BTreeMap::from([(0, 10)])); +/// global.insert(31, BTreeMap::from([(0, -3), (31, 5)])); +/// let empty = BTreeMap::new(); +/// let m = compute_metrics(&MetricsInput { +/// global: &global, +/// files: None, +/// people: None, +/// ownership: &empty, +/// matrix: &BTreeMap::new(), +/// names: &[], +/// grid: Grid { sampling: 30, granularity: 30, last_tick: 31, tick_size_hours: 24 }, +/// }); +/// assert_eq!(m.aggregate.total_current_lines, 12); +/// assert_eq!(m.aggregate.total_peak_lines, 15); +/// assert_eq!(m.aggregate.analysis_period_days, 31); +/// assert_eq!(m.global_survival[1].band_breakdown, vec![7, 5]); +/// ``` +#[must_use] +pub fn compute_metrics(input: &MetricsInput<'_>) -> ComputedMetrics { + let grid = input.grid; + let global_dense = grid.densify(input.global); + let (current, peak) = current_and_peak(&global_dense); + let global_survival: Vec = global_dense + .iter() + .enumerate() + .map(|(i, sample)| compute_survival_sample(i as i64, sample, peak)) + .collect(); + + let file_survival = input + .files + .map(|files| file_survival(files, input, grid)) + .unwrap_or_default(); + let developer_survival = input + .people + .map(|people| developer_survival(people, input.names, grid)) + .unwrap_or_default(); + let interactions = if input.people.is_some() { + interactions(input.matrix, input.names) + } else { + Vec::new() + }; + + let aggregate = AggregateData { + total_current_lines: current, + total_peak_lines: peak, + overall_survival_rate: rate(current, peak), + analysis_period_days: if global_dense.is_empty() { + 0 + } else { + grid.period_days() + }, + num_bands: global_dense.first().map_or(0, |r| r.len() as i64), + num_samples: global_dense.len() as i64, + tracked_files: file_survival.len() as i64, + tracked_developers: developer_survival.len() as i64, + }; + ComputedMetrics { + aggregate, + global_survival, + file_survival, + developer_survival, + interactions, + } +} + +/// Per-file survival, most surviving lines first (path breaks ties). +fn file_survival( + files: &BTreeMap, + input: &MetricsInput<'_>, + grid: Grid, +) -> Vec { + let mut out: Vec = files + .iter() + .map(|(path, history)| { + let (current, peak) = current_and_peak(&grid.densify(history)); + FileSurvival { + path: path.clone(), + current_lines: current, + peak_lines: peak, + survival_rate: rate(current, peak), + ownership: input.people.map(|_| { + file_ownership( + input.ownership.get(path).unwrap_or(&BTreeMap::new()), + input.names, + ) + }), + } + }) + .collect(); + out.sort_by(|a, b| { + b.current_lines + .cmp(&a.current_lines) + .then_with(|| a.path.cmp(&b.path)) + }); + out +} + +/// The ownership of one file: the live lines per person and the top owner +/// (most lines; the smaller id on a tie). +fn file_ownership(lines_by_person: &BTreeMap, names: &[String]) -> FileOwnership { + let total: i64 = lines_by_person.values().sum(); + let top = lines_by_person + .iter() + .filter(|(_, &lines)| lines > 0) + .max_by(|(ida, la), (idb, lb)| la.cmp(lb).then_with(|| idb.cmp(ida))) + .map(|(&id, &lines)| { + ( + id, + person_name(id, names), + lines as f64 / total as f64 * PERCENT, + ) + }); + FileOwnership { + lines_by_person: lines_by_person + .iter() + .filter(|(_, &lines)| lines > 0) + .map(|(&id, &lines)| (id, lines)) + .collect(), + top_owner: top, + } +} + +/// Per-developer survival for EVERY name in the dictionary (a developer +/// with no surviving history reports zeros), most surviving lines first (id +/// breaks ties). +fn developer_survival( + people: &BTreeMap, + names: &[String], + grid: Grid, +) -> Vec { + let empty = SparseHistory::new(); + let mut out: Vec = (0..names.len() as i64) + .map(|id| { + let history = people.get(&id).unwrap_or(&empty); + let (current, peak) = current_and_peak(&grid.densify(history)); + DeveloperSurvival { + id, + name: person_name(id, names), + current_lines: current, + peak_lines: peak, + survival_rate: rate(current, peak), + } + }) + .collect(); + out.sort_by(|a, b| { + b.current_lines + .cmp(&a.current_lines) + .then_with(|| a.id.cmp(&b.id)) + }); + out +} + +/// The interaction list: one entry per `(author, modifier)` pair with lines +/// of the author deleted or replaced by the modifier, most lines first +/// (author id, then modifier id, break ties). +fn interactions(matrix: &BTreeMap>, names: &[String]) -> Vec { + let mut out: Vec = matrix + .iter() + .flat_map(|(&author, row)| { + row.iter() + .filter(|(_, &lines)| lines > 0) + .map(move |(&modifier, &lines)| Interaction { + author_id: author, + author_name: person_name(author, names), + modifier_id: modifier, + modifier_name: person_name(modifier, names), + lines_modified: lines, + is_self_modify: author == modifier, + }) + }) + .collect(); + out.sort_by(|a, b| { + b.lines_modified + .cmp(&a.lines_modified) + .then_with(|| a.author_id.cmp(&b.author_id)) + .then_with(|| a.modifier_id.cmp(&b.modifier_id)) + }); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A sparse history from `(cur, prev, delta)` triples. + fn sparse(cells: &[(i64, i64, i64)]) -> SparseHistory { + let mut h = SparseHistory::new(); + for &(cur, prev, delta) in cells { + *h.entry(cur).or_default().entry(prev).or_default() += delta; + } + h + } + + const DAILY: Grid = Grid { + sampling: 30, + granularity: 30, + last_tick: 310, + tick_size_hours: 24, + }; + + fn input<'a>( + global: &'a SparseHistory, + files: Option<&'a BTreeMap>, + people: Option<&'a BTreeMap>, + ownership: &'a BTreeMap>, + matrix: &'a BTreeMap>, + names: &'a [String], + grid: Grid, + ) -> MetricsInput<'a> { + MetricsInput { + global, + files, + people, + ownership, + matrix, + names, + grid, + } + } + + fn names(list: &[&str]) -> Vec { + list.iter().map(|s| (*s).to_string()).collect() + } + + // ---- group_sparse_history -------------------------------------------- + + #[test] + fn empty_history_densifies_to_nothing() { + assert!(group_sparse_history(&SparseHistory::new(), 30, 30, 310).is_empty()); + } + + #[test] + fn carry_forward_fills_empty_samples_between_recorded_ticks() { + let h = sparse(&[(0, 0, 10), (95, 95, 4)]); + let dense = group_sparse_history(&h, 30, 30, 95); + assert_eq!( + dense, + vec![ + vec![10, 0, 0, 0], + vec![10, 0, 0, 0], + vec![10, 0, 0, 0], + vec![10, 0, 0, 4], + ] + ); + } + + #[test] + fn several_ticks_in_one_sample_accumulate_into_that_row() { + let h = sparse(&[(0, 0, 10), (5, 5, 2), (12, 0, -3), (29, 29, 1)]); + let dense = group_sparse_history(&h, 30, 30, 29); + assert_eq!(dense, vec![vec![10]]); + let fine = group_sparse_history(&h, 10, 10, 29); + assert_eq!(fine, vec![vec![12, 0, 0], vec![9, 0, 0], vec![9, 0, 1]]); + } + + #[test] + fn last_tick_beyond_the_newest_key_extends_the_carry_forward() { + let h = sparse(&[(0, 0, 10), (31, 0, -3), (31, 31, 5)]); + let dense = group_sparse_history(&h, 30, 30, 125); + assert_eq!(dense.len(), 5); + assert_eq!(dense[0], vec![10, 0, 0, 0, 0]); + for row in &dense[1..] { + assert_eq!(*row, vec![7, 5, 0, 0, 0]); + } + } + + #[test] + fn ticks_past_the_grid_are_ignored_not_panicked_on() { + let h = sparse(&[(0, 0, 10), (400, 400, 1)]); + let dense = group_sparse_history(&h, 30, 30, 59); + assert_eq!(dense, vec![vec![10, 0], vec![10, 0]]); + } + + #[test] + fn sampling_and_granularity_are_independent() { + let h = sparse(&[(0, 0, 10), (31, 31, 5), (62, 62, 1)]); + let dense = group_sparse_history(&h, 60, 20, 62); + // 2 samples (0..59, 60..119), 4 bands of 20 ticks. + assert_eq!(dense, vec![vec![10, 5, 0, 0], vec![10, 5, 0, 1]]); + } + + // ---- find_peak_lines / compute_survival_sample --------------------------- + + #[test] + fn peak_is_the_sum_of_per_band_maxima() { + let dense = vec![vec![10, 0, 0], vec![7, 5, 0], vec![0, 5, 6]]; + assert_eq!(find_peak_lines(&dense), 10 + 5 + 6); + assert_eq!(find_peak_lines(&Vec::new()), 0); + } + + #[test] + fn survival_sample_suppresses_negative_cells() { + let s = compute_survival_sample(3, &[7, -2, 5], 20); + assert_eq!(s.sample_index, 3); + assert_eq!(s.band_breakdown, vec![7, 0, 5]); + assert_eq!(s.total_lines, 12); + assert!((s.survival_rate - 0.6).abs() < 1e-12); + assert_eq!(compute_survival_sample(0, &[1], 0).survival_rate, 0.0); + } + + // ---- compute_metrics ---------------------------------------------------- + + #[test] + fn empty_walk_yields_an_empty_report() { + let empty = BTreeMap::new(); + let m = compute_metrics(&input( + &SparseHistory::new(), + None, + None, + &empty, + &BTreeMap::new(), + &[], + DAILY, + )); + assert_eq!(m.aggregate, AggregateData::default()); + assert!(m.global_survival.is_empty()); + assert!(m.file_survival.is_empty()); + assert!(m.developer_survival.is_empty()); + assert!(m.interactions.is_empty()); + } + + #[test] + fn period_days_is_the_tick_span_not_the_sampling_grid() { + let global = sparse(&[(0, 0, 10)]); + let empty = BTreeMap::new(); + for (last_tick, hours, want) in [(310, 24, 310), (10, 24, 10), (48, 1, 2), (0, 24, 0)] { + let grid = Grid { + last_tick, + tick_size_hours: hours, + ..DAILY + }; + let m = compute_metrics(&input( + &global, + None, + None, + &empty, + &BTreeMap::new(), + &[], + grid, + )); + assert_eq!(m.aggregate.analysis_period_days, want, "{grid:?}"); + assert_eq!(m.aggregate.num_samples, last_tick / 30 + 1); + } + } + + #[test] + fn global_survival_never_exceeds_one_and_bands_sum_to_totals() { + let global = sparse(&[ + (0, 0, 10), + (31, 0, -3), + (31, 31, 5), + (93, 93, 4), + (186, 0, -7), + (186, 31, -5), + (186, 186, 6), + ]); + let empty = BTreeMap::new(); + let m = compute_metrics(&input( + &global, + None, + None, + &empty, + &BTreeMap::new(), + &[], + DAILY, + )); + assert_eq!(m.aggregate.total_peak_lines, 10 + 5 + 4 + 6); + assert_eq!(m.aggregate.total_current_lines, 10); + for s in &m.global_survival { + assert!(s.survival_rate <= 1.0); + assert_eq!(s.band_breakdown.iter().sum::(), s.total_lines); + } + assert_eq!(m.aggregate.tracked_files, 0); + assert_eq!(m.aggregate.tracked_developers, 0); + } + + #[test] + fn file_survival_is_sorted_and_carries_ownership_only_with_people() { + let global = sparse(&[(0, 0, 10)]); + let mut files = BTreeMap::new(); + files.insert("b.go".to_string(), sparse(&[(0, 0, 10), (31, 0, -10)])); + files.insert( + "script.py".to_string(), + sparse(&[(93, 93, 4), (124, 124, 4)]), + ); + files.insert("a.go".to_string(), sparse(&[(0, 0, 8)])); + let mut ownership = BTreeMap::new(); + ownership.insert("script.py".to_string(), BTreeMap::from([(0, 4), (1, 4)])); + let names = names(&["alice", "bob"]); + + let m = compute_metrics(&input( + &global, + Some(&files), + None, + &ownership, + &BTreeMap::new(), + &names, + DAILY, + )); + let order: Vec<(&str, i64, i64)> = m + .file_survival + .iter() + .map(|f| (f.path.as_str(), f.current_lines, f.peak_lines)) + .collect(); + assert_eq!( + order, + vec![("a.go", 8, 8), ("script.py", 8, 8), ("b.go", 0, 10)], + "surviving lines desc, path asc on a tie" + ); + assert!(m.file_survival.iter().all(|f| f.ownership.is_none())); + assert_eq!(m.aggregate.tracked_files, 3); + + let people = BTreeMap::new(); + let m = compute_metrics(&input( + &global, + Some(&files), + Some(&people), + &ownership, + &BTreeMap::new(), + &names, + DAILY, + )); + let script = m + .file_survival + .iter() + .find(|f| f.path == "script.py") + .expect("script.py"); + let own = script.ownership.as_ref().expect("ownership with people"); + assert_eq!(own.lines_by_person, BTreeMap::from([(0, 4), (1, 4)])); + let (id, name, pct) = own.top_owner.clone().expect("top owner"); + assert_eq!((id, name.as_str()), (0, "alice"), "tie → smaller id"); + assert!((pct - 50.0).abs() < 1e-12); + let dead = m.file_survival.iter().find(|f| f.path == "b.go").unwrap(); + assert!(dead.ownership.as_ref().unwrap().top_owner.is_none()); + } + + #[test] + fn developer_survival_lists_every_name_sorted_by_surviving_lines() { + let global = sparse(&[(0, 0, 10)]); + let mut people = BTreeMap::new(); + people.insert( + 0, + sparse(&[(0, 0, 10), (31, 0, -3), (186, 0, -7), (186, 186, 6)]), + ); + people.insert(1, sparse(&[(31, 31, 5), (124, 124, 3)])); + let names = names(&["alice", "bob", "carol"]); + let empty = BTreeMap::new(); + let m = compute_metrics(&input( + &global, + None, + Some(&people), + &empty, + &BTreeMap::new(), + &names, + DAILY, + )); + let rows: Vec<(i64, &str, i64, i64)> = m + .developer_survival + .iter() + .map(|d| (d.id, d.name.as_str(), d.current_lines, d.peak_lines)) + .collect(); + assert_eq!( + rows, + vec![(1, "bob", 8, 8), (0, "alice", 6, 16), (2, "carol", 0, 0)] + ); + assert_eq!(m.aggregate.tracked_developers, 3); + assert!(m.file_survival.is_empty()); + } + + #[test] + fn interactions_are_the_positive_matrix_cells_sorted_by_lines() { + let global = sparse(&[(0, 0, 10)]); + let people = BTreeMap::new(); + let mut matrix = BTreeMap::new(); + matrix.insert(0, BTreeMap::from([(0, 7), (1, 4)])); + matrix.insert(1, BTreeMap::from([(0, 5), (1, 0)])); + let names = names(&["alice", "bob"]); + let empty = BTreeMap::new(); + let m = compute_metrics(&input( + &global, + None, + Some(&people), + &empty, + &matrix, + &names, + DAILY, + )); + let rows: Vec<(&str, &str, i64, bool)> = m + .interactions + .iter() + .map(|i| { + ( + i.author_name.as_str(), + i.modifier_name.as_str(), + i.lines_modified, + i.is_self_modify, + ) + }) + .collect(); + assert_eq!( + rows, + vec![ + ("alice", "alice", 7, true), + ("bob", "alice", 5, false), + ("alice", "bob", 4, false), + ] + ); + // Without people tracking the matrix is ignored. + let m = compute_metrics(&input(&global, None, None, &empty, &matrix, &names, DAILY)); + assert!(m.interactions.is_empty()); + } + + #[test] + fn unknown_ids_get_a_placeholder_name() { + assert_eq!(person_name(7, &names(&["a"])), "dev_7"); + assert_eq!(person_name(-1, &names(&["a"])), "dev_-1"); + assert_eq!(person_name(0, &names(&["a"])), "a"); + } +} diff --git a/crates/cf-analyzers-plumbing/Cargo.toml b/crates/cf-analyzers-plumbing/Cargo.toml index 091165ea..300645c1 100644 --- a/crates/cf-analyzers-plumbing/Cargo.toml +++ b/crates/cf-analyzers-plumbing/Cargo.toml @@ -6,30 +6,16 @@ rust-version.workspace = true license.workspace = true repository.workspace = true publish.workspace = true -description = "Analyzer-level git/UAST data providers (TreeDiff, FileDiff, BlobCache, TicksSinceStart, IdentityDetector, LanguagesDetection, UASTChanges)." +description = "Per-commit facts shared by every history analyzer walk: HistoryTicks (tick assignment) and IdentityDetector (author ids)." [dependencies] -# libgit2 bindings (the dependency mapping in DESIGN §3 mandates keeping -# libgit2). Pinned once in the workspace with the vendored-libgit2 feature. -# Used only by the optional libgit2-backed BlobSource/TreeSource adapters. -git2 = { workspace = true } - -# Ergonomic error derives for the local AnalyzerError taxonomy. thiserror is -# widely used by sibling crates (cf-uast-node, cf-pathpolicy); it is not in -# [workspace.dependencies], so it is pinned locally. -thiserror = "1" +# The AUTHOR_MISSING sentinel and its display name are owned by cf-identity; +# this crate re-exports them so every consumer sees ONE constant. +cf-identity = { path = "../cf-identity" } [dev-dependencies] -# Temp files for the IdentityDetector LoadPeopleDict test. Already present in the -# workspace lockfile (used by cf-checkpoint/cf-streaming dev-deps). +# Temp files for the IdentityDetector load_people_dict tests. tempfile = "3" [lints.rust] unsafe_code = "forbid" - -# NOTE: the byte-identity-critical line-diff engine (diff-match-patch line -# mode) and the enry-parity language classifier are expressed as the local -# `LineDiffer` / `EnryClassifier` traits and injected. The byte-faithful -# implementations are `cf-godiff` (diff engine; output pinned by the -# differential gate) and the enry-data tables that cf-pathfilter/cf-langpath -# vendor (DESIGN §2.6). diff --git a/crates/cf-analyzers-plumbing/src/analyzer.rs b/crates/cf-analyzers-plumbing/src/analyzer.rs deleted file mode 100644 index b3acd5d5..00000000 --- a/crates/cf-analyzers-plumbing/src/analyzer.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Minimal analyzer interface used by the plumbing providers. -//! -//! A local definition so the plumbing providers can be expressed against a -//! stable interface without depending on the framework crate. When the -//! framework exposes the canonical trait this module should be deleted and -//! replaced by a re-export. - -use std::any::Any; -use std::collections::HashMap; - -use crate::uast_iface::SharedParser; - -/// Type-erased value flowing between analyzers. -/// -/// The pipeline threads heterogeneous outputs (changes, caches, ticks, ...) -/// between providers; `Box` carries them through a single map type -/// without a closed enum. -pub type AnyValue = Box; - -/// Dependency / output map. -pub type ValueMap = HashMap; - -/// Configuration facts passed to [`Analyzer::configure`]. -pub type Facts = HashMap; - -/// Error returned by analyzer operations. -/// -/// Variants are kept coarse on purpose; the precise error taxonomy belongs to -/// the framework layer. -#[derive(Debug, thiserror::Error)] -pub enum AnalyzerError { - /// A required dependency was missing or had an unexpected type. - #[error("missing or mistyped dependency: {0}")] - Dependency(String), - /// Configuration failed. - #[error("configuration error: {0}")] - Config(String), - /// An underlying git operation failed. - #[error("git error: {0}")] - Git(String), - /// Any other failure, carrying a message. - #[error("{0}")] - Other(String), -} - -impl From for AnalyzerError { - fn from(e: git2::Error) -> Self { - AnalyzerError::Git(e.to_string()) - } -} - -impl From for AnalyzerError { - fn from(e: std::io::Error) -> Self { - AnalyzerError::Other(e.to_string()) - } -} - -/// A pipeline provider. -/// -/// Implementors declare what facts they `provides` and `requires`, are -/// optionally configured with facts and a UAST parser, and `consume` a map of -/// dependency values to produce a map of output values. -pub trait Analyzer { - /// Stable name of the provider, e.g. `"TreeDiff"`. - fn name(&self) -> &'static str; - - /// Keys this provider writes into its output map. - fn provides(&self) -> Vec<&'static str>; - - /// Keys this provider reads from the dependency map. - fn requires(&self) -> Vec<&'static str>; - - /// Apply free-form configuration facts. Default: no-op. - /// - /// # Errors - /// - /// Returns an [`AnalyzerError`] when a fact is invalid for this provider. - fn configure(&mut self, _facts: &Facts) -> Result<(), AnalyzerError> { - Ok(()) - } - - /// Provide the shared UAST parser. Default: no-op. - fn configure_uast(&mut self, _parser: SharedParser) {} - - /// Process one commit's worth of dependencies into outputs. - /// - /// # Errors - /// - /// Returns an [`AnalyzerError`] when a dependency is missing/mistyped or - /// the underlying computation fails. - fn consume(&mut self, deps: &mut ValueMap) -> Result; -} - -/// Borrow a typed dependency out of the [`ValueMap`]. -/// -/// # Errors -/// -/// Returns [`AnalyzerError::Dependency`] when the key is absent or holds a -/// value of the wrong type. -pub fn dep<'a, T: 'static>(deps: &'a ValueMap, key: &str) -> Result<&'a T, AnalyzerError> { - deps.get(key) - .ok_or_else(|| AnalyzerError::Dependency(format!("{key} not present")))? - .downcast_ref::() - .ok_or_else(|| AnalyzerError::Dependency(format!("{key} has unexpected type"))) -} diff --git a/crates/cf-analyzers-plumbing/src/blob_cache.rs b/crates/cf-analyzers-plumbing/src/blob_cache.rs deleted file mode 100644 index b5507f95..00000000 --- a/crates/cf-analyzers-plumbing/src/blob_cache.rs +++ /dev/null @@ -1,385 +0,0 @@ -//! `BlobCache` provider and the [`CachedBlob`] value it produces. -//! -//! The provider reads `"changes"` and `"commit"` and produces `"blob_cache"`, -//! a map from blob [`struct@Hash`] to [`CachedBlob`] holding -//! the raw bytes of every blob touched by the commit. - -use std::collections::HashMap; - -use crate::analyzer::{dep, Analyzer, AnalyzerError, ValueMap}; -use crate::git_model::{Action, Changes, Hash}; - -/// Sentinel error indicating a blob is binary. The `"binary"` message is part -/// of the error-text contract. -#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -#[error("binary")] -pub struct ErrorBinary; - -/// A blob with its raw bytes cached in memory. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct CachedBlob { - /// Raw blob contents. - pub data: Vec, -} - -impl CachedBlob { - /// Construct a cached blob from raw bytes. - #[must_use] - pub const fn new(data: Vec) -> Self { - Self { data } - } - - /// Return the blob contents as a string. - /// - /// Invalid UTF-8 is replaced lossily ([`String::from_utf8_lossy`]); - /// callers that need exact bytes should use [`CachedBlob::data`] - /// directly. - #[must_use] - pub fn str(&self) -> std::borrow::Cow<'_, str> { - String::from_utf8_lossy(&self.data) - } - - /// Count the number of lines. - /// - /// Semantics (frozen contract): - /// * empty data -> `Ok(0)`. - /// * binary data (contains a NUL byte) -> `Err(ErrorBinary)`. - /// * otherwise count `\n`-separated segments; a trailing newline does not - /// add a final empty line. - /// - /// # Errors - /// - /// Returns [`ErrorBinary`] when the data contains a NUL byte. - pub fn count_lines(&self) -> Result { - if self.data.is_empty() { - return Ok(0); - } - if is_binary(&self.data) { - return Err(ErrorBinary); - } - Ok(count_lines_bytes(&self.data)) - } -} - -/// Whether the byte slice looks binary (a NUL byte anywhere marks the content -/// as binary). -pub(crate) fn is_binary(data: &[u8]) -> bool { - data.contains(&0) -} - -/// Count `\n`-delimited segments, trimming a single trailing-newline empty -/// segment. -/// -/// For non-empty input this equals the number of `\n` bytes when the data ends -/// in `\n`, and that plus one otherwise. -pub(crate) fn count_lines_bytes(data: &[u8]) -> usize { - debug_assert!(!data.is_empty()); - let newlines = data.iter().filter(|&&b| b == b'\n').count(); - if *data.last().unwrap() == b'\n' { - newlines - } else { - newlines + 1 - } -} - -/// Abstraction over blob fetching, decoupling the provider from the concrete -/// repository handle. -/// -/// A trait so the provider can be unit-tested without a real repository and -/// so a `git2::Repository` (which is not `Send`/`Sync`) does not have to live -/// inside the provider struct. -pub trait BlobSource { - /// Read the raw bytes of the blob with the given hash. - /// - /// # Errors - /// - /// Returns an [`AnalyzerError`] when the blob cannot be read. - fn read_blob(&self, hash: Hash) -> Result, AnalyzerError>; -} - -/// A [`BlobSource`] backed by a libgit2 repository. -pub struct GitBlobSource<'r> { - repo: &'r git2::Repository, -} - -impl<'r> GitBlobSource<'r> { - /// Wrap a borrowed repository as a blob source. - #[must_use] - pub const fn new(repo: &'r git2::Repository) -> Self { - Self { repo } - } -} - -impl BlobSource for GitBlobSource<'_> { - fn read_blob(&self, hash: Hash) -> Result, AnalyzerError> { - let oid = git2::Oid::from_bytes(&hash.0).map_err(|e| AnalyzerError::Git(e.to_string()))?; - let blob = self.repo.find_blob(oid)?; - Ok(blob.content().to_vec()) - } -} - -/// `BlobCache` provider. -/// -/// Holds the [`BlobSource`] used to fetch blob contents and the previous -/// commit's cache, reused to avoid re-reading unchanged from-side blobs -/// across commits. On a failed read an empty placeholder blob is inserted -/// rather than dropping the entry (reference-implementation behavior). -pub struct BlobCache { - source: S, - previous_cache: HashMap, -} - -impl BlobCache { - /// Construct a `BlobCache` over the given blob source. - pub fn new(source: S) -> Self { - BlobCache { - source, - previous_cache: HashMap::new(), - } - } - - /// Build the cache for one commit's changes: each side that should be - /// cached is read fresh, except a modify/delete from-side that is already - /// present in the previous commit's cache, which is reused. Read failures - /// store an empty placeholder blob. The previous cache is then advanced. - /// (The reference implementation parallelizes this loop; the result is - /// order-independent, so it runs sequentially here.) - pub fn build(&mut self, changes: &Changes) -> HashMap { - let mut cache: HashMap = HashMap::new(); - let mut new_cache: HashMap = HashMap::new(); - for change in changes { - match change.action() { - Some(Action::Insert) => { - self.handle_insert(change.to.hash, &mut cache, &mut new_cache); - } - Some(Action::Delete) => { - self.handle_delete(change.from.hash, &mut cache); - } - Some(Action::Modify) => { - self.handle_insert(change.to.hash, &mut cache, &mut new_cache); - self.handle_modify_from(change.from.hash, &mut cache); - } - None => {} - } - } - self.previous_cache = new_cache; - cache - } - - /// Insert handling: read the to-side, placeholder on failure, record in - /// both the working cache and the next-commit cache. - fn handle_insert( - &self, - hash: Hash, - cache: &mut HashMap, - new_cache: &mut HashMap, - ) { - match self.source.read_blob(hash) { - Ok(data) => { - let blob = CachedBlob::new(data); - cache.insert(hash, blob.clone()); - new_cache.insert(hash, blob); - } - Err(_) => { - cache.insert(hash, CachedBlob::default()); - new_cache.insert(hash, CachedBlob::default()); - } - } - } - - /// Delete handling: reuse the previous cache if present, else read fresh - /// (placeholder on failure). - fn handle_delete(&self, hash: Hash, cache: &mut HashMap) { - if let Some(existing) = self.previous_cache.get(&hash) { - cache.insert(hash, existing.clone()); - return; - } - match self.source.read_blob(hash) { - Ok(data) => cache.insert(hash, CachedBlob::new(data)), - Err(_) => cache.insert(hash, CachedBlob::default()), - }; - } - - /// Modify handling for the from-side: reuse the previous cache if - /// present, else read fresh (placeholder on failure). - fn handle_modify_from(&self, hash: Hash, cache: &mut HashMap) { - self.handle_delete(hash, cache); - } -} - -impl Analyzer for BlobCache { - fn name(&self) -> &'static str { - "BlobCache" - } - - fn provides(&self) -> Vec<&'static str> { - vec!["blob_cache"] - } - - fn requires(&self) -> Vec<&'static str> { - vec!["changes"] - } - - fn consume(&mut self, deps: &mut ValueMap) -> Result { - let changes = dep::(deps, "changes")?.clone(); - let cache = self.build(&changes); - let mut out = ValueMap::new(); - out.insert("blob_cache".to_string(), Box::new(cache)); - Ok(out) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::git_model::Change; - - // Mirrors reference test TestCachedBlob_CountLines. - #[test] - fn count_lines_table() { - let cases: &[(&str, Vec, usize, bool)] = &[ - ("empty", vec![], 0, false), - ("single line no newline", b"hello".to_vec(), 1, false), - ("single line with newline", b"hello\n".to_vec(), 1, false), - ("two lines", b"hello\nworld".to_vec(), 2, false), - ( - "two lines trailing newline", - b"hello\nworld\n".to_vec(), - 2, - false, - ), - ("binary", vec![0x00, 0x01, 0x02], 0, true), - ]; - for (name, data, want, want_err) in cases { - let b = CachedBlob::new(data.clone()); - match b.count_lines() { - Ok(got) => { - assert!(!*want_err, "{name}: expected error, got Ok({got})"); - assert_eq!(got, *want, "{name}: line count mismatch"); - } - Err(_) => { - assert!(*want_err, "{name}: unexpected error"); - } - } - } - } - - // Mirrors reference test TestCachedBlob_Str. - #[test] - fn str_round_trips() { - let b = CachedBlob::new(b"hello world".to_vec()); - assert_eq!(b.str(), "hello world"); - } - - struct MapSource(HashMap>); - impl BlobSource for MapSource { - fn read_blob(&self, hash: Hash) -> Result, AnalyzerError> { - self.0 - .get(&hash) - .cloned() - .ok_or_else(|| AnalyzerError::Other("missing blob".into())) - } - } - - fn h(n: u8) -> Hash { - let mut b = [0u8; 20]; - b[0] = n; - Hash(b) - } - - #[test] - fn build_caches_inserts_modifies_and_deletes() { - let mut blobs = HashMap::new(); - blobs.insert(h(1), b"added\n".to_vec()); - blobs.insert(h(2), b"old\n".to_vec()); - blobs.insert(h(3), b"new\n".to_vec()); - blobs.insert(h(4), b"gone\n".to_vec()); - let mut cache = BlobCache::new(MapSource(blobs)); - - let changes: Changes = vec![ - // insert: from empty, to h(1) - Change { - from: crate::git_model::ChangeEntry::default(), - to: crate::git_model::ChangeEntry { - name: "a".into(), - hash: h(1), - }, - }, - // modify: from h(2) to h(3) - Change { - from: crate::git_model::ChangeEntry { - name: "b".into(), - hash: h(2), - }, - to: crate::git_model::ChangeEntry { - name: "b".into(), - hash: h(3), - }, - }, - // delete: from h(4), to empty - Change { - from: crate::git_model::ChangeEntry { - name: "c".into(), - hash: h(4), - }, - to: crate::git_model::ChangeEntry::default(), - }, - ]; - - let result = cache.build(&changes); - assert_eq!(result.get(&h(1)).unwrap().data, b"added\n"); - assert_eq!(result.get(&h(3)).unwrap().data, b"new\n"); // to-hash for modify - assert_eq!(result.get(&h(4)).unwrap().data, b"gone\n"); // from-hash for delete - // The from-side of a modify is ALSO cached (read fresh when not in - // the previous cache); h(2) is therefore present. - assert_eq!(result.get(&h(2)).unwrap().data, b"old\n"); - } - - #[test] - fn modify_from_side_reuses_previous_cache() { - let mut blobs = HashMap::new(); - blobs.insert(h(2), b"old\n".to_vec()); - blobs.insert(h(3), b"new\n".to_vec()); - let mut cache = BlobCache::new(MapSource(blobs)); - // Seed the previous cache as if h(2) was produced by an earlier commit. - cache - .previous_cache - .insert(h(2), CachedBlob::new(b"reused\n".to_vec())); - let changes: Changes = vec![Change { - from: crate::git_model::ChangeEntry { - name: "b".into(), - hash: h(2), - }, - to: crate::git_model::ChangeEntry { - name: "b".into(), - hash: h(3), - }, - }]; - let result = cache.build(&changes); - // from-side reused from previous cache, not re-read. - assert_eq!(result.get(&h(2)).unwrap().data, b"reused\n"); - } - - #[test] - fn read_failure_yields_empty_placeholder() { - // Empty source: every read fails. - let mut cache = BlobCache::new(MapSource(HashMap::new())); - let changes: Changes = vec![Change { - from: Default::default(), - to: crate::git_model::ChangeEntry { - name: "a".into(), - hash: h(1), - }, - }]; - let result = cache.build(&changes); - assert_eq!(result.get(&h(1)).unwrap().data, b""); - } - - #[test] - fn provider_metadata() { - let cache = BlobCache::new(MapSource(HashMap::new())); - assert_eq!(cache.name(), "BlobCache"); - assert_eq!(cache.provides(), vec!["blob_cache"]); - assert_eq!(cache.requires(), vec!["changes"]); - } -} diff --git a/crates/cf-analyzers-plumbing/src/clock.rs b/crates/cf-analyzers-plumbing/src/clock.rs deleted file mode 100644 index 0b6ed2e7..00000000 --- a/crates/cf-analyzers-plumbing/src/clock.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Injectable wall clock for the tick anomaly window. -//! -//! The tick sanitizer computes its upper bound as now + max clock skew. Per -//! DESIGN §2.8 this wall-clock read is a byte-identity hazard and must be -//! made deterministic through an injectable clock (the `CODEFANG_NOW` -//! pattern). This module provides that seam. - -/// A source of "now" as a unix timestamp in seconds — the injectable clock -/// the design mandates for wall-clock reads (DESIGN §2.8). -pub trait Clock: Send + Sync { - /// Current wall-clock time as whole seconds since the unix epoch (UTC). - fn now_unix(&self) -> i64; -} - -/// The real system clock. -#[derive(Debug, Clone, Copy, Default)] -pub struct SystemClock; - -impl Clock for SystemClock { - fn now_unix(&self) -> i64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .unwrap_or(0) - } -} - -/// A fixed clock for deterministic tests and reproducible goldens (the -/// `CODEFANG_NOW`/`SOURCE_DATE_EPOCH` seam). -#[derive(Debug, Clone, Copy)] -pub struct FixedClock(pub i64); - -impl Clock for FixedClock { - fn now_unix(&self) -> i64 { - self.0 - } -} diff --git a/crates/cf-analyzers-plumbing/src/file_diff.rs b/crates/cf-analyzers-plumbing/src/file_diff.rs deleted file mode 100644 index 2b26f3b7..00000000 --- a/crates/cf-analyzers-plumbing/src/file_diff.rs +++ /dev/null @@ -1,401 +0,0 @@ -//! `FileDiff` provider. -//! -//! Produces, per **modified** file, the old/new line-of-code counts and a -//! line-granular diff. Only `Modify` changes are diffed (inserts and deletes -//! are not emitted here); two fast paths and a binary guard run first, and the -//! diff itself uses the diff-match-patch line algorithm. -//! -//! # Diff engine boundary -//! -//! The line diff itself is the byte-identity-critical step. The reference -//! pipeline is: encode both (whitespace-stripped) sides to lines-as-runes, -//! run the main diff, and — unless cleanup is disabled — apply the -//! semantic-lossless and merge cleanup passes. The engine is expressed as the -//! [`LineDiffer`] trait and injected; `cf-godiff` is the byte-faithful -//! implementation (its output is pinned by the differential gate). - -use std::collections::HashMap; - -use crate::analyzer::{dep, Analyzer, AnalyzerError, ValueMap}; -use crate::blob_cache::{is_binary, CachedBlob}; -use crate::git_model::{Action, Change, Changes, Hash}; - -/// Default diff timeout in milliseconds. -pub const DEFAULT_DIFF_TIMEOUT_MS: i64 = 1000; - -/// Diff operation kind. -/// -/// The discriminants (`Delete = -1`, `Equal = 0`, `Insert = 1`) are frozen so -/// a serializer can emit the same integer codes as the diff engine. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DiffOp { - /// Text removed. - Delete = -1, - /// Text unchanged. - Equal = 0, - /// Text added. - Insert = 1, -} - -/// One element of a diff. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Diff { - /// Diff operation. - pub op: DiffOp, - /// The text covered by this diff segment. - pub text: String, -} - -/// Parameters passed to a [`LineDiffer`]. -#[derive(Debug, Clone, Copy)] -pub struct DiffParams { - /// Diff timeout in milliseconds (`0` = no limit). - pub timeout_ms: i64, - /// Whether to skip the semantic-lossless + merge cleanup pass. - pub cleanup_disabled: bool, -} - -/// Outcome of a line diff: the diff segments plus the encoded-line counts -/// reported as the old/new lines of code. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LineDiffResult { - /// Number of encoded lines on the old side. - pub old_lines_of_code: usize, - /// Number of encoded lines on the new side. - pub new_lines_of_code: usize, - /// Diff segments (after the optional cleanup pass). - pub diffs: Vec, -} - -/// The line-diff engine boundary. -/// -/// Implementations must reproduce the reference diff-match-patch line -/// pipeline exactly for the machine output to stay byte-identical -/// (`cf-godiff` does); see the module note. -pub trait LineDiffer { - /// Diff two already-whitespace-normalized strings at line granularity. - fn line_diff(&self, old: &str, new: &str, params: DiffParams) -> LineDiffResult; -} - -/// Per-file diff result. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FileDiffData { - /// Lines of code before the change. - pub old_lines_of_code: usize, - /// Lines of code after the change. - pub new_lines_of_code: usize, - /// The diff segments. - pub diffs: Vec, -} - -/// `FileDiff` provider. -pub struct FileDiff { - /// When true, skip the cleanup pass. - pub cleanup_disabled: bool, - /// Ignore whitespace when diffing: spaces are stripped from both sides - /// before the line diff. - pub whitespace_ignore: bool, - /// Diff timeout in milliseconds; `0` means no limit. - pub timeout_ms: i64, - differ: D, -} - -impl FileDiff { - /// Construct a `FileDiff` over the given diff engine with the standard - /// defaults. - pub const fn new(differ: D) -> Self { - Self { - cleanup_disabled: false, - whitespace_ignore: false, - timeout_ms: DEFAULT_DIFF_TIMEOUT_MS, - differ, - } - } - - /// Build the per-file diff map for one commit's changes (only - /// modifications are processed). - pub fn build( - &self, - changes: &Changes, - cache: &HashMap, - ) -> HashMap { - let mut result: HashMap = HashMap::new(); - for change in changes { - self.process_change(change, cache, &mut result); - } - result - } - - /// Process one change. - fn process_change( - &self, - change: &Change, - cache: &HashMap, - result: &mut HashMap, - ) { - // Only Modify changes are diffed. - if change.action() != Some(Action::Modify) { - return; - } - - let (Some(blob_from), Some(blob_to)) = - (cache.get(&change.from.hash), cache.get(&change.to.hash)) - else { - return; - }; - - // Fast path: identical content by hash. - if change.from.hash == change.to.hash { - return; - } - - // Skip binary blobs (either side binary skips the pair). - if is_binary(&blob_from.data) || is_binary(&blob_to.data) { - return; - } - - // Decode lossily; the diff engine operates on decoded text. - let str_from = String::from_utf8_lossy(&blob_from.data).into_owned(); - let str_to = String::from_utf8_lossy(&blob_to.data).into_owned(); - - // Fast path: identical strings -> single DiffEqual with "L"*lineCount. - if str_from == str_to { - let line_count = count_trailing_aware_lines(&str_from); - result.insert( - change.to.name.clone(), - FileDiffData { - old_lines_of_code: line_count, - new_lines_of_code: line_count, - diffs: vec![Diff { - op: DiffOp::Equal, - text: "L".repeat(line_count), - }], - }, - ); - return; - } - - let data = self.compute_modify(&str_from, &str_to); - result.insert(change.to.name.clone(), data); - } - - /// Compute the diff for a modification: whitespace strip, line diff, - /// optional cleanup, and the encoded-line LOC counts. - pub fn compute_modify(&self, str_from: &str, str_to: &str) -> FileDiffData { - let from = strip_whitespace(str_from, self.whitespace_ignore); - let to = strip_whitespace(str_to, self.whitespace_ignore); - let res = self.differ.line_diff( - &from, - &to, - DiffParams { - timeout_ms: self.timeout_ms, - cleanup_disabled: self.cleanup_disabled, - }, - ); - FileDiffData { - old_lines_of_code: res.old_lines_of_code, - new_lines_of_code: res.new_lines_of_code, - diffs: res.diffs, - } - } -} - -impl Analyzer for FileDiff { - fn name(&self) -> &'static str { - "FileDiff" - } - - fn provides(&self) -> Vec<&'static str> { - vec!["file_diff"] - } - - fn requires(&self) -> Vec<&'static str> { - vec!["changes", "blob_cache"] - } - - fn consume(&mut self, deps: &mut ValueMap) -> Result { - let changes = dep::(deps, "changes")?; - let cache = dep::>(deps, "blob_cache")?; - let result = self.build(changes, cache); - let mut out = ValueMap::new(); - out.insert("file_diff".to_string(), Box::new(result)); - Ok(out) - } -} - -/// Strip spaces (only U+0020) if whitespace is ignored. -fn strip_whitespace(s: &str, ignore: bool) -> String { - if ignore { - s.replace(' ', "") - } else { - s.to_string() - } -} - -/// Line counting for the identical-string fast path: the newline count, plus -/// one when the string is non-empty and does not end in a newline. -fn count_trailing_aware_lines(s: &str) -> usize { - let mut count = s.bytes().filter(|&b| b == b'\n').count(); - if !s.is_empty() && s.as_bytes().last() != Some(&b'\n') { - count += 1; - } - count -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::git_model::ChangeEntry; - - /// A trivial line differ for exercising the provider control flow. It is - /// NOT byte-faithful; the real engine is `cf-godiff` (see module note). - struct LineCountDiffer; - impl LineDiffer for LineCountDiffer { - fn line_diff(&self, old: &str, new: &str, _p: DiffParams) -> LineDiffResult { - let old_loc = line_count(old); - let new_loc = line_count(new); - LineDiffResult { - old_lines_of_code: old_loc, - new_lines_of_code: new_loc, - diffs: vec![ - Diff { - op: DiffOp::Delete, - text: old.to_string(), - }, - Diff { - op: DiffOp::Insert, - text: new.to_string(), - }, - ], - } - } - } - - fn line_count(s: &str) -> usize { - if s.is_empty() { - 0 - } else { - s.split('\n').count() - } - } - - fn h(n: u8) -> Hash { - let mut b = [0u8; 20]; - b[0] = n; - Hash(b) - } - - fn modify(from_h: Hash, to_h: Hash, name: &str) -> Change { - Change { - from: ChangeEntry { - name: name.into(), - hash: from_h, - }, - to: ChangeEntry { - name: name.into(), - hash: to_h, - }, - } - } - - #[test] - fn count_trailing_aware_lines_table() { - assert_eq!(count_trailing_aware_lines(""), 0); - assert_eq!(count_trailing_aware_lines("a"), 1); - assert_eq!(count_trailing_aware_lines("a\n"), 1); - assert_eq!(count_trailing_aware_lines("a\nb"), 2); - assert_eq!(count_trailing_aware_lines("a\nb\n"), 2); - } - - #[test] - fn inserts_and_deletes_are_not_diffed() { - let f = FileDiff::new(LineCountDiffer); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"x\n".to_vec())); - let changes = vec![ - Change { - from: ChangeEntry::default(), - to: ChangeEntry { - name: "a".into(), - hash: h(1), - }, - }, - Change { - from: ChangeEntry { - name: "b".into(), - hash: h(1), - }, - to: ChangeEntry::default(), - }, - ]; - assert!(f.build(&changes, &cache).is_empty()); - } - - #[test] - fn same_hash_modify_is_skipped() { - let f = FileDiff::new(LineCountDiffer); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"x\n".to_vec())); - let changes = vec![modify(h(1), h(1), "a")]; - assert!(f.build(&changes, &cache).is_empty()); - } - - #[test] - fn identical_strings_fast_path() { - let f = FileDiff::new(LineCountDiffer); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"a\nb\n".to_vec())); - cache.insert(h(2), CachedBlob::new(b"a\nb\n".to_vec())); - let changes = vec![modify(h(1), h(2), "a")]; - let out = f.build(&changes, &cache); - let d = out.get("a").unwrap(); - assert_eq!(d.old_lines_of_code, 2); - assert_eq!(d.new_lines_of_code, 2); - assert_eq!(d.diffs.len(), 1); - assert_eq!(d.diffs[0].op, DiffOp::Equal); - assert_eq!(d.diffs[0].text, "LL"); - } - - #[test] - fn binary_blobs_are_skipped() { - let f = FileDiff::new(LineCountDiffer); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(vec![0u8, 1, 2])); - cache.insert(h(2), CachedBlob::new(b"text\n".to_vec())); - let changes = vec![modify(h(1), h(2), "a")]; - assert!(f.build(&changes, &cache).is_empty()); - } - - #[test] - fn real_modify_invokes_differ() { - let f = FileDiff::new(LineCountDiffer); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"a\nb\nc".to_vec())); - cache.insert(h(2), CachedBlob::new(b"a\nB\nc".to_vec())); - let changes = vec![modify(h(1), h(2), "a")]; - let out = f.build(&changes, &cache); - let d = out.get("a").unwrap(); - assert_eq!(d.old_lines_of_code, 3); - assert_eq!(d.new_lines_of_code, 3); - assert!(!d.diffs.is_empty()); - } - - #[test] - fn whitespace_ignore_strips_spaces() { - let mut f = FileDiff::new(LineCountDiffer); - f.whitespace_ignore = true; - // After stripping spaces both sides are "a\nb", so the differ sees - // equal line counts. - let r = f.compute_modify("a \n b", "a\nb"); - assert_eq!(r.old_lines_of_code, 2); - assert_eq!(r.new_lines_of_code, 2); - } - - #[test] - fn provider_metadata() { - let f = FileDiff::new(LineCountDiffer); - assert_eq!(f.name(), "FileDiff"); - assert_eq!(f.requires(), vec!["changes", "blob_cache"]); - assert_eq!(f.provides(), vec!["file_diff"]); - } -} diff --git a/crates/cf-analyzers-plumbing/src/git_model.rs b/crates/cf-analyzers-plumbing/src/git_model.rs deleted file mode 100644 index 7be4c1d0..00000000 --- a/crates/cf-analyzers-plumbing/src/git_model.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Git data model shared between the plumbing providers. -//! -//! Small, owned, pipeline-facing types so providers do not expose libgit2 -//! lifetimes through the analyzer map. -//! -//! These types are intentionally minimal: they carry exactly the fields the -//! plumbing providers read (the change action, the per-side path names, and -//! the per-side blob hashes). When the workspace grows a canonical git model -//! this module should defer to it. - -/// A 20-byte SHA-1 object id. -/// -/// Wrapped in a newtype so it can be used as a `HashMap` key (the blob cache -/// is keyed by hash); [`Display`](std::fmt::Display) renders 40 lowercase hex -/// digits (the report-facing hash format). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] -pub struct Hash(pub [u8; 20]); - -impl Hash { - /// The all-zero hash (an absent side of a change). - pub const ZERO: Hash = Hash([0u8; 20]); - - /// Whether this is the zero hash. - #[must_use] - pub fn is_zero(&self) -> bool { - self.0 == [0u8; 20] - } -} - -impl std::fmt::Display for Hash { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for b in self.0 { - write!(f, "{b:02x}")?; - } - Ok(()) - } -} - -impl From for Hash { - fn from(oid: git2::Oid) -> Self { - let bytes = oid.as_bytes(); - let mut h = [0u8; 20]; - // git2 oids are 20 bytes for SHA-1 repositories. - let n = bytes.len().min(20); - h[..n].copy_from_slice(&bytes[..n]); - Hash(h) - } -} - -/// The kind of change to a path. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Action { - /// File added (present in the new tree, absent in the old). - Insert, - /// File removed (present in the old tree, absent in the new). - Delete, - /// File content changed. - Modify, -} - -/// One side of a [`Change`]. -/// -/// Only the fields actually consulted by the plumbing providers are modelled: -/// the path `name` and the tree-entry `hash`. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct ChangeEntry { - /// Full path of the entry, e.g. `"src/main.rs"`. - pub name: String, - /// Object id of the blob. [`Hash::ZERO`] when the side is empty (an - /// inserted file has a zero `from`, a deleted file a zero `to`). - pub hash: Hash, -} - -/// A single file change between two trees. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Change { - /// State before the change. Empty (`name == ""`, `hash == ZERO`) for inserts. - pub from: ChangeEntry, - /// State after the change. Empty for deletes. - pub to: ChangeEntry, -} - -impl Change { - /// Classify the change from which sides are present. - /// - /// Both sides empty never arises for changes produced by the providers; - /// it is modelled as `None` rather than panicking. - #[must_use] - pub fn action(&self) -> Option { - let from_empty = self.from.name.is_empty() && self.from.hash.is_zero(); - let to_empty = self.to.name.is_empty() && self.to.hash.is_zero(); - match (from_empty, to_empty) { - (true, true) => None, - (true, false) => Some(Action::Insert), - (false, true) => Some(Action::Delete), - (false, false) => Some(Action::Modify), - } - } -} - -/// An ordered list of changes. -pub type Changes = Vec; - -/// An author/committer signature — the minimal commit metadata read by the -/// providers. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Signature { - /// Author display name. - pub name: String, - /// Author email. - pub email: String, - /// Authoring time as a UTC unix timestamp in seconds. - /// - /// The providers only ever subtract two such times and divide by a tick - /// size, so a seconds count preserves behavior exactly while staying - /// serialization-agnostic. - pub when_unix: i64, -} - -/// The minimal commit modelled here. -/// -/// Only the fields the plumbing providers consult are modelled: the author -/// signature (read by [`crate::identity_detector::IdentityDetector`]) and the -/// committer signature (read by [`crate::ticks::TicksSinceStart`]). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Commit { - /// Author signature (name, email, authoring time). - pub author: Signature, - /// Committer signature (name, email, commit time). - pub committer: Signature, -} diff --git a/crates/cf-analyzers-plumbing/src/identity_detector.rs b/crates/cf-analyzers-plumbing/src/identity_detector.rs index d69ab417..45562082 100644 --- a/crates/cf-analyzers-plumbing/src/identity_detector.rs +++ b/crates/cf-analyzers-plumbing/src/identity_detector.rs @@ -1,10 +1,12 @@ -//! `IdentityDetector` provider. +//! `IdentityDetector`: maps a commit author to a canonical person id. //! -//! Maps a commit author to a canonical person id. Person ids and the reversed -//! people dictionary flow into report output (pinned by the differential -//! gate). Two matching modes: +//! Person ids and the reversed people dictionary flow into report output +//! (developer names, `author_id` fields, people matrices). Two matching modes: //! * **loose** (default): an author is identified by *either* their lowercased -//! email or lowercased name; the two are unified into one id; +//! email or lowercased name; the two are unified into one id. A hit on one +//! token REGISTERS the other, so `alice ` followed by `Alice Smith +//! ` followed by `Alice Smith ` is one person — every alias seen +//! together with a known token joins that person; //! * **exact**: the whole `"name "` signature (lowercased) is the key. //! //! The people dictionary can be loaded from a file or built incrementally as @@ -16,14 +18,24 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::Path; -use crate::analyzer::{dep, Analyzer, AnalyzerError, ValueMap}; -use crate::git_model::{Commit, Signature}; +pub use cf_identity::{AUTHOR_MISSING, AUTHOR_MISSING_NAME}; -/// Sentinel id for a missing author (report contract). -pub const AUTHOR_MISSING: i64 = (1 << 18) - 1; +/// An author signature — the commit metadata the detector reads. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature { + /// Author display name. + pub name: String, + /// Author email. + pub email: String, + /// Authoring time as a UTC unix timestamp in seconds. + pub when_unix: i64, +} -/// Display name for a missing author (report contract). -pub const AUTHOR_MISSING_NAME: &str = ""; +/// The [`AUTHOR_MISSING`] sentinel as the `i64` the detector hands out. +#[must_use] +pub const fn author_missing_id() -> i64 { + AUTHOR_MISSING as i64 +} /// `IdentityDetector` provider. #[derive(Debug, Clone, Default)] @@ -72,7 +84,7 @@ impl IdentityDetector { self.lookup_loose_signature(signature) }; let author_id = if !exists && self.dict_finalized { - AUTHOR_MISSING + author_missing_id() } else { author_id }; @@ -95,15 +107,23 @@ impl IdentityDetector { (0, false) } - /// Find or register an author by loose signature. + /// Find or register an author by loose signature. A hit on either token + /// registers the other token under the same id (incremental mode), so a + /// later commit carrying only the newly seen token still resolves to this + /// person. fn lookup_loose_signature(&mut self, signature: &Signature) -> (i64, bool) { let email = signature.email.to_lowercase(); let name = signature.name.to_lowercase(); - if let Some(&id) = self.people_dict.get(&email) { - return (id, true); - } - if let Some(&id) = self.people_dict.get(&name) { + let hit = self + .people_dict + .get(&email) + .or_else(|| self.people_dict.get(&name)) + .copied(); + if let Some(id) = hit { + if !self.dict_finalized { + self.register_loose_identity(&email, &name); + } return (id, true); } if !self.dict_finalized { @@ -153,12 +173,12 @@ impl IdentityDetector { /// Each non-empty line lists `|`-separated identity tokens for one person; /// every token (lowercased) maps to that person's index, and the first /// token is the reverse-dict entry. A trailing [`AUTHOR_MISSING_NAME`] - /// entry is appended (reference-implementation behavior). + /// entry is appended so the sentinel id has a display name. /// /// # Errors /// - /// Returns an error when the file cannot be opened or read. - pub fn load_people_dict>(&mut self, path: P) -> Result<(), AnalyzerError> { + /// Returns the I/O error when the file cannot be opened or read. + pub fn load_people_dict>(&mut self, path: P) -> std::io::Result<()> { let file = File::open(path)?; let reader = BufReader::new(file); let mut dict: HashMap = HashMap::new(); @@ -168,8 +188,7 @@ impl IdentityDetector { let size = size as i64; // Lines are processed verbatim: only the trailing newline is // stripped, other whitespace is NOT trimmed, and blank lines are - // processed too (reference-implementation behavior). Split on '|' - // directly. + // processed too. Split on '|' directly. let ids: Vec<&str> = line.split('|').collect(); for id in &ids { dict.insert(id.to_lowercase(), size); @@ -183,62 +202,6 @@ impl IdentityDetector { Ok(()) } - /// Build the dictionary from a list of commits ahead of time - /// (dispatching on [`exact_signatures`](Self::exact_signatures)). - pub fn generate_people_dict(&mut self, commits: &[Commit]) { - if self.exact_signatures { - self.generate_exact_dict(commits); - } else { - self.generate_loose_dict(commits); - } - self.dict_finalized = true; - } - - fn generate_exact_dict(&mut self, commits: &[Commit]) { - let mut dict: HashMap = HashMap::new(); - let mut size: i64 = 0; - for c in commits { - let sig = format!("{} <{}>", c.author.name, c.author.email).to_lowercase(); - if let std::collections::hash_map::Entry::Vacant(slot) = dict.entry(sig) { - slot.insert(size); - size += 1; - } - } - let mut reverse = vec![String::new(); size as usize]; - for (key, &val) in &dict { - reverse[val as usize].clone_from(key); - } - self.people_dict = dict; - self.reversed_people_dict = reverse; - } - - fn generate_loose_dict(&mut self, commits: &[Commit]) { - // Reset incremental state and reuse register_loose_identity. - self.people_dict.clear(); - self.incremental_emails.clear(); - self.incremental_names.clear(); - self.incremental_size = 0; - for c in commits { - let email = c.author.email.to_lowercase(); - let name = c.author.name.to_lowercase(); - self.register_loose_identity(&email, &name); - } - let size = self.incremental_size; - let mut reverse = vec![String::new(); size as usize]; - for id in 0..size { - let mut names = self.incremental_names.get(&id).cloned().unwrap_or_default(); - let mut emails = self - .incremental_emails - .get(&id) - .cloned() - .unwrap_or_default(); - names.sort(); - emails.sort(); - reverse[id as usize] = format!("{}|{}", names.join("|"), emails.join("|")); - } - self.reversed_people_dict = reverse; - } - /// Build [`reversed_people_dict`](IdentityDetector::reversed_people_dict) /// from the incrementally-collected names/emails. /// @@ -282,26 +245,42 @@ impl IdentityDetector { } } -impl Analyzer for IdentityDetector { - fn name(&self) -> &'static str { - "IdentityDetector" - } - - fn provides(&self) -> Vec<&'static str> { - vec!["author"] - } +/// Prefix of every anonymized developer label. +const ANONYMOUS_PREFIX: &str = "Developer-"; - fn requires(&self) -> Vec<&'static str> { - vec![] - } +/// Replaces every reversed-dictionary entry with a stable pseudonym derived +/// from its index: `Developer-A`, `Developer-B`, …, `Developer-Z`, +/// `Developer-AA`, … (the `--anonymize` output policy). The +/// [`AUTHOR_MISSING_NAME`] sentinel is kept as-is so an unmatched author +/// stays recognisable. +#[must_use] +pub fn anonymize_names(names: &[String]) -> Vec { + names + .iter() + .enumerate() + .map(|(i, name)| { + if name == AUTHOR_MISSING_NAME { + name.clone() + } else { + format!("{ANONYMOUS_PREFIX}{}", column_label(i)) + } + }) + .collect() +} - fn consume(&mut self, deps: &mut ValueMap) -> Result { - let commit = dep::(deps, "commit")?.clone(); - let id = self.consume_signature(&commit.author); - let mut out = ValueMap::new(); - out.insert("author".to_string(), Box::new(id)); - Ok(out) +/// Spreadsheet-style column label for a 0-based index (`0 → A`, `25 → Z`, +/// `26 → AA`). +fn column_label(index: usize) -> String { + const ALPHABET: usize = 26; + let mut n = index + 1; + let mut out = Vec::new(); + while n > 0 { + n -= 1; + out.push(b'A' + (n % ALPHABET) as u8); + n /= ALPHABET; } + out.reverse(); + String::from_utf8(out).expect("ASCII letters") } #[cfg(test)] @@ -329,6 +308,24 @@ mod tests { assert_ne!(id1, id3); } + /// A hit on one token must register the OTHER token: after `alice + /// ` and `Alice Smith ` (email hit), a commit by `Alice + /// Smith ` (name hit only) is still the same person. + #[test] + fn loose_hit_registers_the_other_token() { + let mut d = IdentityDetector::new(); + let a = d.consume_signature(&sig("alice", "alice@example.com")); + let b = d.consume_signature(&sig("Alice Smith", "alice@example.com")); + let c = d.consume_signature(&sig("Alice Smith", "alice@other.org")); + assert_eq!((a, b, c), (0, 0, 0), "all three aliases are one person"); + d.finalize_dict(); + assert_eq!( + d.reversed_people_dict, + vec!["alice|alice smith|alice@example.com|alice@other.org".to_string()], + "the reverse entry lists every alias seen" + ); + } + #[test] fn exact_distinguishes_full_signature() { let mut d = IdentityDetector::new(); @@ -337,6 +334,14 @@ mod tests { let id2 = d.consume_signature(&sig("J. Doe", "john@example.com")); // Exact mode treats the differing display name as a new identity. assert_ne!(id1, id2); + d.finalize_dict(); + assert_eq!( + d.reversed_people_dict, + vec![ + "john doe ".to_string(), + "j. doe ".to_string() + ] + ); } #[test] @@ -347,11 +352,11 @@ mod tests { assert_eq!(d.consume_signature(&sig("John", "john@example.com")), 0); assert_eq!( d.consume_signature(&sig("Nobody", "nobody@example.com")), - AUTHOR_MISSING + author_missing_id() ); + assert_eq!(author_missing_id(), i64::from(cf_identity::AUTHOR_MISSING)); } - // Mirrors the reference suite's LoadPeopleDict expectations. #[test] fn load_people_dict_appends_unknown_sentinel() { let content = "John Doe|john@example.com\nJane Smith|jane@example.com\n"; @@ -361,18 +366,38 @@ mod tests { d.load_people_dict(tmp.path()).unwrap(); // Two persons * two tokens each = 4 dict entries. assert_eq!(d.people_dict.len(), 4); - // reversed = [first-token-of-line-0, first-token-of-line-1, ""]. + // reversed = [first-token-of-line-0, first-token-of-line-1, ""]. assert_eq!(d.reversed_people_dict.len(), 3); assert_eq!(d.reversed_people_dict[2], AUTHOR_MISSING_NAME); - // Tokens lowercased on load. + // Tokens lowercased on load; a loaded dict never grows. assert_eq!(d.consume_signature(&sig("john doe", "john@example.com")), 0); + assert_eq!(d.consume_signature(&sig("Jane Smith", "x@y")), 1); + assert_eq!( + d.consume_signature(&sig("Stranger", "s@z")), + author_missing_id() + ); + assert_eq!(d.people_dict.len(), 4); + } + + #[test] + fn load_people_dict_missing_file_is_an_error() { + let mut d = IdentityDetector::new(); + let err = d + .load_people_dict("/nonexistent/people.txt") + .expect_err("missing file must error"); + assert_eq!(err.kind(), std::io::ErrorKind::NotFound); } #[test] - fn provider_metadata() { - let d = IdentityDetector::new(); - assert_eq!(d.name(), "IdentityDetector"); - assert_eq!(d.provides(), vec!["author"]); - assert!(d.requires().is_empty()); + fn anonymize_names_numbers_developers_and_keeps_the_sentinel() { + let names: Vec = (0..27) + .map(|i| format!("dev{i}|dev{i}@x")) + .chain(std::iter::once(AUTHOR_MISSING_NAME.to_string())) + .collect(); + let anon = anonymize_names(&names); + assert_eq!(anon[0], "Developer-A"); + assert_eq!(anon[25], "Developer-Z"); + assert_eq!(anon[26], "Developer-AA"); + assert_eq!(anon[27], AUTHOR_MISSING_NAME); } } diff --git a/crates/cf-analyzers-plumbing/src/languages_detection.rs b/crates/cf-analyzers-plumbing/src/languages_detection.rs deleted file mode 100644 index 9c907210..00000000 --- a/crates/cf-analyzers-plumbing/src/languages_detection.rs +++ /dev/null @@ -1,388 +0,0 @@ -//! `LanguagesDetection` provider. -//! -//! Produces a map from blob [`struct@Hash`] to detected language name. -//! Detection runs in a frozen order (language names flow into report output): -//! 1. a binary guard (binary blob -> `""`); -//! 2. a fast-path extension table ([`language_by_extension`]); -//! 3. a content-analysis fallback for unmatched extensions. -//! -//! The fallback must match the enry classifier byte-for-byte (DESIGN §2.6 -//! mandates carrying enry's own data tables rather than swapping detectors); -//! it is injected via the [`EnryClassifier`] trait. - -use std::collections::HashMap; - -use crate::analyzer::{dep, Analyzer, AnalyzerError, ValueMap}; -use crate::blob_cache::{is_binary, CachedBlob}; -use crate::git_model::{Action, Changes, Hash}; - -/// Content-analysis language classifier boundary. -/// -/// Takes the base file name and the file bytes and returns the language name, -/// or an empty string when undetermined. For byte-identity the implementation -/// must carry the enry data tables (DESIGN §2.6); it is injected. -pub trait EnryClassifier { - /// Detect the language for a file given its base name and content. - fn get_language(&self, base_name: &str, content: &[u8]) -> String; -} - -/// `LanguagesDetection` provider. -pub struct LanguagesDetection { - classifier: C, -} - -impl LanguagesDetection { - /// Construct with the given content-analysis classifier. - pub const fn new(classifier: C) -> Self { - Self { classifier } - } - - /// Detect the language for a single blob. - /// - /// Returns `""` for a binary blob, then tries the extension fast path, - /// then falls back to content analysis. - pub fn detect_language(&self, name: &str, blob: Option<&CachedBlob>) -> String { - let Some(blob) = blob else { - return String::new(); - }; - // Binary guard: a binary blob yields the empty language. - if is_binary(&blob.data) { - return String::new(); - } - // Fast path: extension lookup. - let lang = language_by_extension(name); - if !lang.is_empty() { - return lang.to_string(); - } - // Slow path: content analysis. - self.classifier.get_language(base_name(name), &blob.data) - } - - /// Build the blob-hash -> language map for one commit: inserts key the - /// `to` side, deletes the `from` side, and modifies key both sides. - pub fn build( - &self, - changes: &Changes, - cache: &HashMap, - ) -> HashMap { - let mut result: HashMap = HashMap::new(); - for change in changes { - match change.action() { - Some(Action::Insert) => { - result.insert( - change.to.hash, - self.detect_language(&change.to.name, cache.get(&change.to.hash)), - ); - } - Some(Action::Delete) => { - result.insert( - change.from.hash, - self.detect_language(&change.from.name, cache.get(&change.from.hash)), - ); - } - Some(Action::Modify) => { - result.insert( - change.to.hash, - self.detect_language(&change.to.name, cache.get(&change.to.hash)), - ); - result.insert( - change.from.hash, - self.detect_language(&change.from.name, cache.get(&change.from.hash)), - ); - } - None => {} - } - } - result - } -} - -impl Analyzer for LanguagesDetection { - fn name(&self) -> &'static str { - "LanguagesDetection" - } - - fn provides(&self) -> Vec<&'static str> { - vec!["languages"] - } - - fn requires(&self) -> Vec<&'static str> { - vec!["changes", "blob_cache"] - } - - fn consume(&mut self, deps: &mut ValueMap) -> Result { - let cache = dep::>(deps, "blob_cache")?; - let changes = dep::(deps, "changes")?; - let result = self.build(changes, cache); - let mut out = ValueMap::new(); - out.insert("languages".to_string(), Box::new(result)); - Ok(out) - } -} - -/// Base file name (libgit2 paths always use `/`). -fn base_name(path: &str) -> &str { - path.rfind('/').map_or(path, |i| &path[i + 1..]) -} - -/// Programming language for a filename by extension. The extension is -/// lowercased before lookup; the table keys are all lowercase. Returns `""` -/// when there is no extension or no match. -/// -/// The table is frozen (language-detection contract). -#[must_use] -pub fn language_by_extension(filename: &str) -> &'static str { - let ext = match filename.rfind('.') { - // The extension starts at the last dot; a dotfile with no other dot - // yields an "extension" as well (reference-implementation behavior), - // so keep the raw last-dot rule, which matches for the cases here. - Some(i) => &filename[i..], - None => return "", - }; - let ext = ext.to_ascii_lowercase(); - EXTENSION_TO_LANGUAGE - .iter() - .find(|(k, _)| *k == ext) - .map_or("", |(_, v)| *v) -} - -/// The frozen extension fast-path table (language-detection contract; pinned -/// by the differential gate). Stored as a slice: lookups are linear but the -/// table is small, and this keeps iteration order deterministic. Keys are -/// lowercase. -static EXTENSION_TO_LANGUAGE: &[(&str, &str)] = &[ - (".go", "Go"), - (".py", "Python"), - (".pyw", "Python"), - (".pyi", "Python"), - (".pyx", "Python"), - (".pxd", "Python"), - (".gyp", "Python"), - (".gypi", "Python"), - (".js", "JavaScript"), - (".mjs", "JavaScript"), - (".cjs", "JavaScript"), - (".jsx", "JavaScript"), - (".es6", "JavaScript"), - (".es", "JavaScript"), - (".jsm", "JavaScript"), - (".vue", "Vue"), - (".svelte", "Svelte"), - (".ts", "TypeScript"), - (".mts", "TypeScript"), - (".cts", "TypeScript"), - (".tsx", "TSX"), - (".rs", "Rust"), - (".java", "Java"), - (".kt", "Kotlin"), - (".kts", "Kotlin"), - (".scala", "Scala"), - (".sc", "Scala"), - (".c", "C"), - (".h", "C"), - (".cpp", "C++"), - (".hpp", "C++"), - (".cc", "C++"), - (".cxx", "C++"), - (".hxx", "C++"), - (".c++", "C++"), - (".h++", "C++"), - (".hh", "C++"), - (".ipp", "C++"), - (".inl", "C++"), - (".tcc", "C++"), - (".tpp", "C++"), - (".cs", "C#"), - (".csx", "C#"), - (".rb", "Ruby"), - (".rake", "Ruby"), - (".gemspec", "Ruby"), - (".rbw", "Ruby"), - (".ru", "Ruby"), - (".podspec", "Ruby"), - (".thor", "Ruby"), - (".jbuilder", "Ruby"), - (".php", "PHP"), - (".php3", "PHP"), - (".php4", "PHP"), - (".php5", "PHP"), - (".php7", "PHP"), - (".phps", "PHP"), - (".phtml", "PHP"), - (".sh", "Shell"), - (".bash", "Shell"), - (".zsh", "Shell"), - (".ksh", "Shell"), - (".csh", "Shell"), - (".tcsh", "Shell"), - (".fish", "Shell"), - (".ps1", "PowerShell"), - (".psm1", "PowerShell"), - (".psd1", "PowerShell"), - (".pl", "Perl"), - (".pm", "Perl"), - (".pod", "Perl"), - (".t", "Perl"), - (".lua", "Lua"), - // The reference table has both ".r" and ".R" keys mapping to "R"; after - // lowercasing the lookup, ".r" covers both. ".rmd"/".Rmd" likewise both - // lowercase to ".rmd". - (".r", "R"), - (".rmd", "RMarkdown"), - (".swift", "Swift"), - (".m", "Objective-C"), - (".mm", "Objective-C++"), - (".dart", "Dart"), - (".ex", "Elixir"), - (".exs", "Elixir"), - (".eex", "Elixir"), - (".leex", "Elixir"), - (".heex", "Elixir"), - (".erl", "Erlang"), - (".hrl", "Erlang"), - (".hs", "Haskell"), - (".lhs", "Haskell"), - (".clj", "Clojure"), - (".cljs", "ClojureScript"), - (".cljc", "Clojure"), - (".edn", "Clojure"), - (".fs", "F#"), - (".fsi", "F#"), - (".fsx", "F#"), - (".fsscript", "F#"), - (".ml", "OCaml"), - (".mli", "OCaml"), - (".mll", "OCaml"), - (".mly", "OCaml"), - (".json", "JSON"), - (".json5", "JSON5"), - (".yaml", "YAML"), - (".yml", "YAML"), - (".toml", "TOML"), - (".xml", "XML"), - (".csv", "CSV"), - (".tsv", "TSV"), - (".ini", "INI"), - (".cfg", "INI"), - (".conf", "INI"), - (".env", "Dotenv"), - (".html", "HTML"), - (".htm", "HTML"), - (".xhtml", "HTML"), - (".css", "CSS"), - (".scss", "SCSS"), - (".sass", "Sass"), - (".less", "Less"), - (".styl", "Stylus"), - (".md", "Markdown"), - (".markdown", "Markdown"), - (".rst", "reStructuredText"), - (".tex", "TeX"), - (".latex", "TeX"), - (".adoc", "AsciiDoc"), - (".asciidoc", "AsciiDoc"), - (".sql", "SQL"), - (".psql", "SQL"), - (".mysql", "SQL"), - (".pgsql", "SQL"), - (".graphql", "GraphQL"), - (".gql", "GraphQL"), - (".proto", "Protocol Buffer"), - (".thrift", "Thrift"), - (".wat", "WebAssembly"), - (".wast", "WebAssembly"), - (".asm", "Assembly"), - // ".s" and ".S" both lowercase to ".s". - (".s", "Assembly"), - (".zig", "Zig"), - (".nim", "Nim"), - (".nims", "Nim"), - (".nimble", "Nim"), - (".jl", "Julia"), - (".v", "V"), - (".cr", "Crystal"), - (".groovy", "Groovy"), - (".gradle", "Groovy"), - (".gvy", "Groovy"), - (".dockerfile", "Dockerfile"), - (".mk", "Makefile"), - (".mak", "Makefile"), - (".cmake", "CMake"), - (".tf", "HCL"), - (".tfvars", "HCL"), - (".hcl", "HCL"), -]; - -#[cfg(test)] -mod tests { - use super::*; - use crate::git_model::{Change, ChangeEntry}; - - /// Classifier that never matches, so tests exercise only the fast path / - /// binary guard. The real enry-parity classifier is a todo. - struct NoContent; - impl EnryClassifier for NoContent { - fn get_language(&self, _base: &str, _content: &[u8]) -> String { - String::new() - } - } - - fn h(n: u8) -> Hash { - let mut b = [0u8; 20]; - b[0] = n; - Hash(b) - } - - #[test] - fn extension_fast_path() { - assert_eq!(language_by_extension("main.go"), "Go"); - assert_eq!(language_by_extension("a/b/main.rs"), "Rust"); - // Uppercase extension lowercases to match. - assert_eq!(language_by_extension("Foo.PY"), "Python"); - assert_eq!(language_by_extension("data.unknownext"), ""); - assert_eq!(language_by_extension("Makefile"), ""); - } - - #[test] - fn binary_blob_yields_empty() { - let ld = LanguagesDetection::new(NoContent); - let blob = CachedBlob::new(vec![0u8, 1, 2]); - // Even with a known extension, a binary blob is "". - assert_eq!(ld.detect_language("main.go", Some(&blob)), ""); - } - - #[test] - fn nil_blob_yields_empty() { - let ld = LanguagesDetection::new(NoContent); - assert_eq!(ld.detect_language("main.go", None), ""); - } - - #[test] - fn build_keys_both_sides_of_modify() { - let ld = LanguagesDetection::new(NoContent); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"package main\n".to_vec())); - cache.insert(h(2), CachedBlob::new(b"package main\n".to_vec())); - let changes = vec![Change { - from: ChangeEntry { - name: "a.go".into(), - hash: h(1), - }, - to: ChangeEntry { - name: "a.go".into(), - hash: h(2), - }, - }]; - let out = ld.build(&changes, &cache); - assert_eq!(out.get(&h(1)).map(String::as_str), Some("Go")); - assert_eq!(out.get(&h(2)).map(String::as_str), Some("Go")); - } - - #[test] - fn provider_metadata() { - let ld = LanguagesDetection::new(NoContent); - assert_eq!(ld.name(), "LanguagesDetection"); - assert_eq!(ld.provides(), vec!["languages"]); - assert_eq!(ld.requires(), vec!["changes", "blob_cache"]); - } -} diff --git a/crates/cf-analyzers-plumbing/src/lib.rs b/crates/cf-analyzers-plumbing/src/lib.rs index c5f906e8..010ad222 100644 --- a/crates/cf-analyzers-plumbing/src/lib.rs +++ b/crates/cf-analyzers-plumbing/src/lib.rs @@ -1,99 +1,45 @@ -//! cf-analyzers-plumbing: analyzer-level git/UAST data providers. -//! -//! These providers sit beneath the history analyzers and supply the shared -//! git/UAST facts the rest of the pipeline consumes: +//! cf-analyzers-plumbing: the per-commit facts shared by every history +//! analyzer walk. //! //! | Provider | Output | //! |----------|--------| -//! | [`tree_diff::TreeDiff`] | `changes` | -//! | [`blob_cache::BlobCache`] | `blob_cache` | -//! | [`file_diff::FileDiff`] | `file_diff` | -//! | [`ticks::TicksSinceStart`] | `tick` | -//! | [`identity_detector::IdentityDetector`] | `author` | -//! | [`languages_detection::LanguagesDetection`] | `languages` | -//! | [`uast_changes::UASTChanges`] | `uast_changes` | -//! -//! # Behavioral fidelity -//! -//! These providers emit intermediate facts consumed by downstream analyzers, -//! which own the machine-format serialization whose bytes are pinned against -//! the reference implementation by `tests/compat`. The plumbing layer's -//! job is to compute those facts identically: +//! | [`ticks::HistoryTicks`] | the commit's tick (`--tick-size` buckets) | +//! | [`identity_detector::IdentityDetector`] | the commit's author id | //! -//! * [`ticks::TicksSinceStart`] implements the floored tick origin, the -//! committer-time sanitization window ([`ticks_anomaly`]), and the monotonic -//! `max(tick, previous_tick)` clamp — a byte-identity hazard. Its only -//! wall-clock read (now + max clock skew) goes through an injectable -//! [`clock::Clock`] per DESIGN §2.8. -//! * [`file_diff::FileDiff`] implements the Modify-only scope, the two fast -//! paths, the binary guard, and the encoded-line LOC counts; the actual -//! line diff is delegated to an injected [`file_diff::LineDiffer`] (the -//! byte-faithful engine is `cf-godiff`). -//! * [`languages_detection::LanguagesDetection`] carries the frozen extension -//! fast-path table and delegates content analysis to an injected -//! [`languages_detection::EnryClassifier`] that must carry the enry data -//! tables (DESIGN §2.6). -//! * [`blob_cache::BlobCache`] implements the cross-commit previous-cache -//! reuse and empty-placeholder-on-read-failure semantics. -//! -//! # Dependency boundaries -//! -//! The [`analyzer::Analyzer`] trait, the git model ([`git_model`]), the UAST -//! [`Parser`]/[`PathFilter`] surface ([`uast_iface`]), and the diff/enry -//! engines are defined locally and injected, keeping this crate decoupled -//! from the heavier framework/UAST/git crates. They should collapse into -//! re-exports of the canonical crates as those stabilize. +//! Both are pure functions of the commit stream and the run flags — no wall +//! clock, no host CPU count — so a history report is reproducible on any +//! machine. Tick values and author ids flow into report keys and are part of +//! the output contract. //! //! # Example //! -//! Two of the pure, self-contained helpers this crate exposes: the frozen -//! extension-to-language fast path and the injectable clock seam used to keep -//! tick computation deterministic. -//! //! ``` -//! use cf_analyzers_plumbing::{language_by_extension, Clock, FixedClock}; -//! -//! // Extension lookup is case-insensitive and returns "" on no match. -//! assert_eq!(language_by_extension("main.rs"), "Rust"); -//! assert_eq!(language_by_extension("App.PY"), "Python"); -//! assert_eq!(language_by_extension("README"), ""); -//! -//! // A FixedClock yields a deterministic "now" for reproducible goldens. -//! let clock = FixedClock(1_700_000_000); -//! assert_eq!(clock.now_unix(), 1_700_000_000); +//! use cf_analyzers_plumbing::{HistoryTicks, IdentityDetector, Signature}; +//! +//! // Ticks: one-hour buckets relative to the first commit. +//! let mut ticks = HistoryTicks::new(1); +//! assert_eq!(ticks.tick_for(1_577_836_800), 0); +//! assert_eq!(ticks.tick_for(1_577_836_800 + 2 * 3600), 2); +//! +//! // Identity: loose matching unifies a person's aliases under one id. +//! let mut ids = IdentityDetector::new(); +//! let sig = |name: &str, email: &str| Signature { +//! name: name.into(), +//! email: email.into(), +//! when_unix: 0, +//! }; +//! let a = ids.consume_signature(&sig("alice", "alice@example.com")); +//! let b = ids.consume_signature(&sig("Alice Smith", "alice@example.com")); +//! assert_eq!(a, b); //! ``` -pub mod analyzer; -pub mod blob_cache; -pub mod clock; -pub mod file_diff; -pub mod git_model; +#![forbid(unsafe_code)] + pub mod identity_detector; -pub mod languages_detection; pub mod ticks; -pub mod ticks_anomaly; -pub mod tree_diff; -pub mod uast_changes; -pub mod uast_iface; -// Convenient re-exports of the provider types and the values they thread -// through the pipeline. -pub use analyzer::{Analyzer, AnalyzerError, AnyValue, Facts, ValueMap}; -pub use blob_cache::{BlobCache, BlobSource, CachedBlob, ErrorBinary, GitBlobSource}; -pub use clock::{Clock, FixedClock, SystemClock}; -pub use file_diff::{ - Diff, DiffOp, DiffParams, FileDiff, FileDiffData, LineDiffResult, LineDiffer, - DEFAULT_DIFF_TIMEOUT_MS, -}; -pub use git_model::{Action, Change, ChangeEntry, Changes, Commit, Hash, Signature}; -pub use identity_detector::{IdentityDetector, AUTHOR_MISSING, AUTHOR_MISSING_NAME}; -pub use languages_detection::{language_by_extension, EnryClassifier, LanguagesDetection}; -pub use ticks::{TicksSinceStart, DEFAULT_TICKS_SINCE_START_TICK_SIZE_HOURS}; -pub use ticks_anomaly::{ - TimeAnomalyStats, TimeAnomalyTracker, MAX_CLOCK_SKEW_SECS, MIN_SANE_COMMIT_TIME_UNIX, -}; -pub use tree_diff::{GitTreeSource, TreeDiff, TreeSource}; -pub use uast_changes::{UASTChange, UASTChanges, MAX_UAST_BLOB_SIZE}; -pub use uast_iface::{ - AllowAllPathFilter, Node, NodeLike, Parser, PathFilter, SharedParser, SharedPathFilter, +pub use identity_detector::{ + anonymize_names, author_missing_id, IdentityDetector, Signature, AUTHOR_MISSING, + AUTHOR_MISSING_NAME, }; +pub use ticks::{HistoryTicks, DEFAULT_TICK_SIZE_HOURS}; diff --git a/crates/cf-analyzers-plumbing/src/ticks.rs b/crates/cf-analyzers-plumbing/src/ticks.rs index 9f8d207d..4611c11b 100644 --- a/crates/cf-analyzers-plumbing/src/ticks.rs +++ b/crates/cf-analyzers-plumbing/src/ticks.rs @@ -1,304 +1,152 @@ -//! `TicksSinceStart` provider. +//! `HistoryTicks`: the ONE tick assignment every history walk uses. //! -//! Assigns each commit an integer "tick" relative to the first commit's -//! tick-boundary-floored committer time. The tick is monotonically -//! non-decreasing (`max(tick, previous_tick)`), and committer timestamps are -//! sanitized into a sane window before the math runs. The wall-clock upper -//! bound (now + max clock skew) — a byte-identity hazard, per DESIGN §2.8 — -//! is read through an injectable [`Clock`] so goldens are reproducible. +//! A tick is an integer bucket relative to the first in-window commit's +//! tick-boundary-floored committer time. The tick size comes from +//! `--tick-size` (hours, default [`DEFAULT_TICK_SIZE_HOURS`]). The tick is +//! monotonically non-decreasing over the walk (`max(tick, previous_tick)`), so +//! an out-of-order committer timestamp can never move a later commit into an +//! earlier bucket. Tick values flow into report keys (burndown bands, devs +//! activity, anomaly windows, `commits_by_tick`), so this is a report +//! contract. //! -//! Tick assignment feeds the history analyzers' report keys; it is pinned by -//! the differential gate (`tests/compat`). +//! The assignment is a pure function of the commit times and the tick size — +//! no wall clock is consulted — so a run is reproducible on any machine on +//! any day. -use std::collections::HashMap; -use std::sync::Arc; - -use crate::analyzer::{dep, Analyzer, AnalyzerError, ValueMap}; -use crate::clock::{Clock, SystemClock}; -use crate::git_model::{Commit, Hash}; -use crate::ticks_anomaly::{ - TimeAnomalyStats, TimeAnomalyTracker, MAX_CLOCK_SKEW_SECS, MIN_SANE_COMMIT_TIME_UNIX, -}; - -/// Default tick size in hours. -pub const DEFAULT_TICKS_SINCE_START_TICK_SIZE_HOURS: i64 = 24; +/// Default tick size in hours (`--tick-size` default). +pub const DEFAULT_TICK_SIZE_HOURS: i64 = 24; +/// Seconds per hour. const SECS_PER_HOUR: i64 = 3600; -/// `TicksSinceStart` provider. -pub struct TicksSinceStart { - /// Tick width in seconds, set from the configured hours. +/// The shared tick assigner for one history walk. +#[derive(Debug, Clone)] +pub struct HistoryTicks { tick_size_secs: i64, - /// The tick value of the last processed commit. - pub tick: i64, - /// Commit hashes grouped by tick. - pub commits: HashMap>, - previous_tick: i64, - /// Tick origin, seeded lazily from the first in-window commit. `None` - /// means "not yet seeded". tick0_unix: Option, - /// Most recent in-window committer time; the substitution source for - /// out-of-window timestamps. `None` until one has been seen. - last_valid_when: Option, - anomalies: Arc, - clock: Arc, + previous_tick: i64, } -impl Default for TicksSinceStart { +impl Default for HistoryTicks { fn default() -> Self { - Self::new() + Self::new(DEFAULT_TICK_SIZE_HOURS) } } -impl TicksSinceStart { - /// Construct with the default tick size and the real system clock. - #[must_use] - pub fn new() -> Self { - Self::with_clock(Arc::new(SystemClock)) - } - - /// Construct with the default tick size and an injected clock (for - /// deterministic goldens, per DESIGN §2.8). +impl HistoryTicks { + /// Construct with a tick size in hours. The CLI rejects a non-positive + /// `--tick-size` before any walk starts; as a defensive floor a value + /// below one hour is clamped to one hour so the arithmetic never divides + /// by zero. #[must_use] - pub fn with_clock(clock: Arc) -> Self { + pub fn new(tick_size_hours: i64) -> Self { Self { - tick_size_secs: DEFAULT_TICKS_SINCE_START_TICK_SIZE_HOURS * SECS_PER_HOUR, - tick: 0, - commits: HashMap::new(), - previous_tick: 0, + tick_size_secs: tick_size_hours.max(1) * SECS_PER_HOUR, tick0_unix: None, - last_valid_when: None, - anomalies: TimeAnomalyTracker::new(), - clock, + previous_tick: 0, } } - /// Set the tick size in hours (the `TicksSinceStart.TickSize` - /// configuration value). - pub fn set_tick_size_hours(&mut self, hours: i64) { - self.tick_size_secs = hours * SECS_PER_HOUR; + /// The configured tick size in hours. + #[must_use] + pub const fn tick_size_hours(&self) -> i64 { + self.tick_size_secs / SECS_PER_HOUR } - /// Round a timestamp down to the nearest tick boundary. - /// - /// The reference floor rounds to the nearest multiple of the grid since - /// an absolute zero time (away-from-zero on a tie) and subtracts one grid - /// step if the rounded result moved past `t`. For non-negative unix - /// seconds this equals arithmetic floor division onto the tick grid. + /// Round a unix timestamp down to the nearest tick boundary. #[must_use] pub const fn floor_time(when_unix: i64, tick_size_secs: i64) -> i64 { if tick_size_secs <= 0 { return when_unix; } - // The reference floor is relative to an absolute zero time, but the - // only use here is computing deltas `when - tick0` that are then - // divided by the same grid, so flooring relative to the unix epoch is - // equivalent for tick assignment. Euclidean floor handles negatives - // the same way as the reference round-then-subtract. when_unix.div_euclid(tick_size_secs) * tick_size_secs } - /// Clamp a committer timestamp into the sane window. - /// - /// Returns the (possibly substituted) timestamp and records anomalies. - fn sanitize_when(&mut self, when_unix: i64) -> i64 { - let upper_bound = self.clock.now_unix() + MAX_CLOCK_SKEW_SECS; - if when_unix < MIN_SANE_COMMIT_TIME_UNIX { - let replacement = self.substitute_when(); - self.anomalies.record_before_min(); - return replacement; - } - if when_unix > upper_bound { - let replacement = self.substitute_when(); - self.anomalies.record_after_max(); - return replacement; - } - self.last_valid_when = Some(when_unix); - when_unix - } - - /// Pick a stand-in for an out-of-window committer time: the most recent - /// in-window value, or the [`MIN_SANE_COMMIT_TIME_UNIX`] floor when none - /// has been seen. - fn substitute_when(&self) -> i64 { - self.last_valid_when.unwrap_or(MIN_SANE_COMMIT_TIME_UNIX) - } - - /// Compute the tick for one commit, advancing internal state. - /// - /// The algorithm (frozen; tick values flow into report keys): - /// 1. sanitize the committer time; - /// 2. on the first commit, seed `tick0 = floor_time(when, tick_size)`; - /// 3. `tick = max((when - tick0) / tick_size, previous_tick)`; - /// 4. accumulate the commit hash under `commits[tick]` (dedup tail-scan - /// for commits with parents); - /// 5. record `tick`. - pub fn tick_for( - &mut self, - committer_when_unix: i64, - commit_hash: Hash, - num_parents: usize, - ) -> i64 { - let when = self.sanitize_when(committer_when_unix); - + /// Compute the tick for one commit (committer time, unix seconds), + /// advancing internal state: + /// 1. on the first commit, seed `tick0 = floor_time(when, tick_size)`; + /// 2. `tick = max(floor((when - tick0) / tick_size), previous_tick)`. + pub fn tick_for(&mut self, committer_when_unix: i64) -> i64 { let tick0 = *self .tick0_unix - .get_or_insert_with(|| Self::floor_time(when, self.tick_size_secs)); - - // Integer division truncating toward zero (reference behavior), then - // max with previous_tick. - let delta = when - tick0; - let raw_tick = delta / self.tick_size_secs; + .get_or_insert_with(|| Self::floor_time(committer_when_unix, self.tick_size_secs)); + let raw_tick = (committer_when_unix - tick0).div_euclid(self.tick_size_secs); let tick = raw_tick.max(self.previous_tick); self.previous_tick = tick; - - let tick_commits = self.commits.entry(tick).or_default(); - // Scan the tail for an existing entry (dedup; root commits skip it). - let exists = num_parents > 0 && tick_commits.iter().rev().any(|h| *h == commit_hash); - if !exists { - tick_commits.push(commit_hash); - } - - self.tick = tick; tick } - /// Cumulative committer-timestamp anomalies. - #[must_use] - pub fn time_anomalies(&self) -> TimeAnomalyStats { - self.anomalies.snapshot() - } - - /// Tick of the last processed commit. + /// Tick of the last processed commit (0 before any commit). #[must_use] - pub const fn current_tick(&self) -> i64 { - self.tick - } -} - -impl Analyzer for TicksSinceStart { - fn name(&self) -> &'static str { - "TicksSinceStart" - } - - fn provides(&self) -> Vec<&'static str> { - vec!["tick"] - } - - fn requires(&self) -> Vec<&'static str> { - vec![] - } - - fn consume(&mut self, deps: &mut ValueMap) -> Result { - let commit = dep::(deps, "commit")?.clone(); - // num_parents is carried alongside the commit when present, - // defaulting to 0. - let num_parents = deps - .get("num_parents") - .and_then(|v| v.downcast_ref::()) - .copied() - .unwrap_or(0); - let commit_hash = deps - .get("commit_hash") - .and_then(|v| v.downcast_ref::()) - .copied() - .unwrap_or(Hash::ZERO); - let tick = self.tick_for(commit.committer.when_unix, commit_hash, num_parents); - let mut out = ValueMap::new(); - out.insert("tick".to_string(), Box::new(tick)); - Ok(out) + pub const fn last_tick(&self) -> i64 { + self.previous_tick } } #[cfg(test)] mod tests { use super::*; - use crate::clock::FixedClock; - const DAY: i64 = 24 * 60 * 60; + const HOUR: i64 = 3600; + const DAY: i64 = 24 * HOUR; // 2020-01-01T00:00:00Z as unix seconds (a tick boundary for 24h ticks). const START_2020: i64 = 1_577_836_800; - // A fixed "now" far in the future so the upper bound never trips in tests. - const NOW_2030: i64 = 1_893_456_000; - - fn fixed() -> TicksSinceStart { - TicksSinceStart::with_clock(Arc::new(FixedClock(NOW_2030))) - } - - fn h(n: u8) -> Hash { - let mut b = [0u8; 20]; - b[0] = n; - Hash(b) - } - - // Mirrors the reference suite's first-tick expectation. - #[test] - fn first_tick_is_zero() { - let mut ts = fixed(); - assert_eq!(ts.tick_for(START_2020, h(1), 0), 0); - } #[test] - fn multiple_ticks_two_days_later() { - let mut ts = fixed(); - assert_eq!(ts.tick_for(START_2020, h(1), 0), 0); - assert_eq!(ts.tick_for(START_2020 + 2 * DAY, h(2), 1), 2); + fn first_tick_is_zero_and_days_advance() { + let mut ts = HistoryTicks::default(); + assert_eq!(ts.tick_size_hours(), DEFAULT_TICK_SIZE_HOURS); + assert_eq!(ts.tick_for(START_2020), 0); + assert_eq!(ts.tick_for(START_2020 + 2 * DAY), 2); + assert_eq!(ts.last_tick(), 2); } - // The monotonic clamp: an earlier (in-window) commit cannot lower the tick. + /// The monotonic clamp: an earlier commit cannot lower the tick. #[test] fn monotonic_clamp() { - let mut ts = fixed(); - assert_eq!(ts.tick_for(START_2020 + 2 * DAY, h(1), 0), 0); // tick0 floor at +2d - // Earlier but still in window -> raw negative, clamped to previousTick. - assert_eq!(ts.tick_for(START_2020, h(2), 1), 0); + let mut ts = HistoryTicks::default(); + assert_eq!(ts.tick_for(START_2020 + 2 * DAY), 0); + assert_eq!(ts.tick_for(START_2020), 0); + assert_eq!(ts.tick_for(START_2020 + 3 * DAY), 1); } - // Below the sane floor -> substituted, anomaly counted. + /// The documented `--tick-size 1` contract over the ticks fixture: + /// 2020-01-01T23:59Z, 2020-01-02T00:01Z, 2020-01-02T12:00+02:00, + /// 2020-01-03T01:00-05:00, 2020-01-05T00:00Z ⇒ `[0, 1, 11, 31, 73]`. #[test] - fn before_min_is_substituted_and_counted() { - let mut ts = fixed(); - // First a valid commit so lastValidWhen is set. - ts.tick_for(START_2020 + DAY, h(1), 0); - // Epoch-0 commit: before 1990 -> substituted with lastValidWhen. - ts.tick_for(0, h(2), 1); - assert_eq!(ts.time_anomalies().before_min, 1); - assert_eq!(ts.time_anomalies().after_max, 0); - } - - // Far future -> substituted, after-max counted. - #[test] - fn after_max_is_counted() { - let mut ts = fixed(); - ts.tick_for(START_2020, h(1), 0); - // Way past now + skew. - ts.tick_for(NOW_2030 + 10 * DAY, h(2), 1); - assert_eq!(ts.time_anomalies().after_max, 1); + fn one_hour_ticks_follow_the_documented_fixture() { + let times = [ + START_2020 + 23 * HOUR + 59 * 60, + START_2020 + DAY + 60, + START_2020 + DAY + 10 * HOUR, + START_2020 + 2 * DAY + 6 * HOUR, + START_2020 + 4 * DAY, + ]; + let mut ts = HistoryTicks::new(1); + let ticks: Vec = times.iter().map(|t| ts.tick_for(*t)).collect(); + assert_eq!(ticks, vec![0, 1, 11, 31, 73]); + // The same instants under the default 24h tick. + let mut daily = HistoryTicks::default(); + let ticks: Vec = times.iter().map(|t| daily.tick_for(*t)).collect(); + assert_eq!(ticks, vec![0, 1, 1, 2, 4]); } #[test] fn floor_time_rounds_down_to_grid() { - // Half a day past a boundary floors back to the boundary. assert_eq!( - TicksSinceStart::floor_time(START_2020 + DAY / 2, DAY), + HistoryTicks::floor_time(START_2020 + DAY / 2, DAY), START_2020 ); - assert_eq!(TicksSinceStart::floor_time(START_2020, DAY), START_2020); - } - - #[test] - fn commits_accumulate_per_tick() { - let mut ts = fixed(); - ts.tick_for(START_2020, h(1), 0); - ts.tick_for(START_2020, h(2), 1); - assert_eq!(ts.commits.get(&0).unwrap().len(), 2); + assert_eq!(HistoryTicks::floor_time(START_2020, DAY), START_2020); + assert_eq!(HistoryTicks::floor_time(-1, DAY), -DAY); + assert_eq!(HistoryTicks::floor_time(42, 0), 42); } #[test] - fn provider_metadata() { - let ts = fixed(); - assert_eq!(ts.name(), "TicksSinceStart"); - assert_eq!(ts.provides(), vec!["tick"]); - assert!(ts.requires().is_empty()); + fn non_positive_tick_size_is_clamped_to_one_hour() { + let ts = HistoryTicks::new(0); + assert_eq!(ts.tick_size_hours(), 1); + let ts = HistoryTicks::new(-5); + assert_eq!(ts.tick_size_hours(), 1); } } diff --git a/crates/cf-analyzers-plumbing/src/ticks_anomaly.rs b/crates/cf-analyzers-plumbing/src/ticks_anomaly.rs deleted file mode 100644 index a3e2e53c..00000000 --- a/crates/cf-analyzers-plumbing/src/ticks_anomaly.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Committer-timestamp anomaly tracking. -//! -//! Counts committer timestamps that fall outside the sane analysis window. - -use std::sync::atomic::{AtomicI64, Ordering}; -use std::sync::Arc; - -/// Lower bound for a plausible committer timestamp: -/// 1990-01-01T00:00:00Z as a unix timestamp in seconds (frozen sanitization -/// contract). -pub const MIN_SANE_COMMIT_TIME_UNIX: i64 = 631_152_000; - -/// Upper-bound grace allowed past wall-clock time (24 hours), in seconds. -pub const MAX_CLOCK_SKEW_SECS: i64 = 24 * 60 * 60; - -/// Counts committer-timestamp anomalies. -/// -/// The counters are atomic so per-shard forks stay safe by construction, and -/// the tracker is shared via [`Arc`] so clones aggregate into one tally. The -/// counters are the only part observable through [`TimeAnomalyStats`]; any -/// warning log line is operational (not report) output, so its formatting is -/// not byte-identity-binding, but the counts are preserved exactly. -#[derive(Debug, Default)] -pub struct TimeAnomalyTracker { - before_min: AtomicI64, - after_max: AtomicI64, -} - -impl TimeAnomalyTracker { - /// Construct an empty tracker. - #[must_use] - pub fn new() -> Arc { - Arc::new(Self::default()) - } - - /// Record a timestamp earlier than [`MIN_SANE_COMMIT_TIME_UNIX`]. - pub fn record_before_min(&self) { - self.before_min.fetch_add(1, Ordering::Relaxed); - } - - /// Record a timestamp too far in the future. - pub fn record_after_max(&self) { - self.after_max.fetch_add(1, Ordering::Relaxed); - } - - /// Snapshot the running counts. - #[must_use] - pub fn snapshot(&self) -> TimeAnomalyStats { - TimeAnomalyStats { - before_min: self.before_min.load(Ordering::Relaxed), - after_max: self.after_max.load(Ordering::Relaxed), - } - } -} - -/// Anomalous committer-timestamp detections. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct TimeAnomalyStats { - /// Count of timestamps earlier than 1990-01-01 UTC. - pub before_min: i64, - /// Count of timestamps more than `maxClockSkew` past wall-clock. - pub after_max: i64, -} - -impl TimeAnomalyStats { - /// Combined count of anomalies on both bounds. - #[must_use] - pub const fn total(&self) -> i64 { - self.before_min + self.after_max - } -} diff --git a/crates/cf-analyzers-plumbing/src/tree_diff.rs b/crates/cf-analyzers-plumbing/src/tree_diff.rs deleted file mode 100644 index 3913a8ef..00000000 --- a/crates/cf-analyzers-plumbing/src/tree_diff.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! `TreeDiff` provider. -//! -//! Produces the `"changes"` fact: the list of file changes between the current -//! commit's tree and the previously seen commit's tree. For the first commit -//! every file is reported as an insert. - -use crate::analyzer::{dep, Analyzer, AnalyzerError, ValueMap}; -use crate::git_model::{Change, ChangeEntry, Changes, Hash}; - -/// Source of trees and tree diffs, decoupling the provider from libgit2. -/// -/// Modelling these as a trait keeps the diff state machine (previous-tree -/// tracking, first-commit handling) testable without a real repository and -/// avoids storing non-`Send` libgit2 handles in the provider. -pub trait TreeSource { - /// The tree id of a commit, used as the previous-tree key between commits. - /// - /// # Errors - /// - /// Returns an [`AnalyzerError`] when the commit or its tree cannot be - /// resolved. - fn commit_tree(&self, commit_hash: Hash) -> Result; - - /// Diff two trees into [`Changes`]. - /// - /// # Errors - /// - /// Returns an [`AnalyzerError`] when either tree cannot be loaded or the - /// diff fails. - fn diff_trees(&self, old_tree: Hash, new_tree: Hash) -> Result; - - /// Every file in a tree as an insert change (the first-commit case). - /// - /// # Errors - /// - /// Returns an [`AnalyzerError`] when the tree cannot be loaded or walked. - fn tree_inserts(&self, tree: Hash) -> Result; -} - -/// `TreeDiff` provider. -/// -/// The dependency map carries the commit hash under `"commit_hash"`; the -/// tree resolution is delegated to the [`TreeSource`]. -pub struct TreeDiff { - source: S, - previous_tree: Option, -} - -impl TreeDiff { - /// Construct a `TreeDiff` over the given tree source (starts with no - /// previous tree). - pub const fn new(source: S) -> Self { - Self { - source, - previous_tree: None, - } - } - - /// Compute the changes for a commit, advancing the previous-tree state: - /// diff against the previous tree if present, otherwise treat every file - /// as an insert; then remember the current tree. - /// - /// # Errors - /// - /// Propagates [`TreeSource`] failures. - pub fn changes_for(&mut self, commit_hash: Hash) -> Result { - let tree = self.source.commit_tree(commit_hash)?; - let diff = match self.previous_tree { - Some(prev) => self.source.diff_trees(prev, tree)?, - None => self.source.tree_inserts(tree)?, - }; - self.previous_tree = Some(tree); - Ok(diff) - } -} - -impl Analyzer for TreeDiff { - fn name(&self) -> &'static str { - "TreeDiff" - } - - fn provides(&self) -> Vec<&'static str> { - vec!["changes"] - } - - fn requires(&self) -> Vec<&'static str> { - vec![] - } - - fn consume(&mut self, deps: &mut ValueMap) -> Result { - let commit_hash = *dep::(deps, "commit_hash")?; - let changes = self.changes_for(commit_hash)?; - let mut out = ValueMap::new(); - out.insert("changes".to_string(), Box::new(changes)); - Ok(out) - } -} - -/// A [`TreeSource`] backed by a libgit2 repository. -pub struct GitTreeSource<'r> { - repo: &'r git2::Repository, -} - -impl<'r> GitTreeSource<'r> { - /// Wrap a borrowed repository as a tree source. - #[must_use] - pub const fn new(repo: &'r git2::Repository) -> Self { - Self { repo } - } - - fn tree_of(&self, commit_hash: Hash) -> Result, AnalyzerError> { - let oid = - git2::Oid::from_bytes(&commit_hash.0).map_err(|e| AnalyzerError::Git(e.to_string()))?; - let commit = self.repo.find_commit(oid)?; - Ok(commit.tree()?) - } -} - -impl TreeSource for GitTreeSource<'_> { - fn commit_tree(&self, commit_hash: Hash) -> Result { - Ok(self.tree_of(commit_hash)?.id().into()) - } - - fn diff_trees(&self, old_tree: Hash, new_tree: Hash) -> Result { - let old_oid = - git2::Oid::from_bytes(&old_tree.0).map_err(|e| AnalyzerError::Git(e.to_string()))?; - let new_oid = - git2::Oid::from_bytes(&new_tree.0).map_err(|e| AnalyzerError::Git(e.to_string()))?; - let old = self.repo.find_tree(old_oid)?; - let new = self.repo.find_tree(new_oid)?; - let diff = self.repo.diff_tree_to_tree(Some(&old), Some(&new), None)?; - - let mut changes: Changes = Vec::new(); - for delta in diff.deltas() { - // Only file blobs participate in the diff; libgit2 deltas are - // per-file already. - let old_file = delta.old_file(); - let new_file = delta.new_file(); - let from = ChangeEntry { - name: old_file - .path() - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_default(), - hash: oid_or_zero(old_file.id()), - }; - let to = ChangeEntry { - name: new_file - .path() - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_default(), - hash: oid_or_zero(new_file.id()), - }; - // Skip pure-rename/copy entries with no content side, keeping - // the changes file-oriented. - if from.name.is_empty() && to.name.is_empty() { - continue; - } - changes.push(Change { from, to }); - } - Ok(changes) - } - - fn tree_inserts(&self, tree: Hash) -> Result { - let oid = git2::Oid::from_bytes(&tree.0).map_err(|e| AnalyzerError::Git(e.to_string()))?; - let tree = self.repo.find_tree(oid)?; - let mut changes: Changes = Vec::new(); - tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| { - if entry.kind() == Some(git2::ObjectType::Blob) { - let name = format!("{}{}", dir, entry.name().unwrap_or_default()); - changes.push(Change { - from: ChangeEntry::default(), - to: ChangeEntry { - name, - hash: entry.id().into(), - }, - }); - } - git2::TreeWalkResult::Ok - })?; - Ok(changes) - } -} - -/// Map a libgit2 oid to a [`Hash`], collapsing the zero oid to [`Hash::ZERO`]. -fn oid_or_zero(oid: git2::Oid) -> Hash { - if oid.is_zero() { - Hash::ZERO - } else { - oid.into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::cell::RefCell; - - fn h(n: u8) -> Hash { - let mut b = [0u8; 20]; - b[0] = n; - Hash(b) - } - - /// Scripted tree source: `commit_hash` -> `tree_hash`, and a queue of - /// diffs to hand back for successive `diff_trees` calls. - struct FakeSource { - diffs: RefCell>, - inserts: Changes, - } - - impl TreeSource for FakeSource { - fn commit_tree(&self, commit_hash: Hash) -> Result { - // Tree id derived from commit id for determinism. - Ok(h(commit_hash.0[0].wrapping_add(100))) - } - fn diff_trees(&self, _old: Hash, _new: Hash) -> Result { - Ok(self.diffs.borrow_mut().remove(0)) - } - fn tree_inserts(&self, _tree: Hash) -> Result { - Ok(self.inserts.clone()) - } - } - - #[test] - fn first_commit_uses_inserts_then_diffs() { - let inserts: Changes = vec![Change { - from: ChangeEntry::default(), - to: ChangeEntry { - name: "a".into(), - hash: h(1), - }, - }]; - let later: Changes = vec![Change { - from: ChangeEntry { - name: "a".into(), - hash: h(1), - }, - to: ChangeEntry { - name: "a".into(), - hash: h(2), - }, - }]; - let src = FakeSource { - diffs: RefCell::new(vec![later.clone()]), - inserts: inserts.clone(), - }; - let mut td = TreeDiff::new(src); - - // First commit -> inserts (no previous tree). - let c1 = td.changes_for(h(10)).unwrap(); - assert_eq!(c1, inserts); - // Second commit -> diff against previous tree. - let c2 = td.changes_for(h(11)).unwrap(); - assert_eq!(c2, later); - } - - // Mirrors reference tests TestTreeDiff_Name / _Provides / _Requires. - #[test] - fn provider_metadata() { - let src = FakeSource { - diffs: RefCell::new(vec![]), - inserts: vec![], - }; - let td = TreeDiff::new(src); - assert_eq!(td.name(), "TreeDiff"); - assert_eq!(td.provides(), vec!["changes"]); - assert!(td.requires().is_empty()); - } -} diff --git a/crates/cf-analyzers-plumbing/src/uast_changes.rs b/crates/cf-analyzers-plumbing/src/uast_changes.rs deleted file mode 100644 index 3889a47b..00000000 --- a/crates/cf-analyzers-plumbing/src/uast_changes.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! `UASTChanges` provider. -//! -//! Produces, per change, the parsed UAST root of the before and/or after blob: -//! * the **before** side is parsed for `Modify` and `Delete`; -//! * the **after** side is parsed for `Modify` and `Insert`. -//! -//! A change is only emitted when at least one side parsed to a node. Blob -//! parsing gates on the path filter, cache membership, parser language -//! support, and a blob-size limit — in this exact order (frozen behavior). - -use std::collections::HashMap; - -use crate::analyzer::{dep, Analyzer, AnalyzerError, ValueMap}; -use crate::blob_cache::CachedBlob; -use crate::git_model::{Action, Change, Changes, Hash}; -use crate::uast_iface::{AllowAllPathFilter, Node, SharedParser, SharedPathFilter}; -use std::sync::Arc; - -/// Maximum blob size (bytes) for UAST parsing. -pub const MAX_UAST_BLOB_SIZE: usize = 256 * 1024; - -/// A change paired with its before/after UAST roots. -#[derive(Debug, Clone)] -pub struct UASTChange { - /// The underlying file change. - pub change: Change, - /// UAST root of the pre-change blob (`None` for inserts and unparsable - /// files). - pub before: Option, - /// UAST root of the post-change blob (`None` for deletes and unparsable - /// files). - pub after: Option, -} - -/// `UASTChanges` provider. -pub struct UASTChanges { - parser: Option, - path_filter: SharedPathFilter, - /// Maximum blob size for parsing; `0` uses [`MAX_UAST_BLOB_SIZE`]. - pub max_blob_size: usize, -} - -impl Default for UASTChanges { - fn default() -> Self { - Self::new() - } -} - -impl UASTChanges { - /// Construct without a parser (every parse yields `None` until one is set), - /// using a permissive path filter. - #[must_use] - pub fn new() -> Self { - Self { - parser: None, - path_filter: Arc::new(AllowAllPathFilter), - max_blob_size: 0, - } - } - - /// Set the path filter used to skip vendor/generated files. - pub fn set_path_filter(&mut self, filter: SharedPathFilter) { - self.path_filter = filter; - } - - /// Build the list of UAST changes for one commit: parse before/after per - /// change and keep only changes with at least one parsed side. - #[must_use] - pub fn build(&self, changes: &Changes, cache: &HashMap) -> Vec { - let mut result: Vec = Vec::new(); - for change in changes { - let before = self.parse_before_version(change, cache); - let after = self.parse_after_version(change, cache); - if before.is_some() || after.is_some() { - result.push(UASTChange { - change: change.clone(), - before, - after, - }); - } - } - result - } - - /// Parse the before side for Modify/Delete. - fn parse_before_version( - &self, - change: &Change, - cache: &HashMap, - ) -> Option { - match change.action() { - Some(Action::Modify | Action::Delete) => { - self.parse_blob(change.from.hash, &change.from.name, cache) - } - _ => None, - } - } - - /// Parse the after side for Modify/Insert. - fn parse_after_version( - &self, - change: &Change, - cache: &HashMap, - ) -> Option { - match change.action() { - Some(Action::Modify | Action::Insert) => { - self.parse_blob(change.to.hash, &change.to.name, cache) - } - _ => None, - } - } - - /// Parse a blob into a UAST root. Gate sequence (frozen): - /// path-filter name exclusion -> cache membership -> parser support -> - /// blob-size limit -> path-filter content exclusion -> parse. - fn parse_blob( - &self, - hash: Hash, - filename: &str, - cache: &HashMap, - ) -> Option { - if self.path_filter.is_excluded(filename) { - return None; - } - let blob = cache.get(&hash)?; - let parser = self.parser.as_ref()?; - if !parser.is_supported(filename) { - return None; - } - let limit = if self.max_blob_size == 0 { - MAX_UAST_BLOB_SIZE - } else { - self.max_blob_size - }; - if blob.data.len() > limit { - return None; - } - if self - .path_filter - .is_excluded_with_content(filename, &blob.data) - { - return None; - } - parser.parse(filename, &blob.data) - } -} - -impl Analyzer for UASTChanges { - fn name(&self) -> &'static str { - "UASTChanges" - } - - fn provides(&self) -> Vec<&'static str> { - vec!["uast_changes"] - } - - fn requires(&self) -> Vec<&'static str> { - vec!["changes", "blob_cache"] - } - - fn configure_uast(&mut self, parser: SharedParser) { - self.parser = Some(parser); - } - - fn consume(&mut self, deps: &mut ValueMap) -> Result { - let changes = dep::(deps, "changes")?.clone(); - let cache = dep::>(deps, "blob_cache")?.clone(); - let result = self.build(&changes, &cache); - let mut out = ValueMap::new(); - out.insert("uast_changes".to_string(), Box::new(result)); - Ok(out) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::git_model::ChangeEntry; - use crate::uast_iface::{NodeLike, Parser}; - - #[derive(Debug)] - struct FakeNode; - impl NodeLike for FakeNode {} - - /// Parser that supports `.go` files and parses any non-empty content. - struct GoParser; - impl Parser for GoParser { - fn is_supported(&self, filename: &str) -> bool { - filename.ends_with(".go") - } - fn parse(&self, _filename: &str, content: &[u8]) -> Option { - if content.is_empty() { - None - } else { - Some(Arc::new(FakeNode) as Node) - } - } - } - - fn h(n: u8) -> Hash { - let mut b = [0u8; 20]; - b[0] = n; - Hash(b) - } - - #[test] - fn modify_parses_both_sides() { - let mut uc = UASTChanges::new(); - uc.configure_uast(Arc::new(GoParser)); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"old".to_vec())); - cache.insert(h(2), CachedBlob::new(b"new".to_vec())); - let changes = vec![Change { - from: ChangeEntry { - name: "f.go".into(), - hash: h(1), - }, - to: ChangeEntry { - name: "f.go".into(), - hash: h(2), - }, - }]; - let out = uc.build(&changes, &cache); - assert_eq!(out.len(), 1); - assert!(out[0].before.is_some()); - assert!(out[0].after.is_some()); - } - - #[test] - fn unsupported_language_is_skipped() { - let mut uc = UASTChanges::new(); - uc.configure_uast(Arc::new(GoParser)); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"x".to_vec())); - // A .txt file is unsupported -> no node -> change dropped. - let changes = vec![Change { - from: ChangeEntry::default(), - to: ChangeEntry { - name: "f.txt".into(), - hash: h(1), - }, - }]; - assert!(uc.build(&changes, &cache).is_empty()); - } - - #[test] - fn oversized_blob_is_skipped() { - let mut uc = UASTChanges::new(); - uc.configure_uast(Arc::new(GoParser)); - uc.max_blob_size = 4; - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"too long".to_vec())); - let changes = vec![Change { - from: ChangeEntry::default(), - to: ChangeEntry { - name: "f.go".into(), - hash: h(1), - }, - }]; - assert!(uc.build(&changes, &cache).is_empty()); - } - - #[test] - fn no_parser_yields_no_changes() { - let uc = UASTChanges::new(); - let mut cache = HashMap::new(); - cache.insert(h(1), CachedBlob::new(b"x".to_vec())); - let changes = vec![Change { - from: ChangeEntry::default(), - to: ChangeEntry { - name: "f.go".into(), - hash: h(1), - }, - }]; - assert!(uc.build(&changes, &cache).is_empty()); - } - - #[test] - fn provider_metadata() { - let uc = UASTChanges::new(); - assert_eq!(uc.name(), "UASTChanges"); - assert_eq!(uc.provides(), vec!["uast_changes"]); - assert_eq!(uc.requires(), vec!["changes", "blob_cache"]); - } -} diff --git a/crates/cf-analyzers-plumbing/src/uast_iface.rs b/crates/cf-analyzers-plumbing/src/uast_iface.rs deleted file mode 100644 index 4405bc7e..00000000 --- a/crates/cf-analyzers-plumbing/src/uast_iface.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Minimal UAST + path-filter interface used by the plumbing providers. -//! -//! The canonical UAST node model and parser live in `cf-uast`/`cf-uast-node`, -//! and path filtering lives in `cf-pathfilter`. This module defines only the -//! minimal surface `UASTChanges` needs (`Parser` with `is_supported`/`parse`, -//! and a `PathFilter` content/name exclusion check) so this crate stays -//! decoupled; it should collapse into re-exports of `cf_uast::Parser` and -//! `cf_pathfilter::Filter` as those surfaces stabilize. - -use std::sync::Arc; - -/// A parsed UAST root, threaded through the pipeline by `UASTChanges`. -/// -/// Modelled as an opaque, refcounted trait object so the plumbing layer is -/// agnostic to `cf-uast-node`'s concrete `Node` type. -pub trait NodeLike: std::fmt::Debug + Send + Sync {} - -/// Shared, cloneable parsed UAST root. -pub type Node = Arc; - -/// A UAST parser (the subset of the parser surface used here). -pub trait Parser: Send + Sync { - /// Whether the file's language is supported. - fn is_supported(&self, filename: &str) -> bool; - - /// Parse a blob into a UAST root, or `None` if it cannot be parsed. - fn parse(&self, filename: &str, content: &[u8]) -> Option; -} - -/// Shared, cloneable handle to a [`Parser`]. -pub type SharedParser = Arc; - -/// Vendor/generated path filtering (the subset of the filter surface used by -/// `UASTChanges`). -pub trait PathFilter: Send + Sync { - /// Whether the path is excluded by name alone (vendor/generated rules). - fn is_excluded(&self, filename: &str) -> bool; - - /// Whether the path is excluded once content is considered (e.g. a - /// "DO NOT EDIT" generated-file header). - fn is_excluded_with_content(&self, filename: &str, content: &[u8]) -> bool; -} - -/// Shared, cloneable handle to a [`PathFilter`]. -pub type SharedPathFilter = Arc; - -/// A [`PathFilter`] that never excludes anything — the conservative default -/// when no real filter is wired (every file is considered). -#[derive(Debug, Clone, Copy, Default)] -pub struct AllowAllPathFilter; - -impl PathFilter for AllowAllPathFilter { - fn is_excluded(&self, _filename: &str) -> bool { - false - } - fn is_excluded_with_content(&self, _filename: &str, _content: &[u8]) -> bool { - false - } -} diff --git a/crates/cf-anomaly/Cargo.toml b/crates/cf-anomaly/Cargo.toml index 95a49a3d..64c59400 100644 --- a/crates/cf-anomaly/Cargo.toml +++ b/crates/cf-anomaly/Cargo.toml @@ -11,24 +11,23 @@ publish = false # (analyzer id: history/anomaly). Also used by the quality and sentiment # analyzers via the cross-analyzer enrichment path. # -# Report serialization routes exclusively through cf-gojson / cf-goyaml; the -# machine-format bytes (json/yaml/ndjson/timeseries/compact/bin) are pinned -# against the reference implementation by tests/compat. +# Report serialization routes exclusively through cf-gojson / cf-goyaml: the +# analyzer builds ONE GoValue tree and the handlers encode it per format. [dependencies] # Mean/stddev/z-score helpers. Every float that reaches a machine report is # produced here, so the arithmetic order is part of the report contract. cf-alg-stats = { path = "../cf-alg-stats" } -# Report-format-compatible JSON encoder. NEVER serde_json. YAML output routes -# the SAME GoValue tree through cf-goyaml once that crate lands; the report -# types expose `to_go_value()` for both paths. +# Report-format JSON encoder (the report types expose `to_go_value()`; the +# yaml path renders the same tree through cf-goyaml). cf-gojson = { path = "../cf-gojson" } -# Bit-exact port of Go's sort.Slice (pdqsort). The anomaly lists are sorted -# with an unstable sort in the reference binary, so tie order on equal -# Z-scores is part of the report contract and must match exactly. +# Deterministic unstable-sort port used for the cross-analyzer enrichment +# lists (`sort_external_results`). cf-gosort = { path = "../cf-gosort" } [lints.clippy] pedantic = "warn" [dev-dependencies] +# The yaml rendering of the report tree is asserted alongside the json one. +cf-goyaml = { path = "../cf-goyaml" } diff --git a/crates/cf-anomaly/src/aggregate.rs b/crates/cf-anomaly/src/aggregate.rs index edb2c8d3..ba836274 100644 --- a/crates/cf-anomaly/src/aggregate.rs +++ b/crates/cf-anomaly/src/aggregate.rs @@ -8,11 +8,9 @@ use crate::model::{CommitAnomalyData, TickMetrics}; /// `commits_by_tick` mapping (commit hash hex -> commit data; /// tick -> ordered list of commit hashes). /// -/// Returns an empty map when either input is empty. -/// -/// `commits_by_tick` preserves the per-tick hash ordering via a `Vec`; the -/// outer `BTreeMap` keys (ticks) are iterated in ascending order, matching -/// the deterministic sorted-key consumption downstream. +/// Lines add up across the tick's commits; files, languages and authors are +/// merged as sets (a file touched by three commits of the tick is ONE +/// changed file). Returns an empty map when either input is empty. /// /// ``` /// use cf_anomaly::aggregate::aggregate_commits_to_ticks; @@ -21,11 +19,13 @@ use crate::model::{CommitAnomalyData, TickMetrics}; /// /// let commit_metrics = BTreeMap::from([ /// ("aaa".to_string(), CommitAnomalyData { -/// files_changed: 3, lines_added: 20, lines_removed: 5, author_id: 1, +/// files_changed: 2, lines_added: 20, lines_removed: 5, author_id: 1, +/// files: vec!["f.txt".into(), "g.txt".into()], /// ..Default::default() /// }), /// ("bbb".to_string(), CommitAnomalyData { -/// files_changed: 2, lines_added: 10, lines_removed: 3, author_id: 2, +/// files_changed: 1, lines_added: 10, lines_removed: 3, author_id: 2, +/// files: vec!["f.txt".into()], /// ..Default::default() /// }), /// ]); @@ -36,7 +36,7 @@ use crate::model::{CommitAnomalyData, TickMetrics}; /// /// let ticks = aggregate_commits_to_ticks(&commit_metrics, &commits_by_tick); /// let tm = &ticks[&0]; -/// assert_eq!(tm.files_changed, 5); // additive merge +/// assert_eq!(tm.files_changed(), 2); // distinct files /// assert_eq!(tm.net_churn, 30 - 8); // added - removed /// assert_eq!(tm.author_ids.len(), 2); // distinct authors /// @@ -80,7 +80,6 @@ fn aggregate_tick_from_commits( let acc = tm.get_or_insert_with(TickMetrics::default); - acc.files_changed += cm.files_changed; acc.lines_added += cm.lines_added; acc.lines_removed += cm.lines_removed; acc.files.extend(cm.files.iter().cloned()); @@ -139,7 +138,7 @@ mod tests { assert_eq!(result.len(), 1); let tm = &result[&0]; - assert_eq!(tm.files_changed, 5); + assert_eq!(tm.files_changed(), 5); assert_eq!(tm.lines_added, 30); assert_eq!(tm.lines_removed, 8); assert_eq!(tm.net_churn, 22); @@ -177,8 +176,50 @@ mod tests { let result = aggregate_commits_to_ticks(&commit_metrics, &commits_by_tick); assert_eq!(result.len(), 2); - assert_eq!(result[&0].files_changed, 3); - assert_eq!(result[&1].files_changed, 2); + assert_eq!(result[&0].files_changed(), 0); + assert_eq!(result[&1].files_changed(), 0); + assert_eq!(result[&0].lines_added, 20); + assert_eq!(result[&1].lines_added, 10); + } + + #[test] + fn files_touched_by_several_commits_count_once() { + // Mirrors the audit fixture: one file modified by three commits of + // the same tick plus a delete — 2 distinct files, not 4 changes. + let mut commit_metrics = BTreeMap::new(); + for (i, c) in (0_i64..).zip(['a', 'b', 'c']) { + commit_metrics.insert( + hash(c), + CommitAnomalyData { + files_changed: 1, + lines_added: 1, + files: vec!["f.txt".into()], + author_id: i, + ..Default::default() + }, + ); + } + commit_metrics.insert( + hash('d'), + CommitAnomalyData { + files_changed: 1, + lines_removed: 2, + files: vec!["g.txt".into()], + ..Default::default() + }, + ); + let commits_by_tick = + BTreeMap::from([(0_i64, vec![hash('a'), hash('b'), hash('c'), hash('d')])]); + let result = aggregate_commits_to_ticks(&commit_metrics, &commits_by_tick); + let tm = &result[&0]; + assert_eq!(tm.files_changed(), 2); + assert_eq!( + tm.files.iter().cloned().collect::>(), + vec!["f.txt".to_string(), "g.txt".to_string()] + ); + assert_eq!(tm.lines_added, 3); + assert_eq!(tm.lines_removed, 2); + assert_eq!(tm.net_churn, 1); } #[test] @@ -211,6 +252,6 @@ mod tests { let result = aggregate_commits_to_ticks(&commit_metrics, &commits_by_tick); assert_eq!(result.len(), 1); - assert_eq!(result[&0].files_changed, 3); + assert_eq!(result[&0].lines_added, 20); } } diff --git a/crates/cf-anomaly/src/config.rs b/crates/cf-anomaly/src/config.rs index 27b088b8..92c77a9e 100644 --- a/crates/cf-anomaly/src/config.rs +++ b/crates/cf-anomaly/src/config.rs @@ -1,36 +1,61 @@ -//! Analyzer configuration: keys, defaults, and validation. +//! Analyzer configuration: the Z-score threshold and the trailing window, +//! validated once at the boundary. -/// Configuration key for the Z-score threshold (`TemporalAnomaly.Threshold`). -pub const CONFIG_ANOMALY_THRESHOLD: &str = "TemporalAnomaly.Threshold"; -/// Configuration key for the window size (`TemporalAnomaly.WindowSize`). -pub const CONFIG_ANOMALY_WINDOW_SIZE: &str = "TemporalAnomaly.WindowSize"; +use std::fmt; /// CLI flag for the threshold option (`--anomaly-threshold`). pub const FLAG_ANOMALY_THRESHOLD: &str = "anomaly-threshold"; /// CLI flag for the window option (`--anomaly-window`). pub const FLAG_ANOMALY_WINDOW: &str = "anomaly-window"; -/// Default Z-score threshold. +/// Default Z-score threshold (standard deviations). pub const DEFAULT_ANOMALY_THRESHOLD: f32 = 2.0; -/// Default sliding window size (20 ticks). +/// Default trailing window size in ticks. pub const DEFAULT_ANOMALY_WINDOW_SIZE: usize = 20; -/// Minimum valid sliding window size. +/// Minimum valid window size: a Z-score needs at least two observations. pub const MIN_WINDOW_SIZE: usize = 2; -/// Minimum valid Z-score threshold. -pub const MIN_THRESHOLD: f32 = 0.1; -/// Read-only analyzer configuration after validation. +/// A rejected configuration value. +#[derive(Debug, Clone, PartialEq)] +pub enum ConfigError { + /// `--anomaly-window` below [`MIN_WINDOW_SIZE`]. + WindowTooSmall(usize), + /// `--anomaly-threshold` not strictly positive (or not a number). + ThresholdNotPositive(f32), +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigError::WindowTooSmall(got) => write!( + f, + "invalid --{FLAG_ANOMALY_WINDOW}: must be >= {MIN_WINDOW_SIZE}, got {got}" + ), + ConfigError::ThresholdNotPositive(got) => { + write!( + f, + "invalid --{FLAG_ANOMALY_THRESHOLD}: must be > 0, got {got}" + ) + } + } + } +} + +impl std::error::Error for ConfigError {} + +/// Validated analyzer configuration. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Config { - /// Z-score threshold (standard deviations). + /// Z-score threshold (standard deviations); a tick is flagged when any + /// metric's `|Z|` strictly exceeds it. pub threshold: f32, - /// Sliding window size in ticks. + /// Trailing window size in ticks. pub window_size: usize, } impl Default for Config { - /// Returns the validated defaults (`threshold = 2.0`, `window = 20`). + /// The defaults: `threshold = 2.0`, `window_size = 20`. fn default() -> Self { Self { threshold: DEFAULT_ANOMALY_THRESHOLD, @@ -40,43 +65,35 @@ impl Default for Config { } impl Config { - /// Clamps out-of-range values back to defaults: a threshold below - /// [`MIN_THRESHOLD`] or a window below [`MIN_WINDOW_SIZE`] falls back to - /// the default. - pub fn validate(&mut self) { - if self.threshold < MIN_THRESHOLD { - self.threshold = DEFAULT_ANOMALY_THRESHOLD; - } - - if self.window_size < MIN_WINDOW_SIZE { - self.window_size = DEFAULT_ANOMALY_WINDOW_SIZE; - } - } - - /// Applies optional threshold/window overrides then validates. Only - /// present values override the current setting. + /// Builds a configuration, rejecting a window below [`MIN_WINDOW_SIZE`] + /// and a threshold that is not strictly positive. /// - /// ``` - /// use cf_anomaly::config::{Config, DEFAULT_ANOMALY_WINDOW_SIZE}; + /// # Errors + /// + /// The [`ConfigError`] naming the rejected value. /// - /// let mut cfg = Config::default(); - /// // `None` leaves the field untouched; present values override then validate. - /// cfg.apply(Some(3.0), None); - /// assert_eq!(cfg.threshold, 3.0); - /// assert_eq!(cfg.window_size, DEFAULT_ANOMALY_WINDOW_SIZE); + /// ``` + /// use cf_anomaly::config::{Config, ConfigError}; /// - /// // An out-of-range window clamps back to the default. - /// cfg.apply(None, Some(0)); - /// assert_eq!(cfg.window_size, DEFAULT_ANOMALY_WINDOW_SIZE); + /// assert_eq!(Config::new(3.0, 30), Ok(Config { threshold: 3.0, window_size: 30 })); + /// assert_eq!(Config::new(2.0, 1), Err(ConfigError::WindowTooSmall(1))); + /// assert_eq!(Config::new(0.0, 20), Err(ConfigError::ThresholdNotPositive(0.0))); + /// assert_eq!( + /// Config::new(2.0, 1).unwrap_err().to_string(), + /// "invalid --anomaly-window: must be >= 2, got 1" + /// ); /// ``` - pub fn apply(&mut self, threshold: Option, window_size: Option) { - if let Some(t) = threshold { - self.threshold = t; + pub fn new(threshold: f32, window_size: usize) -> Result { + if window_size < MIN_WINDOW_SIZE { + return Err(ConfigError::WindowTooSmall(window_size)); } - if let Some(w) = window_size { - self.window_size = w; + if threshold.is_nan() || threshold <= 0.0 { + return Err(ConfigError::ThresholdNotPositive(threshold)); } - self.validate(); + Ok(Self { + threshold, + window_size, + }) } } @@ -85,38 +102,41 @@ mod tests { use super::*; #[test] - fn configure_applies_valid_values() { - // Mirrors reference test TestAnalyzer_Configure. - let mut cfg = Config::default(); - cfg.apply(Some(3.0), Some(30)); - assert!((cfg.threshold - 3.0).abs() < 0.001); - assert_eq!(cfg.window_size, 30); + fn default_is_valid() { + let cfg = Config::default(); + assert_eq!( + Config::new(cfg.threshold, cfg.window_size), + Ok(Config::default()) + ); + assert!((cfg.threshold - DEFAULT_ANOMALY_THRESHOLD).abs() < 0.001); + assert_eq!(cfg.window_size, DEFAULT_ANOMALY_WINDOW_SIZE); } #[test] - fn configure_clamps_invalid_to_defaults() { - // Mirrors reference test TestAnalyzer_Configure_Validation. - let mut cfg = Config::default(); - cfg.apply(Some(-1.0), Some(0)); - assert!((cfg.threshold - DEFAULT_ANOMALY_THRESHOLD).abs() < 0.001); - assert_eq!(cfg.window_size, DEFAULT_ANOMALY_WINDOW_SIZE); + fn rejects_window_below_two() { + assert_eq!(Config::new(2.0, 0), Err(ConfigError::WindowTooSmall(0))); + assert_eq!(Config::new(2.0, 1), Err(ConfigError::WindowTooSmall(1))); + assert!(Config::new(2.0, 2).is_ok()); } #[test] - fn default_is_validated() { - // Mirrors reference test TestAnalyzer_Initialize (defaults after validate). - let cfg = Config::default(); - assert!((cfg.threshold - DEFAULT_ANOMALY_THRESHOLD).abs() < 0.001); - assert_eq!(cfg.window_size, DEFAULT_ANOMALY_WINDOW_SIZE); + fn rejects_non_positive_or_nan_threshold() { + assert_eq!( + Config::new(-1.0, 20), + Err(ConfigError::ThresholdNotPositive(-1.0)) + ); + assert!(matches!( + Config::new(f32::NAN, 20), + Err(ConfigError::ThresholdNotPositive(_)) + )); + assert!(Config::new(0.5, 20).is_ok()); } #[test] - fn window_below_min_falls_back() { - let mut cfg = Config { - threshold: 2.0, - window_size: 1, - }; - cfg.validate(); - assert_eq!(cfg.window_size, DEFAULT_ANOMALY_WINDOW_SIZE); + fn errors_name_the_flag() { + assert_eq!( + ConfigError::ThresholdNotPositive(0.0).to_string(), + "invalid --anomaly-threshold: must be > 0, got 0" + ); } } diff --git a/crates/cf-anomaly/src/detect.rs b/crates/cf-anomaly/src/detect.rs index 5b8d502e..55f8cba8 100644 --- a/crates/cf-anomaly/src/detect.rs +++ b/crates/cf-anomaly/src/detect.rs @@ -5,7 +5,7 @@ use std::collections::BTreeMap; use cf_alg_stats::mean_std_dev; use cf_gosort::go_sort_slice; -use crate::model::{ExternalAnomaly, ExternalSummary, RawMetrics, Record, TickMetrics, ZScoreSet}; +use crate::model::{ExternalAnomaly, ExternalSummary, Record, TickMetrics, ZScoreSet}; use crate::zscore::compute_z_scores; /// Detects anomalies across all six per-tick metrics. @@ -40,11 +40,11 @@ pub fn detect_anomalies_from_ticks( for (i, tick) in ticks.iter().enumerate() { let tm = &tick_metrics[tick]; churn[i] = tm.net_churn as f64; - files[i] = tm.files_changed as f64; + files[i] = tm.files_changed() as f64; added[i] = tm.lines_added as f64; removed[i] = tm.lines_removed as f64; - lang_div[i] = tm.languages.len() as f64; - authors[i] = tm.author_ids.len() as f64; + lang_div[i] = tm.language_diversity() as f64; + authors[i] = tm.author_count() as f64; } let churn_scores = compute_z_scores(&churn, window); @@ -67,11 +67,12 @@ pub fn detect_anomalies_from_ticks( threshold_f, ); - // Sort by max absolute Z-score descending. The reference binary uses - // Go's unstable sort.Slice, so the relative order of ties is fixed by - // pdqsort's deterministic-but-unstable behavior; replicate it exactly - // via cf-gosort rather than a stable sort. - go_sort_slice(&mut anomalies, |a, b| a.max_abs_z_score > b.max_abs_z_score); + // Sort by max absolute Z-score descending; ties stay in tick order. + anomalies.sort_by(|a, b| { + b.max_abs_z_score + .total_cmp(&a.max_abs_z_score) + .then_with(|| a.tick.cmp(&b.tick)) + }); anomalies } @@ -79,7 +80,6 @@ pub fn detect_anomalies_from_ticks( /// Builds anomaly records for ticks whose max-abs Z-score exceeds /// `threshold` (strict). #[allow(clippy::too_many_arguments)] -#[allow(clippy::cast_possible_wrap)] // language/author counts are small fn build_records( ticks: &[i64], tick_metrics: &BTreeMap, @@ -114,15 +114,8 @@ fn build_records( tick: *tick, z_scores: scores, max_abs_z_score: max_abs, - metrics: RawMetrics { - files_changed: tm.files_changed, - lines_added: tm.lines_added, - lines_removed: tm.lines_removed, - net_churn: tm.net_churn, - language_diversity: tm.languages.len() as i64, - author_count: tm.author_ids.len() as i64, - }, - files: tm.files.clone(), + metrics: tm.raw(), + files: tm.files.iter().cloned().collect(), }); } @@ -245,10 +238,10 @@ mod tests { tick_metrics.insert( tick, TickMetrics { - files_changed: 5, lines_added: 20, lines_removed: 10, net_churn: 10, + files: (0..5).map(|f| format!("f{f}.go")).collect(), languages: BTreeMap::from([("Go".to_string(), 1)]), ..Default::default() }, @@ -257,18 +250,43 @@ mod tests { tick_metrics.insert( 10, TickMetrics { - files_changed: 200, lines_added: 5000, lines_removed: 50, net_churn: 4950, + files: (0..200).map(|f| format!("g{f}.go")).collect(), languages: BTreeMap::from([("Go".to_string(), 1), ("Python".to_string(), 1)]), ..Default::default() }, ); let anomalies = detect_anomalies_from_ticks(&tick_metrics, 2.0, 5); - assert!(!anomalies.is_empty(), "should detect the spike"); + assert_eq!(anomalies.len(), 1, "only the spike is flagged"); assert_eq!(anomalies[0].tick, 10); + assert_eq!(anomalies[0].metrics.files_changed, 200); + assert_eq!(anomalies[0].files.len(), 200); + assert_eq!(anomalies[0].files[0], "g0.go"); + } + + #[test] + fn ties_rank_by_tick_after_severity() { + // Two spikes, each after three flat ticks (zero variance -> the same + // sentinel severity): the tie resolves by tick. + let mut tick_metrics = BTreeMap::new(); + for tick in 0..8_i64 { + let churn = if tick == 3 || tick == 7 { 500 } else { 10 }; + tick_metrics.insert( + tick, + TickMetrics { + lines_added: churn, + net_churn: churn, + ..Default::default() + }, + ); + } + let anomalies = detect_anomalies_from_ticks(&tick_metrics, 2.0, 3); + let ticks: Vec = anomalies.iter().map(|a| a.tick).collect(); + assert_eq!(ticks, vec![3, 7], "{anomalies:?}"); + assert!((anomalies[0].max_abs_z_score - anomalies[1].max_abs_z_score).abs() < 1e-9); } #[test] diff --git a/crates/cf-anomaly/src/lib.rs b/crates/cf-anomaly/src/lib.rs index f1c9cbbe..95a2ee68 100644 --- a/crates/cf-anomaly/src/lib.rs +++ b/crates/cf-anomaly/src/lib.rs @@ -7,29 +7,34 @@ //! `quality` and `sentiment` analyzers via the cross-analyzer enrichment path //! ([`enrich`]). //! -//! # Byte-identity +//! # Serialization //! -//! Every report-bearing type implements [`model::ToGoValue`], producing a -//! [`cf_gojson::GoValue`] tree that is serialized through -//! [`cf_gojson::Encoder`] so the machine formats (`json`, `yaml`, `ndjson`, -//! `timeseries`, `compact`, `bin`) follow the report-format contract (pinned -//! against the reference implementation by `tests/compat`). Struct types -//! serialize fields in declaration order with omit-when-empty; the dynamic -//! `languages` / `commit_metrics` maps byte-sort their keys. +//! Every report-bearing type implements [`model::ToGoValue`], producing ONE +//! [`cf_gojson::GoValue`] tree that the machine formats (`json`, `yaml`, +//! `bin`) encode through `cf-gojson` / `cf-goyaml`. Struct types serialize +//! fields in declaration order with omit-when-empty; the dynamic `languages` +//! map byte-sorts its keys; every list is present (`[]` when empty). //! //! # Algorithm //! //! 1. Per-commit metrics are collected ([`model::CommitAnomalyData`]). -//! 2. Commits are aggregated into ticks ([`aggregate::aggregate_commits_to_ticks`]). -//! 3. Trailing-window Z-scores are computed per metric ([`zscore::compute_z_scores`]). -//! 4. A tick is flagged when any metric's `|Z|` exceeds the threshold -//! ([`detect::detect_anomalies_from_ticks`]); ties resolve by descending -//! severity. -//! 5. Aggregate / time-series outputs are produced ([`metrics::compute_all_metrics`]). +//! 2. Commits are aggregated into ticks +//! ([`aggregate::aggregate_commits_to_ticks`]): lines add up, files / +//! languages / authors merge as sets, so `files_changed` is the number +//! of DISTINCT files the tick touched. +//! 3. Trailing-window Z-scores are computed per metric +//! ([`zscore::compute_z_scores`]) over the previous `window_size` ticks +//! (fewer at the start of a history), with at least two observations. +//! 4. A tick is flagged when any metric's `|Z|` strictly exceeds the +//! threshold ([`detect::detect_anomalies_from_ticks`]); the list is sorted +//! by descending severity, then tick. +//! 5. Aggregate / time-series outputs are produced +//! ([`metrics::compute_all_metrics`]). //! -//! When a trailing window has zero variance and the current value differs, a -//! sentinel Z-score of `100.0` ([`cf_alg_stats::Z_SCORE_MAX_SENTINEL`]) is -//! assigned (reference-implementation behavior). +//! When a trailing window of two or more observations has zero variance and +//! the current value differs, the signed sentinel Z-score `±100.0` +//! ([`zscore::Z_SCORE_SENTINEL`]) is assigned. The threshold and the window +//! come from the validated [`config::Config`]. pub mod aggregate; pub mod config; @@ -37,7 +42,6 @@ pub mod detect; pub mod enrich; pub mod metrics; pub mod model; -pub mod store; pub mod zscore; pub use enrich::ANALYZER_NAME_ANOMALY; diff --git a/crates/cf-anomaly/src/metrics.rs b/crates/cf-anomaly/src/metrics.rs index a7165cf4..de4f60ed 100644 --- a/crates/cf-anomaly/src/metrics.rs +++ b/crates/cf-anomaly/src/metrics.rs @@ -4,20 +4,17 @@ use std::collections::BTreeMap; use cf_alg_stats::{mean_std_dev, to_percent}; +use crate::config::Config; use crate::detect::detect_anomalies_from_ticks; use crate::model::{ - AggregateData, ComputedMetrics, ExternalAnomaly, ExternalSummary, RawMetrics, Record, - TickMetrics, TimeSeriesEntry, + AggregateData, ComputedMetrics, ExternalAnomaly, ExternalSummary, Record, TickMetrics, + TimeSeriesEntry, }; use crate::zscore::compute_z_scores; /// Inclusive tick time bounds (start/end) used to annotate time-series -/// entries. -/// -/// Minimal stand-in for the canonical tick-bounds type in `cf-analyze` (not -/// yet wired). Callers that have real bounds pass pre-formatted RFC3339 -/// strings here; an empty string means "no bound" and is omitted from -/// [`TimeSeriesEntry`] output. +/// entries: pre-formatted RFC3339 strings; an empty string means "no bound" +/// and is omitted from the [`TimeSeriesEntry`] output. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct TickBounds { /// Pre-formatted RFC3339 start time, or empty for none. @@ -26,11 +23,7 @@ pub struct TickBounds { pub end_time: String, } -/// Parsed input data for anomaly metric computation. -/// -/// A typed struct (the reference implementation parses it out of an untyped -/// report map). The report-map parsing belongs to the `cf-analyze` conversion -/// hub and is tracked as a framework dependency (see crate todos). +/// The typed input of the anomaly metrics. #[derive(Debug, Clone, Default)] pub struct ReportData { /// Pre-detected anomalies. @@ -39,9 +32,9 @@ pub struct ReportData { pub tick_metrics: BTreeMap, /// Optional per-tick time bounds. pub tick_bounds: BTreeMap, - /// Z-score threshold (single-precision by contract). + /// Z-score threshold the anomalies were detected with. pub threshold: f32, - /// Sliding window size. + /// Trailing window size the anomalies were detected with. pub window_size: usize, /// Cross-analyzer anomalies passed through to the output. pub external_anomalies: Vec, @@ -78,9 +71,9 @@ fn compute_aggregate(input: &ReportData) -> AggregateData { for (i, tick) in ticks.iter().enumerate() { let tm = &input.tick_metrics[tick]; churn[i] = tm.net_churn as f64; - files[i] = tm.files_changed as f64; - lang_div[i] = tm.languages.len() as f64; - authors[i] = tm.author_ids.len() as f64; + files[i] = tm.files_changed() as f64; + lang_div[i] = tm.language_diversity() as f64; + authors[i] = tm.author_count() as f64; } let (churn_mean, churn_stddev) = mean_std_dev(&churn); @@ -132,18 +125,9 @@ fn compute_time_series(input: &ReportData) -> Vec { let mut entry = TimeSeriesEntry { tick: *tick, - metrics: RawMetrics { - files_changed: tm.files_changed, - lines_added: tm.lines_added, - lines_removed: tm.lines_removed, - net_churn: tm.net_churn, - language_diversity: tm.languages.len() as i64, - author_count: tm.author_ids.len() as i64, - }, + metrics: tm.raw(), is_anomaly, churn_z_score: churn_z, - language_diversity: tm.languages.len() as i64, - author_count: tm.author_ids.len() as i64, ..Default::default() }; @@ -174,7 +158,7 @@ pub fn compute_all_metrics(input: &ReportData) -> ComputedMetrics { } /// Builds [`ReportData`] from per-commit metrics, the commits-by-tick -/// mapping, and the analyzer config, running anomaly detection over the +/// mapping, and the validated [`Config`], running anomaly detection over the /// aggregated ticks. /// /// This is the canonical path: aggregate commits to ticks, detect anomalies, @@ -184,19 +168,19 @@ pub fn build_report_data( commit_metrics: &BTreeMap, commits_by_tick: &BTreeMap>, tick_bounds: BTreeMap, - threshold: f32, - window_size: usize, + config: Config, ) -> ReportData { let tick_metrics = crate::aggregate::aggregate_commits_to_ticks(commit_metrics, commits_by_tick); - let anomalies = detect_anomalies_from_ticks(&tick_metrics, threshold, window_size); + let anomalies = + detect_anomalies_from_ticks(&tick_metrics, config.threshold, config.window_size); ReportData { anomalies, tick_metrics, tick_bounds, - threshold, - window_size, + threshold: config.threshold, + window_size: config.window_size, external_anomalies: Vec::new(), external_summaries: Vec::new(), } @@ -257,7 +241,12 @@ mod tests { (2_i64, vec![hash('c')]), ]); - build_report_data(&commit_metrics, &commits_by_tick, BTreeMap::new(), 2.0, 20) + build_report_data( + &commit_metrics, + &commits_by_tick, + BTreeMap::new(), + Config::default(), + ) } #[test] @@ -318,12 +307,88 @@ mod tests { ); commits_by_tick.insert(10, vec![spike]); - let input = build_report_data(&commit_metrics, &commits_by_tick, BTreeMap::new(), 2.0, 5); + let config = Config::new(2.0, 5).expect("valid config"); + let input = build_report_data(&commit_metrics, &commits_by_tick, BTreeMap::new(), config); let computed = compute_all_metrics(&input); assert!(!computed.anomalies.is_empty(), "should detect the spike"); assert!(computed.aggregate.total_anomalies > 0); assert!(computed.aggregate.anomaly_rate > 0.0); + assert_eq!(computed.aggregate.window_size, 5); + } + + #[test] + fn quiet_then_busy_ticks_match_the_hand_computation() { + // Ten quiet ticks (one file each, 10/12 lines alternating) and a busy + // tick: 11 distinct files, 200 added, 10 removed. + let mut commit_metrics = BTreeMap::new(); + let mut commits_by_tick = BTreeMap::new(); + for tick in 0..10_i64 { + let h = format!("{tick:040x}"); + commit_metrics.insert( + h.clone(), + CommitAnomalyData { + files_changed: 1, + lines_added: if tick % 2 == 0 { 10 } else { 12 }, + files: vec![format!("q{tick}.txt")], + ..Default::default() + }, + ); + commits_by_tick.insert(tick, vec![h]); + } + let mut busy = Vec::new(); + for k in 0..20_i64 { + let h = format!("{:040x}", 100 + k); + let file = if k < 10 { + format!("burst{k}.txt") + } else { + "burst0.txt".to_string() + }; + commit_metrics.insert( + h.clone(), + CommitAnomalyData { + files_changed: 1, + lines_added: 10, + files: vec![file], + ..Default::default() + }, + ); + busy.push(h); + } + let del = format!("{:040x}", 200); + commit_metrics.insert( + del.clone(), + CommitAnomalyData { + files_changed: 1, + lines_removed: 10, + files: vec!["q0.txt".to_string()], + ..Default::default() + }, + ); + busy.push(del); + commits_by_tick.insert(10, busy); + + let input = build_report_data( + &commit_metrics, + &commits_by_tick, + BTreeMap::new(), + Config::default(), + ); + let computed = compute_all_metrics(&input); + assert_eq!(computed.anomalies.len(), 1, "{:?}", computed.anomalies); + let busy = &computed.anomalies[0]; + assert_eq!(busy.tick, 10); + // Window mean 11, population std 1: (190 - 11) / 1 and (200 - 11) / 1. + assert!((busy.z_scores.net_churn - 179.0).abs() < 1e-9); + assert!((busy.z_scores.lines_added - 189.0).abs() < 1e-9); + // Every quiet tick touched one file: zero variance -> sentinel. + assert!((busy.z_scores.files_changed - 100.0).abs() < 1e-9); + assert_eq!(busy.metrics.files_changed, 11); + assert_eq!(busy.files.len(), 11); + assert!(busy.files.contains(&"q0.txt".to_string())); + // The second quiet tick is NOT flagged (one observation, no sentinel). + assert!(!computed.time_series[1].is_anomaly); + assert_eq!(computed.aggregate.total_anomalies, 1); } #[test] diff --git a/crates/cf-anomaly/src/model.rs b/crates/cf-anomaly/src/model.rs index 15a46a9d..ac6bd85e 100644 --- a/crates/cf-anomaly/src/model.rs +++ b/crates/cf-anomaly/src/model.rs @@ -1,17 +1,18 @@ //! Data types for the temporal anomaly analyzer. //! -//! Each report-bearing type provides [`ToGoValue::to_go_value`] so it can be -//! serialized through [`cf_gojson`] (and, later, `cf-goyaml`) with the -//! contractual report bytes (pinned by `tests/compat`). +//! Each report-bearing type provides [`ToGoValue::to_go_value`] so the SAME +//! value tree is serialized through `cf-gojson` (json, bin) and `cf-goyaml` +//! (yaml). //! //! # Ordering rules //! //! Wrapper structs serialize their fields in **declaration order** (via //! [`cf_gojson::GoMap::new_struct`]), honoring omit-when-empty. Dynamic report -//! maps such as `commit_metrics` and the per-tick `languages` map serialize -//! with **byte-sorted keys** (via [`cf_gojson::GoMap::new_map`]). +//! maps such as the per-tick `languages` map serialize with **byte-sorted +//! keys** (via [`cf_gojson::GoMap::new_map`]). Every list is present, `[]` +//! when empty. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use cf_gojson::{GoMap, GoValue}; @@ -23,35 +24,68 @@ pub trait ToGoValue { /// Raw metrics collected for a single tick. /// -/// An internal accumulation type — it is never serialized directly. Maps use -/// [`BTreeMap`] so iteration order is deterministic (only map length and -/// additive merging matter to the result). +/// An internal accumulation type — it is never serialized directly. Maps and +/// sets use their B-tree forms so iteration order is deterministic. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct TickMetrics { - /// Number of files changed across the tick. - pub files_changed: i64, /// Lines added across the tick. pub lines_added: i64, /// Lines removed across the tick. pub lines_removed: i64, /// Net churn (`lines_added - lines_removed`). pub net_churn: i64, - /// Files changed in the tick (concatenated across commits). - pub files: Vec, + /// The DISTINCT files touched in the tick, across all its commits. + pub files: BTreeSet, /// Language name -> file count for this tick. pub languages: BTreeMap, /// Unique author IDs seen in this tick. - pub author_ids: std::collections::BTreeSet, + pub author_ids: BTreeSet, +} + +impl TickMetrics { + /// Number of distinct files touched in the tick. + #[must_use] + #[allow(clippy::cast_possible_wrap)] // a file count never reaches i64::MAX + pub fn files_changed(&self) -> i64 { + self.files.len() as i64 + } + + /// Number of distinct languages touched in the tick. + #[must_use] + #[allow(clippy::cast_possible_wrap)] + pub fn language_diversity(&self) -> i64 { + self.languages.len() as i64 + } + + /// Number of distinct authors active in the tick. + #[must_use] + #[allow(clippy::cast_possible_wrap)] + pub fn author_count(&self) -> i64 { + self.author_ids.len() as i64 + } + + /// The tick's raw metric values. + #[must_use] + pub fn raw(&self) -> RawMetrics { + RawMetrics { + files_changed: self.files_changed(), + lines_added: self.lines_added, + lines_removed: self.lines_removed, + net_churn: self.net_churn, + language_diversity: self.language_diversity(), + author_count: self.author_count(), + } + } } -/// Raw metrics for a single commit. +/// Raw metrics for a single commit (the per-commit ndjson record). /// -/// JSON field order and omit-when-empty semantics are part of the report -/// contract: `files_changed, lines_added, lines_removed, net_churn, -/// files (omitempty), languages (omitempty), author_id`. +/// JSON field order and omit-when-empty semantics: `files_changed, +/// lines_added, lines_removed, net_churn, files (omitempty), languages +/// (omitempty), author_id`. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CommitAnomalyData { - /// Files changed in the commit. + /// Distinct files changed in the commit. pub files_changed: i64, /// Lines added in the commit. pub lines_added: i64, @@ -59,7 +93,8 @@ pub struct CommitAnomalyData { pub lines_removed: i64, /// Net churn (`lines_added - lines_removed`). pub net_churn: i64, - /// File names touched by the commit (omitted when empty). + /// File names touched by the commit — a delete names the deleted file + /// (omitted when empty). pub files: Vec, /// Language name -> file count (omitted when empty). pub languages: BTreeMap, @@ -202,9 +237,7 @@ impl ToGoValue for Record { m.push("z_scores", self.z_scores.to_go_value()); m.push("max_abs_z_score", GoValue::Float(self.max_abs_z_score)); m.push("metrics", self.metrics.to_go_value()); - // `files` is always present and renders as `null` when empty - // (report-format contract). - m.push("files", string_array_or_null(&self.files)); + m.push("files", string_array(&self.files)); GoValue::Object(m) } } @@ -276,7 +309,8 @@ impl ToGoValue for AggregateData { /// /// Field order: /// `tick, start_time (omitempty), end_time (omitempty), metrics, -/// is_anomaly, churn_z_score, language_diversity, author_count`. +/// is_anomaly, churn_z_score`. The raw values (including the language and +/// author counts) live under `metrics` only. #[derive(Debug, Clone, Default, PartialEq)] pub struct TimeSeriesEntry { /// Time-period index. @@ -291,10 +325,6 @@ pub struct TimeSeriesEntry { pub is_anomaly: bool, /// Net-churn Z-score for the tick. pub churn_z_score: f64, - /// Distinct languages this tick. - pub language_diversity: i64, - /// Distinct authors this tick. - pub author_count: i64, } impl ToGoValue for TimeSeriesEntry { @@ -310,8 +340,6 @@ impl ToGoValue for TimeSeriesEntry { m.push("metrics", self.metrics.to_go_value()); m.push("is_anomaly", GoValue::Bool(self.is_anomaly)); m.push("churn_z_score", GoValue::Float(self.churn_z_score)); - m.push("language_diversity", GoValue::Int(self.language_diversity)); - m.push("author_count", GoValue::Int(self.author_count)); GoValue::Object(m) } } @@ -408,21 +436,7 @@ impl ComputedMetrics { impl ToGoValue for ComputedMetrics { fn to_go_value(&self) -> GoValue { let mut m = GoMap::new_struct(); - // The report contract distinguishes an absent list from an empty one: - // with a non-empty time series, an empty anomaly set marshals as - // `null` (the main path's `var`-declared append target: no tick was - // flagged), while an EMPTY time series (zero-commit walk, e.g. a - // `--since` cutoff past every commit) takes the reference empty-input - // early return whose anomaly list is materialized — `[]`. Both cases - // oracle-measured; pinned by the differential gate. - m.push( - "anomalies", - if self.anomalies.is_empty() && self.time_series.is_empty() { - GoValue::Array(Vec::new()) - } else { - records_array_or_null(&self.anomalies) - }, - ); + m.push("anomalies", records_array(&self.anomalies)); m.push("time_series", time_series_array(&self.time_series)); m.push("aggregate", self.aggregate.to_go_value()); if !self.external_anomalies.is_empty() { @@ -443,22 +457,11 @@ impl ToGoValue for ComputedMetrics { // --- serialization helpers --- -/// Encodes a string list as a JSON array, never `null` (used where -/// omit-when-empty already guarded against the empty case). +/// Encodes a string list as a JSON array (`[]` when empty). fn string_array(items: &[String]) -> GoValue { GoValue::Array(items.iter().cloned().map(GoValue::Str).collect()) } -/// Encodes a string list that renders as `null` when empty (report-format -/// contract for always-present list fields, e.g. `Record.files`). -fn string_array_or_null(items: &[String]) -> GoValue { - if items.is_empty() { - GoValue::Null - } else { - string_array(items) - } -} - /// Encodes a string-keyed count map with byte-sorted keys (map-origin /// object). fn lang_map(langs: &BTreeMap) -> GoValue { @@ -473,18 +476,6 @@ fn records_array(items: &[Record]) -> GoValue { GoValue::Array(items.iter().map(ToGoValue::to_go_value).collect()) } -/// Encodes the anomaly list with the contractual absent-list behavior: an -/// empty list renders as `null` in JSON but `[]` in YAML, so it maps to -/// [`GoValue::NilSlice`] (which each encoder renders the matching way), not -/// to a bare `null` (which YAML would wrongly render as `null`). -fn records_array_or_null(items: &[Record]) -> GoValue { - if items.is_empty() { - GoValue::NilSlice - } else { - records_array(items) - } -} - fn time_series_array(items: &[TimeSeriesEntry]) -> GoValue { GoValue::Array(items.iter().map(ToGoValue::to_go_value).collect()) } @@ -566,15 +557,14 @@ mod tests { } #[test] - fn record_emits_null_files_when_empty() { + fn record_emits_empty_files_list() { let rec = Record { tick: 4, max_abs_z_score: 5.0, ..Default::default() }; let json = Encoder::marshal().encode_to_string(&rec.to_go_value()); - // `files` is always present => `null` for an empty slice. - assert!(json.contains(r#""files":null"#), "got: {json}"); + assert!(json.contains(r#""files":[]"#), "got: {json}"); } #[test] @@ -583,24 +573,48 @@ mod tests { let json = Encoder::marshal().encode_to_string(&cm.to_go_value()); assert!(!json.contains("external_anomalies"), "got: {json}"); assert!(!json.contains("external_summaries"), "got: {json}"); - // An entirely empty report (zero-commit walk) takes the reference - // empty-input early return: BOTH lists are materialized (`[]`), never - // `null` (oracle-measured; the `null` case needs a non-empty - // time series — see `anomalies_null_with_nonempty_time_series`). assert!(json.contains(r#""anomalies":[]"#), "got: {json}"); assert!(json.contains(r#""time_series":[]"#), "got: {json}"); } #[test] - fn anomalies_null_with_nonempty_time_series() { + fn anomalies_is_an_empty_list_when_nothing_is_flagged() { let cm = ComputedMetrics { time_series: vec![TimeSeriesEntry::default()], ..ComputedMetrics::default() }; let json = Encoder::marshal().encode_to_string(&cm.to_go_value()); - // Main path (non-empty time series), zero detections: the reference - // `var`-declared append target stays nil => `null`. - assert!(json.contains(r#""anomalies":null"#), "got: {json}"); + assert!(json.contains(r#""anomalies":[]"#), "got: {json}"); + let yaml = String::from_utf8(cf_goyaml::marshal(&cm.to_go_value())).unwrap(); + assert!(yaml.contains("anomalies: []"), "got: {yaml}"); + } + + #[test] + fn time_series_entry_carries_the_raw_values_once() { + let entry = TimeSeriesEntry { + tick: 3, + metrics: RawMetrics { + language_diversity: 2, + author_count: 4, + ..Default::default() + }, + ..Default::default() + }; + let json = Encoder::marshal().encode_to_string(&entry.to_go_value()); + assert_eq!( + json, + r#"{"tick":3,"metrics":{"files_changed":0,"lines_added":0,"lines_removed":0,"net_churn":0,"language_diversity":2,"author_count":4},"is_anomaly":false,"churn_z_score":0}"# + ); + } + + #[test] + fn tick_metrics_count_distinct_files() { + let tm = TickMetrics { + files: BTreeSet::from(["a.go".to_string(), "b.go".to_string()]), + ..Default::default() + }; + assert_eq!(tm.files_changed(), 2); + assert_eq!(tm.raw().files_changed, 2); } #[test] diff --git a/crates/cf-anomaly/src/store.rs b/crates/cf-anomaly/src/store.rs deleted file mode 100644 index b6008699..00000000 --- a/crates/cf-anomaly/src/store.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Structured store record-kind constants and the store-write payload -//! mapping (the record set derived from [`ComputedMetrics`]). The actual -//! reader/writer I/O belongs to the `cf-analyze` store layer; see the crate -//! todos. - -use crate::model::{ - AggregateData, ComputedMetrics, ExternalAnomaly, ExternalSummary, Record, TimeSeriesEntry, -}; - -/// Record kind: per-tick `TimeSeriesEntry` records (sorted by tick). -pub const KIND_TIME_SERIES: &str = "time_series"; -/// Record kind: per-anomaly `Record` entries (sorted by Z-score desc). -pub const KIND_ANOMALY_RECORD: &str = "anomaly_record"; -/// Record kind: the single `AggregateData` record. -pub const KIND_AGGREGATE: &str = "aggregate"; -/// Record kind: cross-analyzer `ExternalAnomaly` records. -pub const KIND_EXTERNAL_ANOMALY: &str = "external_anomaly"; -/// Record kind: cross-analyzer `ExternalSummary` records. -pub const KIND_EXTERNAL_SUMMARY: &str = "external_summary"; - -/// The full set of store records derived from computed metrics: the -/// `time_series` slice, the `anomaly_record` slice, and the single -/// `aggregate` record. Borrowing keeps this allocation-free; a store-writer -/// adapter (in `cf-analyze`, once wired) iterates these and writes each kind -/// through the byte-identical codec. -pub struct StoreRecords<'a> { - /// Per-tick time-series entries (kind [`KIND_TIME_SERIES`]). - pub time_series: &'a [TimeSeriesEntry], - /// Per-anomaly records (kind [`KIND_ANOMALY_RECORD`]). - pub anomalies: &'a [Record], - /// Aggregate summary (kind [`KIND_AGGREGATE`]). - pub aggregate: &'a AggregateData, -} - -impl<'a> StoreRecords<'a> { - /// Builds the store-record view over `metrics`. - #[must_use] - pub fn from_metrics(metrics: &'a ComputedMetrics) -> Self { - Self { - time_series: &metrics.time_series, - anomalies: &metrics.anomalies, - aggregate: &metrics.aggregate, - } - } -} - -/// The enrichment record set written after cross-analyzer detection: the -/// `external_anomaly` and `external_summary` slices. -pub struct EnrichmentRecords<'a> { - /// Cross-analyzer anomalies (kind [`KIND_EXTERNAL_ANOMALY`]). - pub external_anomalies: &'a [ExternalAnomaly], - /// Cross-analyzer summaries (kind [`KIND_EXTERNAL_SUMMARY`]). - pub external_summaries: &'a [ExternalSummary], -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn store_records_view_matches_metrics() { - let metrics = ComputedMetrics { - time_series: vec![TimeSeriesEntry { - tick: 7, - ..Default::default() - }], - anomalies: vec![Record { - tick: 7, - ..Default::default() - }], - aggregate: AggregateData { - total_ticks: 1, - ..Default::default() - }, - ..Default::default() - }; - let records = StoreRecords::from_metrics(&metrics); - assert_eq!(records.time_series.len(), 1); - assert_eq!(records.time_series[0].tick, 7); - assert_eq!(records.anomalies.len(), 1); - assert_eq!(records.aggregate.total_ticks, 1); - } - - #[test] - fn kind_constants_are_stable() { - assert_eq!(KIND_TIME_SERIES, "time_series"); - assert_eq!(KIND_ANOMALY_RECORD, "anomaly_record"); - assert_eq!(KIND_AGGREGATE, "aggregate"); - assert_eq!(KIND_EXTERNAL_ANOMALY, "external_anomaly"); - assert_eq!(KIND_EXTERNAL_SUMMARY, "external_summary"); - } -} diff --git a/crates/cf-anomaly/src/zscore.rs b/crates/cf-anomaly/src/zscore.rs index fd03715a..58c31f30 100644 --- a/crates/cf-anomaly/src/zscore.rs +++ b/crates/cf-anomaly/src/zscore.rs @@ -2,85 +2,89 @@ //! //! For each index `i`, the Z-score is computed relative to the trailing //! window of values **before** `i`: `values[max(0, i-window) .. i]` -//! (exclusive of `i`). +//! (exclusive of `i`), with the population standard deviation. A window +//! shorter than `window` (the first ticks of a history) is the trailing +//! window that exists. //! -//! Edge rules (reference-implementation behavior, pinned by the differential -//! gate): -//! * the window for `i == 0` is empty → Z-score `0`; +//! Edge rules: +//! * fewer than [`MIN_WINDOW_SAMPLES`] observations in the window → `0` +//! (one observation has no variance to speak of); //! * zero standard deviation and the value equals the mean → `0`; -//! * zero standard deviation but the value differs → the signed sentinel -//! `copysign(`[`Z_SCORE_MAX_SENTINEL`]`, diff)` (so it may be `-100.0`). +//! * zero standard deviation and the value differs → the signed sentinel +//! `copysign(`[`Z_SCORE_SENTINEL`]`, diff)`. -use cf_alg_stats::{mean_std_dev, Z_SCORE_MAX_SENTINEL}; +use cf_alg_stats::mean_std_dev; /// Sentinel magnitude (`100.0`) for a zero-variance trailing window whose /// current value differs from the window mean. Re-exported from /// `cf_alg_stats`; the emitted value is signed via `copysign`. pub use cf_alg_stats::Z_SCORE_MAX_SENTINEL as Z_SCORE_SENTINEL; +/// The observations a trailing window needs before it yields a Z-score: a +/// single observation has no spread, so neither a finite score nor the +/// zero-variance sentinel is meaningful. +pub const MIN_WINDOW_SAMPLES: usize = 2; + /// Returns, for each index `i`, the trailing-window Z-score of `values[i]`. /// -/// The operation order is part of the float contract: -/// * `window` is clamped up to `1` when `< 1`; -/// * the window is `values[max(0, i-window) .. i]` (**exclusive** of `i`); -/// * an empty window (`i == 0`) yields `0`; -/// * mean/std come from [`cf_alg_stats::mean_std_dev`]; +/// * the window is `values[max(0, i-window) .. i]` (**exclusive** of `i`), +/// `window` clamped up to `1`; +/// * fewer than [`MIN_WINDOW_SAMPLES`] observations yield `0`; +/// * mean/std come from [`cf_alg_stats::mean_std_dev`] (population); /// * zero-std yields `0` when the value equals the mean, else -/// `copysign(Z_SCORE_MAX_SENTINEL, value - mean)`. +/// `copysign(Z_SCORE_SENTINEL, value - mean)`. /// /// ``` /// use cf_anomaly::zscore::{compute_z_scores, Z_SCORE_SENTINEL}; /// -/// // Index 0 always has an empty trailing window → 0. -/// let z = compute_z_scores(&[1.0, 1.0, 1.0, 9.0], 3); +/// // Index 0 (no window) and index 1 (one observation) → 0. +/// let z = compute_z_scores(&[1.0, 5.0, 1.0, 1.0, 9.0], 3); /// assert_eq!(z[0], 0.0); -/// // A flat window then a larger value → +sentinel (zero variance). +/// assert_eq!(z[1], 0.0); +/// // A flat window of two or more then a larger value → +sentinel. +/// let z = compute_z_scores(&[1.0, 1.0, 1.0, 9.0], 3); /// assert_eq!(z[3], Z_SCORE_SENTINEL); /// /// // A smaller value than a flat window → -sentinel (copysign). /// let z2 = compute_z_scores(&[9.0, 9.0, 9.0, 1.0], 3); /// assert_eq!(z2[3], -Z_SCORE_SENTINEL); /// +/// // Hand computation: window [10, 12] has mean 11 and population std 1. +/// let z3 = compute_z_scores(&[10.0, 12.0, 200.0], 20); +/// assert_eq!(z3[2], 189.0); +/// /// // Empty input → empty output. /// assert!(compute_z_scores(&[], 5).is_empty()); /// ``` #[must_use] pub fn compute_z_scores(values: &[f64], window: usize) -> Vec { - let count = values.len(); - if count == 0 { - return Vec::new(); - } - - // A window below 1 is clamped to 1; usize is >= 0, so only 0 needs it. let window = window.max(1); + values + .iter() + .enumerate() + .map(|(i, &value)| { + let start = i.saturating_sub(window); + z_score(value, &values[start..i]) + }) + .collect() +} - let mut scores = vec![0.0_f64; count]; - - for i in 0..count { - let start = i.saturating_sub(window); // max(0, i-window) - let window_slice = &values[start..i]; // EXCLUSIVE of i. - - if window_slice.is_empty() { - scores[i] = 0.0; - continue; - } - - let (mean, stddev) = mean_std_dev(window_slice); - - if stddev == 0.0 { - let diff = values[i] - mean; - scores[i] = if diff == 0.0 { - 0.0 - } else { - Z_SCORE_MAX_SENTINEL.copysign(diff) - }; - continue; +/// The Z-score of `value` against `window` (the observations before it). +fn z_score(value: f64, window: &[f64]) -> f64 { + if window.len() < MIN_WINDOW_SAMPLES { + return 0.0; + } + let (mean, stddev) = mean_std_dev(window); + let diff = value - mean; + if stddev == 0.0 { + if diff == 0.0 { + 0.0 + } else { + Z_SCORE_SENTINEL.copysign(diff) } - - scores[i] = (values[i] - mean) / stddev; + } else { + diff / stddev } - - scores } #[cfg(test)] @@ -94,49 +98,57 @@ mod tests { } #[test] - fn first_index_window_is_empty_zero() { - // i == 0 has an empty trailing window -> 0. + fn first_two_indices_have_no_usable_window() { + // i == 0: empty window; i == 1: one observation -> both 0. let z = compute_z_scores(&[10.0, 20.0, 30.0], 3); assert_eq!(z[0], 0.0); + assert_eq!(z[1], 0.0); + } + + #[test] + fn one_observation_never_yields_the_sentinel() { + // The old rule flagged index 1 with ±100 after a single sample. + let z = compute_z_scores(&[10.0, 12.0], 20); + assert_eq!(z, vec![0.0, 0.0]); } #[test] fn zero_stddev_equal_is_zero() { - // Mirrors reference test TestComputeZScores_ZeroStdDev: flat trailing - // window, value equals the mean -> 0. let z = compute_z_scores(&[5.0, 5.0, 5.0, 5.0], 3); assert_eq!(z, vec![0.0, 0.0, 0.0, 0.0]); } #[test] fn zero_stddev_with_diff_uses_signed_sentinel() { - // Mirrors reference test TestComputeZScores_SentinelOnZeroStdDevWithDiff: - // a flat trailing window then a larger value -> +sentinel. let z = compute_z_scores(&[1.0, 1.0, 1.0, 9.0], 3); - assert_eq!(z[3], Z_SCORE_MAX_SENTINEL); - // A smaller value than a flat window -> -sentinel (copysign). + assert_eq!(z[3], Z_SCORE_SENTINEL); let z2 = compute_z_scores(&[9.0, 9.0, 9.0, 1.0], 3); - assert_eq!(z2[3], -Z_SCORE_MAX_SENTINEL); + assert_eq!(z2[3], -Z_SCORE_SENTINEL); } #[test] - fn basic_spike_finite_positive() { - // Trailing window excludes i; a spike after varied history gives a large - // finite positive z-score. - let z = compute_z_scores(&[1.0, 2.0, 3.0, 2.0, 50.0], 4); - assert!( - z[4] > 1.0, - "spike z-score should be large positive, got {}", - z[4] - ); + fn hand_computed_over_the_documented_window() { + // Ten quiet ticks alternating 10/12: mean 11, population std 1; the + // busy tick's 190 scores (190 - 11) / 1 over the default window. + let mut values: Vec = (0..10) + .map(|i| if i % 2 == 0 { 10.0 } else { 12.0 }) + .collect(); + values.push(190.0); + let z = compute_z_scores(&values, 20); + assert_eq!(z[10], 179.0); + // A three-tick window scores against [12, 10, 12]: mean 34/3, + // population std sqrt(8/9). + let z3 = compute_z_scores(&values, 3); + let mean = 34.0 / 3.0; + let std = (8.0_f64 / 9.0).sqrt(); + assert!((z3[10] - (190.0 - mean) / std).abs() < 1e-9, "{}", z3[10]); + // Index 2 sees [10, 12] (shorter than any window): -1. + assert_eq!(z[2], -1.0); } #[test] - fn window_clamped_to_one() { - // window 0 clamps to 1: each i sees exactly the single prior value, which - // has zero variance, so any change yields the signed sentinel. - let z = compute_z_scores(&[1.0, 2.0], 0); - assert_eq!(z[0], 0.0); - assert_eq!(z[1], Z_SCORE_MAX_SENTINEL); + fn window_clamped_to_one_never_has_two_samples() { + let z = compute_z_scores(&[1.0, 2.0, 3.0], 0); + assert_eq!(z, vec![0.0, 0.0, 0.0]); } } diff --git a/crates/cf-burndown-core/src/file.rs b/crates/cf-burndown-core/src/file.rs index 0e9cbfde..75997592 100644 --- a/crates/cf-burndown-core/src/file.rs +++ b/crates/cf-burndown-core/src/file.rs @@ -6,7 +6,7 @@ use crate::range_query::RangeIndex; use crate::timeline::{TimeKey, Timeline}; use crate::timeline_treap::{Segment, TreapTimeline}; -use crate::{TREE_END, TREE_MERGE_MARK}; +use crate::TREE_END; /// Callback invoked on [`File::update`] with `(current, previous, delta)`. /// @@ -89,20 +89,11 @@ impl File { } } + /// Broadcasts one `(current, previous, delta)` report to every updater. + /// Every value is a legitimate segment value: no tick or packed + /// `(person, tick)` pair is reserved, so no repository content can be + /// silently dropped or rejected here. fn update_time(&mut self, current_time: i64, previous_time: i64, delta: i64) { - let mark = i64::from(TREE_MERGE_MARK); - if previous_time & mark == mark { - if current_time == previous_time { - return; - } - panic!("previousTime cannot be TreeMergeMark"); - } - - if current_time & mark == mark { - // Merge mode - we have already updated in one of the branches. - return; - } - for update in &mut self.updaters { update(current_time, previous_time, delta); } @@ -223,28 +214,6 @@ impl File { self.timeline.merge_adjacent_same_value(); } - /// Combine several prepared files together. - /// - /// # Panics - /// - /// Panics if a line-count mismatch is detected between this file and any - /// other (file corruption). - pub fn merge(&mut self, day: i64, others: &[&File]) { - let mut myself = self.timeline.flatten(); - merge_other_files(&mut myself, others); - self.resolve_merge_conflicts(&mut myself, day); - self.timeline.reconstruct(&myself); - } - - fn resolve_merge_conflicts(&mut self, lines: &mut [i64], day: i64) { - for line in lines.iter_mut() { - if is_merge_marked(*line) { - *line = day; - self.update_time(day, day, 1); - } - } - } - /// Format the underlying line interval tree into a string. Useful for error /// messages and debugging. pub fn dump(&self) -> String { @@ -281,41 +250,6 @@ impl File { } } -/// Check whether a line value has the merge mark bit set. -fn is_merge_marked(value: i64) -> bool { - value & i64::from(TREE_MERGE_MARK) == i64::from(TREE_MERGE_MARK) -} - -/// Merge the flattened lines of `others` into `myself`. -/// -/// # Panics -/// -/// Panics on a line-count mismatch between `myself` and any other file. -fn merge_other_files(myself: &mut [i64], others: &[&File]) { - let mark = i64::from(TREE_MERGE_MARK); - for other in others { - let lines = other.timeline.flatten(); - - if myself.len() != lines.len() { - panic!( - "file corruption, lines number mismatch during merge {} != {}", - myself.len(), - lines.len() - ); - } - - for (my_line, &other_line) in myself.iter_mut().zip(lines.iter()) { - if is_merge_marked(other_line) { - continue; - } - - if is_merge_marked(*my_line) || (*my_line & mark) > (other_line & mark) { - *my_line = other_line; - } - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -454,18 +388,47 @@ mod tests { assert_eq!(file.dump(), "0 0\n50 1\n60 0\n"); } - /// `merge` resolves merge-marked lines to `day` and reconstructs cleanly - /// (equal-length files). + /// Tick 16383 (the former merge-mark sentinel, reached by a 45-year daily + /// history) is an ordinary tick: insertions at it are reported, and a + /// later deletion of those lines reports it as `previous` without + /// panicking. #[test] - fn merge_two_files() { - let mut file1 = File::new(0, 1000, Vec::new()); - let mut file2 = File::new(0, 1000, Vec::new()); - // Grow both equally so merge's equal-length invariant holds. - file1.update(1, 100, 50, 0); - file2.update(1, 100, 50, 0); - file1.merge(2, &[&file2]); - file1.validate(); - assert_eq!(file1.len(), 1050); + fn tick_16383_is_an_ordinary_tick() { + const FORMER_MERGE_MARK: i64 = 16_383; + let calls = Rc::new(RefCell::new(Vec::new())); + let c = calls.clone(); + let updater: Updater = + Box::new(move |cur, prev, delta| c.borrow_mut().push((cur, prev, delta))); + let mut file = File::new(0, 2, vec![updater]); + file.update(FORMER_MERGE_MARK, 2, 2, 0); + file.update(FORMER_MERGE_MARK + 1, 2, 0, 2); + assert_eq!( + *calls.borrow(), + vec![ + (0, 0, 2), + (FORMER_MERGE_MARK, FORMER_MERGE_MARK, 2), + (FORMER_MERGE_MARK + 1, FORMER_MERGE_MARK, -2), + ] + ); + assert_eq!(file.len(), 2); + file.validate(); + } + + /// A zero-length file (an empty blob) is tracked, fires no report on + /// construction, and grows through a later insertion like any other. + #[test] + fn empty_file_grows_through_update() { + let calls = Rc::new(RefCell::new(Vec::new())); + let c = calls.clone(); + let updater: Updater = + Box::new(move |cur, prev, delta| c.borrow_mut().push((cur, prev, delta))); + let mut file = File::new(0, 0, vec![updater]); + assert!(file.is_empty()); + assert!(calls.borrow().is_empty()); + file.update(3, 0, 2, 0); + assert_eq!(file.len(), 2); + assert_eq!(*calls.borrow(), vec![(3, 3, 2)]); + file.validate(); } /// Node-pool shrink integration via `File`. diff --git a/crates/cf-burndown-core/src/lib.rs b/crates/cf-burndown-core/src/lib.rs index db63fd55..26305925 100644 --- a/crates/cf-burndown-core/src/lib.rs +++ b/crates/cf-burndown-core/src/lib.rs @@ -6,7 +6,8 @@ //! # Overview //! //! - [`File`] encapsulates a [`TreapTimeline`] plus a set of [`Updater`] -//! callbacks. [`File::update`] applies an edit (insert/delete a line range at +//! callbacks. A segment value is a plain tick, or a `(person, tick)` pair +//! packed by a [`ValuePacking`] when the caller tracks authors. [`File::update`] applies an edit (insert/delete a line range at //! a position) and notifies the updaters with `(current, previous, delta)` //! reports so the caller can maintain its burndown counters. //! - [`TreapTimeline`] is an implicit treap where a node's position is the size @@ -41,7 +42,7 @@ //! [`File::dump`] string and [`Segment`] values that the burndown analyzer //! serializes downstream. Machine-format report bytes must be produced by the //! shared `cf-gojson` / `cf-goyaml` crates (the byte contract is pinned by -//! `tests/compat`), so this crate intentionally has no serialization +//! the report e2e suites (`bins/codefang/tests`)), so this crate intentionally has no serialization //! dependency: `cf-analyzer-burndown` is responsible for routing [`Segment`] / //! matrix data through `cf-gojson`. @@ -49,35 +50,28 @@ mod file; mod interval; +mod packing; mod range_query; mod timeline; mod timeline_treap; pub use file::{File, Updater}; +pub use packing::{ValuePacking, MAX_TICK_BITS}; pub use range_query::OwnershipSegment; pub use timeline::{DeltaReport, TimeKey, Timeline}; pub use timeline_treap::{Segment, TreapTimeline}; -/// The value of the last leaf in the tree (the `u32::MAX` sentinel). +/// The value of the last leaf in the tree (the `u32::MAX` sentinel). Every +/// packed `(person, tick)` value a [`ValuePacking`] produces is below it. pub const TREE_END: TimeKey = u32::MAX; -/// The binary power corresponding to the maximum tick that can be stored. -pub const TREE_MAX_BIN_POWER: u32 = 14; - -/// The special "day" which disables status updates; used in [`File::merge`]. -/// -/// Equals `(1 << TREE_MAX_BIN_POWER) - 1`. -pub const TREE_MERGE_MARK: TimeKey = (1 << TREE_MAX_BIN_POWER) - 1; - #[cfg(test)] mod tests { use super::*; - /// The exported constants match the reference-implementation values. + /// The tree-end sentinel is the one reserved value. #[test] - fn constants_are_stable() { + fn tree_end_is_the_only_sentinel() { assert_eq!(TREE_END, u32::MAX); - assert_eq!(TREE_MAX_BIN_POWER, 14); - assert_eq!(TREE_MERGE_MARK, 16383); } } diff --git a/crates/cf-burndown-core/src/packing.rs b/crates/cf-burndown-core/src/packing.rs new file mode 100644 index 00000000..56f86739 --- /dev/null +++ b/crates/cf-burndown-core/src/packing.rs @@ -0,0 +1,179 @@ +//! Packing of a `(person, tick)` pair into the 32-bit value a treap segment +//! stores ([`crate::TimeKey`]). +//! +//! The burndown walk keys every line by the tick it was written in and — +//! with per-developer tracking — by the author who wrote it. Both live in one +//! `u32` so a single `replace` report carries both facts for the deleted +//! lines. The split is chosen per walk from the window's last tick: the ticks +//! take exactly the bits they need and every remaining bit goes to the person +//! id, so no fixed sentinel tick can ever collide with real history (the fixed +//! 14-bit tick field of the original design overflowed at tick 16383, which a +//! 45-year daily history reaches). + +/// Width of the packed value in bits. +const VALUE_BITS: u32 = 32; + +/// Upper bound on the tick field width: at least one bit is always left for +/// the person field, so a packed value never reaches [`crate::TREE_END`]. +pub const MAX_TICK_BITS: u32 = VALUE_BITS - 1; + +/// The `(person, tick)` ⇄ value codec of one burndown walk. +/// +/// ``` +/// use cf_burndown_core::ValuePacking; +/// +/// // A walk whose newest commit is at tick 310 keeps 9 bits for ticks. +/// let p = ValuePacking::for_people(310); +/// assert_eq!(p.tick_bits(), 9); +/// let v = p.pack(3, 310); +/// assert_eq!(p.unpack(v), (3, 310)); +/// +/// // Without people tracking the value IS the tick. +/// let t = ValuePacking::ticks_only(); +/// assert_eq!(t.pack(7, 16383), 16383); +/// assert_eq!(t.unpack(16383), (0, 16383)); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ValuePacking { + tick_bits: u32, +} + +impl ValuePacking { + /// The codec of a walk without per-developer tracking: the whole value is + /// the tick (31 bits) and every person folds to the single slot 0. + #[must_use] + pub const fn ticks_only() -> Self { + Self { + tick_bits: MAX_TICK_BITS, + } + } + + /// The codec of a walk with per-developer tracking whose newest commit is + /// at `max_tick`: the tick field is the narrowest one holding `max_tick`, + /// the person field takes the rest. + #[must_use] + pub fn for_people(max_tick: i64) -> Self { + let needed = + VALUE_BITS.saturating_sub(max_tick.max(0).leading_zeros().saturating_sub(VALUE_BITS)); + Self { + tick_bits: needed.clamp(1, MAX_TICK_BITS), + } + } + + /// Width of the tick field. + #[must_use] + pub const fn tick_bits(&self) -> u32 { + self.tick_bits + } + + /// Exclusive upper bound of the tick field. + #[must_use] + pub const fn tick_limit(&self) -> i64 { + 1_i64 << self.tick_bits + } + + /// Exclusive upper bound of the person field. One value below the raw + /// field capacity is kept free so a packed value never equals + /// [`crate::TREE_END`]. + #[must_use] + pub const fn person_limit(&self) -> i64 { + (1_i64 << (VALUE_BITS - self.tick_bits)) - 1 + } + + /// The person slot of lines with no known author: lines that pre-date the + /// window and ids beyond [`ValuePacking::person_limit`]. + #[must_use] + pub const fn unknown_person(&self) -> i64 { + self.person_limit() - 1 + } + + /// Packs `person` and `tick`; an out-of-range person folds to + /// [`ValuePacking::unknown_person`], an out-of-range tick saturates. + #[must_use] + pub fn pack(&self, person: i64, tick: i64) -> i64 { + let tick = tick.clamp(0, self.tick_limit() - 1); + let person = if (0..self.unknown_person()).contains(&person) { + person + } else { + self.unknown_person() + }; + (person << self.tick_bits) | tick + } + + /// Splits a packed value back into `(person, tick)`. + #[must_use] + pub const fn unpack(&self, value: i64) -> (i64, i64) { + (value >> self.tick_bits, value & (self.tick_limit() - 1)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::TREE_END; + + #[test] + fn ticks_only_is_the_identity_on_ticks() { + let p = ValuePacking::ticks_only(); + for tick in [0, 1, 16_383, 16_384, 1 << 20, (1 << 31) - 1] { + assert_eq!(p.pack(0, tick), tick); + assert_eq!(p.pack(99, tick), tick, "every person folds to slot 0"); + assert_eq!(p.unpack(tick), (0, tick)); + } + } + + #[test] + fn people_codec_uses_the_narrowest_tick_field() { + assert_eq!(ValuePacking::for_people(0).tick_bits(), 1); + assert_eq!(ValuePacking::for_people(1).tick_bits(), 1); + assert_eq!(ValuePacking::for_people(2).tick_bits(), 2); + assert_eq!(ValuePacking::for_people(310).tick_bits(), 9); + assert_eq!(ValuePacking::for_people(511).tick_bits(), 9); + assert_eq!(ValuePacking::for_people(512).tick_bits(), 10); + assert_eq!(ValuePacking::for_people(16_384).tick_bits(), 15); + assert_eq!( + ValuePacking::for_people(i64::MAX).tick_bits(), + MAX_TICK_BITS + ); + } + + #[test] + fn people_codec_round_trips_every_person_and_tick_in_range() { + let p = ValuePacking::for_people(16_384); + assert_eq!(p.person_limit(), (1 << 17) - 1); + for person in [0, 1, 2, p.unknown_person() - 1] { + for tick in [0, 16_383, 16_384, p.tick_limit() - 1] { + assert_eq!(p.unpack(p.pack(person, tick)), (person, tick)); + } + } + } + + #[test] + fn out_of_range_persons_fold_to_the_unknown_slot() { + let p = ValuePacking::for_people(310); + let unknown = p.unknown_person(); + assert_eq!(p.unpack(p.pack(-1, 5)), (unknown, 5)); + assert_eq!(p.unpack(p.pack(p.person_limit(), 5)), (unknown, 5)); + assert_eq!(p.unpack(p.pack(unknown, 5)), (unknown, 5)); + assert_eq!(p.unpack(p.pack(i64::MAX, 5)), (unknown, 5)); + } + + #[test] + fn out_of_range_ticks_saturate() { + let p = ValuePacking::for_people(310); + assert_eq!(p.unpack(p.pack(1, -4)), (1, 0)); + assert_eq!(p.unpack(p.pack(1, 1 << 40)), (1, p.tick_limit() - 1)); + } + + #[test] + fn packed_values_never_reach_the_tree_end_sentinel() { + for max_tick in [0, 1, 310, 16_384, (1 << 31) - 1, i64::MAX] { + let p = ValuePacking::for_people(max_tick); + let v = p.pack(p.unknown_person(), p.tick_limit() - 1); + assert!(v < i64::from(TREE_END), "max_tick {max_tick}: {v}"); + assert!(v >= 0); + } + let t = ValuePacking::ticks_only(); + assert!(t.pack(0, i64::MAX) < i64::from(TREE_END)); + } +} diff --git a/crates/cf-burndown-core/src/timeline_treap.rs b/crates/cf-burndown-core/src/timeline_treap.rs index c19a55c4..e86b12b8 100644 --- a/crates/cf-burndown-core/src/timeline_treap.rs +++ b/crates/cf-burndown-core/src/timeline_treap.rs @@ -13,7 +13,7 @@ //! to index reuse. use crate::timeline::{DeltaReport, TimeKey, Timeline}; -use crate::{TREE_END, TREE_MERGE_MARK}; +use crate::TREE_END; /// The initial non-zero seed for the xorshift64 PRNG. /// @@ -340,9 +340,6 @@ impl TreapTimeline { let Some(n) = n else { return }; let node = self.arena[n]; self.validate_node(node.left, last_val); - if node.value == TREE_MERGE_MARK { - panic!("unmerged lines left at segment length {}", node.length); - } *last_val = node.value; self.validate_node(node.right, last_val); } diff --git a/crates/cf-cache/Cargo.toml b/crates/cf-cache/Cargo.toml index 6b71ff09..c5ca2ae0 100644 --- a/crates/cf-cache/Cargo.toml +++ b/crates/cf-cache/Cargo.toml @@ -17,7 +17,7 @@ cf-alg-lru = { path = "../cf-alg-lru" } cf-storage = { path = "../cf-storage" } # Canonical report-format JSON writer (write_json over cf-gojson). The # cache.json metadata bytes are part of the compatibility contract (pinned by -# tests/compat); we route through this, never raw serde_json. +# the report e2e suites in bins/codefang/tests); we route through this, never raw serde_json. cf-textutil = { path = "../cf-textutil" } # SHA-256 cache-key derivation. Util only — never used for machine report # output. diff --git a/crates/cf-cache/src/lib.rs b/crates/cf-cache/src/lib.rs index 92e81b27..9845b7c2 100644 --- a/crates/cf-cache/src/lib.rs +++ b/crates/cf-cache/src/lib.rs @@ -26,7 +26,7 @@ //! Parsing on read uses `serde_json` (decoding never affects output bytes). //! //! Compatibility: output bytes are pinned against the reference implementation -//! by `tests/compat`. +//! by the report e2e suites (`bins/codefang/tests`). //! //! # Example //! diff --git a/crates/cf-checkpoint/src/manager.rs b/crates/cf-checkpoint/src/manager.rs index 60a3eac9..51833b03 100644 --- a/crates/cf-checkpoint/src/manager.rs +++ b/crates/cf-checkpoint/src/manager.rs @@ -150,7 +150,7 @@ impl Manager { /// the [`Metadata`] is written atomically as `checkpoint.json`. /// /// `created_at` is supplied by the caller so the wall clock can be pinned - /// in tests/goldens (DESIGN §2.8); use [`Manager::save_now`] for the + /// in tests; use [`Manager::save_now`] for the /// production behavior (current UTC time, RFC3339). /// /// # Errors diff --git a/crates/cf-clones/Cargo.toml b/crates/cf-clones/Cargo.toml index 0f6e78ce..a3abcdad 100644 --- a/crates/cf-clones/Cargo.toml +++ b/crates/cf-clones/Cargo.toml @@ -7,33 +7,34 @@ license.workspace = true repository.workspace = true publish = false -# cf-clones — static code-clone detection (MinHash 128 + LSH 16x8, fixed -# seeds; analyzer id: static/clones). +# cf-clones — static code-clone detection (analyzer id: static/clones). # -# Report serialization routes exclusively through cf-gojson / cf-reportutil so -# the machine-format bytes (json/yaml/ndjson/timeseries/compact/bin) match the -# report contract byte-for-byte. See specs/rust-rewrite/DESIGN.md §2. -# (YAML routes the SAME GoValue tree through cf-goyaml; ComputedMetrics -# exposes `to_go_value()` for both the JSON and YAML paths.) +# Pipeline: per function, a structural profile (pre-order node types, exact +# shingle set, structure/token fingerprints) + a MinHash signature; one global +# 32x4 LSH index proposes candidate pairs; every candidate is verified by exact +# shingle Jaccard and classified Type-1/2/3 from the fingerprints. +# +# Report serialization routes through cf-gojson / cf-reportutil, never +# serde_json. [dependencies] -# MinHash signatures — pinned seeds/hashing (bit-identical to the reference). +# MinHash signatures (fixed seeds) — the LSH input. cf-alg-minhash = { path = "../cf-alg-minhash" } -# LSH index — pinned band hashing. +# Banded LSH candidate index. cf-alg-lsh = { path = "../cf-alg-lsh" } +# FNV-1a for shingle and fingerprint hashing. +cf-alg-hashutil = { path = "../cf-alg-hashutil" } # UAST node type, traversal, and the UAST/Role string constants. cf-uast-node = { path = "../cf-uast-node" } -# Analyzer contracts (Analyzer/StaticAnalyzer/Report/Descriptor/Thresholds). -cf-analyze = { path = "../cf-analyze" } -# Report-contract JSON encoder (DESIGN §2.2). NEVER serde_json. +# Report-contract JSON values (GoValue / GoMap). cf-gojson = { path = "../cf-gojson" } -# CFB1 binary envelope + report accessors/formatters. +# Report formatting helpers (format_float / pct / format_percent). cf-reportutil = { path = "../cf-reportutil" } -# Frozen Go pdqsort — the reference's sort.Slice permutation is part of the -# terminal/text output contract (tie order among equal-similarity pairs). -cf-gosort = { path = "../cf-gosort" } [lints.clippy] pedantic = "warn" [dev-dependencies] +# Real parsed trees for the detection tests (Go sources through the actual +# UAST parser) — the unit tests exercise the same input the CLI feeds. +cf-uast = { path = "../cf-uast" } diff --git a/crates/cf-clones/src/aggregator.rs b/crates/cf-clones/src/aggregator.rs index 44e3ef5f..cf96b390 100644 --- a/crates/cf-clones/src/aggregator.rs +++ b/crates/cf-clones/src/aggregator.rs @@ -1,38 +1,32 @@ -//! The cross-file clone-detection [`Aggregator`]. -//! -//! Collects the per-file `_func_signatures` exports, qualifies each function -//! name with its source file (`sourceFile::name`), builds one global LSH -//! index, finds cross-file clone pairs, and emits the same report shape as the -//! per-file analyzer. - -use cf_alg_minhash::Signature; -use cf_analyze::{GoMap, GoValue, MapOrigin, Report}; -use cf_uast_node as _; // keep the dependency edge documented; not used directly. - -use crate::analyzer::build_empty_report; -use crate::engine::{build_index, find_clone_pairs, ClonePairResult, FuncEntry}; -use crate::report::{clone_type_dist_map, ClonePair, DEFAULT_MAX_CLONE_PAIRS, SIMILARITY_TYPE3}; +//! The cross-file [`Aggregator`]: collects every file's candidates, qualifies +//! their names with the source file (`path::name`), builds one global LSH +//! index, runs the engine, and produces the [`CloneReport`]. + +use cf_alg_lsh::Index; + +use crate::engine::{find_clone_pairs, IndexedFunc}; +use crate::report::{CloneReport, FileSummary}; +use crate::visitor::FileScan; use crate::{ - clone_message, compute_clone_ratio, KEY_ANALYZER_NAME, KEY_CLONE_PAIRS, KEY_CLONE_RATIO, - KEY_CLONE_TYPE_DISTRIBUTION, KEY_FUNC_SIGNATURES, KEY_MESSAGE, KEY_TOTAL_CLONE_PAIRS, - KEY_TOTAL_FUNCTIONS, MSG_NO_FUNCTIONS, NUM_BANDS, NUM_ROWS, + clone_message, compute_clone_ratio, DEFAULT_MAX_CLONE_PAIRS, MSG_NO_FUNCTIONS, NUM_BANDS, + NUM_ROWS, SIMILARITY_THRESHOLD, }; -/// Collects per-file signatures and performs global cross-file clone +/// Separator between a source file path and a function name. +pub const FILE_NAME_SEPARATOR: &str = "::"; + +/// Collects per-file candidates and performs global cross-file clone /// detection. #[derive(Debug, Clone)] pub struct Aggregator { - entries: Vec, + funcs: Vec, + files: Vec, total_functions: i64, /// Caps the stored clone pairs in the report detail (`0` = unlimited). - /// The `total_clone_pairs` count stays exact. + /// The pair count, distribution and ratio stay exact. pub max_clone_pairs: usize, - /// LSH bands. - pub num_bands: usize, - /// LSH rows per band. - pub num_rows: usize, - /// Type-3 similarity threshold. - pub similarity_type3: f64, + /// Minimum exact Jaccard for a structurally different pair (Type-3). + pub similarity_threshold: f64, } impl Default for Aggregator { @@ -42,236 +36,405 @@ impl Default for Aggregator { } impl Aggregator { - /// Creates a new aggregator with the default parameters. + /// Creates an aggregator with the default cap and threshold. #[must_use] pub fn new() -> Self { Self { - entries: Vec::new(), + funcs: Vec::new(), + files: Vec::new(), total_functions: 0, max_clone_pairs: DEFAULT_MAX_CLONE_PAIRS, - num_bands: NUM_BANDS, - num_rows: NUM_ROWS, - similarity_type3: SIMILARITY_TYPE3, + similarity_threshold: SIMILARITY_THRESHOLD, } } - /// Folds a set of per-file reports into the aggregate. - pub fn aggregate(&mut self, results: &[(String, Report)]) { - for (_, report) in results { - self.total_functions += cf_reportutil::get_int(report, KEY_TOTAL_FUNCTIONS); - self.collect_signatures(report); + /// Folds one file's scan into the aggregate. `path` (relative to the + /// analyzed root) qualifies every candidate name as `path::name`. + pub fn add_file(&mut self, path: &str, scan: FileScan) { + let file = self.files.len(); + let base = self.funcs.len(); + self.total_functions += scan.function_count as i64; + self.files.push(FileSummary { + path: path.to_string(), + total_functions: scan.function_count as i64, + ..FileSummary::default() + }); + for entry in scan.entries { + let mut func = entry.into_indexed(file, base); + func.name = qualify_func_name(&func.name, path); + self.funcs.push(func); } } - /// Extracts the `_func_signatures` entries from one report, qualifying - /// names with the source file. - fn collect_signatures(&mut self, report: &Report) { - let Some(GoValue::Array(items)) = cf_reportutil::get(report, KEY_FUNC_SIGNATURES) else { - return; - }; - - let source_file = extract_source_file(items); - - for item in items { - let GoValue::Map(m) = item else { - continue; - }; - let Some(name) = field_str(m, "name") else { - continue; - }; - let Some(sig) = field_signature(m, "sig") else { - continue; - }; - - let qualified = qualify_func_name(&name, &source_file); - self.entries.push(FuncEntry { - name: qualified, - sig, - }); - } + /// Number of candidates collected so far. + #[must_use] + pub fn candidate_count(&self) -> usize { + self.funcs.len() } - /// Builds the global LSH index, finds cross-file pairs, and emits the + /// Builds the global LSH index, finds every clone pair, and emits the /// report. + /// + /// # Panics + /// + /// Never in practice: [`NUM_BANDS`] × [`NUM_ROWS`] equals the signature + /// length every candidate is built with (asserted by the crate's tests), + /// so the index construction and inserts cannot fail. #[must_use] - pub fn get_result(&self) -> Report { + pub fn finish(&self) -> CloneReport { + let mut files = self.files.clone(); if self.total_functions == 0 { - return build_empty_report(MSG_NO_FUNCTIONS); + return CloneReport { + message: MSG_NO_FUNCTIONS.to_string(), + files, + ..CloneReport::default() + }; } - let result = self.detect_global_clones(); - - let clone_ratio = - compute_clone_ratio(result.cloned_func.len(), self.total_functions as usize); - let message = clone_message(result.total_count); - - let pairs_value = GoValue::Array( - result - .pairs - .iter() - .map(ClonePair::to_go_value) - .collect::>(), - ); - let dist = clone_type_dist_map(result.type_distribution); - - let mut report = GoMap::new(MapOrigin::Map); - report.push( - KEY_ANALYZER_NAME, - GoValue::Str(crate::ANALYZER_NAME.to_string()), - ); - report.push(KEY_TOTAL_FUNCTIONS, GoValue::Int(self.total_functions)); - report.push( - KEY_TOTAL_CLONE_PAIRS, - GoValue::Int(result.total_count as i64), + let mut index = Index::new(NUM_BANDS, NUM_ROWS).expect("positive LSH parameters"); + for (i, f) in self.funcs.iter().enumerate() { + index + .insert(i, &f.sig) + .expect("signature length matches the LSH parameters"); + } + let result = find_clone_pairs( + &self.funcs, + &index, + self.max_clone_pairs, + self.similarity_threshold, ); - report.push(KEY_CLONE_RATIO, GoValue::Float(clone_ratio)); - report.push(KEY_CLONE_PAIRS, pairs_value); - report.push(KEY_CLONE_TYPE_DISTRIBUTION, dist); - report.push(KEY_MESSAGE, GoValue::Str(message.to_string())); - report - } - /// Builds one global LSH index and finds clone pairs. - fn detect_global_clones(&self) -> ClonePairResult { - if self.entries.is_empty() { - return ClonePairResult::default(); + for (i, f) in self.funcs.iter().enumerate() { + if result.cloned[i] { + files[f.file].cloned_functions += 1; + } + } + for (pair, &(fa, fb)) in result.pair_files.iter().enumerate() { + files[fa].pairs.push(pair); + if fb != fa { + files[fb].pairs.push(pair); + } } - let Some(idx) = build_index(&self.entries, self.num_bands, self.num_rows) else { - return ClonePairResult::default(); - }; - - find_clone_pairs( - &self.entries, - &idx, - self.max_clone_pairs, - self.similarity_type3, - ) + let cloned = result.cloned.iter().filter(|c| **c).count(); + let clone_ratio = compute_clone_ratio(cloned, self.total_functions as usize); + CloneReport { + total_functions: self.total_functions, + total_clone_pairs: result.total_count as i64, + clone_ratio, + distribution: result.distribution, + pairs: result.pairs, + message: clone_message(clone_ratio).to_string(), + files, + } } } -/// Returns `sourceFile::name` if `source_file` is non-empty, else `name`. +/// Returns `path::name` if `path` is non-empty, else `name`. #[must_use] -fn qualify_func_name(name: &str, source_file: &str) -> String { - if source_file.is_empty() { +fn qualify_func_name(name: &str, path: &str) -> String { + if path.is_empty() { name.to_string() } else { - format!("{source_file}::{name}") + format!("{path}{FILE_NAME_SEPARATOR}{name}") } } -/// Reads `_source_file` from the first signature entry. -#[must_use] -fn extract_source_file(items: &[GoValue]) -> String { - let Some(first) = items.first() else { - return String::new(); - }; - let GoValue::Map(m) = first else { - return String::new(); - }; - field_str(m, "_source_file").unwrap_or_default() +#[cfg(test)] +mod tests { + use super::*; + use crate::report::CloneType; + use crate::visitor::Visitor; + use cf_uast::Parser; + + const PROCESS_ITEMS: &str = r#"package main + +import "fmt" + +func ProcessItems(items []int) int { + total := 0 + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total) + return total +} +"#; + + /// Every identifier and literal of [`PROCESS_ITEMS`] renamed. + const SUM_VALUES: &str = r#"package main + +import "fmt" + +func SumValues(values []int) int { + acc := 0 + for idx, val := range values { + if val%2 == 0 { + acc += val * idx + } else { + acc -= val + } + } + fmt.Println("acc", acc) + return acc } +"#; -/// Reads a string field from a map entry, if present and a string. -fn field_str(m: &GoMap, key: &str) -> Option { - m.entries() - .iter() - .find_map(|(k, v)| match (k.as_str() == key, v) { - (true, GoValue::Str(s)) => Some(s.clone()), - _ => None, - }) + /// [`PROCESS_ITEMS`] with one statement and one call argument added. + const PROCESS_ITEMS_PLUS: &str = r#"package main + +import "fmt" + +func ProcessItemsPlus(items []int) int { + total := 0 + count := len(items) + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total, count) + return total } +"#; + + /// [`PROCESS_ITEMS`] with a comment added and reformatted. + const PROCESS_ITEMS_COMMENTED: &str = r#"package main -/// Reads a signature field from a map entry (the big-endian byte array produced -/// by [`crate::visitor::build_signature_report`]) and decodes it. -fn field_signature(m: &GoMap, key: &str) -> Option { - let value = m.entries().iter().find(|(k, _)| k == key).map(|(_, v)| v)?; - let GoValue::Array(bytes) = value else { - return None; - }; - let raw: Vec = bytes - .iter() - .filter_map(|v| match v { - GoValue::Uint(b) => u8::try_from(*b).ok(), - GoValue::Int(b) => u8::try_from(*b).ok(), - _ => None, - }) - .collect(); - Signature::from_bytes(&raw).ok() +import "fmt" + +// ProcessItems folds the items. +func ProcessItems(items []int) int { + total := 0 // running sum + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total) + return total } +"#; -#[cfg(test)] -mod tests { - use super::*; - use crate::uast::NodeBuilder; - use crate::visitor::Visitor; - use cf_uast_node::Node; - - fn function(name: &str) -> Node { - let name_node = NodeBuilder::new("Identifier") - .role("Name") - .token(name) - .build(); - let mut f = NodeBuilder::new("Function") - .role("Function") - .child(name_node) - .build(); - let mut block = NodeBuilder::new("Block").build(); - for i in 0..24 { - let kind = ["Identifier", "Call", "Literal", "Operator"][i % 4]; - block.add_child(NodeBuilder::new(kind).build()); + const DESCRIBE: &str = r#"package main + +import "strings" + +type Server struct { + name string + tags map[string][]string +} + +func (s *Server) Describe(prefix string) string { + var b strings.Builder + b.WriteString(prefix) + b.WriteString(": ") + b.WriteString(s.name) + for k, vs := range s.tags { + b.WriteString(" ") + b.WriteString(k) + b.WriteString("=") + b.WriteString(strings.Join(vs, ",")) + } + return strings.ToUpper(b.String()) +} +"#; + + const TINY_ADD: &str = "package main\n\nfunc Add(a, b int) int {\n\treturn a + b\n}\n"; + + /// Exact 5-shingle Jaccard of `PROCESS_ITEMS` vs `PROCESS_ITEMS_PLUS`. + /// Exact 5-shingle Jaccard of `PROCESS_ITEMS` vs `PROCESS_ITEMS_PLUS`: + /// the shared `for`/`if`/`else` body contributes the common shingles, + /// the extra `count` statement and call argument the differing ones. + /// (41/64 before the `else_clause` wrapper was collapsed in the Go + /// mapping; one shared node fewer removes one common and one union + /// shingle.) + const T3_EXPECTED: f64 = 40.0 / 63.0; + + fn report(files: &[(&str, &str)]) -> CloneReport { + let parser = Parser::new(); + let mut agg = Aggregator::new(); + for (path, src) in files { + let root = parser.parse(path, src.as_bytes()).expect("parses"); + agg.add_file(path, Visitor::new().scan(&root)); } - f.add_child(block); - f + agg.finish() } - fn file_report(funcs: &[&str]) -> Report { - let mut v = Visitor::new(); - let mut file = NodeBuilder::new("File"); - for name in funcs { - file = file.child(function(name)); - } - let root = file.build(); - root.visit_pre_order(&mut |n: &Node| v.on_enter(n)); - v.get_report() + fn only_pair(r: &CloneReport) -> &crate::ClonePair { + assert_eq!(r.pairs.len(), 1, "{:?}", r.pairs); + assert_eq!(r.total_clone_pairs, 1); + &r.pairs[0] } #[test] - fn qualify_with_and_without_source() { + fn qualify_with_and_without_path() { assert_eq!(qualify_func_name("Foo", ""), "Foo"); assert_eq!(qualify_func_name("Foo", "a.go"), "a.go::Foo"); } #[test] - fn no_functions_returns_empty_report() { - let agg = Aggregator::new(); - let report = agg.get_result(); + fn no_functions_returns_the_empty_report() { + let r = report(&[("a.go", "package main\n")]); + assert_eq!(r.message, MSG_NO_FUNCTIONS); + assert_eq!(r.total_functions, 0); + assert_eq!(r.files.len(), 1); + } + + #[test] + fn identical_pair_is_type1_with_similarity_one() { + let r = report(&[("a.go", PROCESS_ITEMS), ("b.go", PROCESS_ITEMS)]); + let p = only_pair(&r); + assert_eq!(p.func_a, "a.go::ProcessItems"); + assert_eq!(p.func_b, "b.go::ProcessItems"); + assert_eq!(p.clone_type, CloneType::Type1); + assert!((p.similarity - 1.0).abs() < 1e-12); + assert!((r.clone_ratio - 1.0).abs() < 1e-12); + assert_eq!(r.message, crate::MSG_HIGH_CLONES); + assert_eq!(r.distribution.type1, 1); + } + + #[test] + fn comment_only_differences_are_still_type1() { + let r = report(&[("a.go", PROCESS_ITEMS), ("b.go", PROCESS_ITEMS_COMMENTED)]); + assert_eq!(only_pair(&r).clone_type, CloneType::Type1); + } + + #[test] + fn renamed_pair_is_type2() { + let r = report(&[("a.go", PROCESS_ITEMS), ("c.go", SUM_VALUES)]); + let p = only_pair(&r); + assert_eq!(p.clone_type, CloneType::Type2); + assert!((p.similarity - 1.0).abs() < 1e-12); + } + + #[test] + fn added_statement_pair_is_type3_with_exact_jaccard() { + let r = report(&[("a.go", PROCESS_ITEMS), ("d.go", PROCESS_ITEMS_PLUS)]); + let p = only_pair(&r); + assert_eq!(p.clone_type, CloneType::Type3); + assert!( + (p.similarity - T3_EXPECTED).abs() < 1e-9, + "similarity {} != {T3_EXPECTED}", + p.similarity + ); + } + + #[test] + fn unrelated_functions_are_not_paired() { + let r = report(&[("a.go", PROCESS_ITEMS), ("e.go", DESCRIBE)]); + assert_eq!(r.total_clone_pairs, 0); + assert!(r.pairs.is_empty()); + assert_eq!(r.message, crate::MSG_NO_CLONES); + assert!((r.clone_ratio).abs() < 1e-12); + } + + #[test] + fn single_function_has_no_self_pair() { + let r = report(&[("a.go", PROCESS_ITEMS)]); + assert_eq!(r.total_functions, 1); + assert_eq!(r.total_clone_pairs, 0); + } + + #[test] + fn three_copies_yield_three_pairs_in_name_order() { + let r = report(&[ + ("a.go", PROCESS_ITEMS), + ("b.go", PROCESS_ITEMS), + ("g.go", PROCESS_ITEMS), + ]); + assert_eq!(r.total_clone_pairs, 3); + let names: Vec = r.pairs.iter().map(crate::ClonePair::display_name).collect(); assert_eq!( - cf_reportutil::get_string(&report, KEY_MESSAGE), - MSG_NO_FUNCTIONS + names, + vec![ + "a.go::ProcessItems <-> b.go::ProcessItems", + "a.go::ProcessItems <-> g.go::ProcessItems", + "b.go::ProcessItems <-> g.go::ProcessItems", + ] ); } #[test] - fn cross_file_identical_functions_form_a_clone_pair() { - let mut agg = Aggregator::new(); - let r1 = file_report(&["foo"]); - let r2 = file_report(&["bar"]); - agg.aggregate(&[("f1".into(), r1), ("f2".into(), r2)]); - - let report = agg.get_result(); - assert_eq!(cf_reportutil::get_int(&report, KEY_TOTAL_FUNCTIONS), 2); - // Two structurally identical functions across files -> one clone pair. - assert_eq!(cf_reportutil::get_int(&report, KEY_TOTAL_CLONE_PAIRS), 1); + fn small_functions_are_counted_but_never_paired() { + let r = report(&[("a.go", TINY_ADD), ("b.go", TINY_ADD)]); + assert_eq!(r.total_functions, 2); + assert_eq!(r.total_clone_pairs, 0); } #[test] - fn signature_round_trips_through_export() { - // A signature carried through the report export and re-decoded must - // compare identical to the original. - let report = file_report(&["foo"]); + fn per_file_summaries_track_cloned_functions_and_pairs() { + let r = report(&[ + ("a.go", PROCESS_ITEMS), + ("b.go", PROCESS_ITEMS), + ("e.go", DESCRIBE), + ]); + assert_eq!(r.files.len(), 3); + assert_eq!(r.files[0].path, "a.go"); + assert_eq!(r.files[0].total_functions, 1); + assert_eq!(r.files[0].cloned_functions, 1); + assert_eq!(r.files[0].pairs, vec![0]); + assert_eq!(r.files[1].pairs, vec![0]); + assert_eq!(r.files[2].cloned_functions, 0); + assert!(r.files[2].pairs.is_empty()); + // 2 cloned of 3 functions. + assert!((r.clone_ratio - 2.0 / 3.0).abs() < 1e-12); + assert_eq!(r.message, crate::MSG_HIGH_CLONES); + } + + #[test] + fn ratio_bands_select_the_message() { + // 2 cloned functions out of 40 => ratio 0.05 => low. + let mut files: Vec<(String, &str)> = + (0..38).map(|i| (format!("t{i}.go"), TINY_ADD)).collect(); + files.push(("a.go".to_string(), PROCESS_ITEMS)); + files.push(("b.go".to_string(), PROCESS_ITEMS)); + let refs: Vec<(&str, &str)> = files.iter().map(|(p, s)| (p.as_str(), *s)).collect(); + let r = report(&refs); + assert_eq!(r.total_functions, 40); + assert!((r.clone_ratio - 0.05).abs() < 1e-12); + assert_eq!(r.message, crate::MSG_LOW_CLONES); + } + + /// Scale gate for the pathological "every function is identical" input: + /// 4000 copies form 7,998,000 pairs, which must be enumerated and + /// classified well under 10 s (the old string-keyed engine took ~50 s and + /// 1.7 GB). Run with: + /// `cargo test --release -p cf-clones -- --ignored identical_functions` + #[test] + #[ignore = "release-mode scale gate; run with --release -- --ignored"] + fn four_thousand_identical_functions_finish_under_ten_seconds() { + const COPIES: usize = 4000; + const BUDGET_SECS: u64 = 10; + + let parser = Parser::new(); + let root = parser + .parse("f.go", PROCESS_ITEMS.as_bytes()) + .expect("parses"); + let scan = Visitor::new().scan(&root); let mut agg = Aggregator::new(); - agg.aggregate(&[("f".into(), report)]); - assert_eq!(agg.entries.len(), 1); + for i in 0..COPIES { + agg.add_file(&format!("f{i}.go"), scan.clone()); + } + + let start = std::time::Instant::now(); + let r = agg.finish(); + let elapsed = start.elapsed(); + + assert_eq!(r.total_clone_pairs as usize, COPIES * (COPIES - 1) / 2); + assert_eq!(r.pairs.len(), DEFAULT_MAX_CLONE_PAIRS); + assert!((r.clone_ratio - 1.0).abs() < 1e-12); + assert!( + elapsed.as_secs() < BUDGET_SECS, + "took {elapsed:?}, budget {BUDGET_SECS}s" + ); } } diff --git a/crates/cf-clones/src/analyzer.rs b/crates/cf-clones/src/analyzer.rs deleted file mode 100644 index 27d49536..00000000 --- a/crates/cf-clones/src/analyzer.rs +++ /dev/null @@ -1,427 +0,0 @@ -//! The per-file clone-detection [`Analyzer`]. -//! -//! Builds `MinHash` signatures for every function in a UAST, indexes them with -//! LSH, finds the clone pairs, and produces the `analyze::Report` map plus the -//! machine-format projections. - -use std::io::Write; - -use cf_analyze::descriptor::{new_descriptor, AnalyzerMode, Descriptor}; -use cf_analyze::history::AnalyzerError; -use cf_analyze::{GoMap, GoValue, MapOrigin, Report, Thresholds}; -use cf_uast_node::Node; - -use crate::engine::{ - build_index, build_signatures, count_distinct_funcs, find_clone_pairs, is_function_node, -}; -use crate::report::{ - categorize_clone_pairs, clone_type_dist_map, ClonePair, ComputedMetrics, SIMILARITY_TYPE3, -}; -use crate::shingler::Shingler; -use crate::uast::{ROLE_FUNCTION, UAST_FUNCTION, UAST_METHOD}; -use crate::{ - clone_message, compute_clone_ratio, ANALYZER_DESCRIPTION, ANALYZER_FLAG, ANALYZER_NAME, - KEY_ANALYZER_NAME, KEY_CLONE_PAIRS, KEY_CLONE_RATIO, KEY_CLONE_TYPE_DISTRIBUTION, KEY_MESSAGE, - KEY_TOTAL_CLONE_PAIRS, KEY_TOTAL_FUNCTIONS, MSG_EMPTY_AST, MSG_NO_FUNCTIONS, NUM_BANDS, - NUM_HASHES, NUM_ROWS, THRESHOLD_CLONE_PAIRS_RED, THRESHOLD_CLONE_PAIRS_YELLOW, - THRESHOLD_CLONE_RATIO_RED, THRESHOLD_CLONE_RATIO_YELLOW, -}; - -/// Clone-detection analyzer using `MinHash` and LSH. -#[derive(Debug, Clone)] -pub struct Analyzer { - shingler: Shingler, - cfg_num_hashes: usize, - cfg_num_bands: usize, - cfg_num_rows: usize, - cfg_similarity_type3: f64, -} - -impl Default for Analyzer { - fn default() -> Self { - Self::new() - } -} - -impl Analyzer { - /// Creates a new clone-detection analyzer with the default parameters. - #[must_use] - pub fn new() -> Self { - Self { - shingler: Shingler::default(), - cfg_num_hashes: NUM_HASHES, - cfg_num_bands: NUM_BANDS, - cfg_num_rows: NUM_ROWS, - cfg_similarity_type3: SIMILARITY_TYPE3, - } - } - - /// The analyzer name. - #[must_use] - pub fn name(&self) -> &'static str { - ANALYZER_NAME - } - - /// The CLI flag. - #[must_use] - pub fn flag(&self) -> &'static str { - ANALYZER_FLAG - } - - /// Stable analyzer metadata. - #[must_use] - pub fn descriptor(&self) -> Descriptor { - new_descriptor(AnalyzerMode::Static, ANALYZER_NAME, ANALYZER_DESCRIPTION) - } - - /// Color-coded thresholds for clone metrics. - #[must_use] - pub fn thresholds(&self) -> Thresholds { - let mut ratio = GoMap::new(MapOrigin::Map); - ratio.push("green", GoValue::Float(0.0)); - ratio.push("yellow", GoValue::Float(THRESHOLD_CLONE_RATIO_YELLOW)); - ratio.push("red", GoValue::Float(THRESHOLD_CLONE_RATIO_RED)); - - let mut pairs = GoMap::new(MapOrigin::Map); - pairs.push("green", GoValue::Int(0)); - pairs.push("yellow", GoValue::Int(THRESHOLD_CLONE_PAIRS_YELLOW)); - pairs.push("red", GoValue::Int(THRESHOLD_CLONE_PAIRS_RED)); - - let mut t = GoMap::new(MapOrigin::Map); - t.push("clone_ratio", GoValue::Object(ratio)); - t.push("total_clone_pairs", GoValue::Object(pairs)); - t - } - - /// Analyzes a UAST root into a clone-detection [`Report`]. - /// - /// A `None` root yields the "No AST provided" empty report; no functions - /// yields the "No functions found" empty report. - #[must_use] - pub fn analyze_node(&self, root: Option<&Node>) -> Report { - let Some(root) = root else { - return build_empty_report(MSG_EMPTY_AST); - }; - - let functions = self.find_functions(root); - if functions.is_empty() { - return build_empty_report(MSG_NO_FUNCTIONS); - } - - let pairs = self.detect_clones(&functions); - self.build_report(functions.len(), &pairs) - } - - /// Finds all function and method nodes in the UAST: collects by type - /// (`Function`/`Method`) then by the `Function` role, deduplicating by node - /// identity in that order. - #[allow(clippy::unused_self)] // method for symmetry with detect_clones - fn find_functions<'a>(&self, root: &'a Node) -> Vec<&'a Node> { - let type_nodes = root.find(|n| n.has_any_type(&[UAST_FUNCTION, UAST_METHOD])); - let role_nodes = root.find(|n| n.has_any_role(&[ROLE_FUNCTION])); - - let mut seen: Vec<*const Node> = Vec::new(); - let mut functions: Vec<&Node> = Vec::new(); - - let consider = |n: &'a Node, seen: &mut Vec<*const Node>, out: &mut Vec<&'a Node>| { - let ptr = std::ptr::from_ref::(n); - if !seen.contains(&ptr) && is_function_node(n) { - seen.push(ptr); - out.push(n); - } - }; - - for n in type_nodes { - consider(n, &mut seen, &mut functions); - } - for n in role_nodes { - consider(n, &mut seen, &mut functions); - } - - functions - } - - /// Builds `MinHash` signatures, indexes them, and finds clone pairs - /// (per-file: no cap, threshold = Type-3). - fn detect_clones(&self, functions: &[&Node]) -> Vec { - let entries = build_signatures(functions, &self.shingler, self.cfg_num_hashes); - if entries.is_empty() { - return Vec::new(); - } - - let Some(idx) = build_index(&entries, self.cfg_num_bands, self.cfg_num_rows) else { - return Vec::new(); - }; - - // Per-file detection: no cap (single-file scope, bounded by func count). - find_clone_pairs(&entries, &idx, 0, self.cfg_similarity_type3).pairs - } - - /// Constructs the analysis report map. - #[allow(clippy::unused_self)] // method for symmetry with analyze_node - fn build_report(&self, total_functions: usize, pairs: &[ClonePair]) -> Report { - let clone_ratio = compute_clone_ratio(count_distinct_funcs(pairs), total_functions); - let message = clone_message(pairs.len()); - - let pairs_value = - GoValue::Array(pairs.iter().map(ClonePair::to_go_value).collect::>()); - let dist = clone_type_dist_map(categorize_clone_pairs(pairs)); - - // analyze::Report is a map-origin GoMap: keys byte-sort on encode. - let mut report = GoMap::new(MapOrigin::Map); - report.push(KEY_ANALYZER_NAME, GoValue::Str(ANALYZER_NAME.to_string())); - report.push(KEY_TOTAL_FUNCTIONS, GoValue::Int(total_functions as i64)); - report.push(KEY_TOTAL_CLONE_PAIRS, GoValue::Int(pairs.len() as i64)); - report.push(KEY_CLONE_RATIO, GoValue::Float(clone_ratio)); - report.push(KEY_CLONE_PAIRS, pairs_value); - report.push(KEY_CLONE_TYPE_DISTRIBUTION, dist); - report.push(KEY_MESSAGE, GoValue::Str(message.to_string())); - report - } - - /// Writes the report as indented JSON of [`ComputedMetrics`] (two-space - /// indent). Routes through [`cf_gojson`], never `serde_json`. - /// - /// # Errors - /// Returns [`AnalyzerError`] if the writer fails. - pub fn format_report_json( - &self, - report: &Report, - w: &mut dyn Write, - ) -> Result<(), AnalyzerError> { - let metrics = compute_metrics_from_report(report); - let bytes = cf_gojson::marshal_indent(&metrics.to_go_value()); - w.write_all(&bytes) - .map_err(|e| AnalyzerError::Other(format!("formatreportjson: {e}"))) - } - - /// Writes the report as a CFB1 binary envelope of [`ComputedMetrics`]. - /// - /// # Errors - /// Returns [`AnalyzerError`] if encoding or the writer fails. - pub fn format_report_binary( - &self, - report: &Report, - w: &mut dyn Write, - ) -> Result<(), AnalyzerError> { - let metrics = compute_metrics_from_report(report); - let bytes = cf_reportutil::encode_binary_envelope(&metrics.to_go_value()) - .map_err(|e| AnalyzerError::Other(format!("formatreportbinary: {e}")))?; - w.write_all(&bytes) - .map_err(|e| AnalyzerError::Other(format!("formatreportbinary: {e}"))) - } - - /// Returns the [`ComputedMetrics`] projection used by the YAML path. - /// - /// The YAML emitter (`cf-goyaml`) is wired by the framework; YAML is a - /// non-binding capture (DESIGN §6), so the projection — not the emitter — - /// is the byte-identity-critical part and is exposed here for it. - #[must_use] - pub fn computed_metrics(&self, report: &Report) -> ComputedMetrics { - compute_metrics_from_report(report) - } -} - -/// Creates an empty report with the given message: exactly these five keys -/// (report-format contract). -#[must_use] -pub fn build_empty_report(message: &str) -> Report { - let mut report = GoMap::new(MapOrigin::Map); - report.push(KEY_ANALYZER_NAME, GoValue::Str(ANALYZER_NAME.to_string())); - report.push(KEY_TOTAL_FUNCTIONS, GoValue::Int(0)); - report.push(KEY_TOTAL_CLONE_PAIRS, GoValue::Int(0)); - report.push(KEY_CLONE_RATIO, GoValue::Float(0.0)); - report.push(KEY_MESSAGE, GoValue::Str(message.to_string())); - report -} - -/// Projects a report map into [`ComputedMetrics`]. -#[must_use] -pub fn compute_metrics_from_report(report: &Report) -> ComputedMetrics { - let total_functions = cf_reportutil::get_int(report, KEY_TOTAL_FUNCTIONS); - let total_clone_pairs = cf_reportutil::get_int(report, KEY_TOTAL_CLONE_PAIRS); - let clone_ratio = cf_reportutil::get_float64(report, KEY_CLONE_RATIO); - let message = cf_reportutil::get_string(report, KEY_MESSAGE); - let clone_pairs = extract_clone_pairs(report); - let clone_type_dist = extract_clone_type_dist(report); - - ComputedMetrics { - total_functions, - total_clone_pairs, - clone_ratio, - clone_type_dist, - clone_pairs, - message, - } -} - -/// Extracts the clone pairs from a report (handling the array-of-maps shape -/// produced by [`Analyzer::build_report`]). -/// -/// `None` when the report carries no `clone_pairs` list (the reference -/// `extractClonePairs` returns a `nil` slice → `null`); `Some` — possibly -/// empty — when the key holds a list (the reference builds a non-nil slice -/// via `make([]ClonePair, 0, …)` → `[]`). -fn extract_clone_pairs(report: &Report) -> Option> { - let Some(GoValue::Array(items)) = cf_reportutil::get(report, KEY_CLONE_PAIRS) else { - return None; - }; - - let mut pairs = Vec::with_capacity(items.len()); - for item in items { - if let GoValue::Map(m) = item { - pairs.push(clone_pair_from_map(m)); - } - } - Some(pairs) -} - -/// Extracts a [`ClonePair`] from a map value. -fn clone_pair_from_map(m: &GoMap) -> ClonePair { - let mut pair = ClonePair { - func_a: String::new(), - func_b: String::new(), - similarity: 0.0, - clone_type: String::new(), - }; - for (k, v) in m.entries() { - match (k.as_str(), v) { - ("func_a", GoValue::Str(s)) => pair.func_a.clone_from(s), - ("func_b", GoValue::Str(s)) => pair.func_b.clone_from(s), - ("similarity", GoValue::Float(f)) => pair.similarity = *f, - ("clone_type", GoValue::Str(s)) => pair.clone_type.clone_from(s), - _ => {} - } - } - pair -} - -/// Extracts the `clone_type_distribution` counts, if present. -fn extract_clone_type_dist(report: &Report) -> Option { - let Some(GoValue::Map(m)) = cf_reportutil::get(report, KEY_CLONE_TYPE_DISTRIBUTION) else { - return None; - }; - - let mut counts = crate::report::CloneTypeCounts::default(); - for (k, v) in m.entries() { - if let GoValue::Int(n) = v { - match k.as_str() { - crate::report::CLONE_TYPE1 => counts.type1 = *n, - crate::report::CLONE_TYPE2 => counts.type2 = *n, - crate::report::CLONE_TYPE3 => counts.type3 = *n, - _ => {} - } - } - } - Some(counts) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::uast::NodeBuilder; - use cf_gojson::Encoder; - use cf_uast_node::Node; - - /// Builds a function subtree with at least `MIN_FUNCTION_NODES` nodes and the - /// given identifier name, so it survives the size gate and produces shingles. - fn function(name: &str) -> Node { - let name_node = NodeBuilder::new("Identifier") - .role("Name") - .token(name) - .build(); - let mut f = NodeBuilder::new("Function") - .role("Function") - .child(name_node) - .build(); - // Add a deterministic body of ≥20 nodes so countNodes >= 20 and there - // are enough types (>= k=5) to shingle. - let mut block = NodeBuilder::new("Block").build(); - for i in 0..24 { - let kind = ["Identifier", "Call", "Literal", "Operator"][i % 4]; - block.add_child(NodeBuilder::new(kind).build()); - } - f.add_child(block); - f - } - - #[test] - fn descriptor_is_static_clones() { - let a = Analyzer::new(); - assert_eq!(a.descriptor().id, "static/clones"); - assert_eq!(a.name(), "clones"); - assert_eq!(a.flag(), "clone-detection"); - } - - #[test] - fn nil_root_returns_empty_ast_message() { - let a = Analyzer::new(); - let report = a.analyze_node(None); - assert_eq!( - cf_reportutil::get_string(&report, KEY_MESSAGE), - MSG_EMPTY_AST - ); - assert_eq!(cf_reportutil::get_int(&report, KEY_TOTAL_FUNCTIONS), 0); - } - - #[test] - fn no_functions_returns_no_functions_message() { - let a = Analyzer::new(); - let root = NodeBuilder::new("File").build(); // no function nodes - let report = a.analyze_node(Some(&root)); - assert_eq!( - cf_reportutil::get_string(&report, KEY_MESSAGE), - MSG_NO_FUNCTIONS - ); - } - - #[test] - fn identical_functions_detected_as_type1_clone() { - let a = Analyzer::new(); - // Two structurally identical functions -> similarity 1.0 -> Type-1. - let root = NodeBuilder::new("File") - .child(function("foo")) - .child(function("bar")) - .build(); - let report = a.analyze_node(Some(&root)); - assert_eq!(cf_reportutil::get_int(&report, KEY_TOTAL_FUNCTIONS), 2); - assert_eq!(cf_reportutil::get_int(&report, KEY_TOTAL_CLONE_PAIRS), 1); - - // clone_ratio = 2 distinct cloned funcs / 2 total = 1.0. - assert!((cf_reportutil::get_float64(&report, KEY_CLONE_RATIO) - 1.0).abs() < 1e-12); - - let metrics = compute_metrics_from_report(&report); - let pairs = metrics.clone_pairs.as_deref().expect("pairs list present"); - assert_eq!(pairs.len(), 1); - assert_eq!(pairs[0].clone_type, "Type-1"); - assert!((pairs[0].similarity - 1.0).abs() < 1e-9); - } - - #[test] - fn empty_report_json_round_trips_through_computed_metrics() { - let a = Analyzer::new(); - let report = a.analyze_node(None); - let mut buf = Vec::new(); - a.format_report_json(&report, &mut buf).expect("json ok"); - let json = String::from_utf8(buf).unwrap(); - // Indented; ComputedMetrics field order; no clone_type_distribution - // (omitempty); clone_pairs null. - assert!(json.contains("\"total_functions\": 0")); - assert!(json.contains("\"clone_pairs\": null")); - assert!(json.contains("\"message\": \"No AST provided\"")); - assert!(!json.contains("clone_type_distribution")); - } - - #[test] - fn thresholds_structure_matches_contract() { - let a = Analyzer::new(); - let json = Encoder::marshal().encode_to_string(&GoValue::Object(a.thresholds())); - assert_eq!( - json, - concat!( - r#"{"clone_ratio":{"green":0,"red":0.3,"yellow":0.1},"#, - r#""total_clone_pairs":{"green":0,"red":20,"yellow":5}}"# - ) - ); - } -} diff --git a/crates/cf-clones/src/engine.rs b/crates/cf-clones/src/engine.rs index f97c97b6..0fc90bc1 100644 --- a/crates/cf-clones/src/engine.rs +++ b/crates/cf-clones/src/engine.rs @@ -1,485 +1,413 @@ -//! The `MinHash` + LSH clone-detection core. +//! The clone-detection core: per-function entries, candidate generation +//! through the LSH index, exact verification, classification, and the +//! bounded top-k pair list. //! -//! Signature building and pair finding live in one module because they form a -//! single detection pipeline shared by the [`crate::Analyzer`] (per-file) and -//! the [`crate::Aggregator`] (cross-file). - -use std::collections::{HashMap, HashSet}; - -use cf_alg_lsh::Index; +//! Pairs are enumerated exactly once by querying every function in id order +//! for the candidates *after* it ([`cf_alg_lsh::Index::query_after`]), so no +//! `seen` set is needed and the work is linear in the number of candidate +//! pairs. The stored pair list is a bounded heap keyed on +//! `(similarity desc, func_a asc, func_b asc)`, so the capped detail is +//! always the best pairs under that total order, independent of discovery +//! order. + +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use cf_alg_lsh::{Index, Scratch}; use cf_alg_minhash::Signature; use cf_uast_node::Node; -use crate::report::{classify_clone_type, ClonePair, CloneTypeCounts}; +use crate::profile::{profile, Fingerprint}; +use crate::report::{ClonePair, CloneType, CloneTypeCounts}; use crate::shingler::Shingler; -use crate::uast::{ROLE_DECLARATION, ROLE_FUNCTION, ROLE_PARAMETER, UAST_FUNCTION, UAST_METHOD}; -use crate::{MIN_FUNCTION_NODES, NUM_HASHES}; +use crate::MIN_FUNCTION_NODES; -/// A function's name plus its `MinHash` signature. +/// One clone candidate as produced by the [`crate::Visitor`] for a single +/// file. #[derive(Debug, Clone)] pub struct FuncEntry { - /// The (possibly receiver-qualified) function name. + /// The (receiver-qualified or scope-qualified) function name. pub name: String, - /// The function's `MinHash` signature. + /// `MinHash` signature of the shingle set (the LSH input). pub sig: Signature, + /// Sorted, deduplicated FNV-1a hashes of the function's shingles. + pub shingles: Vec, + /// Fingerprint of the node-type sequence. + pub structure: Fingerprint, + /// Fingerprint of the leaf-token sequence. + pub tokens: Fingerprint, + /// Index (within the same file scan) of the nearest enclosing candidate, + /// for closures nested inside another candidate. + pub parent: Option, } -/// Output of [`find_clone_pairs`]. -#[derive(Default)] -pub struct ClonePairResult { - /// The stored clone pairs (may be capped; see [`find_clone_pairs`]). - pub pairs: Vec, - /// The total count of unique pairs found, regardless of the cap. - pub total_count: usize, - /// Distribution of clone types across all pairs found. - pub type_distribution: CloneTypeCounts, - /// Distinct function names involved in any pair. - pub cloned_func: HashSet, -} - -/// Returns `true` if `n` represents a function. -#[must_use] -pub fn is_function_node(n: &Node) -> bool { - n.has_any_type(&[UAST_FUNCTION, UAST_METHOD]) - || n.has_all_roles(&[ROLE_FUNCTION, ROLE_DECLARATION]) -} - -/// Returns the total number of nodes in a subtree. -#[must_use] -pub fn count_nodes(n: &Node) -> usize { - let mut count = 1; - for child in &n.children { - count += count_nodes(child); - } - count -} - -/// Extracts a unique function name from a node. -/// -/// Uses the entity name (see [`extract_entity_name`], falling back to the node -/// token, then the type); for methods, qualifies with the receiver type (e.g. -/// `"Foo.DoWork"`). -#[must_use] -pub fn extract_func_name(fn_node: &Node) -> String { - let mut name = extract_entity_name(fn_node).unwrap_or_default(); - if name.is_empty() { - name = if fn_node.token.is_empty() { - fn_node.node_type.clone() - } else { - fn_node.token.clone() - }; - } - - if fn_node.node_type == UAST_METHOD { - let recv = extract_receiver_type(fn_node); - if !recv.is_empty() { - return format!("{recv}.{name}"); +impl FuncEntry { + /// Re-bases the entry into the global candidate list: `file` is the + /// file's index, `base` the global index of the file's first entry. + #[must_use] + pub fn into_indexed(self, file: usize, base: usize) -> IndexedFunc { + IndexedFunc { + name: self.name, + file, + sig: self.sig, + shingles: self.shingles, + structure: self.structure, + tokens: self.tokens, + parent: self.parent.map(|p| p + base), } } - - name } -/// Extracts the entity (function/identifier) name from a node. -/// -/// Precedence order (report contract; pinned by the differential gate): -/// 1. the node's `name` prop; -/// 2. the node's OWN token; -/// 3. the **first child** (index 0): its token, then its `name` prop. -/// -/// The node's own token is checked BEFORE descending to a child, and the child -/// step looks only at index 0 (not "the first child with the Name role"). Both -/// points matter: e.g. C `Function` nodes whose token is the full function -/// text must keep that text as the name so distinct functions stay distinct in -/// the LSH index. Returns `None` when nothing usable is found; a present-but- -/// empty `name` prop yields `Some("")`, which `extract_func_name` treats -/// identically to `None`, so the observable name is the same. -#[must_use] -fn extract_entity_name(n: &Node) -> Option { - // 1. the "name" prop — present-key wins even if empty. - if let Some(name) = n.props.get("name") { - return Some(name.clone()); - } - - // 2. node's own token. - if !n.token.is_empty() { - return Some(n.token.clone()); - } - - // 3. first child (index 0): token, then its "name" prop. - if let Some(child) = n.children.first() { - if !child.token.is_empty() { - return Some(child.token.clone()); - } - if let Some(name) = child.props.get("name") { - return Some(name.clone()); - } - } - - None -} - -/// Extracts the receiver type name from a method node. -/// -/// The receiver is the first child with the `Parameter` role whose token looks -/// like `"(v *T)"` / `"(v T)"`; the type `T` is the last whitespace-separated -/// token after stripping the surrounding parens and a leading `*`. -#[must_use] -pub fn extract_receiver_type(fn_node: &Node) -> String { - const MIN_RECEIVER_PARTS: usize = 2; - - for child in &fn_node.children { - if !child.has_any_role(&[ROLE_PARAMETER]) { - continue; - } - - let tok = child.token.trim(); - if tok.is_empty() { - continue; - } - - let tok = tok - .strip_prefix('(') - .unwrap_or(tok) - .strip_suffix(')') - .unwrap_or_else(|| tok.strip_prefix('(').unwrap_or(tok)) - .trim(); - - let parts: Vec<&str> = tok.split_whitespace().collect(); - if parts.len() < MIN_RECEIVER_PARTS { - continue; - } - - let type_name = parts[parts.len() - 1] - .strip_prefix('*') - .unwrap_or(parts[parts.len() - 1]); - if !type_name.is_empty() { - return type_name.to_string(); - } - } - - String::new() +/// A clone candidate in the global (cross-file) candidate list. +#[derive(Debug, Clone)] +pub struct IndexedFunc { + /// `file::name` — the name qualified with its source file. + pub name: String, + /// Index of the source file in the aggregator's file list. + pub file: usize, + /// `MinHash` signature of the shingle set. + pub sig: Signature, + /// Sorted, deduplicated shingle hashes. + pub shingles: Vec, + /// Fingerprint of the node-type sequence. + pub structure: Fingerprint, + /// Fingerprint of the leaf-token sequence. + pub tokens: Fingerprint, + /// Global index of the nearest enclosing candidate, if nested. + pub parent: Option, } -/// Builds a `MinHash` signature for one function subtree, or `None` when the -/// function is too small or produces no shingles. +/// Builds the entry for one function subtree, or `None` when the function +/// has fewer than [`MIN_FUNCTION_NODES`] nodes or yields no shingles. #[must_use] -#[allow(clippy::similar_names)] // `shingler` / `shingles` is the clearest naming -pub fn build_signature( +pub fn build_entry( + name: String, fn_node: &Node, shingler: &Shingler, num_hashes: usize, ) -> Option { - if count_nodes(fn_node) < MIN_FUNCTION_NODES { + let profile = profile(fn_node); + if profile.node_count < MIN_FUNCTION_NODES { return None; } - - let shingles = shingler.extract_shingles(fn_node); - if shingles.is_empty() { + let shingle_set = shingler.shingle_hashes(&profile.types); + if shingle_set.is_empty() { return None; } - let mut sig = Signature::new(num_hashes).ok()?; - for shingle in &shingles { - sig.add(shingle); + for &h in &shingle_set { + sig.add_hash(h); } - Some(FuncEntry { - name: extract_func_name(fn_node), + name, sig, + shingles: shingle_set, + structure: profile.structure, + tokens: profile.tokens, + parent: None, }) } -/// Builds signatures for all functions, using the default [`NUM_HASHES`]. +/// Exact Jaccard index of two sorted, deduplicated hash sets. /// -/// Convenience wrapper over [`build_signature`]. +/// ``` +/// use cf_clones::engine::jaccard; +/// assert!((jaccard(&[1, 2, 3, 4], &[3, 4, 5, 6]) - 2.0 / 6.0).abs() < 1e-12); +/// assert_eq!(jaccard(&[1, 2], &[1, 2]), 1.0); +/// assert_eq!(jaccard(&[], &[]), 0.0); +/// ``` #[must_use] -pub fn build_signatures( - functions: &[&Node], - shingler: &Shingler, - num_hashes: usize, -) -> Vec { - let mut entries = Vec::with_capacity(functions.len()); - for fn_node in functions { - if let Some(entry) = build_signature(fn_node, shingler, num_hashes) { - entries.push(entry); +pub fn jaccard(a: &[u64], b: &[u64]) -> f64 { + let (mut i, mut j, mut shared) = (0, 0, 0usize); + while i < a.len() && j < b.len() { + match a[i].cmp(&b[j]) { + Ordering::Less => i += 1, + Ordering::Greater => j += 1, + Ordering::Equal => { + shared += 1; + i += 1; + j += 1; + } } } - entries + let union = a.len() + b.len() - shared; + if union == 0 { + return 0.0; + } + shared as f64 / union as f64 } -/// Returns a canonical key for a clone pair so `(A,B)` and `(B,A)` collide: -/// the two names are ordered lexicographically. +/// Classifies a candidate pair: identical structure is Type-1 (same tokens) +/// or Type-2 (different tokens) at similarity `1.0`; otherwise the exact +/// shingle Jaccard must reach `threshold` for a Type-3 pair. #[must_use] -fn clone_pair_key(func_a: &str, func_b: &str) -> (String, String) { - if func_a > func_b { - (func_b.to_string(), func_a.to_string()) - } else { - (func_a.to_string(), func_b.to_string()) +pub fn classify_pair(a: &IndexedFunc, b: &IndexedFunc, threshold: f64) -> Option<(CloneType, f64)> { + if a.structure == b.structure { + let clone_type = if a.tokens == b.tokens { + CloneType::Type1 + } else { + CloneType::Type2 + }; + return Some((clone_type, 1.0)); } + let similarity = jaccard(&a.shingles, &b.shingles); + (similarity >= threshold).then_some((CloneType::Type3, similarity)) } -/// Computes a clone pair between an entry and a candidate, or `None` if the -/// candidate is unknown or below `min_similarity`. -#[must_use] -fn compute_clone_pair( - entry: &FuncEntry, - candidate_id: &str, - sig_map: &HashMap, - min_similarity: f64, -) -> Option { - let candidate_sig = sig_map.get(candidate_id)?; - let similarity = entry.sig.similarity(candidate_sig).ok()?; - if similarity < min_similarity { - return None; +/// `true` when `ancestor` encloses `descendant` (a closure and the function +/// it is written in are never a clone pair of each other). +fn is_ancestor(funcs: &[IndexedFunc], ancestor: usize, descendant: usize) -> bool { + let mut cursor = funcs[descendant].parent; + while let Some(p) = cursor { + if p == ancestor { + return true; + } + cursor = funcs[p].parent; } + false +} - Some(ClonePair { - func_a: entry.name.clone(), - func_b: candidate_id.to_string(), - similarity, - clone_type: classify_clone_type(similarity).to_string(), - }) +/// A pair kept in the bounded top-k heap. Its [`Ord`] ranks a WORSE pair as +/// greater — lower similarity first, then the later name — so the max-heap's +/// top is the pair to evict and an ascending sort lists the best pair first. +struct Ranked<'a> { + similarity: f64, + clone_type: CloneType, + a: usize, + b: usize, + name_a: &'a str, + name_b: &'a str, } -/// Builds a name → signature lookup. -#[must_use] -fn build_signature_map(entries: &[FuncEntry]) -> HashMap { - let mut sig_map = HashMap::with_capacity(entries.len()); - for entry in entries { - sig_map.insert(entry.name.clone(), entry.sig.clone()); +impl Ord for Ranked<'_> { + fn cmp(&self, other: &Self) -> Ordering { + other + .similarity + .total_cmp(&self.similarity) + .then_with(|| self.name_a.cmp(other.name_a)) + .then_with(|| self.name_b.cmp(other.name_b)) } - sig_map } -/// Queries the LSH index for every entry, collects unique clone pairs, and sorts -/// the stored pairs by similarity descending. -/// -/// `pair_cap` limits the *stored* `pairs` slice (`0` = unlimited); -/// `total_count` always reflects all unique pairs found. +impl PartialOrd for Ranked<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Ranked<'_> { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for Ranked<'_> {} + +/// Output of [`find_clone_pairs`]. +#[derive(Debug, Default)] +pub struct ClonePairResult { + /// The stored pairs, best first (similarity desc, then name). + pub pairs: Vec, + /// `(file of func_a, file of func_b)` for every stored pair. + pub pair_files: Vec<(usize, usize)>, + /// Exact number of pairs found, regardless of the cap. + pub total_count: usize, + /// Exact per-type distribution over every pair found. + pub distribution: CloneTypeCounts, + /// Per candidate: whether it takes part in at least one pair. + pub cloned: Vec, +} + +/// Queries the index for every candidate, verifies and classifies each +/// candidate pair, and keeps the best `pair_cap` pairs (`0` = unlimited). #[must_use] pub fn find_clone_pairs( - entries: &[FuncEntry], - idx: &Index, + funcs: &[IndexedFunc], + index: &Index, pair_cap: usize, - min_similarity: f64, + threshold: f64, ) -> ClonePairResult { - let sig_map = build_signature_map(entries); - let mut seen: HashSet<(String, String)> = HashSet::new(); - let mut result = ClonePairResult::default(); - - for entry in entries { - let Ok(mut candidates) = idx.query_threshold(&entry.sig, min_similarity) else { + let mut scratch = Scratch::default(); + let mut heap: BinaryHeap> = BinaryHeap::new(); + let mut result = ClonePairResult { + cloned: vec![false; funcs.len()], + ..ClonePairResult::default() + }; + + for (i, f) in funcs.iter().enumerate() { + let Ok(candidates) = index.query_after(&f.sig, i, &mut scratch) else { continue; }; - - // The LSH query collects candidates by iterating a `HashMap`, whose - // order is randomized per run (the reference implementation is equally - // order-nondeterministic here). That randomness propagates into the - // discovery order, which (a) decides which pairs survive the - // `pair_cap`, and (b) is the tie-break order among equal-similarity - // pairs after the final sort. Sorting the candidates here makes - // discovery order deterministic, so the stored pair SET and ORDER are - // reproducible run-to-run (the compat harness canonicalizes the - // reference's nondeterminism away, but requires our determinism). - // - // The order is DESCENDING by name: any fixed order is an equally - // faithful determinization of the reference's random map iteration, - // and the descending choice makes the frozen-pdqsort output land on - // the reference's modal (most-probable) permutation on large - // clone-heavy inputs — measured by Monte-Carlo simulation of the - // reference's per-entry candidate shuffles through the same pdqsort - // (ascending order lands on a low-probability outlier instead). - candidates.sort_unstable_by(|a, b| b.cmp(a)); - - for candidate_id in candidates { - if candidate_id == entry.name { + for j in candidates { + let g = &funcs[j]; + if is_ancestor(funcs, i, j) { continue; } - - let key = clone_pair_key(&entry.name, &candidate_id); - if seen.contains(&key) { + let Some((clone_type, similarity)) = classify_pair(f, g, threshold) else { continue; - } - seen.insert(key); - - if let Some(pair) = compute_clone_pair(entry, &candidate_id, &sig_map, min_similarity) { - result.total_count += 1; - result.type_distribution.increment(&pair.clone_type); - result.cloned_func.insert(pair.func_a.clone()); - result.cloned_func.insert(pair.func_b.clone()); - - if pair_cap == 0 || result.pairs.len() < pair_cap { - result.pairs.push(pair); - } + }; + result.total_count += 1; + result.distribution.increment(clone_type); + result.cloned[i] = true; + result.cloned[j] = true; + + let (a, b) = if f.name <= g.name { (i, j) } else { (j, i) }; + heap.push(Ranked { + similarity, + clone_type, + a, + b, + name_a: &funcs[a].name, + name_b: &funcs[b].name, + }); + if pair_cap > 0 && heap.len() > pair_cap { + heap.pop(); } } } - // The reference sorts pairs by similarity descending with Go's UNSTABLE - // `sort.Slice` (frozen pdqsort) and NO tie-break — the resulting - // within-tie permutation of the discovery order is part of the observable - // output contract (the terminal `text` report exposes the first pairs of - // this list verbatim). Reproduce the exact algorithm over our - // deterministic discovery order: `cf_gosort::go_sort_slice` is the - // bit-faithful port of that pdqsort, and the comparator mirrors the - // reference's `a.Similarity > b.Similarity`. Because our discovery order - // is deterministic (sorted candidates), two identical runs remain - // byte-identical, while the permutation now matches the reference's - // stable emergent ordering on its deterministic discovery core. - cf_gosort::go_sort_slice(&mut result.pairs, |a, b| a.similarity > b.similarity); - - result -} - -/// Builds an LSH index over `entries`, inserting each signature under its -/// name. Insert errors are skipped. Returns `None` if the index parameters are -/// invalid. -#[must_use] -pub fn build_index(entries: &[FuncEntry], num_bands: usize, num_rows: usize) -> Option { - let mut idx = Index::new(num_bands, num_rows).ok()?; - for entry in entries { - let _ = idx.insert(entry.name.clone(), &entry.sig); + for r in heap.into_sorted_vec() { + result.pairs.push(ClonePair { + func_a: r.name_a.to_string(), + func_b: r.name_b.to_string(), + similarity: r.similarity, + clone_type: r.clone_type, + }); + result.pair_files.push((funcs[r.a].file, funcs[r.b].file)); } - Some(idx) -} - -/// Number of distinct function names across all pairs. -#[must_use] -pub fn count_distinct_funcs(pairs: &[ClonePair]) -> usize { - let mut unique: HashSet<&str> = HashSet::with_capacity(pairs.len()); - for p in pairs { - unique.insert(&p.func_a); - unique.insert(&p.func_b); - } - unique.len() + result } -/// Re-export of [`NUM_HASHES`] for callers building signatures from this module. -pub const DEFAULT_NUM_HASHES: usize = NUM_HASHES; - #[cfg(test)] mod tests { use super::*; - use crate::uast::NodeBuilder; - use cf_uast_node::Node; - - fn method_with_receiver(token: &str) -> Node { - let recv = NodeBuilder::new("Parameter") - .role("Parameter") - .token(token) - .build(); - NodeBuilder::new("Method").child(recv).build() + use crate::{NUM_BANDS, NUM_HASHES, NUM_ROWS}; + + /// A candidate over the given shingle hashes with its own token + /// fingerprint derived from `token_salt`. + fn func(name: &str, file: usize, shingles: &[u64], token_salt: u8) -> IndexedFunc { + let mut sig = Signature::new(NUM_HASHES).unwrap(); + for &h in shingles { + sig.add_hash(h); + } + let structure = fingerprint_of(shingles); + let mut salted: Vec = shingles.to_vec(); + salted.push(u64::from(token_salt)); + IndexedFunc { + name: name.to_string(), + file, + sig, + shingles: shingles.to_vec(), + structure, + tokens: fingerprint_of(&salted), + parent: None, + } } - #[test] - fn count_nodes_counts_subtree() { - let tree = NodeBuilder::new("A") - .child(NodeBuilder::new("B").build()) - .child( - NodeBuilder::new("C") - .child(NodeBuilder::new("D").build()) + fn fingerprint_of(values: &[u64]) -> Fingerprint { + let mut n = cf_uast_node::Builder::new().with_type("Function").build(); + for v in values { + n.add_child( + cf_uast_node::Builder::new() + .with_type("Identifier") + .with_token(v.to_string()) .build(), - ) - .build(); - assert_eq!(count_nodes(&tree), 4); + ); + } + profile(&n).tokens } - #[test] - fn is_function_node_by_type_or_roles() { - assert!(is_function_node(&NodeBuilder::new("Function").build())); - assert!(is_function_node(&NodeBuilder::new("Method").build())); - let decl = NodeBuilder::new("Decl") - .role("Function") - .role("Declaration") - .build(); - assert!(is_function_node(&decl)); - assert!(!is_function_node(&NodeBuilder::new("Block").build())); + fn index_of(funcs: &[IndexedFunc]) -> Index { + let mut idx = Index::new(NUM_BANDS, NUM_ROWS).unwrap(); + for (i, f) in funcs.iter().enumerate() { + idx.insert(i, &f.sig).unwrap(); + } + idx + } + + fn range(lo: u64, hi: u64) -> Vec { + (lo..hi).collect() } #[test] - fn extract_receiver_pointer_type() { - let m = method_with_receiver("(f *Foo)"); - assert_eq!(extract_receiver_type(&m), "Foo"); + fn jaccard_of_sorted_sets() { + assert!((jaccard(&[1, 2, 3, 4], &[3, 4, 5, 6]) - 1.0 / 3.0).abs() < 1e-12); + assert!((jaccard(&[1], &[2]) - 0.0).abs() < 1e-12); } #[test] - fn extract_receiver_value_type() { - let m = method_with_receiver("(f Foo)"); - assert_eq!(extract_receiver_type(&m), "Foo"); + fn identical_structure_and_tokens_is_type1() { + let a = func("a", 0, &range(0, 50), 0); + let b = func("b", 1, &range(0, 50), 0); + assert_eq!(classify_pair(&a, &b, 0.5), Some((CloneType::Type1, 1.0))); } #[test] - fn extract_receiver_missing_parts() { - let m = method_with_receiver("(Foo)"); // single token -> no receiver - assert_eq!(extract_receiver_type(&m), ""); + fn identical_structure_different_tokens_is_type2() { + let a = func("a", 0, &range(0, 50), 0); + let b = func("b", 1, &range(0, 50), 1); + assert_eq!(classify_pair(&a, &b, 0.5), Some((CloneType::Type2, 1.0))); } #[test] - fn extract_func_name_qualifies_methods() { - // Entity-name precedence: "name" prop -> own token -> children[0]'s - // token. Here the method's name comes from children[0] ("DoWork"); the - // receiver type is discovered separately by `extract_receiver_type` - // (which scans all children for the Parameter role), yielding - // "Foo.DoWork". - let name = NodeBuilder::new("Identifier") - .role("Name") - .token("DoWork") - .build(); - let recv = NodeBuilder::new("Parameter") - .role("Parameter") - .token("(f *Foo)") - .build(); - let m = NodeBuilder::new("Method").child(name).child(recv).build(); - assert_eq!(extract_func_name(&m), "Foo.DoWork"); + fn different_structure_is_type3_only_above_threshold() { + let a = func("a", 0, &range(0, 60), 0); + let near = func("b", 1, &range(20, 80), 0); // 40 shared / 80 union = 0.5 + let far = func("c", 2, &range(50, 110), 0); // 10 / 110 + let (ct, sim) = classify_pair(&a, &near, 0.5).expect("found"); + assert_eq!(ct, CloneType::Type3); + assert!((sim - 0.5).abs() < 1e-12); + assert_eq!(classify_pair(&a, &far, 0.5), None); } #[test] - fn extract_func_name_own_token_beats_children() { - // The node's OWN token is checked before any child. A C `Function` - // node whose token is the full function text keeps that text as its - // name, so distinct functions stay distinct in the LSH index. - let child = NodeBuilder::new("Identifier") - .role("Name") - .token("U16") - .build(); - let f = NodeBuilder::new("Function") - .token("static U16 LZ4_read16(...)") - .child(child) - .build(); - assert_eq!(extract_func_name(&f), "static U16 LZ4_read16(...)"); + fn pairs_are_found_once_best_first_and_names_sorted() { + let funcs = vec![ + func("z", 0, &range(0, 50), 0), + func("m", 1, &range(0, 50), 0), + func("a", 2, &range(10, 60), 0), // 0.667 with the others + ]; + let idx = index_of(&funcs); + let r = find_clone_pairs(&funcs, &idx, 0, 0.5); + assert_eq!(r.total_count, 3); + assert_eq!(r.distribution.type1, 1); + assert_eq!(r.distribution.type3, 2); + assert!(r.cloned.iter().all(|c| *c)); + let names: Vec = r.pairs.iter().map(ClonePair::display_name).collect(); + assert_eq!(names, vec!["m <-> z", "a <-> m", "a <-> z"]); + assert_eq!(r.pair_files, vec![(1, 0), (2, 1), (2, 0)]); } #[test] - fn extract_func_name_falls_back_to_token_then_type() { - let tokened = NodeBuilder::new("Function").token("plain").build(); - assert_eq!(extract_func_name(&tokened), "plain"); - let typed = NodeBuilder::new("Function").build(); - assert_eq!(extract_func_name(&typed), "Function"); + fn pair_cap_keeps_the_best_pairs_and_the_exact_total() { + let funcs: Vec = (0..5) + .map(|i| func(&format!("f{i}"), i, &range(0, 50), 0)) + .collect(); + let idx = index_of(&funcs); + let r = find_clone_pairs(&funcs, &idx, 3, 0.5); + assert_eq!(r.total_count, 10); + assert_eq!(r.distribution.type1, 10); + let names: Vec = r.pairs.iter().map(ClonePair::display_name).collect(); + assert_eq!(names, vec!["f0 <-> f1", "f0 <-> f2", "f0 <-> f3"]); } #[test] - fn count_distinct_funcs_dedups() { - let pairs = vec![ - ClonePair { - func_a: "a".into(), - func_b: "b".into(), - similarity: 1.0, - clone_type: "Type-1".into(), - }, - ClonePair { - func_a: "b".into(), - func_b: "c".into(), - similarity: 0.9, - clone_type: "Type-2".into(), - }, - ]; - assert_eq!(count_distinct_funcs(&pairs), 3); + fn a_closure_is_never_paired_with_its_enclosing_function() { + let outer = func("outer", 0, &range(0, 60), 0); + let mut inner = func("outer.func#1", 0, &range(5, 60), 0); + inner.parent = Some(0); + let funcs = vec![outer, inner]; + let idx = index_of(&funcs); + let r = find_clone_pairs(&funcs, &idx, 0, 0.5); + assert_eq!(r.total_count, 0, "{:?}", r.pairs); } #[test] - fn clone_pair_key_is_canonical() { - assert_eq!(clone_pair_key("b", "a"), clone_pair_key("a", "b")); - assert_eq!(clone_pair_key("a", "b"), ("a".into(), "b".into())); + fn no_self_pairs() { + let funcs = vec![func("only", 0, &range(0, 50), 0)]; + let idx = index_of(&funcs); + let r = find_clone_pairs(&funcs, &idx, 0, 0.5); + assert_eq!(r.total_count, 0); + assert_eq!(r.cloned, vec![false]); } } diff --git a/crates/cf-clones/src/lib.rs b/crates/cf-clones/src/lib.rs index c36cb9ce..0cb488a4 100644 --- a/crates/cf-clones/src/lib.rs +++ b/crates/cf-clones/src/lib.rs @@ -1,46 +1,53 @@ -//! `cf-clones` — static code-clone detection (`MinHash` + LSH; analyzer id -//! `static/clones`). +//! `cf-clones` — static code-clone detection (analyzer id `static/clones`). //! -//! Detects duplicate and near-duplicate functions in a UAST by compressing -//! each function's structural shingle set into a 128-function `MinHash` -//! signature, indexing the signatures in a 16×8 LSH index, and reporting the -//! candidate pairs whose estimated Jaccard similarity clears the Type-3 -//! threshold. The pipeline is split across the [`shingler`], [`engine`], -//! [`analyzer`], [`aggregator`], [`visitor`], [`report`], and -//! [`report_section`] modules. +//! Finds duplicate and near-duplicate functions across a whole tree of +//! source files. Per function the [`visitor`] builds a [`profile`] (the +//! pre-order node-type sequence, a *structure* fingerprint over it, a *token* +//! fingerprint over the leaf tokens, and the exact set of `k`-gram shingles) +//! plus a `MinHash` signature of that shingle set. The [`aggregator`] indexes +//! every signature in one global banded LSH index ([`NUM_BANDS`] × +//! [`NUM_ROWS`]), and the [`engine`] turns the index's candidate pairs into +//! verified clone pairs: //! -//! # Byte-identity +//! | Clone type | Rule | +//! |---|---| +//! | **Type-1** (exact) | identical structure **and** identical tokens | +//! | **Type-2** (renamed) | identical structure, different identifiers / literals | +//! | **Type-3** (near-miss) | different structure, exact shingle Jaccard ≥ [`SIMILARITY_THRESHOLD`] | //! -//! The machine export path ([`analyzer::Analyzer::format_report_json`] / -//! `_yaml` / `_binary`) does **not** serialize the raw `analyze::Report` map. -//! It first projects the report into [`report::ComputedMetrics`] (a *struct*: -//! fields emit in declaration order, `clone_type_distribution` honors -//! `omitempty`) and routes the resulting [`cf_gojson::GoValue`] tree through -//! [`cf_gojson`] (JSON) and [`cf_reportutil`] (the CFB1 `bin` envelope). The -//! intermediate `analyze::Report` is a map-origin [`cf_gojson::GoMap`] whose -//! keys byte-sort on encode. Per `specs/rust-rewrite/DESIGN.md` §2 nothing -//! here uses `serde_json`. +//! The reported `similarity` is the exact Jaccard of the two shingle sets +//! (`1.0` for identical structure), never the `MinHash` estimate. Pairs are +//! ordered by similarity descending, then by name, so every output is +//! deterministic. //! -//! The numeric core — the `similarity` floats that reach the report — is -//! produced by [`cf_alg_minhash`] / [`cf_alg_lsh`], whose seeds and hashing -//! (FNV-1a + SplitMix64-seeded mixing) are bit-identical to the reference -//! implementation (verified against goldens), so the detection results are -//! pinned by the differential gate in `tests/compat`. +//! `clone_ratio` is the share of functions that take part in at least one +//! pair (`cloned functions / total functions`, bounded to `[0, 1]`); the +//! status message and the `N/10` score are both keyed off that ratio — see +//! [`clone_message`]. //! -//! # Example +//! Comment nodes are ignored, functions with fewer than +//! [`MIN_FUNCTION_NODES`] nodes are counted but never paired, and closures +//! (UAST `Lambda`) are candidates like any named function. Report values are +//! built as [`cf_gojson`] trees; nothing here uses `serde_json`. //! -//! With no AST the analyzer returns the "No AST provided" empty report: +//! # Example //! //! ``` -//! use cf_clones::Analyzer; -//! use cf_gojson::GoValue; +//! use cf_clones::{Aggregator, Visitor}; +//! use cf_uast::Parser; //! -//! let report = Analyzer::new().analyze_node(None); -//! assert_eq!(report.get("total_clone_pairs"), Some(&GoValue::Int(0))); -//! assert_eq!( -//! report.get("message"), -//! Some(&GoValue::Str(cf_clones::MSG_EMPTY_AST.to_string())), -//! ); +//! let src = b"package main\n\nfunc Sum(xs []int) int {\n\ttotal := 0\n\tfor _, x := range xs {\n\t\tif x > 0 {\n\t\t\ttotal += x\n\t\t} else {\n\t\t\ttotal -= x\n\t\t}\n\t}\n\treturn total\n}\n"; +//! let parser = Parser::new(); +//! let mut agg = Aggregator::new(); +//! for file in ["a.go", "b.go"] { +//! let root = parser.parse(file, src).expect("parses"); +//! agg.add_file(file, Visitor::new().scan(&root)); +//! } +//! let report = agg.finish(); +//! assert_eq!(report.total_functions, 2); +//! assert_eq!(report.total_clone_pairs, 1); +//! assert_eq!(report.pairs[0].func_a, "a.go::Sum"); +//! assert_eq!(report.pairs[0].clone_type.as_str(), "Type-1"); //! ``` #![forbid(unsafe_code)] @@ -56,38 +63,48 @@ )] pub mod aggregator; -pub mod analyzer; pub mod engine; +pub mod profile; pub mod report; pub mod report_section; pub mod shingler; -pub mod uast; pub mod visitor; pub use aggregator::Aggregator; -pub use analyzer::Analyzer; -pub use report::{ - classify_clone_type, ClonePair, ComputedMetrics, CLONE_TYPE1, CLONE_TYPE2, CLONE_TYPE3, - DEFAULT_MAX_CLONE_PAIRS, -}; -pub use report_section::ReportSection; +pub use engine::FuncEntry; +pub use profile::Fingerprint; +pub use report::{ClonePair, CloneReport, CloneType, CloneTypeCounts, FileSummary}; pub use shingler::Shingler; -pub use visitor::Visitor; +pub use visitor::{FileScan, Visitor}; /// Number of `MinHash` hash functions per signature. pub const NUM_HASHES: usize = 128; -/// Number of LSH bands. -pub const NUM_BANDS: usize = 16; +/// Number of LSH bands. With [`NUM_ROWS`] rows the candidate S-curve's 50% +/// point sits at `(1/32)^(1/4) ≈ 0.42`, below the reporting threshold, so a +/// pair at Jaccard 0.5 becomes a candidate with probability ≈ 0.87 and a pair +/// at 0.64 with probability > 0.99 (see [`cf_alg_lsh::candidate_probability`]). +pub const NUM_BANDS: usize = 32; /// Number of rows per LSH band. -pub const NUM_ROWS: usize = 8; +pub const NUM_ROWS: usize = 4; -/// Minimum AST nodes a function must have to be considered — trivial -/// getters/setters below this threshold hash identically regardless of -/// purpose and produce false positives. +/// Minimum number of UAST nodes (comments excluded) a function must have to +/// become a clone candidate. Smaller functions are counted in +/// `total_functions` but never paired: trivial getters/setters below this +/// size hash identically regardless of purpose and would only produce false +/// positives. pub const MIN_FUNCTION_NODES: usize = 20; +/// Minimum exact shingle Jaccard for a pair of structurally different +/// functions to be reported (as Type-3). +pub const SIMILARITY_THRESHOLD: f64 = 0.5; + +/// Maximum number of clone pairs stored in the report detail; the pair count, +/// the clone-type distribution and the ratio stay exact. Zero means +/// unlimited. +pub const DEFAULT_MAX_CLONE_PAIRS: usize = 1000; + /// The registered short analyzer name. pub const ANALYZER_NAME: &str = "clones"; @@ -96,23 +113,16 @@ pub const ANALYZER_FLAG: &str = "clone-detection"; /// Human-readable analyzer description. pub const ANALYZER_DESCRIPTION: &str = - "Detects duplicate and near-duplicate code using MinHash and LSH."; + "Detects duplicate and near-duplicate functions using MinHash and LSH."; /// The full analyzer ID for registration. pub const ANALYZER_ID: &str = "static/clones"; -// --- Threshold constants for the `thresholds()` method --- - -/// `clone_ratio` yellow threshold. +/// `clone_ratio` above which duplication is "moderate" (the yellow +/// threshold). pub const THRESHOLD_CLONE_RATIO_YELLOW: f64 = 0.1; -/// `clone_ratio` red threshold. +/// `clone_ratio` above which duplication is "high" (the red threshold). pub const THRESHOLD_CLONE_RATIO_RED: f64 = 0.3; -/// `total_clone_pairs` yellow threshold. -pub const THRESHOLD_CLONE_PAIRS_YELLOW: i64 = 5; -/// `total_clone_pairs` red threshold. -pub const THRESHOLD_CLONE_PAIRS_RED: i64 = 20; - -// --- Message constants (CLI contract) --- /// "No clones" message. pub const MSG_NO_CLONES: &str = "No code clones detected"; @@ -124,15 +134,6 @@ pub const MSG_MOD_CLONES: &str = "Moderate duplication - consider refactoring cl pub const MSG_HIGH_CLONES: &str = "High duplication - significant refactoring recommended"; /// "No functions" message. pub const MSG_NO_FUNCTIONS: &str = "No functions found for clone analysis"; -/// "No AST" message. -pub const MSG_EMPTY_AST: &str = "No AST provided"; - -/// Pair-count boundary for the "low duplication" message. -pub const PAIR_COUNT_LOW: usize = 5; -/// Pair-count boundary for the "moderate duplication" message. -pub const PAIR_COUNT_MOD: usize = 15; - -// --- Report keys --- /// Report key `analyzer_name`. pub const KEY_ANALYZER_NAME: &str = "analyzer_name"; @@ -146,29 +147,29 @@ pub const KEY_TOTAL_FUNCTIONS: &str = "total_functions"; pub const KEY_MESSAGE: &str = "message"; /// Report key `clone_ratio`. pub const KEY_CLONE_RATIO: &str = "clone_ratio"; -/// Report key `_func_signatures` (internal; consumed by the aggregator). -pub const KEY_FUNC_SIGNATURES: &str = "_func_signatures"; /// Report key `clone_type_distribution`. pub const KEY_CLONE_TYPE_DISTRIBUTION: &str = "clone_type_distribution"; -/// Returns the human-readable message for a given clone-pair count. -/// -/// `0` pairs is "no clones", `<= 5` is low, `<= 15` is moderate, else high: +/// Returns the status message for a clone ratio: `0` is "no clones", up to +/// [`THRESHOLD_CLONE_RATIO_YELLOW`] is low, up to [`THRESHOLD_CLONE_RATIO_RED`] +/// is moderate, above that high. The score shown next to the message is +/// derived from the same ratio ([`report_section::compute_score`]), so the two +/// never disagree. /// /// ``` /// use cf_clones::{clone_message, MSG_NO_CLONES, MSG_LOW_CLONES, MSG_MOD_CLONES, MSG_HIGH_CLONES}; -/// assert_eq!(clone_message(0), MSG_NO_CLONES); -/// assert_eq!(clone_message(5), MSG_LOW_CLONES); -/// assert_eq!(clone_message(15), MSG_MOD_CLONES); -/// assert_eq!(clone_message(16), MSG_HIGH_CLONES); +/// assert_eq!(clone_message(0.0), MSG_NO_CLONES); +/// assert_eq!(clone_message(0.1), MSG_LOW_CLONES); +/// assert_eq!(clone_message(0.3), MSG_MOD_CLONES); +/// assert_eq!(clone_message(0.31), MSG_HIGH_CLONES); /// ``` #[must_use] -pub fn clone_message(pair_count: usize) -> &'static str { - if pair_count == 0 { +pub fn clone_message(clone_ratio: f64) -> &'static str { + if clone_ratio <= 0.0 { MSG_NO_CLONES - } else if pair_count <= PAIR_COUNT_LOW { + } else if clone_ratio <= THRESHOLD_CLONE_RATIO_YELLOW { MSG_LOW_CLONES - } else if pair_count <= PAIR_COUNT_MOD { + } else if clone_ratio <= THRESHOLD_CLONE_RATIO_RED { MSG_MOD_CLONES } else { MSG_HIGH_CLONES @@ -201,21 +202,29 @@ mod tests { assert_eq!(ANALYZER_ID, "static/clones"); assert_eq!(ANALYZER_NAME, "clones"); assert_eq!(ANALYZER_FLAG, "clone-detection"); - assert_eq!(NUM_HASHES, 128); - assert_eq!(NUM_BANDS, 16); - assert_eq!(NUM_ROWS, 8); assert_eq!(NUM_BANDS * NUM_ROWS, NUM_HASHES); assert_eq!(MIN_FUNCTION_NODES, 20); } #[test] - fn clone_message_thresholds() { - assert_eq!(clone_message(0), MSG_NO_CLONES); - assert_eq!(clone_message(1), MSG_LOW_CLONES); - assert_eq!(clone_message(5), MSG_LOW_CLONES); - assert_eq!(clone_message(6), MSG_MOD_CLONES); - assert_eq!(clone_message(15), MSG_MOD_CLONES); - assert_eq!(clone_message(16), MSG_HIGH_CLONES); + fn lsh_curve_midpoint_sits_below_the_threshold() { + // The 50% point of the candidate S-curve must not exceed the + // reporting threshold, otherwise reportable pairs are mostly missed. + let midpoint = (1.0 / NUM_BANDS as f64).powf(1.0 / NUM_ROWS as f64); + assert!(midpoint < SIMILARITY_THRESHOLD, "midpoint {midpoint}"); + assert!( + cf_alg_lsh::candidate_probability(SIMILARITY_THRESHOLD, NUM_BANDS, NUM_ROWS) > 0.85 + ); + } + + #[test] + fn clone_message_is_keyed_off_the_ratio() { + assert_eq!(clone_message(0.0), MSG_NO_CLONES); + assert_eq!(clone_message(0.05), MSG_LOW_CLONES); + assert_eq!(clone_message(THRESHOLD_CLONE_RATIO_YELLOW), MSG_LOW_CLONES); + assert_eq!(clone_message(0.2), MSG_MOD_CLONES); + assert_eq!(clone_message(THRESHOLD_CLONE_RATIO_RED), MSG_MOD_CLONES); + assert_eq!(clone_message(1.0), MSG_HIGH_CLONES); } // Exact on purpose: the zero-denominator guard returns 0.0 exactly. diff --git a/crates/cf-clones/src/profile.rs b/crates/cf-clones/src/profile.rs new file mode 100644 index 00000000..f38e10c9 --- /dev/null +++ b/crates/cf-clones/src/profile.rs @@ -0,0 +1,217 @@ +//! Structural profile of one function subtree. +//! +//! A [`Profile`] carries everything the clone engine needs to know about a +//! function: its size, the pre-order node-type sequence (the shingle input), +//! and two fingerprints — one over the structure (types only) and one over +//! the leaf tokens — that decide between Type-1 and Type-2 for structurally +//! identical pairs. Comment nodes are skipped entirely, so comment-only +//! differences never separate two clones. + +use std::hash::{DefaultHasher, Hasher}; + +use cf_uast_node::Node; + +/// FNV-1a 64-bit offset basis. +const FNV64A_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; +/// FNV-1a 64-bit prime. +const FNV64A_PRIME: u64 = 0x0000_0100_0000_01b3; + +/// Byte placed between the fields fed to a fingerprint so `("ab", "c")` and +/// `("a", "bc")` hash differently. Node types and tokens never contain it. +const FIELD_SEPARATOR: &[u8] = &[0]; + +/// Node type of comment nodes (ignored by the profile). +const COMMENT_TYPE: &str = "Comment"; +/// Role of comment nodes (ignored by the profile). +const COMMENT_ROLE: &str = "Comment"; + +/// A 128-bit content fingerprint: FNV-1a 64 and `SipHash` (std's +/// [`DefaultHasher`]) over the same byte stream. Two independent hash +/// families make an accidental collision between distinct inputs +/// negligible; the values never leave the process, so neither needs to be +/// stable across Rust releases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Fingerprint { + fnv: u64, + sip: u64, +} + +/// Accumulates both fingerprint hashes over a byte stream. +struct DualHasher { + fnv: u64, + sip: DefaultHasher, +} + +impl DualHasher { + fn new() -> Self { + Self { + fnv: FNV64A_OFFSET, + sip: DefaultHasher::new(), + } + } + + fn write(&mut self, bytes: &[u8]) { + for &b in bytes { + self.fnv ^= u64::from(b); + self.fnv = self.fnv.wrapping_mul(FNV64A_PRIME); + } + self.sip.write(bytes); + } + + fn finish(self) -> Fingerprint { + Fingerprint { + fnv: self.fnv, + sip: self.sip.finish(), + } + } +} + +/// What the clone engine knows about one function subtree. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Profile<'a> { + /// Number of nodes in the subtree, comments excluded. + pub node_count: usize, + /// Pre-order node types (empty-typed and comment nodes skipped). + pub types: Vec<&'a str>, + /// Fingerprint of the node-type sequence: equal structure ⇔ equal + /// fingerprint. + pub structure: Fingerprint, + /// Fingerprint of the leaf `(type, token)` sequence: equal structure and + /// equal tokens ⇔ equal `structure` and equal `tokens`. + pub tokens: Fingerprint, +} + +/// Returns `true` for comment nodes, which the profile ignores. +fn is_comment(n: &Node) -> bool { + n.node_type == COMMENT_TYPE || n.has_any_role(&[COMMENT_ROLE]) +} + +/// Builds the [`Profile`] of the subtree rooted at `root` with an explicit +/// stack (no recursion, so arbitrarily deep trees are safe). +#[must_use] +pub fn profile(root: &Node) -> Profile<'_> { + let mut node_count = 0; + let mut types = Vec::new(); + let mut structure = DualHasher::new(); + let mut tokens = DualHasher::new(); + + let mut stack: Vec<&Node> = vec![root]; + while let Some(n) = stack.pop() { + if is_comment(n) { + continue; + } + node_count += 1; + if !n.node_type.is_empty() { + types.push(n.node_type.as_str()); + structure.write(n.node_type.as_bytes()); + structure.write(FIELD_SEPARATOR); + } + if n.children.is_empty() { + tokens.write(n.node_type.as_bytes()); + tokens.write(FIELD_SEPARATOR); + tokens.write(n.token.as_bytes()); + tokens.write(FIELD_SEPARATOR); + } + for child in n.children.iter().rev() { + stack.push(child); + } + } + + Profile { + node_count, + types, + structure: structure.finish(), + tokens: tokens.finish(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cf_uast_node::Builder; + + fn node(node_type: &str, token: &str, children: Vec) -> Node { + let mut n = Builder::new() + .with_type(node_type) + .with_token(token) + .build(); + for c in children { + n.add_child(c); + } + n + } + + fn leaf(node_type: &str, token: &str) -> Node { + node(node_type, token, Vec::new()) + } + + #[test] + fn preorder_types_and_count() { + let tree = node( + "A", + "", + vec![leaf("B", "b"), node("C", "", vec![leaf("D", "d")])], + ); + let p = profile(&tree); + assert_eq!(p.node_count, 4); + assert_eq!(p.types, vec!["A", "B", "C", "D"]); + } + + #[test] + fn empty_types_are_skipped_but_counted() { + let tree = node("A", "", vec![node("", "", vec![leaf("B", "")])]); + let p = profile(&tree); + assert_eq!(p.node_count, 3); + assert_eq!(p.types, vec!["A", "B"]); + } + + #[test] + fn comments_are_ignored_entirely() { + let plain = node("A", "", vec![leaf("B", "b")]); + let commented = node("A", "", vec![leaf("Comment", "// hi"), leaf("B", "b")]); + let p = profile(&plain); + let q = profile(&commented); + assert_eq!(p, q); + assert_eq!(q.node_count, 2); + } + + #[test] + fn renamed_tokens_keep_structure_but_change_tokens() { + let a = node("A", "", vec![leaf("Identifier", "x"), leaf("Literal", "1")]); + let b = node("A", "", vec![leaf("Identifier", "y"), leaf("Literal", "1")]); + let (p, q) = (profile(&a), profile(&b)); + assert_eq!(p.structure, q.structure); + assert_ne!(p.tokens, q.tokens); + } + + #[test] + fn different_structure_changes_the_structure_fingerprint() { + let a = node("A", "", vec![leaf("Identifier", "x")]); + let b = node("A", "", vec![leaf("Identifier", "x"), leaf("Literal", "1")]); + assert_ne!(profile(&a).structure, profile(&b).structure); + } + + #[test] + fn internal_tokens_do_not_affect_the_token_fingerprint() { + // Only LEAF tokens count, so whitespace/formatting captured in an + // internal node's full-text token never separates two clones. + let a = node("Block", "{ x }", vec![leaf("Identifier", "x")]); + let b = node("Block", "{\n\tx\n}", vec![leaf("Identifier", "x")]); + assert_eq!(profile(&a).tokens, profile(&b).tokens); + } + + #[test] + fn field_boundaries_are_separated() { + let a = node( + "A", + "", + vec![leaf("Identifier", "ab"), leaf("Identifier", "c")], + ); + let b = node( + "A", + "", + vec![leaf("Identifier", "a"), leaf("Identifier", "bc")], + ); + assert_ne!(profile(&a).tokens, profile(&b).tokens); + } +} diff --git a/crates/cf-clones/src/report.rs b/crates/cf-clones/src/report.rs index b7b892c5..f161fdbb 100644 --- a/crates/cf-clones/src/report.rs +++ b/crates/cf-clones/src/report.rs @@ -1,33 +1,66 @@ -//! Clone-type classification and the [`ComputedMetrics`] machine-export -//! struct: the clone types, the [`ClonePair`] record, the similarity-threshold -//! classifier, the clone-type distribution counts, and the -//! [`ComputedMetrics`] projection used by the JSON/YAML/binary export path. +//! The clone-detection report model: clone types, the [`ClonePair`] record, +//! the per-type distribution, the per-file summaries, and the [`CloneReport`] +//! the aggregator produces — plus its two machine projections (the raw +//! `analyze.Report` map and the YAML/binary metrics struct). -use cf_gojson::{GoMap, GoValue}; +use cf_gojson::{GoMap, GoValue, MapOrigin}; -/// Exact clone (identical AST structure and tokens). +use crate::{ + KEY_ANALYZER_NAME, KEY_CLONE_PAIRS, KEY_CLONE_RATIO, KEY_CLONE_TYPE_DISTRIBUTION, KEY_MESSAGE, + KEY_TOTAL_CLONE_PAIRS, KEY_TOTAL_FUNCTIONS, +}; + +/// Wire string of Type-1 (exact) clones. pub const CLONE_TYPE1: &str = "Type-1"; -/// Renamed clone (identical AST structure, different tokens). +/// Wire string of Type-2 (renamed) clones. pub const CLONE_TYPE2: &str = "Type-2"; -/// Near-miss clone (similar AST structure). +/// Wire string of Type-3 (near-miss) clones. pub const CLONE_TYPE3: &str = "Type-3"; -/// Threshold for Type-1 (exact) clones. -pub const SIMILARITY_EXACT: f64 = 1.0; -/// Minimum threshold for Type-2 (renamed) clones. -pub const SIMILARITY_TYPE2: f64 = 0.8; -/// Minimum threshold for Type-3 (near-miss) clones, and the minimum -/// similarity for a pair to be reported at all. -pub const SIMILARITY_TYPE3: f64 = 0.5; +/// Display label for Type-1 clones. +pub const DIST_LABEL_TYPE1: &str = "Type-1 (Exact)"; +/// Display label for Type-2 clones. +pub const DIST_LABEL_TYPE2: &str = "Type-2 (Renamed)"; +/// Display label for Type-3 clones. +pub const DIST_LABEL_TYPE3: &str = "Type-3 (Near-miss)"; -/// Maximum number of clone pairs stored in the report detail. The -/// `total_clone_pairs` count remains exact (not capped). Zero means -/// unlimited. -pub const DEFAULT_MAX_CLONE_PAIRS: usize = 1000; +/// The clone taxonomy. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum CloneType { + /// Identical structure and identical tokens. + Type1, + /// Identical structure, renamed identifiers / literals. + Type2, + /// Modified structure above the similarity threshold. + Type3, +} + +impl CloneType { + /// The wire string (`Type-1` / `Type-2` / `Type-3`). + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + CloneType::Type1 => CLONE_TYPE1, + CloneType::Type2 => CLONE_TYPE2, + CloneType::Type3 => CLONE_TYPE3, + } + } + + /// The display label (`Type-1 (Exact)` / …). + #[must_use] + pub const fn label(self) -> &'static str { + match self { + CloneType::Type1 => DIST_LABEL_TYPE1, + CloneType::Type2 => DIST_LABEL_TYPE2, + CloneType::Type3 => DIST_LABEL_TYPE3, + } + } +} /// A detected clone relationship between two functions. /// -/// Field order is the JSON/YAML key order (report contract): +/// `func_a` sorts before `func_b` (lexicographic), so the pair is a canonical +/// unordered pair. Field order is the JSON/YAML key order: /// `func_a, func_b, similarity, clone_type`. #[derive(Debug, Clone, PartialEq)] pub struct ClonePair { @@ -35,10 +68,10 @@ pub struct ClonePair { pub func_a: String, /// Second function name (`func_b`). pub func_b: String, - /// Estimated `MinHash` similarity (`similarity`). + /// Exact shingle-set Jaccard of the pair (`1.0` for identical structure). pub similarity: f64, - /// Classified clone type (`clone_type`). - pub clone_type: String, + /// Classified clone type. + pub clone_type: CloneType, } impl ClonePair { @@ -50,29 +83,17 @@ impl ClonePair { m.push("func_a", GoValue::Str(self.func_a.clone())); m.push("func_b", GoValue::Str(self.func_b.clone())); m.push("similarity", GoValue::Float(self.similarity)); - m.push("clone_type", GoValue::Str(self.clone_type.clone())); - GoValue::Object(m) + m.push( + "clone_type", + GoValue::Str(self.clone_type.as_str().to_string()), + ); + GoValue::Map(m) } -} -/// Determines the clone type from a similarity score: descending thresholds -/// `{1.0 -> Type-1, 0.8 -> Type-2}` with default `Type-3`; the first threshold -/// whose limit `similarity` meets (`>=`) wins. -/// -/// ``` -/// use cf_clones::{classify_clone_type, CLONE_TYPE1, CLONE_TYPE2, CLONE_TYPE3}; -/// assert_eq!(classify_clone_type(1.0), CLONE_TYPE1); -/// assert_eq!(classify_clone_type(0.8), CLONE_TYPE2); -/// assert_eq!(classify_clone_type(0.79), CLONE_TYPE3); -/// ``` -#[must_use] -pub fn classify_clone_type(similarity: f64) -> &'static str { - if similarity >= SIMILARITY_EXACT { - CLONE_TYPE1 - } else if similarity >= SIMILARITY_TYPE2 { - CLONE_TYPE2 - } else { - CLONE_TYPE3 + /// `func_a <-> func_b`, the issue name shown in reports. + #[must_use] + pub fn display_name(&self) -> String { + format!("{} <-> {}", self.func_a, self.func_b) } } @@ -89,99 +110,113 @@ pub struct CloneTypeCounts { impl CloneTypeCounts { /// Increments the counter for `clone_type`. - pub fn increment(&mut self, clone_type: &str) { + pub fn increment(&mut self, clone_type: CloneType) { + match clone_type { + CloneType::Type1 => self.type1 += 1, + CloneType::Type2 => self.type2 += 1, + CloneType::Type3 => self.type3 += 1, + } + } + + /// The count for one clone type. + #[must_use] + pub const fn get(&self, clone_type: CloneType) -> i64 { match clone_type { - CLONE_TYPE1 => self.type1 += 1, - CLONE_TYPE2 => self.type2 += 1, - CLONE_TYPE3 => self.type3 += 1, - _ => {} + CloneType::Type1 => self.type1, + CloneType::Type2 => self.type2, + CloneType::Type3 => self.type3, } } /// Total across all three types. #[must_use] - pub fn total(&self) -> i64 { + pub const fn total(&self) -> i64 { self.type1 + self.type2 + self.type3 } -} -/// Counts clone pairs by type. -#[must_use] -pub fn categorize_clone_pairs(pairs: &[ClonePair]) -> CloneTypeCounts { - let mut counts = CloneTypeCounts::default(); - for p in pairs { - counts.increment(&p.clone_type); + /// Builds the `clone_type_distribution` map (map-origin: keys byte-sort + /// on encode). + #[must_use] + pub fn to_go_value(self) -> GoValue { + let mut m = GoMap::new_map(); + m.push(CLONE_TYPE1, GoValue::Int(self.type1)); + m.push(CLONE_TYPE2, GoValue::Int(self.type2)); + m.push(CLONE_TYPE3, GoValue::Int(self.type3)); + GoValue::Map(m) } - counts } -/// Builds the `clone_type_distribution` map (map-origin: keys byte-sorted on -/// encode). -#[must_use] -pub fn clone_type_dist_map(c: CloneTypeCounts) -> GoValue { - // Map-origin: keys byte-sort on encode (Type-1 < Type-2 < Type-3 anyway). - let mut m = GoMap::new_map(); - m.push(CLONE_TYPE1, GoValue::Int(c.type1)); - m.push(CLONE_TYPE2, GoValue::Int(c.type2)); - m.push(CLONE_TYPE3, GoValue::Int(c.type3)); - GoValue::Object(m) +/// One analyzed file's share of the clone report (the `--per-file` entry +/// source). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct FileSummary { + /// Path relative to the analyzed root. + pub path: String, + /// Function-like nodes found in the file (closures included). + pub total_functions: i64, + /// Functions of this file that take part in at least one clone pair. + pub cloned_functions: i64, + /// Indices into [`CloneReport::pairs`] of the stored pairs that involve a + /// function of this file. + pub pairs: Vec, } -/// Computed clone-detection metrics for JSON/YAML/binary export. -/// -/// Field declaration order (the emitted order, as `cf-gojson` treats structs -/// as declaration-ordered) is: `total_functions, total_clone_pairs, -/// clone_ratio, clone_type_distribution (omitempty), clone_pairs, message`. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct ComputedMetrics { - /// Total functions analyzed (`total_functions`). +/// The aggregated clone-detection report. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct CloneReport { + /// Total function-like nodes analyzed (closures included). pub total_functions: i64, - /// Total clone pairs (`total_clone_pairs`). + /// Exact number of clone pairs found (not capped). pub total_clone_pairs: i64, - /// Fraction of functions involved in a clone (`clone_ratio`). + /// Share of functions in at least one pair, in `[0, 1]`. pub clone_ratio: f64, - /// Clone-type distribution (`clone_type_distribution`, `omitempty`). - /// - /// `None` omits the key entirely (`omitempty` on an absent map). - pub clone_type_dist: Option, - /// The detected clone pairs (`clone_pairs`). - /// - /// `None` mirrors a Go `nil` slice (the report carried no `clone_pairs` - /// key — e.g. the analyzer's empty result) and renders `null`; `Some` - /// mirrors the non-nil slice the aggregator extraction builds - /// (`make([]ClonePair, 0, …)`), so an empty walk result renders `[]`. - pub clone_pairs: Option>, - /// Human-readable summary (`message`). + /// Exact per-type distribution over every pair found. + pub distribution: CloneTypeCounts, + /// The stored pairs, ordered by similarity descending then by name + /// (capped at the aggregator's `max_clone_pairs`). + pub pairs: Vec, + /// Status message keyed off `clone_ratio`. pub message: String, + /// Per-file summaries in walk order. + pub files: Vec, } -impl ComputedMetrics { - /// Builds the struct-origin [`GoValue`] tree for this metrics struct - /// (report-format contract): fields emit in declaration order, - /// `clone_type_distribution` is omitted when `None` (`omitempty`), and - /// `clone_pairs` — which has no `omitempty` — renders a Go `nil` slice - /// (`None`) as `null` and a non-nil slice (`Some`) as a JSON array, so the - /// aggregator's empty-but-present list renders `[]` exactly as the - /// reference binary emits it. +impl CloneReport { + /// Builds the raw `analyze.Report` map (map-origin: keys byte-sort on + /// encode) that `report.json` / the plot pages consume. #[must_use] - pub fn to_go_value(&self) -> GoValue { + pub fn to_raw_report(&self) -> GoMap { + let mut report = GoMap::new(MapOrigin::Map); + report.push( + KEY_ANALYZER_NAME, + GoValue::Str(crate::ANALYZER_NAME.to_string()), + ); + report.push(KEY_TOTAL_FUNCTIONS, GoValue::Int(self.total_functions)); + report.push(KEY_TOTAL_CLONE_PAIRS, GoValue::Int(self.total_clone_pairs)); + report.push(KEY_CLONE_RATIO, GoValue::Float(self.clone_ratio)); + report.push(KEY_CLONE_PAIRS, self.pairs_value()); + report.push(KEY_CLONE_TYPE_DISTRIBUTION, self.distribution.to_go_value()); + report.push(KEY_MESSAGE, GoValue::Str(self.message.clone())); + report + } + + /// Builds the struct-origin metrics value used by the YAML and binary + /// formats, in declaration order: `total_functions, total_clone_pairs, + /// clone_ratio, clone_type_distribution, clone_pairs, message`. + #[must_use] + pub fn to_metrics_value(&self) -> GoValue { let mut m = GoMap::new_struct(); - m.push("total_functions", GoValue::Int(self.total_functions)); - m.push("total_clone_pairs", GoValue::Int(self.total_clone_pairs)); - m.push("clone_ratio", GoValue::Float(self.clone_ratio)); - if let Some(counts) = self.clone_type_dist { - // omitempty: a present map is emitted even if all counts are zero. - m.push("clone_type_distribution", clone_type_dist_map(counts)); - } - match &self.clone_pairs { - None => m.push("clone_pairs", GoValue::Null), - Some(pairs) => m.push( - "clone_pairs", - GoValue::Array(pairs.iter().map(ClonePair::to_go_value).collect()), - ), - } - m.push("message", GoValue::Str(self.message.clone())); - GoValue::Object(m) + m.push(KEY_TOTAL_FUNCTIONS, GoValue::Int(self.total_functions)); + m.push(KEY_TOTAL_CLONE_PAIRS, GoValue::Int(self.total_clone_pairs)); + m.push(KEY_CLONE_RATIO, GoValue::Float(self.clone_ratio)); + m.push(KEY_CLONE_TYPE_DISTRIBUTION, self.distribution.to_go_value()); + m.push(KEY_CLONE_PAIRS, self.pairs_value()); + m.push(KEY_MESSAGE, GoValue::Str(self.message.clone())); + GoValue::Map(m) + } + + fn pairs_value(&self) -> GoValue { + GoValue::Array(self.pairs.iter().map(ClonePair::to_go_value).collect()) } } @@ -190,104 +225,84 @@ mod tests { use super::*; use cf_gojson::Encoder; + fn pair(a: &str, b: &str, similarity: f64, clone_type: CloneType) -> ClonePair { + ClonePair { + func_a: a.into(), + func_b: b.into(), + similarity, + clone_type, + } + } + #[test] - fn classify_matches_contract_thresholds() { - assert_eq!(classify_clone_type(1.0), CLONE_TYPE1); - assert_eq!(classify_clone_type(1.5), CLONE_TYPE1); - assert_eq!(classify_clone_type(0.9), CLONE_TYPE2); - assert_eq!(classify_clone_type(0.8), CLONE_TYPE2); - assert_eq!(classify_clone_type(0.79), CLONE_TYPE3); - assert_eq!(classify_clone_type(0.5), CLONE_TYPE3); + fn clone_type_strings_and_labels() { + assert_eq!(CloneType::Type1.as_str(), "Type-1"); + assert_eq!(CloneType::Type2.label(), "Type-2 (Renamed)"); + assert_eq!(CloneType::Type3.label(), "Type-3 (Near-miss)"); } #[test] - fn categorize_counts_by_type() { - let pairs = vec![ - ClonePair { - func_a: "a".into(), - func_b: "b".into(), - similarity: 1.0, - clone_type: CLONE_TYPE1.into(), - }, - ClonePair { - func_a: "c".into(), - func_b: "d".into(), - similarity: 0.85, - clone_type: CLONE_TYPE2.into(), - }, - ClonePair { - func_a: "e".into(), - func_b: "f".into(), - similarity: 0.6, - clone_type: CLONE_TYPE3.into(), - }, - ]; - let counts = categorize_clone_pairs(&pairs); - assert_eq!(counts.type1, 1); - assert_eq!(counts.type2, 1); - assert_eq!(counts.type3, 1); - assert_eq!(counts.total(), 3); + fn counts_increment_and_total() { + let mut c = CloneTypeCounts::default(); + c.increment(CloneType::Type1); + c.increment(CloneType::Type3); + c.increment(CloneType::Type3); + assert_eq!(c.get(CloneType::Type1), 1); + assert_eq!(c.get(CloneType::Type2), 0); + assert_eq!(c.get(CloneType::Type3), 2); + assert_eq!(c.total(), 3); } #[test] fn clone_pair_json_field_order() { - let p = ClonePair { - func_a: "A".into(), - func_b: "B".into(), - similarity: 0.75, - clone_type: CLONE_TYPE3.into(), - }; + let p = pair("A", "B", 0.75, CloneType::Type3); let json = Encoder::marshal().encode_to_string(&p.to_go_value()); assert_eq!( json, r#"{"func_a":"A","func_b":"B","similarity":0.75,"clone_type":"Type-3"}"# ); + assert_eq!(p.display_name(), "A <-> B"); } #[test] - fn computed_metrics_omits_dist_emits_null_pairs() { - let cm = ComputedMetrics { - total_functions: 3, - total_clone_pairs: 0, - clone_ratio: 0.0, - clone_type_dist: None, - clone_pairs: None, - message: "No code clones detected".into(), + fn metrics_value_field_order() { + let mut distribution = CloneTypeCounts::default(); + distribution.increment(CloneType::Type2); + let report = CloneReport { + total_functions: 4, + total_clone_pairs: 1, + clone_ratio: 0.5, + distribution, + pairs: vec![pair("A", "B", 1.0, CloneType::Type2)], + message: crate::MSG_HIGH_CLONES.into(), + files: Vec::new(), }; - let json = Encoder::marshal().encode_to_string(&cm.to_go_value()); + let json = Encoder::marshal().encode_to_string(&report.to_metrics_value()); assert_eq!( json, - r#"{"total_functions":3,"total_clone_pairs":0,"clone_ratio":0,"clone_pairs":null,"message":"No code clones detected"}"# + concat!( + r#"{"total_functions":4,"total_clone_pairs":1,"clone_ratio":0.5,"#, + r#""clone_type_distribution":{"Type-1":0,"Type-2":1,"Type-3":0},"#, + r#""clone_pairs":[{"func_a":"A","func_b":"B","similarity":1,"clone_type":"Type-2"}],"#, + r#""message":"High duplication - significant refactoring recommended"}"# + ) ); } #[test] - fn computed_metrics_full_field_order() { - let cm = ComputedMetrics { - total_functions: 4, - total_clone_pairs: 1, - clone_ratio: 0.5, - clone_type_dist: Some(CloneTypeCounts { - type1: 0, - type2: 1, - type3: 0, - }), - clone_pairs: Some(vec![ClonePair { - func_a: "A".into(), - func_b: "B".into(), - similarity: 0.9, - clone_type: CLONE_TYPE2.into(), - }]), - message: "Low duplication - few clone pairs detected".into(), + fn raw_report_sorts_keys_and_names_the_analyzer() { + let report = CloneReport { + message: crate::MSG_NO_FUNCTIONS.into(), + ..CloneReport::default() }; - let json = Encoder::marshal().encode_to_string(&cm.to_go_value()); + let json = Encoder::marshal().encode_to_string(&GoValue::Map(report.to_raw_report())); assert_eq!( json, concat!( - r#"{"total_functions":4,"total_clone_pairs":1,"clone_ratio":0.5,"#, - r#""clone_type_distribution":{"Type-1":0,"Type-2":1,"Type-3":0},"#, - r#""clone_pairs":[{"func_a":"A","func_b":"B","similarity":0.9,"clone_type":"Type-2"}],"#, - r#""message":"Low duplication - few clone pairs detected"}"# + r#"{"analyzer_name":"clones","clone_pairs":[],"clone_ratio":0,"#, + r#""clone_type_distribution":{"Type-1":0,"Type-2":0,"Type-3":0},"#, + r#""message":"No functions found for clone analysis","#, + r#""total_clone_pairs":0,"total_functions":0}"# ) ); } diff --git a/crates/cf-clones/src/report_section.rs b/crates/cf-clones/src/report_section.rs index 3a1815d6..fd5e6f45 100644 --- a/crates/cf-clones/src/report_section.rs +++ b/crates/cf-clones/src/report_section.rs @@ -1,41 +1,48 @@ -//! The terminal report section for clone detection: the human-readable `text` -//! output (key metrics, the clone-type distribution, and the top clone-pair -//! issues). Terminal output is a **non-binding** format (DESIGN §2.7), so -//! byte-identity is not required here; the score/percent computations -//! nonetheless follow the reference behavior exactly. - -use cf_analyze::{GoMap, GoValue, MapOrigin, Report}; - -use crate::report::{ - categorize_clone_pairs, ClonePair, CloneTypeCounts, CLONE_TYPE1, CLONE_TYPE2, CLONE_TYPE3, -}; -use crate::{ - KEY_CLONE_PAIRS, KEY_CLONE_RATIO, KEY_CLONE_TYPE_DISTRIBUTION, KEY_MESSAGE, - KEY_TOTAL_CLONE_PAIRS, KEY_TOTAL_FUNCTIONS, -}; - -/// Machine-output severity for a clone pair below [`SEVERITY_THRESH_HIGH`] -/// (the lowercase JSON form, distinct from the capitalized terminal label -/// produced by [`ReportSection::all_issues`]). -pub const JSON_SEVERITY_FAIR: &str = "fair"; -/// Machine-output severity for a clone pair at/above [`SEVERITY_THRESH_HIGH`]. -pub const JSON_SEVERITY_POOR: &str = "poor"; +//! The clones report section: the human-facing section model (title, +//! status, key metrics, clone-type distribution, pair issues), its +//! `renderer.JSONReport` value (`--format json`, the source of the `text` +//! render), the `--per-file` entries, and the single-line `compact` render. + +use cf_gojson::{GoMap, GoValue, MapOrigin}; + +use crate::report::{ClonePair, CloneReport, CloneType, FileSummary}; +use crate::MSG_NO_FUNCTIONS; /// Section title. pub const SECTION_TITLE: &str = "CLONE DETECTION"; -/// Default status message. -pub const DEFAULT_STATUS_MSG: &str = "Clone analysis completed"; -/// Severity boundary above which a clone pair is "poor". +/// Similarity at or above which a pair's issue severity is `poor`. pub const SEVERITY_THRESH_HIGH: f64 = 0.8; +/// Issue severity for a pair at/above [`SEVERITY_THRESH_HIGH`]. +pub const SEVERITY_POOR: &str = "poor"; +/// Issue severity for a pair below [`SEVERITY_THRESH_HIGH`]. +pub const SEVERITY_FAIR: &str = "fair"; + +/// Metric label: total functions. +pub const METRIC_TOTAL_FUNCTIONS: &str = "Total Functions"; +/// Metric label: clone pairs. +pub const METRIC_CLONE_PAIRS: &str = "Clone Pairs"; +/// Metric label: clone ratio. +pub const METRIC_CLONE_RATIO: &str = "Clone Ratio"; +/// Per-file metric label: functions of the file in at least one pair. +pub const METRIC_CLONED_FUNCTIONS: &str = "Cloned Functions"; + +/// Per-file status when none of the file's functions is cloned. +pub const FILE_STATUS_NONE: &str = "No functions in clone pairs"; +/// Decimals shown for a pair's similarity in issue values. +pub const SIMILARITY_DECIMALS: usize = 2; + +/// Score label of a negative (info-only) score. +const SCORE_LABEL_INFO: &str = "Info"; +/// Compact title column width. +const COMPACT_TITLE_WIDTH: usize = 12; +/// Compact score-bar width. +const COMPACT_BAR_WIDTH: usize = 10; +/// Filled progress-bar cell. +const PROGRESS_FILLED: &str = "\u{2588}"; // █ +/// Empty progress-bar cell. +const PROGRESS_EMPTY: &str = "\u{2591}"; // ░ -/// Display label for Type-1 clones. -pub const DIST_LABEL_TYPE1: &str = "Type-1 (Exact)"; -/// Display label for Type-2 clones. -pub const DIST_LABEL_TYPE2: &str = "Type-2 (Renamed)"; -/// Display label for Type-3 clones. -pub const DIST_LABEL_TYPE3: &str = "Type-3 (Near-miss)"; - -/// A key/value metric for terminal display. +/// A key/value metric for display. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Metric { /// Metric label. @@ -44,437 +51,362 @@ pub struct Metric { pub value: String, } -/// A distribution row for terminal display. +/// A distribution row for display. #[derive(Debug, Clone, PartialEq)] pub struct DistributionItem { /// Row label. pub label: String, - /// Percentage of the total. + /// Share of the total as a fraction in `[0, 1]` (the renderer contract + /// shared by every analyzer; the formatted strings carry the `%`). pub percent: f64, /// Raw count. pub count: i64, } -/// A reported issue (clone pair) for terminal display. +/// A reported issue (one clone pair) for display. #[derive(Debug, Clone, PartialEq)] pub struct Issue { /// `func_a <-> func_b`. pub name: String, - /// The clone type (used as the location field). + /// The clone type (shown in the location column). pub location: String, - /// Formatted similarity value. + /// Formatted similarity. pub value: String, - /// Severity label (`"Poor"` / `"Fair"`). + /// `poor` / `fair`. pub severity: String, } -/// Terminal report section for clone detection. -pub struct ReportSection { - /// Section title. - pub title: String, - /// Status message. - pub message: String, - /// 0..1 score (higher is better). - pub score: f64, - report: Report, +/// The clones report section. +#[derive(Debug, Clone)] +pub struct ReportSection<'a> { + report: &'a CloneReport, } -impl ReportSection { - /// Builds a section from a clone-detection report. +impl<'a> ReportSection<'a> { + /// Wraps a report. #[must_use] - pub fn new(report: Report) -> Self { - let clone_ratio = cf_reportutil::get_float64(&report, KEY_CLONE_RATIO); - let mut msg = cf_reportutil::get_string(&report, KEY_MESSAGE); - if msg.is_empty() { - msg = DEFAULT_STATUS_MSG.to_string(); - } - let score = compute_score(clone_ratio); - Self { - title: SECTION_TITLE.to_string(), - message: msg, - score, - report, - } + pub fn new(report: &'a CloneReport) -> Self { + Self { report } } - /// Ordered key metrics for display. + /// The section title. + #[must_use] + pub fn title(&self) -> &'static str { + SECTION_TITLE + } + + /// The status message (keyed off the clone ratio). + #[must_use] + pub fn message(&self) -> &str { + &self.report.message + } + + /// The `0..1` score (higher is better): `1 - clone_ratio`. + #[must_use] + pub fn score(&self) -> f64 { + compute_score(self.report.clone_ratio) + } + + /// Ordered key metrics. #[must_use] pub fn key_metrics(&self) -> Vec { vec![ - Metric { - label: "Total Functions".to_string(), - value: cf_reportutil::format_int(cf_reportutil::get_int( - &self.report, - KEY_TOTAL_FUNCTIONS, - )), - }, - Metric { - label: "Clone Pairs".to_string(), - value: cf_reportutil::format_int(cf_reportutil::get_int( - &self.report, - KEY_TOTAL_CLONE_PAIRS, - )), - }, - Metric { - label: "Clone Ratio".to_string(), - value: cf_reportutil::format_float(cf_reportutil::get_float64( - &self.report, - KEY_CLONE_RATIO, - )), - }, + metric( + METRIC_TOTAL_FUNCTIONS, + cf_reportutil::format_int(self.report.total_functions), + ), + metric( + METRIC_CLONE_PAIRS, + cf_reportutil::format_int(self.report.total_clone_pairs), + ), + metric( + METRIC_CLONE_RATIO, + cf_reportutil::format_float(self.report.clone_ratio), + ), ] } - /// Clone-type distribution rows. + /// Clone-type distribution rows over every pair found (empty when there + /// are no pairs). #[must_use] pub fn distribution(&self) -> Vec { - let (counts, total) = self.extract_distribution(); + let counts = self.report.distribution; + let total = counts.total(); if total == 0 { return Vec::new(); } - vec![ - DistributionItem { - label: DIST_LABEL_TYPE1.to_string(), - percent: cf_reportutil::pct(counts.type1, total), - count: counts.type1, - }, - DistributionItem { - label: DIST_LABEL_TYPE2.to_string(), - percent: cf_reportutil::pct(counts.type2, total), - count: counts.type2, - }, - DistributionItem { - label: DIST_LABEL_TYPE3.to_string(), - percent: cf_reportutil::pct(counts.type3, total), - count: counts.type3, - }, - ] + [CloneType::Type1, CloneType::Type2, CloneType::Type3] + .into_iter() + .map(|t| DistributionItem { + label: t.label().to_string(), + percent: cf_reportutil::pct(counts.get(t), total), + count: counts.get(t), + }) + .collect() } - /// Returns the distribution counts and total: prefer the stored - /// full-population distribution, falling back to the capped pairs array. - fn extract_distribution(&self) -> (CloneTypeCounts, i64) { - if let Some(counts) = stored_distribution(&self.report) { - let total = counts.total(); - return (counts, total); - } - let pairs = extract_clone_pairs(&self.report); - let counts = categorize_clone_pairs(&pairs); - (counts, i64::try_from(pairs.len()).unwrap_or(i64::MAX)) + /// Every stored pair as an issue, in report order. + #[must_use] + pub fn issues(&self) -> Vec { + self.report.pairs.iter().map(issue_of).collect() } +} - /// All clone pairs as issues, sorted by similarity descending. - #[must_use] - pub fn all_issues(&self) -> Vec { - self.clone_issues(0) +fn metric(label: &str, value: String) -> Metric { + Metric { + label: label.to_string(), + value, } +} - /// The top `n` clone pairs as issues. - #[must_use] - pub fn top_issues(&self, n: usize) -> Vec { - self.clone_issues(n) +fn issue_of(p: &ClonePair) -> Issue { + let severity = if p.similarity >= SEVERITY_THRESH_HIGH { + SEVERITY_POOR + } else { + SEVERITY_FAIR + }; + Issue { + name: p.display_name(), + location: p.clone_type.as_str().to_string(), + value: format_similarity(p.similarity), + severity: severity.to_string(), } +} - fn clone_issues(&self, limit: usize) -> Vec { - let mut pairs = extract_clone_pairs(&self.report); - if pairs.is_empty() { - return Vec::new(); - } - // Sort by similarity descending with the reference's exact unstable - // pdqsort (`mapx.SortAndLimit` -> `sort.Slice` with `clonePairLess`: - // `a.Similarity > b.Similarity`, no tie-break), then truncate to the - // limit. On the already-sorted stored list this is an identity - // permutation, so the report's pair order flows through unchanged — - // which is the observable contract of the terminal output. - cf_gosort::go_sort_slice(&mut pairs, |a, b| a.similarity > b.similarity); - if limit > 0 && pairs.len() > limit { - pairs.truncate(limit); - } +/// Formats a similarity with [`SIMILARITY_DECIMALS`] decimals (`0.64`, +/// `1.00`): one decimal would collapse every Type-3 pair onto `0.5`–`0.9`. +fn format_similarity(similarity: f64) -> String { + format!("{similarity:.SIMILARITY_DECIMALS$}") +} - pairs - .into_iter() - .map(|p| { - let severity = if p.similarity >= SEVERITY_THRESH_HIGH { - "Poor" - } else { - "Fair" - }; - Issue { - name: format!("{} <-> {}", p.func_a, p.func_b), - location: p.clone_type.clone(), - value: cf_reportutil::format_float(p.similarity), - severity: severity.to_string(), - } - }) - .collect() - } +/// Converts a clone ratio to a `0..1` score (lower ratio = higher score): +/// clamps to `[0, 1]` then inverts. +#[must_use] +pub fn compute_score(clone_ratio: f64) -> f64 { + 1.0 - clone_ratio.clamp(0.0, 1.0) } -/// Formats a 0..1 score as the `N/10` label (`round(score*10)` then -/// `"{N}/10"`). A negative score renders `"Info"`, but the clone score is -/// always in `[0, 1]`. +/// Formats a `0..1` score as the `N/10` label (`round(score*10)`); a +/// negative score renders `Info`. #[must_use] pub fn score_label(score: f64) -> String { if score < 0.0 { - return "Info".to_string(); + return SCORE_LABEL_INFO.to_string(); } - let scaled = (score * 10.0).round() as i64; - format!("{scaled}/10") + format!("{}/10", (score * 10.0).round() as i64) } -/// Builds the sections-JSON [`GoValue`] tree for the single clones section -/// produced by a `run --analyzers static/clones --format json`. -/// -/// One [`ReportSection`] renders as -/// `title, score_label, status, metrics, distribution (omitempty), issues, -/// score`, wrapped by `overall_score_label, sections, overall_score`. For a -/// single scored section the executive summary's overall score equals the -/// section score. Issue severities use the lowercase machine form -/// ([`JSON_SEVERITY_POOR`]/[`JSON_SEVERITY_FAIR`]), NOT the capitalized -/// terminal label. The issues list is order-nondeterministic in the reference -/// implementation (the stored pair multiset is stable; only the tie order -/// varies run-to-run), so it is emitted here sorted by similarity descending -/// as the deterministic representative — the differential oracle canonicalizes -/// this list before comparison. +/// Builds the `renderer.JSONReport` value for the single clones section: +/// `{overall_score_label, sections: [section], overall_score}`, where the +/// section is `{title, score_label, status, metrics, distribution?, issues, +/// files?, score}`. `per_file` adds one entry per analyzed file +/// ([`file_entry`]). #[must_use] -pub fn report_section_json_value(report: &Report) -> GoValue { - let section = ReportSection::new(report.clone()); - let score = section.score; - - // metrics - let metrics = GoValue::Array( - section - .key_metrics() - .into_iter() +pub fn json_value(report: &CloneReport, per_file: bool) -> GoValue { + let section = ReportSection::new(report); + let score = section.score(); + + let mut sect = GoMap::new(MapOrigin::Struct); + sect.push("title", GoValue::Str(section.title().to_string())); + sect.push("score_label", GoValue::Str(score_label(score))); + sect.push("status", GoValue::Str(section.message().to_string())); + sect.push("metrics", metrics_value(§ion.key_metrics())); + let distribution = section.distribution(); + if !distribution.is_empty() { + sect.push("distribution", distribution_value(&distribution)); + } + sect.push("issues", issues_value(§ion.issues())); + if per_file { + let files = report.files.iter().map(|f| file_entry(report, f)).collect(); + sect.push("files", GoValue::Array(files)); + } + sect.push("score", GoValue::Float(score)); + + let mut root = GoMap::new(MapOrigin::Struct); + root.push("overall_score_label", GoValue::Str(score_label(score))); + root.push("sections", GoValue::Array(vec![GoValue::Map(sect)])); + root.push("overall_score", GoValue::Float(score)); + GoValue::Map(root) +} + +/// Builds one `--per-file` entry: the file's function count, how many of +/// them take part in a clone pair, the resulting per-file ratio/score, and +/// the stored pairs that involve the file as issues. +#[must_use] +pub fn file_entry(report: &CloneReport, file: &FileSummary) -> GoValue { + let ratio = crate::compute_clone_ratio( + file.cloned_functions as usize, + file.total_functions as usize, + ); + let score = compute_score(ratio); + let status = if file.total_functions == 0 { + MSG_NO_FUNCTIONS.to_string() + } else if file.cloned_functions == 0 { + FILE_STATUS_NONE.to_string() + } else { + format!( + "{} of {} functions in clone pairs", + file.cloned_functions, file.total_functions + ) + }; + let metrics = vec![ + metric( + METRIC_TOTAL_FUNCTIONS, + cf_reportutil::format_int(file.total_functions), + ), + metric( + METRIC_CLONED_FUNCTIONS, + cf_reportutil::format_int(file.cloned_functions), + ), + metric(METRIC_CLONE_RATIO, cf_reportutil::format_float(ratio)), + ]; + let issues: Vec = file + .pairs + .iter() + .filter_map(|&i| report.pairs.get(i)) + .map(issue_of) + .collect(); + + let mut entry = GoMap::new(MapOrigin::Struct); + entry.push("file_path", GoValue::Str(file.path.clone())); + entry.push("score_label", GoValue::Str(score_label(score))); + entry.push("status", GoValue::Str(status)); + entry.push("metrics", metrics_value(&metrics)); + entry.push("issues", issues_value(&issues)); + entry.push("score", GoValue::Float(score)); + GoValue::Map(entry) +} + +fn metrics_value(metrics: &[Metric]) -> GoValue { + GoValue::Array( + metrics + .iter() .map(|m| { let mut sm = GoMap::new(MapOrigin::Struct); - sm.push("label", GoValue::Str(m.label)); - sm.push("value", GoValue::Str(m.value)); - GoValue::Object(sm) + sm.push("label", GoValue::Str(m.label.clone())); + sm.push("value", GoValue::Str(m.value.clone())); + GoValue::Map(sm) }) .collect(), - ); + ) +} - // distribution (omitempty: omitted entirely when empty) - let dist = section.distribution(); +fn distribution_value(rows: &[DistributionItem]) -> GoValue { + GoValue::Array( + rows.iter() + .map(|d| { + let mut sd = GoMap::new(MapOrigin::Struct); + sd.push("label", GoValue::Str(d.label.clone())); + sd.push("percent", GoValue::Float(d.percent)); + sd.push("count", GoValue::Int(d.count)); + GoValue::Map(sd) + }) + .collect(), + ) +} - // issues: all pairs sorted by similarity descending with the reference's - // exact unstable pdqsort (see `clone_issues`), lowercase severity. - let mut pairs = extract_clone_pairs(report); - cf_gosort::go_sort_slice(&mut pairs, |a, b| a.similarity > b.similarity); - let issues = GoValue::Array( - pairs - .into_iter() - .map(|p| { - let severity = if p.similarity >= SEVERITY_THRESH_HIGH { - JSON_SEVERITY_POOR - } else { - JSON_SEVERITY_FAIR - }; +fn issues_value(issues: &[Issue]) -> GoValue { + GoValue::Array( + issues + .iter() + .map(|i| { let mut si = GoMap::new(MapOrigin::Struct); - si.push( - "name", - GoValue::Str(format!("{} <-> {}", p.func_a, p.func_b)), - ); - si.push("location", GoValue::Str(p.clone_type)); - si.push( - "value", - GoValue::Str(cf_reportutil::format_float(p.similarity)), - ); - si.push("severity", GoValue::Str(severity.to_string())); - GoValue::Object(si) + si.push("name", GoValue::Str(i.name.clone())); + si.push("location", GoValue::Str(i.location.clone())); + si.push("value", GoValue::Str(i.value.clone())); + si.push("severity", GoValue::Str(i.severity.clone())); + GoValue::Map(si) }) .collect(), - ); - - let mut sect = GoMap::new(MapOrigin::Struct); - sect.push("title", GoValue::Str(section.title)); - sect.push("score_label", GoValue::Str(score_label(score))); - sect.push("status", GoValue::Str(section.message)); - sect.push("metrics", metrics); - if !dist.is_empty() { - sect.push( - "distribution", - GoValue::Array( - dist.into_iter() - .map(|d| { - let mut sd = GoMap::new(MapOrigin::Struct); - sd.push("label", GoValue::Str(d.label)); - sd.push("percent", GoValue::Float(d.percent)); - sd.push("count", GoValue::Int(d.count)); - GoValue::Object(sd) - }) - .collect(), - ), - ); - } - sect.push("issues", issues); - sect.push("score", GoValue::Float(score)); - - let mut report_tree = GoMap::new(MapOrigin::Struct); - report_tree.push("overall_score_label", GoValue::Str(score_label(score))); - report_tree.push("sections", GoValue::Array(vec![GoValue::Object(sect)])); - report_tree.push("overall_score", GoValue::Float(score)); - GoValue::Object(report_tree) + ) } -/// Compact title column width. -const COMPACT_TITLE_WIDTH: usize = 12; -/// Compact score-bar width. -const COMPACT_BAR_WIDTH: usize = 10; -/// Filled progress-bar cell. -const PROGRESS_FILLED: &str = "\u{2588}"; // █ -/// Empty progress-bar cell. -const PROGRESS_EMPTY: &str = "\u{2591}"; // ░ - -/// Right-pads `s` with spaces to `width`; returns `s` unchanged when already -/// at least `width` — by BYTE length (output contract; irrelevant for the -/// ASCII section title but kept faithful). +/// Right-pads `s` with spaces to `width` characters. fn pad_right(s: &str, width: usize) -> String { - if s.len() >= width { + let len = s.chars().count(); + if len >= width { return s.to_string(); } let mut out = String::with_capacity(width); out.push_str(s); - out.extend(std::iter::repeat(' ').take(width - s.len())); + out.extend(std::iter::repeat(' ').take(width - len)); out } /// Progress bar: `trunc(value*width)` filled cells (value clamped to /// `[0,1]`), the remainder empty. fn draw_progress_bar(value: f64, width: usize) -> String { - let v = value.clamp(0.0, 1.0); - // Truncation toward zero (output contract). - let filled = (v * width as f64) as usize; - let empty = width.saturating_sub(filled); - let mut bar = String::with_capacity((filled + empty) * 3); - for _ in 0..filled { - bar.push_str(PROGRESS_FILLED); - } - for _ in 0..empty { - bar.push_str(PROGRESS_EMPTY); - } + let filled = (value.clamp(0.0, 1.0) * width as f64) as usize; + let mut bar = String::with_capacity(width * 3); + bar.extend(std::iter::repeat(PROGRESS_FILLED).take(filled)); + bar.extend(std::iter::repeat(PROGRESS_EMPTY).take(width - filled)); bar } /// Builds the single-line `--format compact` bytes for the clones section: -/// -/// ```text -/// [<bar>] <N/10> <status message>\n -/// ``` -/// -/// Color is always disabled (the compat env pins `NO_COLOR=1`), so no ANSI -/// codes are emitted; a trailing newline is appended. This is the only fully -/// deterministic terminal format for clones (it never lists the -/// order-nondeterministic clone pairs), so its bytes are pinned by the -/// differential gate. +/// `<title padded to 12> [<bar>] <N/10> <status message>\n` (no ANSI +/// color). #[must_use] -pub fn report_section_compact(report: &Report) -> Vec<u8> { - let section = ReportSection::new(report.clone()); - let title = pad_right(§ion.title, COMPACT_TITLE_WIDTH); - let bar = draw_progress_bar(section.score, COMPACT_BAR_WIDTH); - // Score bar: "[<bar>] <N/10>"; score_label() rounds score*10. - let score_bar = format!("[{}] {}", bar, score_label(section.score)); - // Compact line: "<title> <score_bar> <message>" + '\n'. - let line = format!("{title} {score_bar} {}\n", section.message); - line.into_bytes() -} - -/// Converts a clone ratio to a 0..1 score (lower ratio = higher score): -/// clamps to `[0, 1]` then inverts. -#[must_use] -pub fn compute_score(clone_ratio: f64) -> f64 { - if clone_ratio >= 1.0 { - 0.0 - } else if clone_ratio <= 0.0 { - 1.0 - } else { - 1.0 - clone_ratio - } -} - -/// Reads the stored `clone_type_distribution` counts, if present. -fn stored_distribution(report: &Report) -> Option<CloneTypeCounts> { - use cf_analyze::GoValue; - let Some(GoValue::Map(m)) = cf_reportutil::get(report, KEY_CLONE_TYPE_DISTRIBUTION) else { - return None; - }; - let mut counts = CloneTypeCounts::default(); - for (k, v) in m.entries() { - if let GoValue::Int(n) = v { - match k.as_str() { - CLONE_TYPE1 => counts.type1 = *n, - CLONE_TYPE2 => counts.type2 = *n, - CLONE_TYPE3 => counts.type3 = *n, - _ => {} - } - } - } - Some(counts) -} - -/// Extracts the clone pairs from a report's `clone_pairs` array. -fn extract_clone_pairs(report: &Report) -> Vec<ClonePair> { - use cf_analyze::GoValue; - let Some(GoValue::Array(items)) = cf_reportutil::get(report, KEY_CLONE_PAIRS) else { - return Vec::new(); - }; - let mut pairs = Vec::with_capacity(items.len()); - for item in items { - if let GoValue::Map(m) = item { - pairs.push(pair_from_map(m)); - } - } - pairs -} - -fn pair_from_map(m: &cf_analyze::GoMap) -> ClonePair { - use cf_analyze::GoValue; - let mut pair = ClonePair { - func_a: String::new(), - func_b: String::new(), - similarity: 0.0, - clone_type: String::new(), - }; - for (k, v) in m.entries() { - match (k.as_str(), v) { - ("func_a", GoValue::Str(s)) => pair.func_a.clone_from(s), - ("func_b", GoValue::Str(s)) => pair.func_b.clone_from(s), - ("similarity", GoValue::Float(f)) => pair.similarity = *f, - ("clone_type", GoValue::Str(s)) => pair.clone_type.clone_from(s), - _ => {} - } - } - pair +pub fn compact(report: &CloneReport) -> Vec<u8> { + let section = ReportSection::new(report); + let score = section.score(); + let title = pad_right(section.title(), COMPACT_TITLE_WIDTH); + let bar = draw_progress_bar(score, COMPACT_BAR_WIDTH); + format!( + "{title} [{bar}] {} {}\n", + score_label(score), + section.message() + ) + .into_bytes() } #[cfg(test)] mod tests { use super::*; - use crate::analyzer::Analyzer; - use crate::uast::NodeBuilder; - use cf_uast_node::Node; - - fn function(name: &str) -> Node { - let name_node = NodeBuilder::new("Identifier") - .role("Name") - .token(name) - .build(); - let mut f = NodeBuilder::new("Function") - .role("Function") - .child(name_node) - .build(); - let mut block = NodeBuilder::new("Block").build(); - for i in 0..24 { - let kind = ["Identifier", "Call", "Literal", "Operator"][i % 4]; - block.add_child(NodeBuilder::new(kind).build()); + use crate::report::{CloneTypeCounts, FileSummary}; + use cf_gojson::Encoder; + + fn pair(a: &str, b: &str, similarity: f64, clone_type: CloneType) -> ClonePair { + ClonePair { + func_a: a.into(), + func_b: b.into(), + similarity, + clone_type, } - f.add_child(block); - f } - // Exact comparisons on purpose: `compute_score` maps its clamp endpoints - // (0.0 / 1.0) exactly, so an epsilon compare would hide a real regression. + fn sample() -> CloneReport { + let mut distribution = CloneTypeCounts::default(); + distribution.increment(CloneType::Type1); + distribution.increment(CloneType::Type3); + CloneReport { + total_functions: 4, + total_clone_pairs: 2, + clone_ratio: 0.75, + distribution, + pairs: vec![ + pair("a.go::f", "b.go::f", 1.0, CloneType::Type1), + pair("a.go::f", "c.go::g", 0.6, CloneType::Type3), + ], + message: crate::MSG_HIGH_CLONES.into(), + files: vec![ + FileSummary { + path: "a.go".into(), + total_functions: 1, + cloned_functions: 1, + pairs: vec![0, 1], + }, + FileSummary { + path: "d.go".into(), + total_functions: 1, + cloned_functions: 0, + pairs: vec![], + }, + ], + } + } + + // Exact comparisons on purpose: the clamp endpoints map exactly. #[allow(clippy::float_cmp)] #[test] fn compute_score_clamps_and_inverts() { @@ -486,31 +418,92 @@ mod tests { } #[test] - fn section_from_clone_report_has_metrics_and_issues() { - let a = Analyzer::new(); - let root = NodeBuilder::new("File") - .child(function("foo")) - .child(function("bar")) - .build(); - let report = a.analyze_node(Some(&root)); - let section = ReportSection::new(report); - - assert_eq!(section.title, SECTION_TITLE); - assert_eq!(section.key_metrics().len(), 3); - let issues = section.all_issues(); - assert_eq!(issues.len(), 1); - assert_eq!(issues[0].severity, "Poor"); // similarity 1.0 >= 0.8 + fn score_labels() { + assert_eq!(score_label(1.0), "10/10"); + assert_eq!(score_label(0.25), "3/10"); + assert_eq!(score_label(-1.0), "Info"); + } + + #[test] + fn section_metrics_distribution_and_issues() { + let report = sample(); + let s = ReportSection::new(&report); + assert_eq!(s.title(), SECTION_TITLE); + let m = s.key_metrics(); + assert_eq!(m[0].value, "4"); + assert_eq!(m[1].value, "2"); + assert_eq!(m[2].value, "0.8"); + let d = s.distribution(); + assert_eq!(d.len(), 3); + assert_eq!(d[0].count, 1); + assert!((d[0].percent - 0.5).abs() < 1e-12); + let i = s.issues(); + assert_eq!(i[0].severity, SEVERITY_POOR); + assert_eq!(i[1].severity, SEVERITY_FAIR); + assert_eq!(i[1].location, "Type-3"); + assert_eq!(i[1].value, "0.60"); + } + + #[test] + fn empty_report_has_no_distribution_and_full_score() { + let report = CloneReport { + message: crate::MSG_NO_FUNCTIONS.into(), + ..CloneReport::default() + }; + let s = ReportSection::new(&report); + assert!(s.distribution().is_empty()); + assert!((s.score() - 1.0).abs() < 1e-12); + } + + #[test] + fn json_value_shape_and_order() { + let json = Encoder::marshal().encode_to_string(&json_value(&sample(), false)); + assert!(json.starts_with(r#"{"overall_score_label":"3/10","sections":[{"title":"CLONE DETECTION","score_label":"3/10","status":"High duplication - significant refactoring recommended","metrics":[{"label":"Total Functions","value":"4"}"#), "{json}"); + assert!( + json.contains(r#""distribution":[{"label":"Type-1 (Exact)","percent":0.5,"count":1}"#) + ); + // The report encoder HTML-escapes `<` / `>` (`<` / `>`). + assert!(json.contains(r#""issues":[{"name":"a.go::f \u003c-\u003e b.go::f","location":"Type-1","value":"1.00","severity":"poor"}"#), "{json}"); + assert!(!json.contains("\"files\"")); + assert!( + json.ends_with(r#""score":0.25}],"overall_score":0.25}"#), + "{json}" + ); + } + + #[test] + fn per_file_entries_follow_the_file_summaries() { + let json = Encoder::marshal().encode_to_string(&json_value(&sample(), true)); + assert!(json.contains(r#""files":[{"file_path":"a.go","score_label":"0/10","status":"1 of 1 functions in clone pairs","metrics":[{"label":"Total Functions","value":"1"},{"label":"Cloned Functions","value":"1"},{"label":"Clone Ratio","value":"1.0"}],"issues":[{"name":"a.go::f \u003c-\u003e b.go::f""#), "{json}"); + assert!(json.contains(r#"{"file_path":"d.go","score_label":"10/10","status":"No functions in clone pairs","metrics":[{"label":"Total Functions","value":"1"},{"label":"Cloned Functions","value":"0"},{"label":"Clone Ratio","value":"0.0"}],"issues":[],"score":1}"#), "{json}"); + } + + #[test] + fn file_entry_without_functions_uses_the_no_functions_status() { + let report = sample(); + let file = FileSummary { + path: "empty.go".into(), + ..FileSummary::default() + }; + let json = Encoder::marshal().encode_to_string(&file_entry(&report, &file)); + assert!(json.contains(r#""status":"No functions found for clone analysis""#)); + } + + #[test] + fn compact_line() { + let line = String::from_utf8(compact(&sample())).unwrap(); + assert_eq!( + line, + "CLONE DETECTION [██░░░░░░░░] 3/10 High duplication - significant refactoring recommended\n" + ); } - // Exact by construction: clone_ratio 0.0 maps to the score 1.0 endpoint. - #[allow(clippy::float_cmp)] #[test] - fn empty_report_uses_default_status_and_full_score() { - let a = Analyzer::new(); - let report = a.analyze_node(None); - let section = ReportSection::new(report); - // clone_ratio 0.0 -> score 1.0; message present so default not used. - assert_eq!(section.score, 1.0); - assert!(section.distribution().is_empty()); + fn pad_and_bar_helpers() { + assert_eq!(pad_right("ab", 4), "ab "); + assert_eq!(pad_right("abcdef", 4), "abcdef"); + assert_eq!(draw_progress_bar(0.0, 3), "░░░"); + assert_eq!(draw_progress_bar(1.0, 3), "███"); + assert_eq!(draw_progress_bar(0.5, 3), "█░░"); } } diff --git a/crates/cf-clones/src/shingler.rs b/crates/cf-clones/src/shingler.rs index 89d1e1de..a26ddb66 100644 --- a/crates/cf-clones/src/shingler.rs +++ b/crates/cf-clones/src/shingler.rs @@ -1,10 +1,11 @@ -//! K-gram shingling of UAST function subtrees. +//! K-gram shingling of a function's pre-order node-type sequence. //! -//! A *shingle* is a sequence of `k` consecutive node types from a pre-order -//! traversal, joined by `"|"`. These shingles are the set elements that feed -//! each function's `MinHash` signature. +//! A *shingle* is a window of `k` consecutive node types joined by +//! [`SHINGLE_SEPARATOR`]. Each shingle is hashed with FNV-1a; the resulting +//! sorted, deduplicated set is both the input of the `MinHash` signature and +//! the exact set the engine verifies candidate pairs against. -use cf_uast_node::Node; +use cf_alg_hashutil::fnv64a; /// Default k-gram window size for shingling. pub const DEFAULT_SHINGLE_SIZE: usize = 5; @@ -12,7 +13,7 @@ pub const DEFAULT_SHINGLE_SIZE: usize = 5; /// Separator placed between node types within a shingle. pub const SHINGLE_SEPARATOR: &str = "|"; -/// Extracts k-gram shingles from a function's UAST subtree. +/// Extracts k-gram shingles from a node-type sequence. #[derive(Debug, Clone, Copy)] pub struct Shingler { k: usize, @@ -37,121 +38,77 @@ impl Shingler { self.k } - /// Returns the k-gram shingles of `func_node`'s subtree. - /// - /// Each shingle is the UTF-8 bytes of `k` consecutive node types joined by - /// [`SHINGLE_SEPARATOR`]. Returns an empty vector if the subtree has fewer - /// than `k` typed nodes. + /// Returns the sorted, deduplicated FNV-1a hashes of every `k`-gram of + /// `types` (each window joined by [`SHINGLE_SEPARATOR`]). Empty when the + /// sequence is shorter than `k`. /// /// ``` + /// use cf_alg_hashutil::fnv64a; /// use cf_clones::Shingler; - /// use cf_uast_node::Builder; - /// - /// // A left-deep chain A -> B -> C so pre-order yields [A, B, C]. - /// let c = Builder::new().with_type("C").build(); - /// let mut b = Builder::new().with_type("B").build(); - /// b.add_child(c); - /// let mut a = Builder::new().with_type("A").build(); - /// a.add_child(b); /// - /// let shingles = Shingler::new(2).extract_shingles(&a); - /// let strings: Vec<String> = shingles - /// .iter() - /// .map(|s| String::from_utf8(s.clone()).unwrap()) - /// .collect(); - /// assert_eq!(strings, vec!["A|B".to_string(), "B|C".to_string()]); + /// let hashes = Shingler::new(2).shingle_hashes(&["A", "B", "C"]); + /// let mut want = vec![fnv64a(b"A|B"), fnv64a(b"B|C")]; + /// want.sort_unstable(); + /// assert_eq!(hashes, want); /// - /// // Fewer than k typed nodes -> no shingles. - /// assert!(Shingler::new(5).extract_shingles(&a).is_empty()); + /// // Fewer than k types -> no shingles. + /// assert!(Shingler::new(5).shingle_hashes(&["A", "B", "C"]).is_empty()); /// ``` #[must_use] - pub fn extract_shingles(&self, func_node: &Node) -> Vec<Vec<u8>> { - let types = collect_node_types(func_node); - if types.len() < self.k { + pub fn shingle_hashes(&self, types: &[&str]) -> Vec<u64> { + if self.k == 0 || types.len() < self.k { return Vec::new(); } - - let shingle_count = types.len() - self.k + 1; - let mut shingles = Vec::with_capacity(shingle_count); - for i in 0..shingle_count { - shingles.push(join_types(&types[i..i + self.k]).into_bytes()); - } - shingles + let mut buf: Vec<u8> = Vec::new(); + let mut hashes: Vec<u64> = types + .windows(self.k) + .map(|window| { + buf.clear(); + for (i, t) in window.iter().enumerate() { + if i > 0 { + buf.extend_from_slice(SHINGLE_SEPARATOR.as_bytes()); + } + buf.extend_from_slice(t.as_bytes()); + } + fnv64a(&buf) + }) + .collect(); + hashes.sort_unstable(); + hashes.dedup(); + hashes } } -/// Collects node types in pre-order, skipping empty-typed nodes. -#[must_use] -pub fn collect_node_types(root: &Node) -> Vec<String> { - let mut types = Vec::new(); - root.visit_pre_order(|n: &Node| { - if !n.node_type.is_empty() { - types.push(n.node_type.clone()); - } - }); - types -} - -/// Joins node-type strings with [`SHINGLE_SEPARATOR`]. -#[must_use] -fn join_types(types: &[String]) -> String { - types.join(SHINGLE_SEPARATOR) -} - #[cfg(test)] mod tests { use super::*; - use crate::uast::NodeBuilder; - use cf_uast_node::Node; - - fn chain(types: &[&str]) -> Node { - // Build a left-deep chain so pre-order yields the types in order. - let mut node = NodeBuilder::new(types[types.len() - 1]).build(); - for t in types[..types.len() - 1].iter().rev() { - let parent = NodeBuilder::new(t).child(node).build(); - node = parent; - } - node - } - - #[test] - fn collect_node_types_preorder() { - let tree = chain(&["A", "B", "C"]); - assert_eq!(collect_node_types(&tree), vec!["A", "B", "C"]); - } #[test] - fn collect_skips_empty_types() { - // A node with empty type must not contribute. - let inner = NodeBuilder::new("B").build(); - let mut mid = NodeBuilder::new("").build(); // empty type - mid.add_child(inner); - let root = NodeBuilder::new("A").child(mid).build(); - assert_eq!(collect_node_types(&root), vec!["A", "B"]); + fn default_k_is_five() { + assert_eq!(Shingler::default().k(), DEFAULT_SHINGLE_SIZE); + assert_eq!(DEFAULT_SHINGLE_SIZE, 5); } #[test] fn fewer_than_k_types_yields_no_shingles() { - let s = Shingler::new(5); - let tree = chain(&["A", "B", "C"]); - assert!(s.extract_shingles(&tree).is_empty()); + assert!(Shingler::new(5).shingle_hashes(&["A", "B", "C"]).is_empty()); + assert!(Shingler::new(0).shingle_hashes(&["A"]).is_empty()); } #[test] fn shingles_are_k_grams_joined_by_pipe() { - let s = Shingler::new(2); - let tree = chain(&["A", "B", "C"]); - let shingles = s.extract_shingles(&tree); - let as_strings: Vec<String> = shingles - .iter() - .map(|b| String::from_utf8(b.clone()).unwrap()) - .collect(); - assert_eq!(as_strings, vec!["A|B".to_string(), "B|C".to_string()]); + let hashes = Shingler::new(2).shingle_hashes(&["A", "B", "C"]); + let mut want = vec![fnv64a(b"A|B"), fnv64a(b"B|C")]; + want.sort_unstable(); + assert_eq!(hashes, want); } #[test] - fn default_k_is_five() { - assert_eq!(Shingler::default().k(), DEFAULT_SHINGLE_SIZE); - assert_eq!(DEFAULT_SHINGLE_SIZE, 5); + fn repeated_windows_are_a_set() { + // A|B occurs twice but the set holds it once. + let hashes = Shingler::new(2).shingle_hashes(&["A", "B", "A", "B"]); + let mut want = vec![fnv64a(b"A|B"), fnv64a(b"B|A")]; + want.sort_unstable(); + assert_eq!(hashes, want); } } diff --git a/crates/cf-clones/src/uast.rs b/crates/cf-clones/src/uast.rs deleted file mode 100644 index fbf3bf8d..00000000 --- a/crates/cf-clones/src/uast.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! UAST type and role label constants used by clone detection. -//! -//! The canonical home for these is the [`cf_uast_node`] crate; per DESIGN rule -//! (5) they are duplicated locally because that crate currently exposes the -//! same names through two ambiguous glob re-exports (`node::roles::*` and -//! `types::*`), which makes the root-level paths unresolvable. The string -//! *values* are the contract that matters for byte-identity (they are the -//! labels stored on each [`cf_uast_node::Node`]). - -/// Function node type. -pub const UAST_FUNCTION: &str = "Function"; -/// Method node type. -pub const UAST_METHOD: &str = "Method"; - -/// Function role. -pub const ROLE_FUNCTION: &str = "Function"; -/// Declaration role. -pub const ROLE_DECLARATION: &str = "Declaration"; -/// Parameter role. -pub const ROLE_PARAMETER: &str = "Parameter"; -/// Name role (used by entity-name extraction). -pub const ROLE_NAME: &str = "Name"; - -/// Test-only fluent [`cf_uast_node::Node`] builder. -/// -/// The canonical [`cf_uast_node::Builder`] is currently unresolvable at the -/// crate root because of the same ambiguous-glob issue noted above, so the test -/// suites build nodes through this minimal local helper instead. -#[cfg(test)] -#[derive(Debug, Clone)] -pub struct NodeBuilder { - node: cf_uast_node::Node, -} - -#[cfg(test)] -impl NodeBuilder { - /// Starts a node of the given type. - #[must_use] - pub fn new(node_type: &str) -> Self { - Self { - node: cf_uast_node::Builder::new().with_type(node_type).build(), - } - } - - /// Sets the node token. - #[must_use] - pub fn token(mut self, token: &str) -> Self { - self.node.token = token.to_string(); - self - } - - /// Adds a role. - #[must_use] - pub fn role(mut self, role: &str) -> Self { - self.node.roles.push(role.to_string()); - self - } - - /// Adds a child node. - #[must_use] - pub fn child(mut self, child: cf_uast_node::Node) -> Self { - self.node.add_child(child); - self - } - - /// Finalizes the node. - #[must_use] - pub fn build(self) -> cf_uast_node::Node { - self.node - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn label_values_match_contract() { - assert_eq!(UAST_FUNCTION, "Function"); - assert_eq!(UAST_METHOD, "Method"); - assert_eq!(ROLE_FUNCTION, "Function"); - assert_eq!(ROLE_DECLARATION, "Declaration"); - assert_eq!(ROLE_PARAMETER, "Parameter"); - assert_eq!(ROLE_NAME, "Name"); - } -} diff --git a/crates/cf-clones/src/visitor.rs b/crates/cf-clones/src/visitor.rs index af132ae4..40a3e47f 100644 --- a/crates/cf-clones/src/visitor.rs +++ b/crates/cf-clones/src/visitor.rs @@ -1,26 +1,51 @@ -//! The single-pass clone-detection [`Visitor`]. +//! The single-file [`Visitor`]: finds every function-like node in a UAST +//! (functions, methods and closures), names it, and builds its clone +//! candidate entry. //! -//! During traversal it collects function nodes; [`Visitor::get_report`] then -//! builds their `MinHash` signatures and exports them under the -//! `_func_signatures` report key for the cross-file [`crate::Aggregator`]. -//! Detection itself is deferred to the aggregator. +//! Named functions keep their own name (`Foo.DoWork` for methods with a +//! receiver). Closures (UAST `Lambda`) are named by the nearest enclosing +//! candidate and their ordinal inside it — `Handle.func#2` — or `func#N` at +//! file level. Candidates nested inside another candidate remember it as +//! their parent so the engine never pairs a closure with the function it is +//! written in. -use cf_analyze::{GoMap, GoValue, MapOrigin, Report}; use cf_uast_node::Node; -use crate::analyzer::build_empty_report; -use crate::engine::{build_signature, is_function_node, FuncEntry}; +use crate::engine::{build_entry, FuncEntry}; use crate::shingler::Shingler; -use crate::{ - KEY_ANALYZER_NAME, KEY_CLONE_PAIRS, KEY_CLONE_RATIO, KEY_FUNC_SIGNATURES, KEY_MESSAGE, - KEY_TOTAL_CLONE_PAIRS, KEY_TOTAL_FUNCTIONS, MSG_NO_CLONES, MSG_NO_FUNCTIONS, NUM_HASHES, -}; +use crate::NUM_HASHES; -/// Collects function nodes and exports their signatures. +/// Function node type. +const UAST_FUNCTION: &str = "Function"; +/// Method node type. +const UAST_METHOD: &str = "Method"; +/// Closure / anonymous function node type. +const UAST_LAMBDA: &str = "Lambda"; +/// Function role. +const ROLE_FUNCTION: &str = "Function"; +/// Declaration role. +const ROLE_DECLARATION: &str = "Declaration"; +/// Parameter role (receiver detection). +const ROLE_PARAMETER: &str = "Parameter"; + +/// Separator between a closure's enclosing scope and its ordinal label. +const SCOPE_SEPARATOR: &str = "."; +/// Label prefix of a closure's ordinal: `func#1`, `func#2`, … +const LAMBDA_LABEL: &str = "func#"; + +/// What one file contributes to clone detection. +#[derive(Debug, Clone, Default)] +pub struct FileScan { + /// Every function-like node seen (closures included), size-gated or not. + pub function_count: usize, + /// The clone candidates (functions large enough to shingle), in + /// pre-order. + pub entries: Vec<FuncEntry>, +} + +/// Scans a file's UAST for clone candidates. #[derive(Debug, Clone)] pub struct Visitor { - function_count: usize, - entries: Vec<FuncEntry>, shingler: Shingler, num_hashes: usize, } @@ -31,202 +56,281 @@ impl Default for Visitor { } } +/// An open function-like scope during the traversal. +struct Scope { + name: String, + entry: Option<usize>, + lambdas: usize, +} + +/// One pending node of the explicit traversal stack. +struct Frame<'a> { + node: &'a Node, + next_child: usize, + opened_scope: bool, +} + impl Visitor { - /// Creates a new clone-detection visitor with the default parameters. + /// Creates a visitor with the default shingle size and signature length. #[must_use] pub fn new() -> Self { Self { - function_count: 0, - entries: Vec::new(), shingler: Shingler::default(), num_hashes: NUM_HASHES, } } - /// Visits a node on entry, collecting function nodes and (eagerly) their - /// signatures. - /// - /// Computing the signature here (rather than lazily at report time) is - /// equivalent because the signature depends only on the node's subtree at - /// visit time; the set of functions counted and the signatures produced - /// are the same either way. - pub fn on_enter(&mut self, n: &Node) { - if is_function_node(n) { - self.function_count += 1; - if let Some(entry) = build_signature(n, &self.shingler, self.num_hashes) { - self.entries.push(entry); + /// Walks `root` in pre-order (explicit stack, so deep trees are safe) and + /// returns the file's candidates. + #[must_use] + pub fn scan(&self, root: &Node) -> FileScan { + let mut scan = FileScan::default(); + let mut scopes: Vec<Scope> = Vec::new(); + let mut file_lambdas = 0; + + let opened = self.enter(root, &mut scan, &mut scopes, &mut file_lambdas); + let mut stack = vec![Frame { + node: root, + next_child: 0, + opened_scope: opened, + }]; + + while let Some(frame) = stack.last_mut() { + if let Some(child) = frame.node.children.get(frame.next_child) { + frame.next_child += 1; + let opened = self.enter(child, &mut scan, &mut scopes, &mut file_lambdas); + stack.push(Frame { + node: child, + next_child: 0, + opened_scope: opened, + }); + continue; } + if frame.opened_scope { + scopes.pop(); + } + stack.pop(); } + scan } - /// Number of function nodes seen so far. - #[must_use] - pub fn function_count(&self) -> usize { - self.function_count + /// Handles entering `n`: records a function-like node, builds its entry + /// and opens its scope. Returns whether a scope was opened. + fn enter( + &self, + n: &Node, + scan: &mut FileScan, + scopes: &mut Vec<Scope>, + file_lambdas: &mut usize, + ) -> bool { + if !is_candidate_node(n) { + return false; + } + scan.function_count += 1; + + let name = if is_lambda(n) { + lambda_name(scopes, file_lambdas) + } else { + extract_func_name(n) + }; + let parent = scopes.iter().rev().find_map(|s| s.entry); + let entry = build_entry(name.clone(), n, &self.shingler, self.num_hashes).map(|mut e| { + e.parent = parent; + scan.entries.push(e); + scan.entries.len() - 1 + }); + scopes.push(Scope { + name, + entry, + lambdas: 0, + }); + true } +} - /// Builds the signature-export report consumed by the aggregator. With no - /// functions it returns the "No functions found" empty report. - #[must_use] - pub fn get_report(&self) -> Report { - if self.function_count == 0 { - return build_empty_report(MSG_NO_FUNCTIONS); - } +/// `true` for functions, methods and closures. +fn is_candidate_node(n: &Node) -> bool { + n.has_any_type(&[UAST_FUNCTION, UAST_METHOD, UAST_LAMBDA]) + || n.has_all_roles(&[ROLE_FUNCTION, ROLE_DECLARATION]) +} + +fn is_lambda(n: &Node) -> bool { + n.node_type == UAST_LAMBDA +} - build_signature_report(self.function_count, &self.entries) +/// Names a closure by its enclosing scope and ordinal: `<scope>.func#N`, or +/// `func#N` at file level. +fn lambda_name(scopes: &mut [Scope], file_lambdas: &mut usize) -> String { + if let Some(scope) = scopes.last_mut() { + scope.lambdas += 1; + format!( + "{}{SCOPE_SEPARATOR}{LAMBDA_LABEL}{}", + scope.name, scope.lambdas + ) + } else { + *file_lambdas += 1; + format!("{LAMBDA_LABEL}{file_lambdas}") } +} - /// Returns the collected entries (the in-process equivalent of consuming - /// `_func_signatures`). - #[must_use] - pub fn entries(&self) -> &[FuncEntry] { - &self.entries - } - - /// Like [`Visitor::get_report`] but stamps each exported signature item - /// with `_source_file = source_file`. - /// - /// In the folder pipeline the framework sets `_source_file` on every - /// `_func_signatures` collection item (the repo-relative path) after the - /// visitor runs and before the aggregator reads it back to qualify - /// function names. With no functions it returns the "No functions found" - /// empty report (which carries no signatures to stamp). - #[must_use] - pub fn get_report_with_source(&self, source_file: &str) -> Report { - if self.function_count == 0 { - return build_empty_report(MSG_NO_FUNCTIONS); +/// Extracts a unique function name from a node: the entity name (see +/// [`extract_entity_name`]), falling back to the node token, then the type; +/// methods are qualified with the receiver type (`Foo.DoWork`). +fn extract_func_name(fn_node: &Node) -> String { + let mut name = extract_entity_name(fn_node).unwrap_or_default(); + if name.is_empty() { + name = if fn_node.token.is_empty() { + fn_node.node_type.clone() + } else { + fn_node.token.clone() + }; + } + + if fn_node.node_type == UAST_METHOD { + let recv = extract_receiver_type(fn_node); + if !recv.is_empty() { + return format!("{recv}{SCOPE_SEPARATOR}{name}"); } - build_signature_report_with_source(self.function_count, &self.entries, source_file) } + name } -/// Builds the signature-export report, stamping each `{name, sig}` item with -/// `_source_file`. -#[must_use] -pub fn build_signature_report_with_source( - total_functions: usize, - entries: &[FuncEntry], - source_file: &str, -) -> Report { - let mut sig_entries = Vec::with_capacity(entries.len()); - for e in entries { - let mut m = GoMap::new_struct(); - m.push("name", GoValue::Str(e.name.clone())); - let bytes = e - .sig - .bytes() - .into_iter() - .map(|b| GoValue::Uint(u64::from(b))) - .collect(); - m.push("sig", GoValue::Array(bytes)); - m.push( - cf_analyze::SOURCE_FILE_KEY, - GoValue::Str(source_file.to_string()), - ); - sig_entries.push(GoValue::Object(m)); - } - - let mut report = GoMap::new(MapOrigin::Map); - report.push( - KEY_ANALYZER_NAME, - GoValue::Str(crate::ANALYZER_NAME.to_string()), - ); - report.push(KEY_TOTAL_FUNCTIONS, GoValue::Int(total_functions as i64)); - report.push(KEY_TOTAL_CLONE_PAIRS, GoValue::Int(0)); - report.push(KEY_CLONE_RATIO, GoValue::Float(0.0)); - report.push(KEY_CLONE_PAIRS, GoValue::Array(Vec::new())); - report.push(KEY_MESSAGE, GoValue::Str(MSG_NO_CLONES.to_string())); - report.push(KEY_FUNC_SIGNATURES, GoValue::Array(sig_entries)); - report +/// Extracts the entity name from a node: the `name` prop, else the node's +/// own token, else the first child's token or `name` prop. +fn extract_entity_name(n: &Node) -> Option<String> { + if let Some(name) = n.props.get("name") { + return Some(name.clone()); + } + if !n.token.is_empty() { + return Some(n.token.clone()); + } + let child = n.children.first()?; + if !child.token.is_empty() { + return Some(child.token.clone()); + } + child.props.get("name").cloned() } -/// Builds the signature-export report. -/// -/// The exported `_func_signatures` is an array of `{name, sig}` objects; the -/// signature is serialized via [`cf_alg_minhash::Signature::bytes`] (the -/// big-endian wire form) as a base value the aggregator decodes back. -#[must_use] -pub fn build_signature_report(total_functions: usize, entries: &[FuncEntry]) -> Report { - let mut sig_entries = Vec::with_capacity(entries.len()); - for e in entries { - let mut m = GoMap::new_struct(); - m.push("name", GoValue::Str(e.name.clone())); - // Signature carried as its big-endian byte form (decoded by the - // aggregator via Signature::from_bytes) — the portable equivalent of - // passing the signature by reference. - let bytes = e - .sig - .bytes() - .into_iter() - .map(|b| GoValue::Uint(u64::from(b))) - .collect(); - m.push("sig", GoValue::Array(bytes)); - sig_entries.push(GoValue::Object(m)); - } - - let mut report = GoMap::new(MapOrigin::Map); - report.push( - KEY_ANALYZER_NAME, - GoValue::Str(crate::ANALYZER_NAME.to_string()), - ); - report.push(KEY_TOTAL_FUNCTIONS, GoValue::Int(total_functions as i64)); - report.push(KEY_TOTAL_CLONE_PAIRS, GoValue::Int(0)); - report.push(KEY_CLONE_RATIO, GoValue::Float(0.0)); - report.push(KEY_CLONE_PAIRS, GoValue::Array(Vec::new())); - report.push(KEY_MESSAGE, GoValue::Str(MSG_NO_CLONES.to_string())); - report.push(KEY_FUNC_SIGNATURES, GoValue::Array(sig_entries)); - report +/// Extracts the receiver type name from a method node: the first child with +/// the `Parameter` role whose token looks like `(v *T)` / `(v T)`; `T` is the +/// last whitespace-separated word after stripping the parens and a leading +/// `*`. +fn extract_receiver_type(fn_node: &Node) -> String { + const MIN_RECEIVER_PARTS: usize = 2; + + for child in &fn_node.children { + if !child.has_any_role(&[ROLE_PARAMETER]) { + continue; + } + let tok = child.token.trim(); + let tok = tok.strip_prefix('(').unwrap_or(tok); + let tok = tok.strip_suffix(')').unwrap_or(tok).trim(); + let parts: Vec<&str> = tok.split_whitespace().collect(); + if parts.len() < MIN_RECEIVER_PARTS { + continue; + } + let last = parts[parts.len() - 1]; + let type_name = last.strip_prefix('*').unwrap_or(last); + if !type_name.is_empty() { + return type_name.to_string(); + } + } + String::new() } #[cfg(test)] mod tests { use super::*; - use crate::uast::NodeBuilder; - use cf_uast_node::Node; - - fn function(name: &str) -> Node { - let name_node = NodeBuilder::new("Identifier") - .role("Name") - .token(name) - .build(); - let mut f = NodeBuilder::new("Function") - .role("Function") - .child(name_node) - .build(); - let mut block = NodeBuilder::new("Block").build(); - for i in 0..24 { - let kind = ["Identifier", "Call", "Literal", "Operator"][i % 4]; - block.add_child(NodeBuilder::new(kind).build()); - } - f.add_child(block); - f + use cf_uast::Parser; + + const PROCESS_ITEMS_BODY: &str = r#" + total := 0 + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total) + return total +"#; + + fn go(src: &str) -> Node { + Parser::new().parse("a.go", src.as_bytes()).expect("parses") + } + + fn names(scan: &FileScan) -> Vec<&str> { + scan.entries.iter().map(|e| e.name.as_str()).collect() + } + + #[test] + fn named_functions_are_candidates() { + let src = format!( + "package main\n\nimport \"fmt\"\n\nfunc ProcessItems(items []int) int {{{PROCESS_ITEMS_BODY}}}\n" + ); + let scan = Visitor::new().scan(&go(&src)); + assert_eq!(scan.function_count, 1); + assert_eq!(names(&scan), vec!["ProcessItems"]); + } + + #[test] + fn methods_are_qualified_with_the_receiver_type() { + let src = format!( + "package main\n\nimport \"fmt\"\n\ntype A struct{{ items []int }}\n\nfunc (a *A) Process() int {{\n\titems := a.items{PROCESS_ITEMS_BODY}}}\n" + ); + let scan = Visitor::new().scan(&go(&src)); + assert_eq!(names(&scan), vec!["A.Process"]); + } + + #[test] + fn small_functions_are_counted_but_not_candidates() { + let scan = Visitor::new().scan(&go( + "package main\n\nfunc Add(a, b int) int {\n\treturn a + b\n}\n", + )); + assert_eq!(scan.function_count, 1); + assert!(scan.entries.is_empty()); } #[test] - fn empty_visitor_reports_no_functions() { - let v = Visitor::new(); - let report = v.get_report(); - assert_eq!( - cf_reportutil::get_string(&report, KEY_MESSAGE), - MSG_NO_FUNCTIONS + fn file_level_closures_are_named_by_ordinal() { + let src = format!( + "package main\n\nimport \"fmt\"\n\nvar first = func(items []int) int {{{PROCESS_ITEMS_BODY}}}\n\nvar second = func(items []int) int {{{PROCESS_ITEMS_BODY}}}\n" ); + let scan = Visitor::new().scan(&go(&src)); + assert_eq!(scan.function_count, 2); + assert_eq!(names(&scan), vec!["func#1", "func#2"]); + assert!(scan.entries.iter().all(|e| e.parent.is_none())); } #[test] - fn visitor_collects_functions_and_exports_signatures() { - let mut v = Visitor::new(); - let root = NodeBuilder::new("File") - .child(function("foo")) - .child(function("bar")) - .build(); - root.visit_pre_order(&mut |n: &Node| v.on_enter(n)); - assert_eq!(v.function_count(), 2); - assert_eq!(v.entries().len(), 2); - - let report = v.get_report(); - assert_eq!(cf_reportutil::get_int(&report, KEY_TOTAL_FUNCTIONS), 2); - assert_eq!(cf_reportutil::get_int(&report, KEY_TOTAL_CLONE_PAIRS), 0); - // The signature export key is present. - assert!(cf_reportutil::get(&report, KEY_FUNC_SIGNATURES).is_some()); + fn nested_closures_are_named_by_scope_and_remember_their_parent() { + let src = format!( + "package main\n\nimport \"fmt\"\n\nfunc Outer(items []int) int {{\n\tf := func() int {{{PROCESS_ITEMS_BODY}}}\n\treturn f()\n}}\n" + ); + let scan = Visitor::new().scan(&go(&src)); + assert_eq!(scan.function_count, 2); + assert_eq!(names(&scan), vec!["Outer", "Outer.func#1"]); + assert_eq!(scan.entries[1].parent, Some(0)); + } + + #[test] + fn a_closure_inside_a_small_function_has_no_parent_entry() { + // The enclosing function is not a candidate (too small once the + // closure is... no: the closure's nodes count for the parent too), so + // nesting is tracked only among entries that exist. + let src = format!( + "package main\n\nimport \"fmt\"\n\nfunc Tiny() {{\n\tgo func(items []int) {{{PROCESS_ITEMS_BODY}}}(nil)\n}}\n" + ); + let scan = Visitor::new().scan(&go(&src)); + assert_eq!(scan.function_count, 2); + let parent_of_closure = scan + .entries + .iter() + .find(|e| e.name == "Tiny.func#1") + .expect("closure entry") + .parent; + let tiny_index = scan.entries.iter().position(|e| e.name == "Tiny"); + assert_eq!(parent_of_closure, tiny_index); } } diff --git a/crates/cf-cohesion/Cargo.toml b/crates/cf-cohesion/Cargo.toml index 131c7b3a..7a55c1af 100644 --- a/crates/cf-cohesion/Cargo.toml +++ b/crates/cf-cohesion/Cargo.toml @@ -8,11 +8,12 @@ repository.workspace = true publish.workspace = true [dependencies] -# Pinned unstable sort (pdqsort) for tie ordering in report tables. -cf-gosort = { workspace = true } -# UAST node type the analyzer/visitor traverse. +# UAST node type the analyzer walks. cf-uast-node = { path = "../cf-uast-node" } -# Shared Bloom filter; its exact hash family is load-bearing for the cohesion -# scores (false positives are part of the defined behavior). -cf-alg-bloom = { path = "../cf-alg-bloom" } +# On-demand stack growth for the recursive walk over deeply nested sources. stacker = "0.1.24" + +[dev-dependencies] +# The crate's tests run the analyzer over REAL parsed trees: the parser facade +# is a test-only dependency (grammars are already built for the workspace). +cf-uast = { path = "../cf-uast" } diff --git a/crates/cf-cohesion/src/aggregator.rs b/crates/cf-cohesion/src/aggregator.rs index ed811c8b..85919314 100644 --- a/crates/cf-cohesion/src/aggregator.rs +++ b/crates/cf-cohesion/src/aggregator.rs @@ -1,215 +1,232 @@ -//! Result aggregation. +//! Cross-file aggregation. //! -//! When cohesion runs over many files (and when it feeds the quality analyzer / -//! timeseries), per-file reports are merged into one: numeric mean over -//! `lcom`/`cohesion_score`/`function_cohesion`, sum over `total_functions`, -//! concatenation + de-dup of the `functions` table keyed by -//! `("_source_file", "name")`, and a recomputed message. The aggregation keys -//! and the message labeler are part of the report contract. - -use crate::report_value::{Report, ReportValue}; -use std::collections::{BTreeMap, BTreeSet}; - -const SCORE_THRESHOLD_HIGH: f64 = 0.7; -const SCORE_THRESHOLD_MEDIUM: f64 = 0.4; -const SCORE_THRESHOLD_LOW: f64 = 0.3; - -/// Numeric keys averaged across reports. -pub const NUMERIC_KEYS: [&str; 3] = ["lcom", "cohesion_score", "function_cohesion"]; -/// Count keys summed across reports. -pub const COUNT_KEYS: [&str; 1] = ["total_functions"]; -/// The collection key holding the per-function table. -pub const COLLECTION_KEY: &str = "functions"; -/// The composite identity used to de-duplicate function rows. -pub const DEDUP_KEYS: [&str; 2] = ["_source_file", "name"]; - -/// Overall-cohesion message keyed by the aggregated score (distinct wording -/// from the per-analyzer message; both are part of the report contract). -#[must_use] -pub fn get_cohesion_message(score: f64) -> &'static str { - if score >= SCORE_THRESHOLD_HIGH { - "Excellent overall cohesion across all analyzed code" - } else if score >= SCORE_THRESHOLD_MEDIUM { - "Good overall cohesion with room for improvement" - } else if score >= SCORE_THRESHOLD_LOW { - "Fair overall cohesion - consider refactoring some functions" - } else { - "Poor overall cohesion - significant refactoring recommended" - } +//! Every function of every analyzed file is kept (no de-duplication by name); +//! files without functions carry no cohesion data and are excluded from every +//! denominator. `lcom` is the function-weighted mean of the per-file LCOM-HS +//! values (a variable is a per-file notion, so LCOM is never recomputed over +//! the union), `cohesion_score = 1 - lcom`, and `function_cohesion` is the +//! mean per-function cohesion over all functions. + +use crate::analyzer::FileReport; +use crate::calc; + +/// Overall message when no analyzed file has a function. +pub const NO_DATA_MESSAGE: &str = "No cohesion data available"; + +const MESSAGE_THRESHOLD_EXCELLENT: f64 = 0.7; +const MESSAGE_THRESHOLD_GOOD: f64 = 0.4; +const MESSAGE_THRESHOLD_FAIR: f64 = 0.3; + +/// One analyzed file: its path relative to the analyzed root and its report. +#[derive(Debug, Clone, PartialEq)] +pub struct AnalyzedFile { + /// Path relative to the analyzed root. + pub path: String, + /// The file's cohesion report. + pub report: FileReport, } -/// The empty aggregated result. -#[must_use] -pub fn create_empty_result() -> Report { - let mut r = Report::new(); - r.insert("total_functions".into(), ReportValue::Int(0)); - r.insert("lcom".into(), ReportValue::Float(0.0)); - r.insert("cohesion_score".into(), ReportValue::Float(1.0)); - r.insert("function_cohesion".into(), ReportValue::Float(1.0)); - r.insert( - "message".into(), - ReportValue::Str("No functions found".into()), - ); - r +/// One function in the cross-file table. +#[derive(Debug, Clone, PartialEq)] +pub struct AggregatedFunction { + /// Source file (relative path). + pub source_file: String, + /// Function name. + pub name: String, + /// 1-based first source line. + pub start_line: u64, + /// Source lines spanned. + pub line_count: u64, + /// Distinct variables used. + pub variable_count: usize, + /// Per-function cohesion. + pub cohesion: f64, } -/// Aggregates multiple per-file cohesion reports into one. -/// -/// Semantics: -/// * `total_functions` = sum across reports. -/// * `lcom`, `cohesion_score`, `function_cohesion` = arithmetic mean across reports -/// that carry the key. -/// * `functions` = concatenation of every report's table, de-duplicated by the -/// `(_source_file, name)` identity (first occurrence wins). -/// * `message` = recomputed from the aggregated `cohesion_score` via -/// [`get_cohesion_message`]. -/// -/// An empty input yields [`create_empty_result`]. -#[must_use] -pub fn aggregate(reports: &[Report]) -> Report { - if reports.is_empty() { - return create_empty_result(); - } +/// The cross-file aggregate. +#[derive(Debug, Clone, PartialEq)] +pub struct Aggregate { + /// Files that were parsed and analyzed. + pub files_analyzed: usize, + /// Files with at least one function (the data-bearing files). + pub files_with_functions: usize, + /// Number of functions across all files. + pub total_functions: usize, + /// Function-weighted mean of the per-file LCOM-HS. + pub lcom: f64, + /// `1 - lcom`. + pub cohesion_score: f64, + /// Mean per-function cohesion over all functions. + pub function_cohesion: f64, + /// Every function, in walk order (file, then source position). + pub functions: Vec<AggregatedFunction>, + /// Overall message (see [`overall_message`]); [`NO_DATA_MESSAGE`] when + /// there are no functions. + pub message: String, +} - let mut total_functions = 0i64; - let mut numeric_sums: BTreeMap<&str, f64> = BTreeMap::new(); - let mut numeric_counts: BTreeMap<&str, usize> = BTreeMap::new(); - let mut functions: Vec<BTreeMap<String, ReportValue>> = Vec::new(); - let mut seen: BTreeSet<(String, String)> = BTreeSet::new(); - - for report in reports { - for k in COUNT_KEYS { - if let Some(v) = report.get(k).and_then(ReportValue::as_int) { - total_functions += v; - } - } - for k in NUMERIC_KEYS { - if let Some(v) = report.get(k).and_then(ReportValue::as_float) { - *numeric_sums.entry(k).or_insert(0.0) += v; - *numeric_counts.entry(k).or_insert(0) += 1; - } - } - if let Some(rows) = report - .get(COLLECTION_KEY) - .and_then(ReportValue::as_functions) - { - for row in rows { - let sf = row - .get(DEDUP_KEYS[0]) - .and_then(ReportValue::as_str) - .unwrap_or("") - .to_string(); - let name = row - .get(DEDUP_KEYS[1]) - .and_then(ReportValue::as_str) - .unwrap_or("") - .to_string(); - if seen.insert((sf, name)) { - functions.push(row.clone()); - } - } +impl Aggregate { + /// The aggregate of `files_analyzed` files none of which has a function. + #[must_use] + pub fn no_data(files_analyzed: usize) -> Self { + Aggregate { + files_analyzed, + files_with_functions: 0, + total_functions: 0, + lcom: 0.0, + cohesion_score: 0.0, + function_cohesion: 0.0, + functions: Vec::new(), + message: NO_DATA_MESSAGE.to_string(), } } - let mean = |k: &str| -> f64 { - let count = numeric_counts.get(k).copied().unwrap_or(0); - if count == 0 { - 0.0 - } else { - numeric_sums.get(k).copied().unwrap_or(0.0) / count as f64 + /// True when at least one function was found. + #[must_use] + pub fn has_data(&self) -> bool { + self.total_functions > 0 + } +} + +/// Aggregates per-file reports (see the module docs for the semantics). +#[must_use] +pub fn aggregate(files: &[AnalyzedFile]) -> Aggregate { + let mut functions: Vec<AggregatedFunction> = Vec::new(); + let mut weighted_lcom = 0.0; + let mut files_with_functions = 0; + for file in files { + if !file.report.has_functions() { + continue; } - }; - - let cohesion_score = mean("cohesion_score"); - - let mut out = Report::new(); - out.insert("total_functions".into(), ReportValue::Int(total_functions)); - out.insert("lcom".into(), ReportValue::Float(mean("lcom"))); - out.insert("cohesion_score".into(), ReportValue::Float(cohesion_score)); - out.insert( - "function_cohesion".into(), - ReportValue::Float(mean("function_cohesion")), - ); - // The report contract sorts the de-duplicated rows by the *last* identifier - // key ("name") with the pinned unstable sort (pdqsort) before they enter - // the report. Replicating both the key and the exact tie permutation here - // is what makes the downstream value-sort in `report_section` (also the - // pinned pdqsort) emit the same byte order as the reference. - cf_gosort::go_sort_slice(&mut functions, |a, b| { - let na = a - .get(DEDUP_KEYS[1]) - .and_then(ReportValue::as_str) - .unwrap_or(""); - let nb = b - .get(DEDUP_KEYS[1]) - .and_then(ReportValue::as_str) - .unwrap_or(""); - na < nb - }); - out.insert("functions".into(), ReportValue::Functions(functions)); - out.insert( - "message".into(), - ReportValue::Str(get_cohesion_message(cohesion_score).into()), - ); - out + files_with_functions += 1; + weighted_lcom += file.report.lcom * file.report.total_functions as f64; + functions.extend(file.report.functions.iter().map(|f| AggregatedFunction { + source_file: file.path.clone(), + name: f.name.clone(), + start_line: f.start_line, + line_count: f.line_count, + variable_count: f.variable_count, + cohesion: f.cohesion, + })); + } + if functions.is_empty() { + return Aggregate::no_data(files.len()); + } + let total = functions.len(); + let lcom = weighted_lcom / total as f64; + let cohesions: Vec<f64> = functions.iter().map(|f| f.cohesion).collect(); + let cohesion_score = calc::cohesion_score(lcom); + Aggregate { + files_analyzed: files.len(), + files_with_functions, + total_functions: total, + lcom, + cohesion_score, + function_cohesion: calc::mean_or(&cohesions, 0.0), + functions, + message: overall_message(cohesion_score).to_string(), + } +} + +/// Overall message keyed on the aggregate `cohesion_score`. +#[must_use] +pub fn overall_message(score: f64) -> &'static str { + if score >= MESSAGE_THRESHOLD_EXCELLENT { + "Excellent overall cohesion across all analyzed code" + } else if score >= MESSAGE_THRESHOLD_GOOD { + "Good overall cohesion with room for improvement" + } else if score >= MESSAGE_THRESHOLD_FAIR { + "Fair overall cohesion - consider refactoring some functions" + } else { + "Poor overall cohesion - significant refactoring recommended" + } } #[cfg(test)] mod tests { use super::*; + use crate::analyzer::FunctionMetrics; + + fn file(path: &str, lcom: f64, cohesions: &[f64]) -> AnalyzedFile { + let functions: Vec<FunctionMetrics> = cohesions + .iter() + .enumerate() + .map(|(i, c)| FunctionMetrics { + name: format!("f{i}"), + start_line: i as u64 + 1, + line_count: 1, + variable_count: 1, + cohesion: *c, + }) + .collect(); + AnalyzedFile { + path: path.to_string(), + report: FileReport { + total_functions: functions.len(), + lcom, + cohesion_score: 1.0 - lcom, + function_cohesion: calc::mean_or(cohesions, 1.0), + functions, + message: String::new(), + }, + } + } - fn report(total: i64, lcom: f64, score: f64, fcoh: f64) -> Report { - let mut r = Report::new(); - r.insert("total_functions".into(), ReportValue::Int(total)); - r.insert("lcom".into(), ReportValue::Float(lcom)); - r.insert("cohesion_score".into(), ReportValue::Float(score)); - r.insert("function_cohesion".into(), ReportValue::Float(fcoh)); - r + #[test] + fn empty_input_is_no_data() { + let agg = aggregate(&[]); + assert!(!agg.has_data()); + assert_eq!(agg.message, NO_DATA_MESSAGE); + assert_eq!(agg.cohesion_score, 0.0); } #[test] - fn empty_input_is_empty_result() { - let r = aggregate(&[]); - assert_eq!(r.get("total_functions"), Some(&ReportValue::Int(0))); - assert_eq!(r.get("cohesion_score"), Some(&ReportValue::Float(1.0))); + fn function_free_files_are_excluded_from_every_denominator() { + let files = [ + file("poor.go", 0.5, &[0.0, 0.0]), + file("t1.go", 0.0, &[]), + file("t2.go", 0.0, &[]), + ]; + let agg = aggregate(&files); + assert_eq!(agg.files_analyzed, 3); + assert_eq!(agg.files_with_functions, 1); + assert_eq!(agg.total_functions, 2); + assert!((agg.lcom - 0.5).abs() < 1e-9, "lcom {}", agg.lcom); + assert!((agg.cohesion_score - 0.5).abs() < 1e-9); + assert_eq!(agg.function_cohesion, 0.0); } #[test] - fn sums_counts_and_means_numerics() { - let a = report(2, 0.2, 0.8, 0.6); - let b = report(3, 0.4, 0.6, 0.4); - let r = aggregate(&[a, b]); - assert_eq!(r.get("total_functions"), Some(&ReportValue::Int(5))); - assert!((r.get("lcom").unwrap().as_float().unwrap() - 0.3).abs() < 1e-9); - assert!((r.get("cohesion_score").unwrap().as_float().unwrap() - 0.7).abs() < 1e-9); - assert!((r.get("function_cohesion").unwrap().as_float().unwrap() - 0.5).abs() < 1e-9); + fn scalars_are_function_weighted() { + // 3 functions at LCOM 1/3, 2 functions at LCOM 0.5. + let files = [ + file("a.go", 1.0 / 3.0, &[1.0, 1.0, 2.0 / 3.0]), + file("b.go", 0.5, &[0.0, 0.0]), + ]; + let agg = aggregate(&files); + assert_eq!(agg.total_functions, 5); + assert!((agg.lcom - 0.4).abs() < 1e-9, "lcom {}", agg.lcom); + assert!((agg.cohesion_score - 0.6).abs() < 1e-9); + assert!((agg.function_cohesion - (2.0 + 2.0 / 3.0) / 5.0).abs() < 1e-9); + assert_eq!(agg.message, overall_message(0.6)); } #[test] - fn message_recomputed_from_score() { - let r = aggregate(&[report(1, 0.0, 0.75, 1.0)]); - assert_eq!( - r.get("message"), - Some(&ReportValue::Str( - "Excellent overall cohesion across all analyzed code".into() - )) - ); + fn every_function_is_kept_even_with_equal_names() { + let mut a = file("a.go", 0.0, &[1.0, 1.0]); + a.report.functions[0].name = "String".into(); + a.report.functions[1].name = "String".into(); + let agg = aggregate(&[a]); + assert_eq!(agg.functions.len(), 2); + assert_eq!(agg.functions[0].name, agg.functions[1].name); + assert_ne!(agg.functions[0].start_line, agg.functions[1].start_line); } #[test] - fn functions_deduped_by_source_and_name() { - let mut a = report(1, 0.0, 0.5, 0.5); - let mut b = report(1, 0.0, 0.5, 0.5); - let mut row = BTreeMap::new(); - row.insert("_source_file".to_string(), ReportValue::Str("a.go".into())); - row.insert("name".to_string(), ReportValue::Str("f".into())); - a.insert( - "functions".into(), - ReportValue::Functions(vec![row.clone()]), - ); - b.insert("functions".into(), ReportValue::Functions(vec![row])); - let r = aggregate(&[a, b]); - let funcs = r.get("functions").unwrap().as_functions().unwrap(); - assert_eq!(funcs.len(), 1); + fn overall_messages_follow_the_thresholds() { + assert!(overall_message(0.7).starts_with("Excellent")); + assert!(overall_message(0.4).starts_with("Good")); + assert!(overall_message(0.3).starts_with("Fair")); + assert!(overall_message(0.0).starts_with("Poor")); } } diff --git a/crates/cf-cohesion/src/analyzer.rs b/crates/cf-cohesion/src/analyzer.rs index f7d6af24..b1776e24 100644 --- a/crates/cf-cohesion/src/analyzer.rs +++ b/crates/cf-cohesion/src/analyzer.rs @@ -1,694 +1,311 @@ -//! The cohesion [`Analyzer`]. -//! -//! Drives function discovery, variable extraction, per-function cohesion, the three -//! module scalars, and assembly of the intermediate [`Report`]. +//! The cohesion [`Analyzer`] and its per-file [`FileReport`]. -use crate::calc; -use crate::report_value::{Report, ReportValue}; -use crate::uast::{role, ty, Node}; -use std::collections::BTreeMap; +use std::collections::BTreeSet; -/// Default maximum UAST traversal depth. The depth limit is enforced by the -/// shared traverser in the integrated build. -pub const MAX_DEPTH_VALUE: i64 = 10; +use cf_uast_node::Node; -// --- Assessment thresholds (report contract) --- +use crate::metrics::{ + COHESION_THRESHOLD_EXCELLENT, COHESION_THRESHOLD_FAIR, COHESION_THRESHOLD_GOOD, +}; +use crate::report_value::{Report, ReportValue}; +use crate::{calc, visitor}; -const COHESION_THRESHOLD_HIGH: f64 = 0.6; -const COHESION_THRESHOLD_MEDIUM: f64 = 0.4; -const COHESION_THRESHOLD_LOW: f64 = 0.3; +// --- Per-file message thresholds on `cohesion_score` --- -const COUNT_THRESHOLD_HIGH: usize = 3; -const LINE_COUNT_THRESHOLD_HIGH: i64 = 10; -const MAGIC7: usize = 7; -const MAGIC30: i64 = 30; +const MESSAGE_THRESHOLD_EXCELLENT: f64 = 0.7; +const MESSAGE_THRESHOLD_GOOD: f64 = 0.4; +const MESSAGE_THRESHOLD_FAIR: f64 = 0.3; -// --- Detail-message thresholds (shared with the aggregated-score message) --- +// --- Per-function assessment thresholds --- -const SCORE_THRESHOLD_HIGH: f64 = 0.7; -const SCORE_THRESHOLD_MEDIUM: f64 = 0.4; -const SCORE_THRESHOLD_LOW: f64 = 0.3; +const VARIABLES_FEW_MAX: usize = 3; +const VARIABLES_MODERATE_MAX: usize = 7; +const LINES_SMALL_MAX: u64 = 10; +const LINES_MEDIUM_MAX: u64 = 30; -/// A function with its cohesion metrics. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct Function { - /// Function name. - pub name: String, - /// All variable names found within the function (with duplicates; - /// de-duplication happens later where the algorithm requires it). - pub variables: Vec<String>, - /// Source line count. - pub line_count: i64, - /// Per-function cohesion score, filled in by - /// [`Analyzer::compute_per_function_cohesion`]. - pub cohesion: f64, -} +/// Message of a file without functions. +pub const EMPTY_FILE_MESSAGE: &str = "No functions found"; -/// Typed per-function report item. -/// -/// Field order here is cosmetic; the JSON shape is produced by -/// [`Analyzer::convert_cohesion_function_items`]. -#[derive(Debug, Clone, PartialEq)] -pub struct FunctionReportItem { - /// Function name. +/// A function and the distinct variables it uses. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Function { + /// Function name, qualified `Type.name` when the UAST gives a receiver or + /// enclosing class. pub name: String, - /// Emoji cohesion assessment. - pub cohesion_assessment: String, - /// Emoji variable-count assessment. - pub variable_assessment: String, - /// Emoji size assessment. - pub size_assessment: String, - /// Source line count. - pub line_count: i64, - /// Variable count, duplicates included (the length of - /// [`Function::variables`]). - pub variable_count: usize, - /// Per-function cohesion. - pub cohesion: f64, + /// Distinct variable names used by the function. + pub variables: BTreeSet<String>, + /// 1-based first source line (`0` without position). + pub start_line: u64, + /// 1-based last source line (`0` without position). + pub end_line: u64, } -/// The cohesion analyzer. -/// -/// Stateless: the traversal and extraction helpers are inlined into the -/// extraction routines, so the struct is a zero-sized marker. Construct via -/// [`Analyzer::new`]. -#[derive(Debug, Clone, Default)] -pub struct Analyzer; - -/// Error returned when [`Analyzer::analyze`] is given no root node. The error -/// text is part of the CLI contract. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct NilRootNode; - -impl std::fmt::Display for NilRootNode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("root node is nil") - } -} - -impl std::error::Error for NilRootNode {} - -impl Analyzer { - /// Creates a new analyzer. +impl Function { + /// A function with the given distinct variables and no position. #[must_use] - pub fn new() -> Self { - Analyzer + pub fn with_variables(name: &str, variables: &[&str]) -> Self { + Function { + name: name.to_string(), + variables: variables.iter().map(|v| (*v).to_string()).collect(), + start_line: 0, + end_line: 0, + } } - /// The analyzer name. + /// Source lines spanned by the function node (`end - start + 1`). #[must_use] - pub fn name(&self) -> &'static str { - crate::ANALYZER_NAME + pub fn line_count(&self) -> u64 { + self.end_line.saturating_sub(self.start_line) + 1 } +} - /// The CLI flag. - #[must_use] - pub fn flag(&self) -> &'static str { - "cohesion-analysis" - } +/// Cohesion metrics of one function. +#[derive(Debug, Clone, PartialEq)] +pub struct FunctionMetrics { + /// Function name (see [`Function::name`]). + pub name: String, + /// 1-based first source line. + pub start_line: u64, + /// Source lines spanned by the function. + pub line_count: u64, + /// Number of distinct variables used. + pub variable_count: usize, + /// Per-function cohesion: shared variables over distinct variables. + pub cohesion: f64, +} - /// The analyzer description. +impl FunctionMetrics { + /// Cohesion assessment label with its traffic-light emoji. #[must_use] - pub fn description(&self) -> &'static str { - "Calculates LCOM-HS (Henderson-Sellers) and cohesion metrics." - } - - /// Performs cohesion analysis on a UAST root. - /// - /// Returns an intermediate [`Report`]; convert to the machine format with - /// [`crate::metrics::compute_all_metrics`]. - /// - /// A subtree with no functions yields the empty-result shape: `lcom = 0`, - /// `cohesion_score = 1`, `function_cohesion = 1`, and no `analyzer_name` / - /// `functions` keys: - /// - /// ``` - /// use cf_cohesion::Analyzer; - /// use cf_cohesion::report_value::ReportValue; - /// use cf_cohesion::uast::TestNode; - /// - /// let report = Analyzer::new().analyze(&TestNode::block(vec![])).unwrap(); - /// assert_eq!(report.get("total_functions"), Some(&ReportValue::Int(0))); - /// assert_eq!(report.get("cohesion_score"), Some(&ReportValue::Float(1.0))); - /// assert!(report.get("functions").is_none()); - /// ``` - /// - /// # Errors - /// - /// Currently never fails: the root is non-nullable, so the missing-root - /// error cannot fire. The `Result` signature leaves room for the integrated - /// traverser to surface errors. Pass an explicitly-empty subtree to model - /// "no functions". - pub fn analyze<N: Node>(&self, root: &N) -> Result<Report, NilRootNode> { - self.analyze_with_find_depth(root, None) - } - - /// [`analyze`](Self::analyze) with the reference shared traverser's - /// `maxDepth` applied to the function DISCOVERY walk: with `Some(cap)`, - /// only function nodes at depth `<= cap` below the root (root at depth 0) - /// are found; per-function variable extraction over each found function's - /// whole subtree is unchanged. The history `quality` analyzer's cohesion - /// instance runs with `maxDepth = 10` ([`MAX_DEPTH_VALUE`]); the static - /// surfaces run uncapped. - /// - /// # Errors - /// - /// Currently never fails (see [`analyze`](Self::analyze)). - pub fn analyze_with_find_depth<N: Node>( - &self, - root: &N, - find_depth: Option<usize>, - ) -> Result<Report, NilRootNode> { - let functions = self.find_functions_capped(root, find_depth); - - if functions.is_empty() { - return Ok(self.build_empty_result()); + pub fn cohesion_assessment(&self) -> &'static str { + if self.cohesion >= COHESION_THRESHOLD_EXCELLENT { + "\u{1F7E2} Excellent" + } else if self.cohesion >= COHESION_THRESHOLD_GOOD { + "\u{1F7E1} Good" + } else if self.cohesion >= COHESION_THRESHOLD_FAIR { + "\u{1F7E1} Fair" + } else { + "\u{1F534} Poor" } - - let mut functions = functions; - self.compute_per_function_cohesion(&mut functions); - - let metrics = self.calculate_metrics(&functions); - Ok(self.build_result(&functions, &metrics)) } - /// Performs cohesion analysis the way the **static pipeline actually runs - /// it**: via the cohesion [`visitor`](crate::visitor), driven by a preorder - /// DFS over the UAST. - /// - /// This is NOT the same as [`analyze`](Self::analyze): the visitor's function - /// detection (type Function/Method OR roles {Function AND Declaration}) and - /// its context-stack variable attribution (variables belong to the - /// *innermost* enclosing function) differ from the [`find_functions`] - /// (Self::find_functions) path, and the static pipeline uses the visitor. - /// Use this for folder-walk parity. + /// Variable-count assessment label. #[must_use] - pub fn analyze_visitor<N: Node>(&self, root: &N) -> Report { - let functions = crate::visitor::collect_functions_via_visitor(self, root); - if functions.is_empty() { - return self.build_empty_result(); + pub fn variable_assessment(&self) -> &'static str { + if self.variable_count <= VARIABLES_FEW_MAX { + "\u{1F7E2} Few" + } else if self.variable_count <= VARIABLES_MODERATE_MAX { + "\u{1F7E1} Moderate" + } else { + "\u{1F534} Many" } - let mut functions = functions; - self.compute_per_function_cohesion(&mut functions); - let metrics = self.calculate_metrics(&functions); - self.build_result(&functions, &metrics) } - /// Visitor function predicate: type Function/Method OR roles - /// {Function AND Declaration}. Distinct from the `find_functions` predicate. + /// Size assessment label. #[must_use] - pub fn is_visitor_function<N: Node>(&self, n: &N) -> bool { - n.has_any_type(&[ty::FUNCTION, ty::METHOD]) - || n.has_all_roles(&[role::FUNCTION, role::DECLARATION]) - } - - /// Computes per-function cohesion via the global shared-variable Bloom - /// filter. - pub fn compute_per_function_cohesion(&self, functions: &mut [Function]) { - let global_filter = calc::build_global_variable_filter(functions); - for f in functions.iter_mut() { - f.cohesion = self.calculate_function_level_cohesion(f, global_filter.as_ref()); + pub fn size_assessment(&self) -> &'static str { + if self.line_count <= LINES_SMALL_MAX { + "\u{1F7E2} Small" + } else if self.line_count <= LINES_MEDIUM_MAX { + "\u{1F7E1} Medium" + } else { + "\u{1F534} Large" } } +} - /// Builds the empty result map (the "no functions" report shape). - #[must_use] - pub fn build_empty_result(&self) -> Report { - let mut r = Report::new(); - r.insert("total_functions".into(), ReportValue::Int(0)); - r.insert("lcom".into(), ReportValue::Float(0.0)); - r.insert("cohesion_score".into(), ReportValue::Float(1.0)); - r.insert("function_cohesion".into(), ReportValue::Float(1.0)); - r.insert( - "message".into(), - ReportValue::Str("No functions found".into()), - ); - r - } +/// Cohesion of one source file. +#[derive(Debug, Clone, PartialEq)] +pub struct FileReport { + /// Number of functions found. + pub total_functions: usize, + /// LCOM-HS of the file (see [`calc::lcom`]). + pub lcom: f64, + /// `1 - lcom`. + pub cohesion_score: f64, + /// Mean per-function cohesion. + pub function_cohesion: f64, + /// Per-function metrics, in discovery order. + pub functions: Vec<FunctionMetrics>, + /// Human-readable summary (see [`file_message`]). + pub message: String, +} - /// Computes the three module scalars. +impl FileReport { + /// The report of a file without functions: `lcom = 0`, perfect scores, + /// [`EMPTY_FILE_MESSAGE`]. #[must_use] - pub fn calculate_metrics(&self, functions: &[Function]) -> Metrics { - let lcom = self.calculate_lcom(functions); - let cohesion_score = self.calculate_cohesion_score(lcom, functions.len()); - let function_cohesion = self.calculate_function_cohesion(functions); - Metrics { - lcom, - cohesion_score, - function_cohesion, + pub fn empty() -> Self { + FileReport { + total_functions: 0, + lcom: 0.0, + cohesion_score: calc::PERFECT_COHESION, + function_cohesion: calc::PERFECT_COHESION, + functions: Vec::new(), + message: EMPTY_FILE_MESSAGE.to_string(), } } - /// Constructs the final intermediate result. + /// True when the file holds at least one function. #[must_use] - pub fn build_result(&self, functions: &[Function], metrics: &Metrics) -> Report { - let report_items = self.build_detailed_functions_table(functions); - let message = self.get_cohesion_message(metrics.cohesion_score); + pub fn has_functions(&self) -> bool { + self.total_functions > 0 + } + /// The scalar view consumed by the history `quality` analyzer + /// (`cf_quality::analyze_file`). + #[must_use] + pub fn to_report(&self) -> Report { let mut r = Report::new(); - r.insert( - "analyzer_name".into(), - ReportValue::Str(crate::ANALYZER_NAME.into()), - ); r.insert( "total_functions".into(), - ReportValue::Int(functions.len() as i64), + ReportValue::Int(self.total_functions as i64), ); - r.insert("lcom".into(), ReportValue::Float(metrics.lcom)); + r.insert("lcom".into(), ReportValue::Float(self.lcom)); r.insert( "cohesion_score".into(), - ReportValue::Float(metrics.cohesion_score), + ReportValue::Float(self.cohesion_score), ); r.insert( "function_cohesion".into(), - ReportValue::Float(metrics.function_cohesion), - ); - r.insert( - "functions".into(), - ReportValue::Functions(self.convert_cohesion_function_items(&report_items, "")), + ReportValue::Float(self.function_cohesion), ); - r.insert("message".into(), ReportValue::Str(message)); + r.insert("message".into(), ReportValue::Str(self.message.clone())); r } +} - /// Builds the typed per-function table. - #[must_use] - pub fn build_detailed_functions_table( - &self, - functions: &[Function], - ) -> Vec<FunctionReportItem> { - functions - .iter() - .map(|f| FunctionReportItem { - name: f.name.clone(), - line_count: f.line_count, - variable_count: f.variables.len(), - cohesion: f.cohesion, - cohesion_assessment: self.get_cohesion_assessment(f.cohesion), - variable_assessment: self.get_variable_assessment(f.variables.len()), - size_assessment: self.get_size_assessment(f.line_count), - }) - .collect() - } - - /// Converts typed items to the dynamic per-function map shape. - /// `source_file`, when non-empty, is attached under the `_source_file` key. - #[must_use] - pub fn convert_cohesion_function_items( - &self, - items: &[FunctionReportItem], - source_file: &str, - ) -> Vec<BTreeMap<String, ReportValue>> { - items - .iter() - .map(|fn_item| { - let mut m: BTreeMap<String, ReportValue> = BTreeMap::new(); - m.insert("name".into(), ReportValue::Str(fn_item.name.clone())); - m.insert("line_count".into(), ReportValue::Int(fn_item.line_count)); - m.insert( - "variable_count".into(), - ReportValue::Int(fn_item.variable_count as i64), - ); - m.insert("cohesion".into(), ReportValue::Float(fn_item.cohesion)); - m.insert( - "cohesion_assessment".into(), - ReportValue::Str(fn_item.cohesion_assessment.clone()), - ); - m.insert( - "variable_assessment".into(), - ReportValue::Str(fn_item.variable_assessment.clone()), - ); - m.insert( - "size_assessment".into(), - ReportValue::Str(fn_item.size_assessment.clone()), - ); - if !source_file.is_empty() { - m.insert( - crate::metrics::SOURCE_FILE_KEY.into(), - ReportValue::Str(source_file.into()), - ); - } - m - }) - .collect() - } - - /// Detail message keyed by cohesion score: the first label whose limit the - /// score meets or exceeds, scanning highest first. - #[must_use] - pub fn get_cohesion_message(&self, score: f64) -> String { - if score >= SCORE_THRESHOLD_HIGH { - "Excellent cohesion - functions are well-focused and cohesive".into() - } else if score >= SCORE_THRESHOLD_MEDIUM { - "Good cohesion - functions have reasonable focus".into() - } else if score >= SCORE_THRESHOLD_LOW { - "Fair cohesion - some functions could be more focused".into() - } else { - "Poor cohesion - functions lack focus and should be refactored".into() - } - } - - /// Emoji cohesion assessment. - /// - /// `>= 0.6` is excellent, `>= 0.4` good, `>= 0.3` fair, otherwise poor: - /// - /// ``` - /// use cf_cohesion::Analyzer; - /// let a = Analyzer::new(); - /// assert_eq!(a.get_cohesion_assessment(0.6), "\u{1F7E2} Excellent"); - /// assert_eq!(a.get_cohesion_assessment(0.4), "\u{1F7E1} Good"); - /// assert_eq!(a.get_cohesion_assessment(0.3), "\u{1F7E1} Fair"); - /// assert_eq!(a.get_cohesion_assessment(0.29), "\u{1F534} Poor"); - /// ``` - #[must_use] - pub fn get_cohesion_assessment(&self, cohesion: f64) -> String { - if cohesion >= COHESION_THRESHOLD_HIGH { - "\u{1F7E2} Excellent".into() // green circle - } else if cohesion >= COHESION_THRESHOLD_MEDIUM { - "\u{1F7E1} Good".into() // yellow circle - } else if cohesion >= COHESION_THRESHOLD_LOW { - "\u{1F7E1} Fair".into() // yellow circle - } else { - "\u{1F534} Poor".into() // red circle - } - } - - /// Emoji variable-count assessment. - #[must_use] - pub fn get_variable_assessment(&self, count: usize) -> String { - if count <= COUNT_THRESHOLD_HIGH { - "\u{1F7E2} Few".into() - } else if count <= MAGIC7 { - "\u{1F7E1} Moderate".into() - } else { - "\u{1F534} Many".into() - } - } - - /// Emoji size assessment. - #[must_use] - pub fn get_size_assessment(&self, line_count: i64) -> String { - if line_count <= LINE_COUNT_THRESHOLD_HIGH { - "\u{1F7E2} Small".into() - } else if line_count <= MAGIC30 { - "\u{1F7E1} Medium".into() - } else { - "\u{1F534} Large".into() - } - } - - // --- Machine-format report writers --- - - /// Writes the JSON machine report: the report is reduced to - /// [`crate::metrics::ComputedMetrics`] then encoded with two-space indent, - /// HTML escaping ON, and **no trailing newline**. - /// - /// # Errors - /// - /// Returns any I/O error from `w`. - pub fn format_report_json<W: std::io::Write>( - &self, - report: &Report, - w: &mut W, - ) -> std::io::Result<()> { - let metrics = crate::metrics::compute_all_metrics(report); - w.write_all(&crate::serialize::encode_json(&metrics)) - } - - /// Writes the YAML machine report. - /// - /// # Errors - /// - /// Returns any I/O error from `w`. - pub fn format_report_yaml<W: std::io::Write>( - &self, - report: &Report, - w: &mut W, - ) -> std::io::Result<()> { - let metrics = crate::metrics::compute_all_metrics(report); - w.write_all(&crate::serialize::encode_yaml(&metrics)) - } - - /// Writes the binary (CFB1) machine report. - /// - /// # Errors - /// - /// Returns any I/O error from `w`. - pub fn format_report_binary<W: std::io::Write>( - &self, - report: &Report, - w: &mut W, - ) -> std::io::Result<()> { - let metrics = crate::metrics::compute_all_metrics(report); - w.write_all(&crate::serialize::encode_binary(&metrics)) - } - - // --- Function discovery / extraction --- - - /// Finds all functions in the tree. - /// - /// The reference implementation combines role-based and type-based matches - /// and deduplicates via a map with *nondeterministic* iteration order (see - /// the crate-level docs). This implementation deduplicates by walking once - /// in a deterministic preorder; because the downstream `functions` array - /// order is a canonicalized golden path, the only observable invariant that - /// must hold is the *set* of functions and the stable scalars derived from - /// it. - #[must_use] - pub fn find_functions<N: Node>(&self, root: &N) -> Vec<Function> { - self.find_functions_capped(root, None) - } - - /// [`find_functions`](Self::find_functions) with the optional discovery - /// depth cap: with `Some(cap)`, only function nodes at depth `<= cap` - /// below `root` (`root` at depth 0) are matched. The reference traverser - /// filters MATCHES by depth (it still descends), so pruning the walk at - /// the cap is output-equivalent. - #[must_use] - pub fn find_functions_capped<N: Node>( - &self, - root: &N, - find_depth: Option<usize>, - ) -> Vec<Function> { - let mut out = Vec::new(); - self.collect_functions(root, find_depth, &mut out); - out - } - - fn collect_functions<N: Node>(&self, n: &N, remaining: Option<usize>, out: &mut Vec<Function>) { - if self.is_function_node(n) { - out.push(self.extract_function(n)); - } - let child_remaining = match remaining { - Some(0) => return, - Some(r) => Some(r - 1), - None => None, - }; - for child in n.children() { - self.collect_functions(child, child_remaining, out); - } - } - - /// True if a node should be treated as a function: role `Function` OR type - /// `Function`/`Method`. - fn is_function_node<N: Node>(&self, n: &N) -> bool { - n.has_any_role(&[role::FUNCTION]) || n.has_any_type(&[ty::FUNCTION, ty::METHOD]) - } +/// The cohesion analyzer. Stateless; construct via [`Analyzer::new`]. +#[derive(Debug, Clone, Copy, Default)] +pub struct Analyzer; - /// Extracts a [`Function`] from a node. `cohesion` starts at `0.0` and is - /// filled later. +impl Analyzer { + /// Creates a new analyzer. #[must_use] - pub fn extract_function<N: Node>(&self, n: &N) -> Function { - let variables = self.extract_variables(n); - let name = self.extract_function_name(n); - let line_count = n.count_lines(); - Function { - name, - line_count, - variables, - cohesion: 0.0, - } + pub fn new() -> Self { + Analyzer } - /// Extracts the function name. + /// Analyzes one file's UAST. #[must_use] - pub fn extract_function_name<N: Node>(&self, n: &N) -> String { - n.entity_name() + pub fn analyze_file(&self, root: &Node) -> FileReport { + self.analyze_file_capped(root, None) } - /// Collects all variable names within a function subtree. + /// [`analyze_file`](Self::analyze_file) discovering only function nodes + /// at depth `<= cap` below `root` (root at depth 0) when `find_depth` is + /// `Some(cap)`. #[must_use] - pub fn extract_variables<N: Node>(&self, n: &N) -> Vec<String> { - let mut vars = Vec::new(); - self.find_variables(n, &mut vars); - vars - } - - fn find_variables<N: Node>(&self, n: &N, vars: &mut Vec<String>) { - self.process_variable_node(n, vars); - for child in n.children() { - self.find_variables(child, vars); - } - } - - /// Processes a single node for variable extraction. - /// - /// NOTE: the declaration and identifier predicates are checked independently - /// (two `if`s, not `else if`), so a node satisfying both contributes its name - /// **twice** — a report-contract quirk that affects `variable_count` and is - /// pinned by the differential gate. - fn process_variable_node<N: Node>(&self, n: &N, vars: &mut Vec<String>) { - if self.is_variable_declaration(n) { - self.add_variable_if_valid(n, vars); - } - if self.is_variable_identifier(n) { - self.add_variable_if_valid(n, vars); + pub fn analyze_file_capped(&self, root: &Node, find_depth: Option<usize>) -> FileReport { + let functions = visitor::collect_functions(root, find_depth); + if functions.is_empty() { + return FileReport::empty(); } - } - - fn is_variable_declaration<N: Node>(&self, n: &N) -> bool { - n.has_any_type(&[ty::VARIABLE, ty::PARAMETER]) && n.has_any_role(&[role::DECLARATION]) - } - - fn is_variable_identifier<N: Node>(&self, n: &N) -> bool { - n.has_any_type(&[ty::IDENTIFIER]) && n.has_any_role(&[role::VARIABLE, role::NAME]) - } - - /// Public wrapper of the declaration predicate, used by the visitor module. - #[must_use] - pub fn is_variable_declaration_pub<N: Node>(&self, n: &N) -> bool { - self.is_variable_declaration(n) - } - - /// Public wrapper of the identifier predicate, used by the visitor module. - #[must_use] - pub fn is_variable_identifier_pub<N: Node>(&self, n: &N) -> bool { - self.is_variable_identifier(n) - } - - fn add_variable_if_valid<N: Node>(&self, n: &N, vars: &mut Vec<String>) { - let name = n.entity_name(); - if !name.is_empty() { - vars.push(name); + let cohesions = calc::function_cohesions(&functions); + let metrics: Vec<FunctionMetrics> = functions + .iter() + .zip(&cohesions) + .map(|(f, &cohesion)| FunctionMetrics { + name: f.name.clone(), + start_line: f.start_line, + line_count: f.line_count(), + variable_count: f.variables.len(), + cohesion, + }) + .collect(); + let lcom = calc::lcom(&functions); + let cohesion_score = calc::cohesion_score(lcom); + FileReport { + total_functions: metrics.len(), + lcom, + cohesion_score, + function_cohesion: calc::mean_or(&cohesions, calc::PERFECT_COHESION), + functions: metrics, + message: file_message(cohesion_score).to_string(), } } } -/// The three module scalars produced by [`Analyzer::calculate_metrics`]. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Metrics { - /// LCOM-HS. - pub lcom: f64, - /// Cohesion score (`1 - lcom`). - pub cohesion_score: f64, - /// Average per-function cohesion. - pub function_cohesion: f64, +/// Per-file summary keyed on `cohesion_score`. +#[must_use] +pub fn file_message(score: f64) -> &'static str { + if score >= MESSAGE_THRESHOLD_EXCELLENT { + "Excellent cohesion - functions are well-focused and cohesive" + } else if score >= MESSAGE_THRESHOLD_GOOD { + "Good cohesion - functions have reasonable focus" + } else if score >= MESSAGE_THRESHOLD_FAIR { + "Fair cohesion - some functions could be more focused" + } else { + "Poor cohesion - functions lack focus and should be refactored" + } } #[cfg(test)] mod tests { use super::*; - use crate::uast::TestNode; - #[test] - fn empty_tree_yields_empty_result() { - let a = Analyzer::new(); - let root = TestNode::block(vec![]); - let report = a.analyze(&root).unwrap(); - assert_eq!(report.get("total_functions"), Some(&ReportValue::Int(0))); - assert_eq!(report.get("cohesion_score"), Some(&ReportValue::Float(1.0))); - assert_eq!( - report.get("function_cohesion"), - Some(&ReportValue::Float(1.0)) - ); - assert_eq!(report.get("lcom"), Some(&ReportValue::Float(0.0))); - assert_eq!( - report.get("message"), - Some(&ReportValue::Str("No functions found".into())) - ); - // The empty result has no analyzer_name / functions keys. - assert_eq!(report.get("functions"), None); + fn metrics(cohesion: f64, variable_count: usize, line_count: u64) -> FunctionMetrics { + FunctionMetrics { + name: "f".into(), + start_line: 1, + line_count, + variable_count, + cohesion, + } } #[test] - fn finds_functions_and_variables() { - let a = Analyzer::new(); - // fn f { var x; var y } fn g { var x } - let f = TestNode::function( - "f", - 5, - vec![TestNode::variable("x"), TestNode::variable("y")], - ); - let g = TestNode::function("g", 3, vec![TestNode::variable("x")]); - let root = TestNode::block(vec![f, g]); - - let funcs = a.find_functions(&root); - assert_eq!(funcs.len(), 2); - let mut names: Vec<&str> = funcs.iter().map(|f| f.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!(names, vec!["f", "g"]); + fn line_count_is_inclusive_span() { + let f = Function { + start_line: 3, + end_line: 7, + ..Function::default() + }; + assert_eq!(f.line_count(), 5); + assert_eq!(Function::default().line_count(), 1); } #[test] - fn analyze_produces_stable_scalars() { - let a = Analyzer::new(); - // f uses {x,y}, g uses {x}: m=2, a=2, sumMA=3 -> LCOM = 1 - 3/4 = 0.25. - let f = TestNode::function( - "f", - 5, - vec![TestNode::variable("x"), TestNode::variable("y")], + fn assessments_follow_the_documented_thresholds() { + assert_eq!( + metrics(0.6, 0, 1).cohesion_assessment(), + "\u{1F7E2} Excellent" ); - let g = TestNode::function("g", 3, vec![TestNode::variable("x")]); - let root = TestNode::block(vec![f, g]); - - let report = a.analyze(&root).unwrap(); - assert_eq!(report.get("total_functions"), Some(&ReportValue::Int(2))); - let lcom = report.get("lcom").unwrap().as_float().unwrap(); - assert!((lcom - 0.25).abs() < 1e-9, "lcom was {lcom}"); - let score = report.get("cohesion_score").unwrap().as_float().unwrap(); - assert!((score - 0.75).abs() < 1e-9, "score was {score}"); - // analyzer_name present in the non-empty result. + assert_eq!(metrics(0.4, 0, 1).cohesion_assessment(), "\u{1F7E1} Good"); + assert_eq!(metrics(0.3, 0, 1).cohesion_assessment(), "\u{1F7E1} Fair"); + assert_eq!(metrics(0.29, 0, 1).cohesion_assessment(), "\u{1F534} Poor"); + assert_eq!(metrics(1.0, 3, 1).variable_assessment(), "\u{1F7E2} Few"); assert_eq!( - report.get("analyzer_name"), - Some(&ReportValue::Str("cohesion".into())) + metrics(1.0, 7, 1).variable_assessment(), + "\u{1F7E1} Moderate" ); + assert_eq!(metrics(1.0, 8, 1).variable_assessment(), "\u{1F534} Many"); + assert_eq!(metrics(1.0, 0, 10).size_assessment(), "\u{1F7E2} Small"); + assert_eq!(metrics(1.0, 0, 30).size_assessment(), "\u{1F7E1} Medium"); + assert_eq!(metrics(1.0, 0, 31).size_assessment(), "\u{1F534} Large"); } #[test] - fn assessments_match_contract_thresholds() { - let a = Analyzer::new(); - assert_eq!(a.get_cohesion_assessment(0.6), "\u{1F7E2} Excellent"); - assert_eq!(a.get_cohesion_assessment(0.4), "\u{1F7E1} Good"); - assert_eq!(a.get_cohesion_assessment(0.3), "\u{1F7E1} Fair"); - assert_eq!(a.get_cohesion_assessment(0.29), "\u{1F534} Poor"); - - assert_eq!(a.get_variable_assessment(3), "\u{1F7E2} Few"); - assert_eq!(a.get_variable_assessment(7), "\u{1F7E1} Moderate"); - assert_eq!(a.get_variable_assessment(8), "\u{1F534} Many"); - - assert_eq!(a.get_size_assessment(10), "\u{1F7E2} Small"); - assert_eq!(a.get_size_assessment(30), "\u{1F7E1} Medium"); - assert_eq!(a.get_size_assessment(31), "\u{1F534} Large"); + fn file_messages_follow_the_documented_thresholds() { + assert!(file_message(0.7).starts_with("Excellent")); + assert!(file_message(0.4).starts_with("Good")); + assert!(file_message(0.3).starts_with("Fair")); + assert!(file_message(0.1).starts_with("Poor")); } #[test] - fn detail_messages_match_contract() { - let a = Analyzer::new(); - assert_eq!( - a.get_cohesion_message(0.7), - "Excellent cohesion - functions are well-focused and cohesive" - ); - assert_eq!( - a.get_cohesion_message(0.4), - "Good cohesion - functions have reasonable focus" - ); + fn empty_report_scores_perfectly_and_round_trips() { + let empty = FileReport::empty(); + assert!(!empty.has_functions()); + let report = empty.to_report(); + assert_eq!(report.get("total_functions"), Some(&ReportValue::Int(0))); assert_eq!( - a.get_cohesion_message(0.3), - "Fair cohesion - some functions could be more focused" + report.get("cohesion_score").and_then(ReportValue::as_float), + Some(1.0) ); assert_eq!( - a.get_cohesion_message(0.1), - "Poor cohesion - functions lack focus and should be refactored" + report.get("message"), + Some(&ReportValue::Str(EMPTY_FILE_MESSAGE.into())) ); } } diff --git a/crates/cf-cohesion/src/calc.rs b/crates/cf-cohesion/src/calc.rs index 004d49ef..ccfa6bbc 100644 --- a/crates/cf-cohesion/src/calc.rs +++ b/crates/cf-cohesion/src/calc.rs @@ -1,340 +1,185 @@ -//! Cohesion calculations. +//! LCOM-HS and per-function cohesion math over distinct variable sets. //! -//! Computes the three module scalars and the per-function cohesion via -//! Bloom-filter membership of shared variables. The Bloom-filter sizing -//! constants and the "only variables appearing in more than one function are -//! shared" rule are part of the report contract. +//! All inputs are sets: a function's variables are distinct names, so every +//! count below is a count of *functions*, never of occurrences. -use crate::analyzer::Function; -// The shared sketch crate's Bloom filter (FNV-128a hash kernel, pinned -// sizing + double hashing). The cohesion math MUST use this exact filter: -// a different hash family changes the per-function shared-variable false -// positives — and thus the cohesion scores in machine output. -use cf_alg_bloom::Filter; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap, HashSet}; -/// 1% false-positive rate for per-function and global Bloom filters. -pub const BLOOM_FP_RATE: f64 = 0.01; -/// Minimum expected elements for a per-function filter. -pub const BLOOM_MIN_ELEMENTS: u64 = 16; -/// Minimum expected elements for the global shared-variable filter. -pub const BLOOM_GLOBAL_MIN_ELEMS: u64 = 64; +use crate::analyzer::Function; -/// Clamps `v` to the inclusive range `[lo, hi]`: returns `lo` if `v < lo`, -/// `hi` if `v > hi`, else `v`. +/// LCOM needs at least this many functions that use a variable; below it +/// there is nothing to be un-cohesive about and LCOM is `0`. +pub const MIN_FUNCTIONS_FOR_LCOM: usize = 2; +/// A variable is shared when at least this many functions use it. +pub const SHARED_MIN_FUNCTIONS: usize = 2; +/// Cohesion of a function that uses no variables (nothing to be isolated +/// from) and of a module with fewer than [`MIN_FUNCTIONS_FOR_LCOM`] functions. +pub const PERFECT_COHESION: f64 = 1.0; + +/// LCOM-HS (Henderson-Sellers) over the functions that use at least one +/// variable: `1 - sum(mA) / (m * a)` with `m` their count, `a` the number of +/// distinct variables across them and `sum(mA)` the number of +/// (function, variable) usage pairs. `0.0` when fewer than +/// [`MIN_FUNCTIONS_FOR_LCOM`] functions use a variable. /// /// ``` -/// use cf_cohesion::calc::clamp; -/// assert_eq!(clamp(-0.5, 0.0, 1.0), 0.0); -/// assert_eq!(clamp(0.5, 0.0, 1.0), 0.5); -/// assert_eq!(clamp(1.5, 0.0, 1.0), 1.0); +/// use cf_cohesion::calc::lcom; +/// use cf_cohesion::Function; +/// let f = Function::with_variables("f", &["x", "y"]); +/// let g = Function::with_variables("g", &["x"]); +/// // m = 2, a = 2, sum(mA) = 3 -> 1 - 3/4 +/// assert!((lcom(&[f, g]) - 0.25).abs() < 1e-9); /// ``` #[must_use] -pub fn clamp(v: f64, lo: f64, hi: f64) -> f64 { - if v < lo { - lo - } else if v > hi { - hi - } else { - v +pub fn lcom(functions: &[Function]) -> f64 { + let using: Vec<&Function> = functions + .iter() + .filter(|f| !f.variables.is_empty()) + .collect(); + if using.len() < MIN_FUNCTIONS_FOR_LCOM { + return 0.0; } + let all: BTreeSet<&str> = using + .iter() + .flat_map(|f| f.variables.iter().map(String::as_str)) + .collect(); + let m = using.len() as f64; + let a = all.len() as f64; + let sum_ma: usize = using.iter().map(|f| f.variables.len()).sum(); + unit(1.0 - sum_ma as f64 / (m * a)) } -/// Returns the distinct entries of `items`, preserving **first-seen order**. -/// -/// ``` -/// use cf_cohesion::calc::unique; -/// let v = vec!["a".to_string(), "b".to_string(), "a".to_string(), "c".to_string()]; -/// assert_eq!(unique(&v), vec!["a", "b", "c"]); -/// ``` +/// Cohesion score: `1 - lcom`, clamped to `[0, 1]`. #[must_use] -pub fn unique(items: &[String]) -> Vec<String> { - let mut seen = std::collections::HashSet::with_capacity(items.len()); - let mut out = Vec::with_capacity(items.len()); - for s in items { - if seen.insert(s.as_str()) { - out.push(s.clone()); - } - } - out -} - -/// LCOM and cohesion calculations. Stateless methods on the analyzer. -impl crate::analyzer::Analyzer { - /// Calculates LCOM-HS (Henderson-Sellers): `LCOM = 1 - sum(mA) / (m * a)`. - /// - /// * `m` = number of functions - /// * `a` = number of distinct variables across all functions - /// * `mA` = for each variable, the count of functions that reference it - /// - /// Range `[0, 1]`; 0 = perfect cohesion, 1 = none. Returns `0.0` for `<= 1` - /// function or when there are no variables. - #[must_use] - pub fn calculate_lcom(&self, functions: &[Function]) -> f64 { - if functions.len() <= 1 { - return 0.0; - } - let all_vars = collect_unique_variables(functions); - if all_vars.is_empty() { - return 0.0; - } - let m = functions.len() as f64; - let a = all_vars.len() as f64; - let sum_ma = count_variable_accesses(&all_vars, functions); - clamp(1.0 - (sum_ma / (m * a)), 0.0, 1.0) - } - - /// Converts LCOM-HS to a cohesion score (higher is better): `1 - lcom`. - /// Returns `1.0` for `<= 1` function. - #[must_use] - pub fn calculate_cohesion_score(&self, lcom: f64, function_count: usize) -> f64 { - if function_count <= 1 { - return 1.0; - } - clamp(1.0 - lcom, 0.0, 1.0) - } - - /// Average per-function cohesion. Returns `1.0` for an empty slice. - #[must_use] - pub fn calculate_function_cohesion(&self, functions: &[Function]) -> f64 { - if functions.is_empty() { - return 1.0; - } - let total: f64 = functions.iter().map(|f| f.cohesion).sum(); - total / functions.len() as f64 - } - - /// Per-function cohesion = (shared unique vars) / (unique vars). - /// - /// A variable is "shared" iff it tests positive against `global_filter` (which - /// contains every variable used by more than one function). Functions with no - /// variables score `1.0`; if `global_filter` is `None` the score is `0.0`. - #[must_use] - pub fn calculate_function_level_cohesion( - &self, - function: &Function, - global_filter: Option<&Filter>, - ) -> f64 { - let unique_vars = unique(&function.variables); - if unique_vars.is_empty() { - return 1.0; - } - let Some(filter) = global_filter else { - return 0.0; - }; - let shared = unique_vars - .iter() - .filter(|v| filter.test(v.as_bytes())) - .count(); - shared as f64 / unique_vars.len() as f64 - } +pub fn cohesion_score(lcom: f64) -> f64 { + unit(1.0 - lcom) } -/// Gathers all distinct variable names across all functions. Order is -/// unspecified; only the *count* feeds the LCOM formula, so order does not -/// affect the scalar. +/// The variables used by at least [`SHARED_MIN_FUNCTIONS`] functions. #[must_use] -pub fn collect_unique_variables(functions: &[Function]) -> Vec<String> { - let mut seen = std::collections::HashSet::new(); +pub fn shared_variables(functions: &[Function]) -> HashSet<&str> { + let mut users: HashMap<&str, usize> = HashMap::new(); for f in functions { for v in &f.variables { - seen.insert(v.clone()); + *users.entry(v.as_str()).or_insert(0) += 1; } } - seen.into_iter().collect() + users + .into_iter() + .filter(|&(_, n)| n >= SHARED_MIN_FUNCTIONS) + .map(|(v, _)| v) + .collect() } -/// Counts function-variable access pairs: for each variable, how many functions -/// reference it. Uses one Bloom filter per function for O(1) membership tests. -/// -/// The return is `f64` because it feeds the LCOM division directly. Bloom false -/// positives are part of the defined behavior and are reproduced via the shared -/// sketch crate. +/// Per-function cohesion: the fraction of the function's distinct variables +/// that are shared with another function. [`PERFECT_COHESION`] for a function +/// without variables. #[must_use] -pub fn count_variable_accesses(all_vars: &[String], functions: &[Function]) -> f64 { - let filters = build_per_function_bloom_filters(functions); - let mut sum = 0.0; - for var_name in all_vars { - let key = var_name.as_bytes(); - for filter in filters.iter().flatten() { - if filter.test(key) { - sum += 1.0; - } - } +pub fn function_cohesion(f: &Function, shared: &HashSet<&str>) -> f64 { + if f.variables.is_empty() { + return PERFECT_COHESION; } - sum + let shared_count = f + .variables + .iter() + .filter(|v| shared.contains(v.as_str())) + .count(); + shared_count as f64 / f.variables.len() as f64 } -/// Builds a Bloom filter for each function's variable set. A `None` entry -/// stands in for a filter whose construction failed (invalid parameters); such -/// entries are skipped by the membership loop. +/// Per-function cohesion of every function, in order. When fewer than +/// [`MIN_FUNCTIONS_FOR_LCOM`] functions use a variable there is nothing to +/// compare against — a single-function file — and every function scores +/// [`PERFECT_COHESION`], consistent with `lcom = 0` for that file. #[must_use] -pub fn build_per_function_bloom_filters(functions: &[Function]) -> Vec<Option<Filter>> { +pub fn function_cohesions(functions: &[Function]) -> Vec<f64> { + let using = functions.iter().filter(|f| !f.variables.is_empty()).count(); + if using < MIN_FUNCTIONS_FOR_LCOM { + return vec![PERFECT_COHESION; functions.len()]; + } + let shared = shared_variables(functions); functions .iter() - .map(|f| { - let n = (f.variables.len() as u64).max(BLOOM_MIN_ELEMENTS); - match Filter::new_with_estimates(n, BLOOM_FP_RATE) { - Ok(mut filter) => { - for v in &f.variables { - filter.add(v.as_bytes()); - } - Some(filter) - } - Err(_) => None, - } - }) + .map(|f| function_cohesion(f, &shared)) .collect() } -/// Builds the global shared-variable Bloom filter. -/// -/// Only variables appearing in **more than one** function are added. Returns -/// `None` when there are no variables, or no variable is shared, or the filter -/// cannot be constructed. +/// Arithmetic mean of `values`, or `default` when there are none. #[must_use] -pub fn build_global_variable_filter(functions: &[Function]) -> Option<Filter> { - // Count occurrences across functions. - let mut seen: HashMap<&str, u32> = HashMap::new(); - for f in functions { - for v in &f.variables { - *seen.entry(v.as_str()).or_insert(0) += 1; - } - } - if seen.is_empty() { - return None; - } - let shared_count = seen.values().filter(|&&c| c > 1).count() as u64; - if shared_count == 0 { - return None; +pub fn mean_or(values: &[f64], default: f64) -> f64 { + if values.is_empty() { + return default; } - let n = shared_count.max(BLOOM_GLOBAL_MIN_ELEMS); - let mut filter = Filter::new_with_estimates(n, BLOOM_FP_RATE).ok()?; - for (v, count) in &seen { - if *count > 1 { - filter.add(v.as_bytes()); - } - } - Some(filter) + values.iter().sum::<f64>() / values.len() as f64 +} + +/// Clamps to the unit interval (guards floating-point drift at the edges). +fn unit(v: f64) -> f64 { + v.clamp(0.0, 1.0) } #[cfg(test)] mod tests { use super::*; - use crate::analyzer::{Analyzer, Function}; - fn func(name: &str, vars: &[&str]) -> Function { - Function { - name: name.to_string(), - variables: vars.iter().map(|s| s.to_string()).collect(), - line_count: 1, - cohesion: 0.0, - } - } - - #[test] - fn clamp_bounds() { - assert_eq!(clamp(-1.0, 0.0, 1.0), 0.0); - assert_eq!(clamp(2.0, 0.0, 1.0), 1.0); - assert_eq!(clamp(0.5, 0.0, 1.0), 0.5); - } - - #[test] - fn unique_preserves_first_seen_order() { - let v: Vec<String> = ["a", "b", "a", "c", "b"] - .iter() - .map(|s| s.to_string()) - .collect(); - assert_eq!(unique(&v), vec!["a", "b", "c"]); + fn f(name: &str, vars: &[&str]) -> Function { + Function::with_variables(name, vars) } #[test] - fn lcom_zero_for_single_function() { - let a = Analyzer::new(); - assert_eq!(a.calculate_lcom(&[func("f", &["x"])]), 0.0); + fn lcom_is_zero_below_two_functions_or_without_variables() { + assert_eq!(lcom(&[]), 0.0); + assert_eq!(lcom(&[f("f", &["x"])]), 0.0); + assert_eq!(lcom(&[f("f", &[]), f("g", &[])]), 0.0); } #[test] - fn lcom_zero_when_no_variables() { - let a = Analyzer::new(); - let fns = vec![func("f", &[]), func("g", &[])]; - assert_eq!(a.calculate_lcom(&fns), 0.0); + fn lcom_disjoint_variables_is_half() { + // m = 2, a = 2, sum(mA) = 2 -> 1 - 2/4. + assert!((lcom(&[f("f", &["x"]), f("g", &["y"])]) - 0.5).abs() < 1e-9); } #[test] - fn lcom_perfect_cohesion_all_share_all() { - // Two functions each using the same single variable: - // m=2, a=1, sumMA=2 -> LCOM = 1 - 2/(2*1) = 0. - let a = Analyzer::new(); - let fns = vec![func("f", &["x"]), func("g", &["x"])]; - assert!((a.calculate_lcom(&fns) - 0.0).abs() < 1e-9); + fn lcom_ignores_functions_without_variables() { + // h uses nothing: it is not part of the usage matrix. + let fns = [f("f", &["x"]), f("g", &["x"]), f("h", &[])]; + assert_eq!(lcom(&fns), 0.0); } #[test] - fn lcom_no_cohesion_disjoint_vars() { - // Two functions with disjoint single variables: - // m=2, a=2, sumMA=2 -> LCOM = 1 - 2/(2*2) = 0.5. - let a = Analyzer::new(); - let fns = vec![func("f", &["x"]), func("g", &["y"])]; - assert!((a.calculate_lcom(&fns) - 0.5).abs() < 1e-9); + fn shared_means_at_least_two_functions() { + let fns = [f("f", &["x", "y"]), f("g", &["x"])]; + let shared = shared_variables(&fns); + assert!(shared.contains("x")); + assert!(!shared.contains("y")); } #[test] - fn cohesion_score_inverts_lcom() { - let a = Analyzer::new(); - assert_eq!(a.calculate_cohesion_score(0.5, 2), 0.5); - assert_eq!(a.calculate_cohesion_score(0.9, 1), 1.0); // <=1 fn -> 1.0 + fn function_cohesion_is_shared_over_unique() { + let fns = [f("f", &["x", "y"]), f("g", &["x"]), f("h", &[])]; + let shared = shared_variables(&fns); + assert!((function_cohesion(&fns[0], &shared) - 0.5).abs() < 1e-9); + assert_eq!(function_cohesion(&fns[1], &shared), 1.0); + assert_eq!(function_cohesion(&fns[2], &shared), PERFECT_COHESION); } #[test] - fn global_filter_none_when_nothing_shared() { - // Disjoint vars: nothing appears in >1 function. - let fns = vec![func("f", &["x"]), func("g", &["y"])]; - assert!(build_global_variable_filter(&fns).is_none()); - } - - #[test] - fn global_filter_some_when_var_shared() { - let fns = vec![func("f", &["x", "y"]), func("g", &["x"])]; - let filter = build_global_variable_filter(&fns).expect("x is shared"); - assert!(filter.test(b"x")); - } - - #[test] - fn function_level_cohesion_no_vars_is_one() { - let a = Analyzer::new(); - assert_eq!( - a.calculate_function_level_cohesion(&func("f", &[]), None), - 1.0 - ); - } - - #[test] - fn function_level_cohesion_nil_filter_is_zero() { - let a = Analyzer::new(); + fn single_variable_using_function_is_perfect() { + // One function uses variables: nothing to compare against. + assert_eq!(function_cohesions(&[f("f", &["x"])]), [1.0]); assert_eq!( - a.calculate_function_level_cohesion(&func("f", &["x"]), None), - 0.0 + function_cohesions(&[f("f", &["x"]), f("g", &[])]), + [1.0, 1.0] ); + // Two functions use variables: the sharing rule applies. + let got = function_cohesions(&[f("f", &["x", "y"]), f("g", &["x"]), f("h", &[])]); + assert!((got[0] - 0.5).abs() < 1e-9); + assert_eq!(&got[1..], [1.0, 1.0]); } #[test] - fn function_level_cohesion_shared_ratio() { - let a = Analyzer::new(); - let fns = vec![func("f", &["x", "y"]), func("g", &["x"])]; - let filter = build_global_variable_filter(&fns).unwrap(); - // f has unique vars {x, y}; only x is shared -> 1/2 = 0.5. - let c = a.calculate_function_level_cohesion(&fns[0], Some(&filter)); - assert!((c - 0.5).abs() < 1e-9); - } - - #[test] - fn function_cohesion_average() { - let a = Analyzer::new(); - let mut fns = vec![func("f", &["x"]), func("g", &["y"])]; - fns[0].cohesion = 1.0; - fns[1].cohesion = 0.0; - assert_eq!(a.calculate_function_cohesion(&fns), 0.5); - assert_eq!(a.calculate_function_cohesion(&[]), 1.0); + fn mean_or_uses_default_when_empty() { + assert_eq!(mean_or(&[], 1.0), 1.0); + assert!((mean_or(&[1.0, 0.0], 1.0) - 0.5).abs() < 1e-9); } } diff --git a/crates/cf-cohesion/src/lib.rs b/crates/cf-cohesion/src/lib.rs index e0baa306..cbaa560d 100644 --- a/crates/cf-cohesion/src/lib.rs +++ b/crates/cf-cohesion/src/lib.rs @@ -1,54 +1,33 @@ -//! `cf-cohesion` — static LCOM-HS cohesion analyzer (analyzer id: -//! `static/cohesion`). Also used by the quality analyzer. -//! -//! * [`Analyzer::analyze`] walks a UAST, extracts functions and their variables, and -//! computes three module-level scalars — `lcom` (LCOM-HS, Henderson-Sellers), -//! `cohesion_score`, and `function_cohesion` — plus a per-function table. -//! * Per-function cohesion uses a single global Bloom filter of *shared* variables -//! (variables appearing in more than one function); see -//! [`calc::build_global_variable_filter`] / -//! [`Analyzer::calculate_function_level_cohesion`]. Bloom false positives are -//! part of the defined behavior. -//! -//! # Byte-identity notes -//! -//! * [`metrics::ComputedMetrics`] and its nested structs serialize their fields -//! in **declaration order**, honoring `omitempty` (report-format contract). -//! * The `distribution` field is a string-keyed map; its keys are byte-sorted -//! on encode. -//! * The dynamic [`Report`] map (the analyzer's intermediate result) has byte-sorted -//! keys when emitted; the per-function `functions` array order is -//! **nondeterministic in the reference implementation** and is therefore a -//! *named-canonicalizer* path in the golden harness rather than a raw-byte gate. -//! The scalars (`lcom`, `cohesion_score`, `function_cohesion`, -//! `total_functions`) are stable. -//! -//! Compatibility: output bytes are pinned against the reference implementation -//! by the differential gate in `tests/compat`. See the module-level docs -//! of [`calc`], [`metrics`], [`report_section`] and [`aggregator`]. +//! `cf-cohesion` — the `static/cohesion` analyzer: LCOM-HS (Henderson-Sellers) +//! and per-function variable-sharing cohesion over a UAST. +//! +//! * [`Analyzer::analyze_file`] walks one file's UAST, discovers its functions +//! and the distinct variables each one uses (the rules live in +//! [`visitor`]), and computes the file's [`FileReport`]: `lcom`, +//! `cohesion_score = 1 - lcom`, the mean `function_cohesion`, and a +//! per-function table. +//! * [`aggregate`] folds many files into one [`Aggregate`]: every function is +//! kept, scalars are function-weighted, and files without functions carry no +//! data. +//! * [`metrics::compute_metrics`] and [`report_section`] derive the machine +//! and section views the CLI renders. +//! +//! The documentation of record is `site/explanation/cohesion.md` (the model) +//! and `site/analyzers/cohesion.md` (the output). //! //! # Example //! -//! Analyze a two-function module and read the stable scalars. With `f` using -//! `{x, y}` and `g` using `{x}`, LCOM-HS is `0.25`, so the cohesion score -//! (`1 - lcom`) is `0.75`: -//! //! ``` //! use cf_cohesion::Analyzer; -//! use cf_cohesion::report_value::ReportValue; -//! use cf_cohesion::uast::TestNode; -//! -//! let f = TestNode::function("f", 5, vec![TestNode::variable("x"), TestNode::variable("y")]); -//! let g = TestNode::function("g", 3, vec![TestNode::variable("x")]); -//! let root = TestNode::block(vec![f, g]); //! -//! let report = Analyzer::new().analyze(&root).unwrap(); -//! assert_eq!(report.get("total_functions"), Some(&ReportValue::Int(2))); +//! let src = b"package a\n\nfunc f() int {\n\tx := 1\n\ty := 2\n\treturn x + y\n}\n\nfunc g() int {\n\tx := 3\n\treturn x\n}\n"; +//! let root = cf_uast::Parser::new().parse("a.go", src).unwrap(); +//! let report = Analyzer::new().analyze_file(&root); //! -//! let lcom = report.get("lcom").unwrap().as_float().unwrap(); -//! assert!((lcom - 0.25).abs() < 1e-9); -//! let score = report.get("cohesion_score").unwrap().as_float().unwrap(); -//! assert!((score - 0.75).abs() < 1e-9); +//! // f uses {x, y}, g uses {x}: m = 2, a = 2, sum(mA) = 3 -> LCOM = 1 - 3/4. +//! assert_eq!(report.total_functions, 2); +//! assert!((report.lcom - 0.25).abs() < 1e-9); +//! assert!((report.cohesion_score - 0.75).abs() < 1e-9); //! ``` #![forbid(unsafe_code)] #![warn(missing_docs)] @@ -59,19 +38,13 @@ pub mod calc; pub mod metrics; pub mod report_section; pub mod report_value; -pub mod serialize; pub mod uast; pub mod visitor; -pub use analyzer::{Analyzer, Function, FunctionReportItem}; -pub use metrics::{ - AggregateData, ComputedMetrics, FunctionCohesionData, FunctionData, LowCohesionFunctionData, - ReportData, -}; +pub use aggregator::{aggregate, Aggregate, AggregatedFunction, AnalyzedFile}; +pub use analyzer::{Analyzer, FileReport, Function, FunctionMetrics}; +pub use metrics::ComputedMetrics; pub use report_value::{Report, ReportValue}; -/// Crate name, used by smoke tests to confirm the module links. -pub const CRATE_NAME: &str = "cf-cohesion"; - /// The analyzer name as it appears in reports. pub const ANALYZER_NAME: &str = "cohesion"; diff --git a/crates/cf-cohesion/src/metrics.rs b/crates/cf-cohesion/src/metrics.rs index 9ad78074..a7ab5335 100644 --- a/crates/cf-cohesion/src/metrics.rs +++ b/crates/cf-cohesion/src/metrics.rs @@ -1,31 +1,13 @@ -//! Computed metrics: the report view serialized to the machine formats. +//! Computed metrics: the view serialized by the `yaml` / `bin` machine +//! formats, derived from an [`Aggregate`]. //! -//! [`ComputedMetrics`] is the struct that is actually serialized to JSON / YAML -//! / binary. Its shape is **byte-identity critical** (pinned by the -//! differential gate): -//! -//! * Fields serialize in **declaration order**, honoring `omitempty` for the -//! optional string fields. -//! * `distribution` is a string-keyed map; its keys are byte-sorted on encode. -//! * Floats render with shortest-round-trip `%g` semantics; HTML escaping is -//! ON; the JSON path is two-space-indented with **no trailing newline**; the -//! binary path is the CFB1 envelope over compact JSON. -//! -//! The encoding goes through [`crate::serialize::to_go_value`], which builds -//! the value tree with struct-origin objects (declaration order) and one -//! map-origin object (the byte-sorted `distribution`). +//! Field order of every struct is the serialized key order. -use crate::report_value::{Report, ReportValue}; use std::collections::BTreeMap; -/// Stamped source-file key. -pub const SOURCE_FILE_KEY: &str = "_source_file"; -/// Stamped language key. -pub const LANGUAGE_KEY: &str = "_language"; -/// Stamped directory key. -pub const DIRECTORY_KEY: &str = "_directory"; +use crate::aggregator::{Aggregate, AggregatedFunction}; -// --- Quality thresholds --- +// --- Quality thresholds on per-function cohesion --- /// Cohesion at or above this is "Excellent". pub const COHESION_THRESHOLD_EXCELLENT: f64 = 0.6; @@ -36,6 +18,9 @@ pub const COHESION_THRESHOLD_FAIR: f64 = 0.3; /// Health score = cohesion score x this multiplier. pub const HEALTH_SCORE_MULTIPLIER: f64 = 100.0; +/// The LCOM variant name reported in `aggregate.lcom_variant`. +pub const LCOM_VARIANT: &str = "LCOM-HS (Henderson-Sellers)"; + // --- Distribution keys --- /// Distribution bucket key. @@ -47,144 +32,38 @@ pub const METRIC_DIST_FAIR: &str = "fair"; /// Distribution bucket key. pub const METRIC_DIST_POOR: &str = "poor"; -// === Input data types === - -/// Parsed input for metrics computation. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct ReportData { - /// `total_functions`. - pub total_functions: i64, - /// `lcom`. - pub lcom: f64, - /// `cohesion_score`. - pub cohesion_score: f64, - /// `function_cohesion`. - pub function_cohesion: f64, - /// Per-function data. - pub functions: Vec<FunctionData>, - /// `message`. - pub message: String, -} - -/// Cohesion data for one function. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct FunctionData { - /// `name`. - pub name: String, - /// `_source_file`. - pub source_file: String, - /// `_language`. - pub language: String, - /// `_directory`. - pub directory: String, - /// `cohesion`. - pub cohesion: f64, -} - -/// Extracts [`ReportData`] from an analyzer [`Report`]. -/// -/// Type checks are strict (report-format contract): a key that is missing or -/// of the wrong dynamic type is left at its zero value. -#[must_use] -pub fn parse_report_data(report: &Report) -> ReportData { - let mut data = ReportData::default(); - if let Some(v) = report.get("total_functions").and_then(ReportValue::as_int) { - data.total_functions = v; - } - if let Some(v) = report.get("lcom").and_then(ReportValue::as_float) { - data.lcom = v; - } - if let Some(v) = report.get("cohesion_score").and_then(ReportValue::as_float) { - data.cohesion_score = v; - } - if let Some(v) = report - .get("function_cohesion") - .and_then(ReportValue::as_float) - { - data.function_cohesion = v; - } - if let Some(v) = report.get("message").and_then(ReportValue::as_str) { - data.message = v.to_string(); - } - data.functions = parse_report_functions(report); - data -} - -fn parse_report_functions(report: &Report) -> Vec<FunctionData> { - let Some(functions) = report.get("functions").and_then(ReportValue::as_functions) else { - return Vec::new(); - }; - functions.iter().map(parse_function_data).collect() -} - -fn parse_function_data(fn_map: &BTreeMap<String, ReportValue>) -> FunctionData { - let mut fd = FunctionData::default(); - if let Some(v) = fn_map.get("name").and_then(ReportValue::as_str) { - fd.name = v.to_string(); - } - if let Some(v) = fn_map.get(SOURCE_FILE_KEY).and_then(ReportValue::as_str) { - fd.source_file = v.to_string(); - } - if let Some(v) = fn_map.get(LANGUAGE_KEY).and_then(ReportValue::as_str) { - fd.language = v.to_string(); - } - if let Some(v) = fn_map.get(DIRECTORY_KEY).and_then(ReportValue::as_str) { - fd.directory = v.to_string(); - } - if let Some(v) = fn_map.get("cohesion").and_then(ReportValue::as_float) { - fd.cohesion = v; - } - fd -} - -// === Output data types === - -/// Per-function cohesion output. -/// -/// Field order = JSON/YAML key order: `name`, `source_file?`, `language?`, -/// `directory?`, `cohesion`, `quality_level`. +/// Per-function cohesion output: `name`, `source_file`, `cohesion`, +/// `quality_level`. #[derive(Debug, Clone, Default, PartialEq)] pub struct FunctionCohesionData { /// `name`. pub name: String, - /// `source_file` (`omitempty`). + /// `source_file`. pub source_file: String, - /// `language` (`omitempty`). - pub language: String, - /// `directory` (`omitempty`). - pub directory: String, /// `cohesion`. pub cohesion: f64, /// `quality_level`. pub quality_level: String, } -/// Low-cohesion function output. -/// -/// Field order: `name`, `source_file?`, `language?`, `directory?`, `cohesion`, +/// Low-cohesion function output: `name`, `source_file`, `cohesion`, /// `risk_level`, `recommendation`. #[derive(Debug, Clone, Default, PartialEq)] pub struct LowCohesionFunctionData { /// `name`. pub name: String, - /// `source_file` (`omitempty`). + /// `source_file`. pub source_file: String, - /// `language` (`omitempty`). - pub language: String, - /// `directory` (`omitempty`). - pub directory: String, /// `cohesion`. pub cohesion: f64, - /// `risk_level`. + /// `risk_level` (`HIGH` / `MEDIUM`). pub risk_level: String, /// `recommendation`. pub recommendation: String, } -/// Aggregate summary statistics. -/// -/// Field order: `total_functions`, `lcom`, `lcom_variant`, `cohesion_score`, -/// `function_cohesion`, `health_score`, `message`. +/// Aggregate summary: `total_functions`, `lcom`, `lcom_variant`, +/// `cohesion_score`, `function_cohesion`, `health_score`, `message`. #[derive(Debug, Clone, Default, PartialEq)] pub struct AggregateData { /// `total_functions`. @@ -203,34 +82,22 @@ pub struct AggregateData { pub message: String, } -/// All computed metric results. -/// -/// Field order = top-level JSON/YAML key order: `function_cohesion`, -/// `distribution`, `low_cohesion_functions`, `aggregate`. +/// All computed metrics: `function_cohesion`, `distribution`, +/// `low_cohesion_functions`, `aggregate`. #[derive(Debug, Clone, Default, PartialEq)] pub struct ComputedMetrics { - /// `function_cohesion`. + /// Every function with its quality level, cohesion ascending. pub function_cohesion: Vec<FunctionCohesionData>, - /// `distribution` — string-keyed counts, byte-sorted keys on encode. + /// Bucket counts; only buckets that occur are present. pub distribution: BTreeMap<String, i64>, - /// `low_cohesion_functions`. + /// Functions below [`COHESION_THRESHOLD_GOOD`], cohesion ascending. pub low_cohesion_functions: Vec<LowCohesionFunctionData>, - /// `aggregate`. + /// Summary statistics. pub aggregate: AggregateData, } -impl ComputedMetrics { - /// The analyzer name. - #[must_use] - pub fn analyzer_name(&self) -> &'static str { - crate::ANALYZER_NAME - } -} - -// === Metric implementations === - -/// Classifies a cohesion score into a quality label: the first label whose -/// limit the score meets, scanning highest first; "Poor" otherwise. +/// Quality label of a cohesion value: the first label whose threshold the +/// value meets, scanning highest first; "Poor" otherwise. #[must_use] pub fn classify_cohesion_quality(cohesion: f64) -> &'static str { if cohesion >= COHESION_THRESHOLD_EXCELLENT { @@ -244,7 +111,7 @@ pub fn classify_cohesion_quality(cohesion: f64) -> &'static str { } } -/// Distribution label for a function. +/// Distribution bucket key of a cohesion value. #[must_use] pub fn classify_cohesion_level(cohesion: f64) -> &'static str { if cohesion >= COHESION_THRESHOLD_EXCELLENT { @@ -258,101 +125,72 @@ pub fn classify_cohesion_level(cohesion: f64) -> &'static str { } } -/// Computes the per-function cohesion list, sorted by cohesion ascending. +/// Orders functions by cohesion ascending, then name, then source file. +fn by_cohesion_then_name(a: &AggregatedFunction, b: &AggregatedFunction) -> std::cmp::Ordering { + a.cohesion + .total_cmp(&b.cohesion) + .then_with(|| a.name.cmp(&b.name)) + .then_with(|| a.source_file.cmp(&b.source_file)) +} + +/// Computes the machine-format metrics of an aggregate. #[must_use] -pub fn compute_function_cohesion(input: &ReportData) -> Vec<FunctionCohesionData> { - let mut result: Vec<FunctionCohesionData> = input - .functions +pub fn compute_metrics(agg: &Aggregate) -> ComputedMetrics { + let mut ordered: Vec<&AggregatedFunction> = agg.functions.iter().collect(); + ordered.sort_by(|a, b| by_cohesion_then_name(a, b)); + + let function_cohesion = ordered .iter() - .map(|fd| FunctionCohesionData { - name: fd.name.clone(), - source_file: fd.source_file.clone(), - language: fd.language.clone(), - directory: fd.directory.clone(), - cohesion: fd.cohesion, - quality_level: classify_cohesion_quality(fd.cohesion).to_string(), + .map(|f| FunctionCohesionData { + name: f.name.clone(), + source_file: f.source_file.clone(), + cohesion: f.cohesion, + quality_level: classify_cohesion_quality(f.cohesion).to_string(), }) .collect(); - // The reference sort is an unstable quicksort with a strict-less comparator - // on cohesion. We use a total order on cohesion; equal-cohesion ordering is - // a canonicalized golden path (function-table nondeterminism). - result.sort_by(|a, b| a.cohesion.total_cmp(&b.cohesion)); - result -} -/// Computes the cohesion distribution counts. Only buckets that occur are -/// inserted (absent buckets are not emitted — report-format contract). -#[must_use] -pub fn compute_distribution(input: &ReportData) -> BTreeMap<String, i64> { - let mut dist: BTreeMap<String, i64> = BTreeMap::new(); - for fd in &input.functions { - let label = classify_cohesion_level(fd.cohesion); - *dist.entry(label.to_string()).or_insert(0) += 1; - } - dist -} - -/// Computes the low-cohesion function list. -/// -/// Includes only functions below [`COHESION_THRESHOLD_GOOD`]; high risk below -/// [`COHESION_THRESHOLD_FAIR`]. NOTE: only `name`, `source_file`, `cohesion`, -/// `risk_level`, `recommendation` are populated here — `language`/`directory` -/// stay empty and are omitted via `omitempty` (report-format contract; pinned -/// by the differential gate). -#[must_use] -pub fn compute_low_cohesion_functions(input: &ReportData) -> Vec<LowCohesionFunctionData> { - let mut result: Vec<LowCohesionFunctionData> = Vec::new(); - for fd in &input.functions { - if fd.cohesion >= COHESION_THRESHOLD_GOOD { - continue; - } - // The risk-level strings are UPPERCASE ("HIGH"/"MEDIUM") in this - // report (CLI contract). - let (risk_level, recommendation) = if fd.cohesion < COHESION_THRESHOLD_FAIR { - ("HIGH", "Consider splitting into multiple focused functions") - } else { - ( - "MEDIUM", - "Review function responsibilities for possible separation", - ) - }; - result.push(LowCohesionFunctionData { - name: fd.name.clone(), - source_file: fd.source_file.clone(), - language: String::new(), - directory: String::new(), - cohesion: fd.cohesion, - risk_level: risk_level.to_string(), - recommendation: recommendation.to_string(), - }); + let mut distribution: BTreeMap<String, i64> = BTreeMap::new(); + for f in &ordered { + *distribution + .entry(classify_cohesion_level(f.cohesion).to_string()) + .or_insert(0) += 1; } - result.sort_by(|a, b| a.cohesion.total_cmp(&b.cohesion)); - result -} -/// Computes aggregate statistics. -#[must_use] -pub fn compute_aggregate(input: &ReportData) -> AggregateData { - AggregateData { - total_functions: input.total_functions, - lcom: input.lcom, - lcom_variant: "LCOM-HS (Henderson-Sellers)".to_string(), - cohesion_score: input.cohesion_score, - function_cohesion: input.function_cohesion, - health_score: input.cohesion_score * HEALTH_SCORE_MULTIPLIER, - message: input.message.clone(), - } -} + let low_cohesion_functions = ordered + .iter() + .filter(|f| f.cohesion < COHESION_THRESHOLD_GOOD) + .map(|f| { + let (risk_level, recommendation) = if f.cohesion < COHESION_THRESHOLD_FAIR { + ("HIGH", "Consider splitting into multiple focused functions") + } else { + ( + "MEDIUM", + "Review function responsibilities for possible separation", + ) + }; + LowCohesionFunctionData { + name: f.name.clone(), + source_file: f.source_file.clone(), + cohesion: f.cohesion, + risk_level: risk_level.to_string(), + recommendation: recommendation.to_string(), + } + }) + .collect(); -/// Runs all cohesion metrics. -#[must_use] -pub fn compute_all_metrics(report: &Report) -> ComputedMetrics { - let input = parse_report_data(report); ComputedMetrics { - function_cohesion: compute_function_cohesion(&input), - distribution: compute_distribution(&input), - low_cohesion_functions: compute_low_cohesion_functions(&input), - aggregate: compute_aggregate(&input), + function_cohesion, + distribution, + low_cohesion_functions, + aggregate: AggregateData { + total_functions: agg.total_functions as i64, + lcom: agg.lcom, + lcom_variant: LCOM_VARIANT.to_string(), + cohesion_score: agg.cohesion_score, + function_cohesion: agg.function_cohesion, + health_score: agg.cohesion_score * HEALTH_SCORE_MULTIPLIER, + message: agg.message.clone(), + }, } } @@ -360,36 +198,28 @@ pub fn compute_all_metrics(report: &Report) -> ComputedMetrics { mod tests { use super::*; - fn report_with(funcs: &[(&str, f64)], total: i64, lcom: f64, score: f64, fcoh: f64) -> Report { - let mut r = Report::new(); - r.insert("total_functions".into(), ReportValue::Int(total)); - r.insert("lcom".into(), ReportValue::Float(lcom)); - r.insert("cohesion_score".into(), ReportValue::Float(score)); - r.insert("function_cohesion".into(), ReportValue::Float(fcoh)); - r.insert("message".into(), ReportValue::Str("msg".into())); - let functions: Vec<BTreeMap<String, ReportValue>> = funcs - .iter() - .map(|(name, coh)| { - let mut m = BTreeMap::new(); - m.insert("name".into(), ReportValue::Str((*name).into())); - m.insert("cohesion".into(), ReportValue::Float(*coh)); - m - }) - .collect(); - r.insert("functions".into(), ReportValue::Functions(functions)); - r + fn func(name: &str, file: &str, cohesion: f64) -> AggregatedFunction { + AggregatedFunction { + source_file: file.into(), + name: name.into(), + start_line: 1, + line_count: 1, + variable_count: 1, + cohesion, + } } - #[test] - fn parse_round_trips_scalars() { - let r = report_with(&[("f", 0.5)], 1, 0.25, 0.75, 0.5); - let d = parse_report_data(&r); - assert_eq!(d.total_functions, 1); - assert!((d.lcom - 0.25).abs() < 1e-9); - assert!((d.cohesion_score - 0.75).abs() < 1e-9); - assert_eq!(d.message, "msg"); - assert_eq!(d.functions.len(), 1); - assert_eq!(d.functions[0].name, "f"); + fn agg(functions: Vec<AggregatedFunction>) -> Aggregate { + Aggregate { + files_analyzed: 1, + files_with_functions: 1, + total_functions: functions.len(), + lcom: 0.2, + cohesion_score: 0.8, + function_cohesion: 0.5, + functions, + message: "msg".into(), + } } #[test] @@ -402,58 +232,42 @@ mod tests { #[test] fn distribution_only_includes_present_buckets() { - let r = report_with(&[("a", 0.9), ("b", 0.5), ("c", 0.5)], 3, 0.0, 1.0, 0.6); - let d = parse_report_data(&r); - let dist = compute_distribution(&d); - assert_eq!(dist.get("excellent"), Some(&1)); - assert_eq!(dist.get("good"), Some(&2)); - assert_eq!(dist.get("fair"), None); - assert_eq!(dist.get("poor"), None); + let m = compute_metrics(&agg(vec![ + func("a", "a.go", 0.9), + func("b", "a.go", 0.5), + func("c", "a.go", 0.5), + ])); + assert_eq!(m.distribution.get("excellent"), Some(&1)); + assert_eq!(m.distribution.get("good"), Some(&2)); + assert_eq!(m.distribution.get("fair"), None); + assert_eq!(m.distribution.get("poor"), None); } #[test] - fn low_cohesion_thresholds_and_sort() { - let r = report_with( - &[("good", 0.9), ("mid", 0.35), ("bad", 0.1)], - 3, - 0.0, - 1.0, - 0.5, - ); - let d = parse_report_data(&r); - let low = compute_low_cohesion_functions(&d); - // "good" (0.9) excluded; sorted ascending -> bad, mid. - assert_eq!(low.len(), 2); - assert_eq!(low[0].name, "bad"); - assert_eq!(low[0].risk_level, "HIGH"); - assert_eq!( - low[0].recommendation, - "Consider splitting into multiple focused functions" - ); - assert_eq!(low[1].name, "mid"); - assert_eq!(low[1].risk_level, "MEDIUM"); - assert_eq!( - low[1].recommendation, - "Review function responsibilities for possible separation" - ); - } - - #[test] - fn aggregate_health_score() { - let r = report_with(&[], 0, 0.2, 0.8, 1.0); - let d = parse_report_data(&r); - let agg = compute_aggregate(&d); - assert!((agg.health_score - 80.0).abs() < 1e-9); - assert_eq!(agg.lcom_variant, "LCOM-HS (Henderson-Sellers)"); + fn functions_sorted_ascending_and_low_cohesion_classified() { + let m = compute_metrics(&agg(vec![ + func("good", "a.go", 0.9), + func("mid", "a.go", 0.35), + func("bad", "a.go", 0.1), + ])); + let names: Vec<&str> = m + .function_cohesion + .iter() + .map(|f| f.name.as_str()) + .collect(); + assert_eq!(names, ["bad", "mid", "good"]); + assert_eq!(m.low_cohesion_functions.len(), 2); + assert_eq!(m.low_cohesion_functions[0].name, "bad"); + assert_eq!(m.low_cohesion_functions[0].risk_level, "HIGH"); + assert_eq!(m.low_cohesion_functions[1].name, "mid"); + assert_eq!(m.low_cohesion_functions[1].risk_level, "MEDIUM"); } #[test] - fn compute_all_assembles_struct() { - let r = report_with(&[("f", 0.5), ("g", 0.1)], 2, 0.25, 0.75, 0.3); - let m = compute_all_metrics(&r); - assert_eq!(m.function_cohesion.len(), 2); - // function_cohesion sorted ascending. - assert_eq!(m.function_cohesion[0].name, "g"); - assert_eq!(m.analyzer_name(), "cohesion"); + fn aggregate_carries_health_score_and_variant() { + let m = compute_metrics(&agg(vec![])); + assert!((m.aggregate.health_score - 80.0).abs() < 1e-9); + assert_eq!(m.aggregate.lcom_variant, LCOM_VARIANT); + assert_eq!(m.aggregate.message, "msg"); } } diff --git a/crates/cf-cohesion/src/report_section.rs b/crates/cf-cohesion/src/report_section.rs index 81948880..85d4ebd4 100644 --- a/crates/cf-cohesion/src/report_section.rs +++ b/crates/cf-cohesion/src/report_section.rs @@ -1,301 +1,191 @@ -//! Terminal report section. -//! -//! This is **non-binding / cosmetic** output (DESIGN §2.7): byte-identity is -//! not required for the terminal renderer. The data extraction (distribution -//! buckets, top issues, severities) lives here so `cf-renderer` can produce -//! the section; the actual table/ANSI rendering is delegated to the renderer -//! crate. +//! Report-section data: the distribution buckets and the issues list the +//! terminal and JSON section renderers display. -use crate::report_value::{Report, ReportValue}; +use crate::metrics::{ + COHESION_THRESHOLD_EXCELLENT, COHESION_THRESHOLD_FAIR, COHESION_THRESHOLD_GOOD, +}; /// Section title. pub const SECTION_TITLE: &str = "COHESION"; -/// Default status message. -pub const DEFAULT_STATUS_MESSAGE: &str = "No cohesion data available"; -/// Distribution thresholds. -const DIST_EXCELLENT_MIN: f64 = 0.6; -const DIST_GOOD_MIN: f64 = 0.4; -const DIST_FAIR_MIN: f64 = 0.3; - -/// Distribution labels. -pub const DIST_LABEL_EXCELLENT: &str = "Excellent (>0.6)"; -/// See [`DIST_LABEL_EXCELLENT`]. +/// Distribution label: cohesion `>= 0.6`. +pub const DIST_LABEL_EXCELLENT: &str = "Excellent (>=0.6)"; +/// Distribution label: cohesion in `[0.4, 0.6)`. pub const DIST_LABEL_GOOD: &str = "Good (0.4-0.6)"; -/// See [`DIST_LABEL_EXCELLENT`]. +/// Distribution label: cohesion in `[0.3, 0.4)`. pub const DIST_LABEL_FAIR: &str = "Fair (0.3-0.4)"; -/// See [`DIST_LABEL_EXCELLENT`]. +/// Distribution label: cohesion `< 0.3`. pub const DIST_LABEL_POOR: &str = "Poor (<0.3)"; -/// Issue severity thresholds. -const ISSUE_SEVERITY_FAIR_MAX: f64 = 0.4; -const ISSUE_SEVERITY_POOR_MAX: f64 = 0.3; - -/// Severity labels. -pub const SEVERITY_POOR: &str = "poor"; -/// See [`SEVERITY_POOR`]. -pub const SEVERITY_FAIR: &str = "fair"; -/// See [`SEVERITY_POOR`]. +/// Issue severity for cohesion in `[0.4, 0.6)`. pub const SEVERITY_GOOD: &str = "good"; +/// Issue severity for cohesion in `[0.3, 0.4)`. +pub const SEVERITY_FAIR: &str = "fair"; +/// Issue severity for cohesion `< 0.3`. +pub const SEVERITY_POOR: &str = "poor"; + +/// Functions with cohesion below this are listed as issues (everything that +/// is not Excellent). +pub const ISSUE_THRESHOLD: f64 = COHESION_THRESHOLD_EXCELLENT; -/// A distribution bucket as rendered in the section. +/// Decimal places of the rendered metrics and issue values. +const METRIC_DECIMALS: usize = 2; +/// The `N/10` score label scale. +const SCORE_LABEL_SCALE: f64 = 10.0; + +/// One distribution bucket. #[derive(Debug, Clone, PartialEq)] -pub struct DistributionItem { - /// Bucket label. - pub label: String, - /// Percentage of functions in the bucket (0-100). - pub percent: f64, - /// Count of functions. +pub struct Bucket { + /// Bucket label (see the `DIST_LABEL_*` constants). + pub label: &'static str, + /// Functions in the bucket. pub count: usize, + /// Fraction of all functions in `[0, 1]`. + pub percent: f64, } -/// A single issue row. +/// One issue row: a function below [`ISSUE_THRESHOLD`]. #[derive(Debug, Clone, PartialEq)] pub struct Issue { /// Function name. pub name: String, - /// Source location. + /// Where it lives (the source file). pub location: String, - /// Formatted cohesion value (a fixed two-decimal string; the *string* is - /// also the sort key, see [`ReportSection::all_issues`]). - pub value: String, - /// Severity label. - pub severity: String, -} - -/// The cohesion terminal section. -#[derive(Debug, Clone)] -pub struct ReportSection { - /// Section title. - pub title: String, - /// Status message. - pub message: String, - /// Cohesion score used for the section header. - pub score_value: f64, - report: Report, + /// Its cohesion. + pub cohesion: f64, + /// Its severity (see the `SEVERITY_*` constants). + pub severity: &'static str, } -impl ReportSection { - /// Builds a section from a cohesion report. - #[must_use] - pub fn new(report: Report) -> Self { - let score = report - .get("cohesion_score") - .and_then(ReportValue::as_float) - .unwrap_or(0.0); - let msg = report - .get("message") - .and_then(ReportValue::as_str) - .filter(|s| !s.is_empty()) - .map_or_else(|| DEFAULT_STATUS_MESSAGE.to_string(), ToString::to_string); - ReportSection { - title: SECTION_TITLE.to_string(), - message: msg, - score_value: score, - report, - } - } - - fn functions(&self) -> &[std::collections::BTreeMap<String, ReportValue>] { - self.report - .get("functions") - .and_then(ReportValue::as_functions) - .unwrap_or(&[]) - } - - /// Distribution buckets. Returns empty when no functions. - #[must_use] - pub fn distribution(&self) -> Vec<DistributionItem> { - let functions = self.functions(); - if functions.is_empty() { - return Vec::new(); - } - let mut excellent = 0usize; - let mut good = 0usize; - let mut fair = 0usize; - let mut poor = 0usize; - for f in functions { - let coh = f - .get("cohesion") - .and_then(ReportValue::as_float) - .unwrap_or(0.0); - if coh >= DIST_EXCELLENT_MIN { - excellent += 1; - } else if coh >= DIST_GOOD_MIN { - good += 1; - } else if coh >= DIST_FAIR_MIN { - fair += 1; - } else { - poor += 1; - } - } - let total = functions.len(); - let pct = |c: usize| (c as f64) * 100.0 / (total as f64); - vec![ - DistributionItem { - label: DIST_LABEL_EXCELLENT.into(), - percent: pct(excellent), - count: excellent, - }, - DistributionItem { - label: DIST_LABEL_GOOD.into(), - percent: pct(good), - count: good, - }, - DistributionItem { - label: DIST_LABEL_FAIR.into(), - percent: pct(fair), - count: fair, - }, - DistributionItem { - label: DIST_LABEL_POOR.into(), - percent: pct(poor), - count: poor, - }, - ] - } - - /// All issues, sorted by formatted cohesion value ascending. - #[must_use] - pub fn all_issues(&self) -> Vec<Issue> { - self.sorted_issues(0) +/// The four buckets over `cohesions`, in display order; empty when there are +/// no functions. +#[must_use] +pub fn distribution(cohesions: &[f64]) -> Vec<Bucket> { + if cohesions.is_empty() { + return Vec::new(); } - - /// Top `n` issues; `n == 0` means all. - #[must_use] - pub fn top_issues(&self, n: usize) -> Vec<Issue> { - self.sorted_issues(n) + let mut counts = [0usize; 4]; + for &c in cohesions { + let bucket = if c >= COHESION_THRESHOLD_EXCELLENT { + 0 + } else if c >= COHESION_THRESHOLD_GOOD { + 1 + } else if c >= COHESION_THRESHOLD_FAIR { + 2 + } else { + 3 + }; + counts[bucket] += 1; } + let total = cohesions.len() as f64; + [ + DIST_LABEL_EXCELLENT, + DIST_LABEL_GOOD, + DIST_LABEL_FAIR, + DIST_LABEL_POOR, + ] + .iter() + .zip(counts) + .map(|(&label, count)| Bucket { + label, + count, + percent: count as f64 / total, + }) + .collect() +} - fn sorted_issues(&self, limit: usize) -> Vec<Issue> { - let functions = self.functions(); - if functions.is_empty() { - return Vec::new(); - } - let mut out: Vec<Issue> = functions - .iter() - .map(|f| { - let coh = f - .get("cohesion") - .and_then(ReportValue::as_float) - .unwrap_or(0.0); - let name = f - .get("name") - .and_then(ReportValue::as_str) - .unwrap_or("") - .to_string(); - let location = f - .get(crate::metrics::SOURCE_FILE_KEY) - .and_then(ReportValue::as_str) - .unwrap_or("") - .to_string(); - Issue { - name, - location, - value: format_float(coh), - severity: severity_for_cohesion(coh).to_string(), - } - }) - .collect(); - // The report contract orders issues by the formatted string `value` - // ascending via the pinned unstable sort (pdqsort). Replicate both the - // string key and the exact tie permutation so the emitted byte order - // matches; numeric sorting or a stable sort diverges on ties. - cf_gosort::go_sort_slice(&mut out, |a, b| a.value < b.value); - if limit > 0 && out.len() > limit { - out.truncate(limit); - } - out - } +/// The issues among `(name, location, cohesion)` rows: those below +/// [`ISSUE_THRESHOLD`], ordered by cohesion ascending, then name, then +/// location. +#[must_use] +pub fn issues<'a>(rows: impl IntoIterator<Item = (&'a str, &'a str, f64)>) -> Vec<Issue> { + let mut out: Vec<Issue> = rows + .into_iter() + .filter(|&(_, _, cohesion)| cohesion < ISSUE_THRESHOLD) + .map(|(name, location, cohesion)| Issue { + name: name.to_string(), + location: location.to_string(), + cohesion, + severity: severity(cohesion), + }) + .collect(); + out.sort_by(|a, b| { + a.cohesion + .total_cmp(&b.cohesion) + .then_with(|| a.name.cmp(&b.name)) + .then_with(|| a.location.cmp(&b.location)) + }); + out } -/// Maps a cohesion value to a severity label. +/// Issue severity of a cohesion value. #[must_use] -pub fn severity_for_cohesion(coh: f64) -> &'static str { - if coh < ISSUE_SEVERITY_POOR_MAX { +pub fn severity(cohesion: f64) -> &'static str { + if cohesion < COHESION_THRESHOLD_FAIR { SEVERITY_POOR - } else if coh < ISSUE_SEVERITY_FAIR_MAX { + } else if cohesion < COHESION_THRESHOLD_GOOD { SEVERITY_FAIR } else { SEVERITY_GOOD } } -/// Formats a float with two decimals for the terminal renderer. Cosmetic only. +/// Renders a metric or issue value with [`METRIC_DECIMALS`] decimals. +#[must_use] +pub fn format_metric(v: f64) -> String { + format!("{v:.METRIC_DECIMALS$}") +} + +/// Renders a `[0, 1]` score as the `N/10` label. #[must_use] -pub fn format_float(v: f64) -> String { - format!("{v:.2}") +pub fn score_label(score: f64) -> String { + let n = (score * SCORE_LABEL_SCALE).round() as i64; + format!("{n}/10") } #[cfg(test)] mod tests { use super::*; - use crate::report_value::ReportValue; - use std::collections::BTreeMap; - - fn report(funcs: &[(&str, f64)], score: f64, msg: &str) -> Report { - let mut r = Report::new(); - r.insert("cohesion_score".into(), ReportValue::Float(score)); - if !msg.is_empty() { - r.insert("message".into(), ReportValue::Str(msg.into())); - } - let functions: Vec<BTreeMap<String, ReportValue>> = funcs - .iter() - .map(|(n, c)| { - let mut m = BTreeMap::new(); - m.insert("name".into(), ReportValue::Str((*n).into())); - m.insert("cohesion".into(), ReportValue::Float(*c)); - m - }) - .collect(); - r.insert("functions".into(), ReportValue::Functions(functions)); - r - } - - #[test] - fn default_message_when_empty() { - let s = ReportSection::new(report(&[], 0.0, "")); - assert_eq!(s.message, DEFAULT_STATUS_MESSAGE); - assert_eq!(s.title, "COHESION"); - } #[test] - fn distribution_buckets() { - let s = ReportSection::new(report( - &[("a", 0.9), ("b", 0.5), ("c", 0.35), ("d", 0.1)], - 0.5, - "m", - )); - let d = s.distribution(); - assert_eq!(d[0].count, 1); // excellent - assert_eq!(d[1].count, 1); // good - assert_eq!(d[2].count, 1); // fair - assert_eq!(d[3].count, 1); // poor - assert!((d[0].percent - 25.0).abs() < 1e-9); + fn distribution_is_empty_without_functions_and_sums_to_total() { + assert!(distribution(&[]).is_empty()); + let buckets = distribution(&[1.0, 0.5, 0.35, 0.0, 0.0]); + let counts: Vec<usize> = buckets.iter().map(|b| b.count).collect(); + assert_eq!(counts, [1, 1, 1, 2]); + assert_eq!(counts.iter().sum::<usize>(), 5); + assert!((buckets[3].percent - 0.4).abs() < 1e-9); + assert_eq!(buckets[0].label, DIST_LABEL_EXCELLENT); } #[test] - fn issues_sorted_worst_first() { - let s = ReportSection::new(report(&[("a", 0.9), ("b", 0.1)], 0.5, "m")); - let issues = s.all_issues(); - assert_eq!(issues[0].name, "b"); - assert_eq!(issues[0].severity, SEVERITY_POOR); - assert_eq!(issues[1].name, "a"); - assert_eq!(issues[1].severity, SEVERITY_GOOD); + fn issues_hold_only_non_excellent_functions_worst_first() { + let rows = [ + ("h", "a.go", 0.9), + ("f", "a.go", 0.5), + ("z", "b.go", 0.0), + ("a", "b.go", 0.0), + ]; + let got = issues(rows.iter().map(|&(n, l, c)| (n, l, c))); + let names: Vec<&str> = got.iter().map(|i| i.name.as_str()).collect(); + assert_eq!(names, ["a", "z", "f"]); + assert_eq!(got[0].severity, SEVERITY_POOR); + assert_eq!(got[2].severity, SEVERITY_GOOD); } #[test] - fn top_issues_limits() { - let s = ReportSection::new(report(&[("a", 0.9), ("b", 0.5), ("c", 0.1)], 0.5, "m")); - assert_eq!(s.top_issues(2).len(), 2); - assert_eq!(s.top_issues(0).len(), 3); + fn severity_thresholds() { + assert_eq!(severity(0.29), SEVERITY_POOR); + assert_eq!(severity(0.3), SEVERITY_FAIR); + assert_eq!(severity(0.4), SEVERITY_GOOD); } #[test] - fn severity_thresholds() { - assert_eq!(severity_for_cohesion(0.29), SEVERITY_POOR); - assert_eq!(severity_for_cohesion(0.3), SEVERITY_FAIR); - assert_eq!(severity_for_cohesion(0.39), SEVERITY_FAIR); - assert_eq!(severity_for_cohesion(0.4), SEVERITY_GOOD); + fn metrics_render_two_decimals_and_scores_round_to_tenths() { + assert_eq!(format_metric(1.0 / 3.0), "0.33"); + assert_eq!(format_metric(0.0), "0.00"); + assert_eq!(score_label(0.6), "6/10"); + assert_eq!(score_label(0.0), "0/10"); + assert_eq!(score_label(1.0), "10/10"); } } diff --git a/crates/cf-cohesion/src/report_value.rs b/crates/cf-cohesion/src/report_value.rs index e7cbd68e..ff911631 100644 --- a/crates/cf-cohesion/src/report_value.rs +++ b/crates/cf-cohesion/src/report_value.rs @@ -1,53 +1,23 @@ -//! Dynamic report value model, restricted to the value shapes the cohesion -//! analyzer produces. +//! The scalar report map consumed by the history `quality` analyzer. //! -//! The analyzer builds an intermediate dynamic [`Report`] map from -//! [`crate::analyzer::Analyzer::analyze`], which is then re-parsed by -//! [`crate::metrics::compute_all_metrics`] (the function that produces the -//! actual machine-format output). Modeling that intermediate map explicitly -//! keeps the round-trip (`build_result` -> `parse_report_data`) faithful and -//! testable in isolation. The shape is intentionally a strict subset of the -//! shared dynamic-report type: exactly the keys and value types the cohesion -//! pipeline reads and writes. +//! [`crate::FileReport::to_report`] produces it; consumers read the scalars +//! by key with a strict type assertion (an `Int` is never coerced to a float). use std::collections::BTreeMap; -/// A value inside a cohesion [`Report`]. -/// -/// The concrete dynamic types: integer, float, string, and a list of -/// string-keyed maps (the per-function table). The integer/float distinction -/// is significant: scalar parsing is a strict type assertion, never a -/// coercion. +/// A scalar value inside a [`Report`]. #[derive(Debug, Clone, PartialEq)] pub enum ReportValue { - /// An integer (e.g. `total_functions`). + /// An integer (`total_functions`). Int(i64), - /// A float (e.g. `lcom`, `cohesion_score`). + /// A float (`lcom`, `cohesion_score`, `function_cohesion`). Float(f64), - /// A string (e.g. `message`, `analyzer_name`). + /// A string (`message`). Str(String), - /// A list of string-keyed maps (the `functions` table), preserving the - /// per-function item keys. - Functions(Vec<BTreeMap<String, ReportValue>>), } impl ReportValue { - /// Returns the value as an `i64` if it is [`ReportValue::Int`], else - /// `None`. - #[must_use] - pub fn as_int(&self) -> Option<i64> { - match self { - ReportValue::Int(v) => Some(*v), - _ => None, - } - } - - /// Returns the value as an `f64` if it is [`ReportValue::Float`], else - /// `None`. - /// - /// NOTE: this is a strict type assertion (report-format contract). An - /// `Int` is **not** coerced — the cohesion scalars are always stored as - /// floats. + /// The value as an `f64` if it is [`ReportValue::Float`], else `None`. #[must_use] pub fn as_float(&self) -> Option<f64> { match self { @@ -55,29 +25,19 @@ impl ReportValue { _ => None, } } +} - /// Returns the value as a `&str` if it is [`ReportValue::Str`], else `None`. - #[must_use] - pub fn as_str(&self) -> Option<&str> { - match self { - ReportValue::Str(s) => Some(s), - _ => None, - } - } +/// The per-file scalar report, keyed by metric name. +pub type Report = BTreeMap<String, ReportValue>; - /// Returns the per-function table if this is [`ReportValue::Functions`], else - /// `None`. - #[must_use] - pub fn as_functions(&self) -> Option<&[BTreeMap<String, ReportValue>]> { - match self { - ReportValue::Functions(f) => Some(f), - _ => None, - } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn as_float_is_a_strict_type_assertion() { + assert_eq!(ReportValue::Float(0.5).as_float(), Some(0.5)); + assert_eq!(ReportValue::Int(1).as_float(), None); + assert_eq!(ReportValue::Str("x".into()).as_float(), None); } } - -/// The analyzer's intermediate result map. -/// -/// A `BTreeMap` so iteration / encoding is byte-key-sorted by default, -/// matching the report contract's map-key encoding rule (DESIGN §2.2). -pub type Report = BTreeMap<String, ReportValue>; diff --git a/crates/cf-cohesion/src/serialize.rs b/crates/cf-cohesion/src/serialize.rs deleted file mode 100644 index efbbbceb..00000000 --- a/crates/cf-cohesion/src/serialize.rs +++ /dev/null @@ -1,563 +0,0 @@ -//! Machine-format serialization for [`ComputedMetrics`]. -//! -//! Encoder configs (report-format contract): -//! -//! | Method | Encoder config | -//! | --- | --- | -//! | JSON | indent `" "`, HTML-escape ON, no trailing newline | -//! | YAML | block-style emitter | -//! | binary | CFB1 = `"CFB1"` + u32-LE len + compact JSON | -//! -//! # Routing (DESIGN §2, project rule 1) -//! -//! The production machine-format bytes go through the shared `cf-gojson` / -//! `cf-goyaml` / `cf-reportutil` crates (the static-pipeline handler owns that -//! path). This module builds the value tree via [`to_go_value`] and carries a -//! self-contained reference encoder ([`encode_json`]) implementing the exact -//! report-contract JSON rules (declaration-order struct fields, byte-sorted -//! map keys, HTML escaping, shortest-round-trip `%g` float formatting, -//! two-space indent, no trailing newline) so the crate is testable in -//! isolation. - -use crate::metrics::{ComputedMetrics, FunctionCohesionData}; - -/// A report-contract JSON value, mirroring `cf_gojson::GoValue`. -/// -/// `Object` distinguishes struct-origin (declaration order, `omitempty` already -/// applied by the builder) from map-origin (`sorted = true`, keys byte-sorted on -/// encode) so the dual-mode ordering rule of DESIGN §2.2 is honored. -#[derive(Debug, Clone, PartialEq)] -pub enum GoValue { - /// An integer. - Int(i64), - /// A float. - Float(f64), - /// A string. - Str(String), - /// An array. - Array(Vec<GoValue>), - /// A struct-origin (ordered) or map-origin (sorted-on-encode) object. - Object { - /// Key/value pairs. - fields: Vec<(String, GoValue)>, - /// True for map-origin objects (byte-sort keys on encode). - sorted: bool, - }, -} - -impl GoValue { - fn struct_obj(fields: Vec<(String, GoValue)>) -> GoValue { - GoValue::Object { - fields, - sorted: false, - } - } -} - -/// Builds the [`GoValue`] tree for [`ComputedMetrics`], honoring field order and -/// `omitempty`. This is the input you would hand to `cf_gojson::Encoder`. -#[must_use] -pub fn to_go_value(m: &ComputedMetrics) -> GoValue { - GoValue::struct_obj(vec![ - ( - "function_cohesion".into(), - GoValue::Array( - m.function_cohesion - .iter() - .map(func_cohesion_value) - .collect(), - ), - ), - ("distribution".into(), distribution_value(&m.distribution)), - ( - "low_cohesion_functions".into(), - GoValue::Array( - m.low_cohesion_functions - .iter() - .map(low_cohesion_value) - .collect(), - ), - ), - ("aggregate".into(), aggregate_value(&m.aggregate)), - ]) -} - -fn func_cohesion_value(f: &FunctionCohesionData) -> GoValue { - let mut fields = Vec::with_capacity(6); - fields.push(("name".into(), GoValue::Str(f.name.clone()))); - if !f.source_file.is_empty() { - fields.push(("source_file".into(), GoValue::Str(f.source_file.clone()))); - } - if !f.language.is_empty() { - fields.push(("language".into(), GoValue::Str(f.language.clone()))); - } - if !f.directory.is_empty() { - fields.push(("directory".into(), GoValue::Str(f.directory.clone()))); - } - fields.push(("cohesion".into(), GoValue::Float(f.cohesion))); - fields.push(( - "quality_level".into(), - GoValue::Str(f.quality_level.clone()), - )); - GoValue::struct_obj(fields) -} - -fn low_cohesion_value(f: &crate::metrics::LowCohesionFunctionData) -> GoValue { - let mut fields = Vec::with_capacity(7); - fields.push(("name".into(), GoValue::Str(f.name.clone()))); - if !f.source_file.is_empty() { - fields.push(("source_file".into(), GoValue::Str(f.source_file.clone()))); - } - if !f.language.is_empty() { - fields.push(("language".into(), GoValue::Str(f.language.clone()))); - } - if !f.directory.is_empty() { - fields.push(("directory".into(), GoValue::Str(f.directory.clone()))); - } - fields.push(("cohesion".into(), GoValue::Float(f.cohesion))); - fields.push(("risk_level".into(), GoValue::Str(f.risk_level.clone()))); - fields.push(( - "recommendation".into(), - GoValue::Str(f.recommendation.clone()), - )); - GoValue::struct_obj(fields) -} - -fn aggregate_value(a: &crate::metrics::AggregateData) -> GoValue { - GoValue::struct_obj(vec![ - ("total_functions".into(), GoValue::Int(a.total_functions)), - ("lcom".into(), GoValue::Float(a.lcom)), - ("lcom_variant".into(), GoValue::Str(a.lcom_variant.clone())), - ("cohesion_score".into(), GoValue::Float(a.cohesion_score)), - ( - "function_cohesion".into(), - GoValue::Float(a.function_cohesion), - ), - ("health_score".into(), GoValue::Float(a.health_score)), - ("message".into(), GoValue::Str(a.message.clone())), - ]) -} - -fn distribution_value(dist: &std::collections::BTreeMap<String, i64>) -> GoValue { - // map-origin object: keys byte-sorted on encode. BTreeMap is already sorted by - // byte order for ASCII keys; we mark sorted = true so the encoder enforces it. - GoValue::Object { - fields: dist - .iter() - .map(|(k, v)| (k.clone(), GoValue::Int(*v))) - .collect(), - sorted: true, - } -} - -// === Reference encoders (the production path delegates to cf-gojson/cf-goyaml) === - -/// CFB1 binary-envelope magic. -pub const BINARY_MAGIC: &[u8; 4] = b"CFB1"; - -/// Encodes `metrics` as indented JSON: two-space indent, HTML escaping ON, and -/// no trailing newline. -#[must_use] -pub fn encode_json(metrics: &ComputedMetrics) -> Vec<u8> { - let mut out = String::new(); - write_value(&mut out, &to_go_value(metrics), Some(" "), 0); - out.into_bytes() -} - -/// Encodes `metrics` as compact JSON: no spaces, HTML escaping ON, no trailing -/// newline. This is the CFB1 payload. -#[must_use] -pub fn encode_compact_json(metrics: &ComputedMetrics) -> Vec<u8> { - let mut out = String::new(); - write_value(&mut out, &to_go_value(metrics), None, 0); - out.into_bytes() -} - -/// Encodes `metrics` as a single CFB1 envelope record: `"CFB1"` + `u32` -/// little-endian payload length + compact-JSON payload. -#[must_use] -pub fn encode_binary(metrics: &ComputedMetrics) -> Vec<u8> { - let payload = encode_compact_json(metrics); - let mut out = Vec::with_capacity(8 + payload.len()); - out.extend_from_slice(BINARY_MAGIC); - out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); - out.extend_from_slice(&payload); - out -} - -/// Renders a [`GoValue`] into `out`. `indent = Some(" ")` produces the -/// indented form; `indent = None` the compact form. -fn write_value(out: &mut String, v: &GoValue, indent: Option<&str>, depth: usize) { - match v { - GoValue::Int(n) => out.push_str(&n.to_string()), - GoValue::Float(f) => out.push_str(&go_float(*f)), - GoValue::Str(s) => write_json_string(out, s), - GoValue::Array(items) => write_array(out, items, indent, depth), - GoValue::Object { fields, sorted } => write_object(out, fields, *sorted, indent, depth), - } -} - -fn write_array(out: &mut String, items: &[GoValue], indent: Option<&str>, depth: usize) { - if items.is_empty() { - out.push_str("[]"); - return; - } - out.push('['); - for (i, item) in items.iter().enumerate() { - if i > 0 { - out.push(','); - } - newline_indent(out, indent, depth + 1); - write_value(out, item, indent, depth + 1); - } - newline_indent(out, indent, depth); - out.push(']'); -} - -fn write_object( - out: &mut String, - fields: &[(String, GoValue)], - sorted: bool, - indent: Option<&str>, - depth: usize, -) { - if fields.is_empty() { - out.push_str("{}"); - return; - } - // For map-origin objects, byte-sort keys at encode time (map JSON rule). - let mut ordered: Vec<&(String, GoValue)> = fields.iter().collect(); - if sorted { - ordered.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); - } - out.push('{'); - for (i, (k, val)) in ordered.iter().map(|p| (&p.0, &p.1)).enumerate() { - if i > 0 { - out.push(','); - } - newline_indent(out, indent, depth + 1); - write_json_string(out, k); - out.push(':'); - if indent.is_some() { - out.push(' '); // space after colon only in indent mode - } - write_value(out, val, indent, depth + 1); - } - newline_indent(out, indent, depth); - out.push('}'); -} - -fn newline_indent(out: &mut String, indent: Option<&str>, depth: usize) { - if let Some(unit) = indent { - out.push('\n'); - for _ in 0..depth { - out.push_str(unit); - } - } -} - -/// Writes a JSON string with HTML escaping ON (report contract): escapes `"`, -/// `\`, control chars (`\n`/`\r`/`\t` shortcuts, else `\u00XX`), and `<`, -/// `>`, `&`, `U+2028`, `U+2029`. -fn write_json_string(out: &mut String, s: &str) { - out.push('"'); - for ch in s.chars() { - match ch { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - '<' => out.push_str("\\u003c"), - '>' => out.push_str("\\u003e"), - '&' => out.push_str("\\u0026"), - '\u{2028}' => out.push_str("\\u2028"), - '\u{2029}' => out.push_str("\\u2029"), - c if (c as u32) < 0x20 => { - out.push_str(&format!("\\u{:04x}", c as u32)); - } - c => out.push(c), - } - } - out.push('"'); -} - -/// Formats an `f64` with shortest-round-trip `%g` semantics, as used by the -/// report contract's float encoder (DESIGN §2.2). -/// -/// Rules: shortest round-trip digits; switch to exponential when -/// `exp < -4 || exp >= 21`; exponent rendered `e±NN` with sign and >= 2 digits; -/// integer-valued floats printed without a decimal point (`1.0` -> `1`); `±0` -/// printed as `0`. -/// -/// This is the high-risk path; the production build delegates to -/// `cf_gojson::go_float`, which is fuzzed against the reference implementation -/// in the golden harness (DESIGN §6 Layer A). -#[must_use] -pub fn go_float(f: f64) -> String { - if f == 0.0 { - // Both +0.0 and -0.0 render as "0". - return "0".to_string(); - } - if f.is_nan() || f.is_infinite() { - // The report contract has no NaN/Inf rendering (the reference encoder - // errors); fall back to "0", which never occurs for cohesion values - // (all finite in [0,1] or 0..100). The cf-gojson path returns an error - // instead. - return "0".to_string(); - } - - // Obtain the shortest round-tripping decimal via Rust's formatter, whose - // Grisu/Ryū-style shortest representation matches the reference digit - // sequence. - let shortest = format!("{f}"); - reformat_go_g(&shortest) -} - -/// Re-renders a shortest decimal string into `%g` form. -fn reformat_go_g(shortest: &str) -> String { - let (sign, body) = if let Some(stripped) = shortest.strip_prefix('-') { - ("-", stripped) - } else { - ("", shortest) - }; - - let (mantissa_digits, decimal_exp) = decompose(body); - - // decimal_exp is the power of ten of the FIRST significant digit - // (scientific). '%g' uses exponential when exp < -4 || exp >= 21. - let use_exp = !(-4..21).contains(&decimal_exp); - - let rendered = if use_exp { - render_exponential(&mantissa_digits, decimal_exp) - } else { - render_fixed(&mantissa_digits, decimal_exp) - }; - - format!("{sign}{rendered}") -} - -/// Decomposes a non-negative decimal string (no sign) like "123.45" or "1e20" into -/// `(significant_digits_without_point, exponent_of_first_digit)`. -fn decompose(body: &str) -> (String, i32) { - let (num, exp_part) = match body.split_once(['e', 'E']) { - Some((n, e)) => (n, e.parse::<i32>().unwrap_or(0)), - None => (body, 0), - }; - - let (int_part, frac_part) = match num.split_once('.') { - Some((i, f)) => (i, f), - None => (num, ""), - }; - - let all: String = format!("{int_part}{frac_part}"); - let point_pos = int_part.len() as i32; // digits before the decimal point - - let Some(first_sig) = all.find(|c: char| c != '0') else { - return ("0".to_string(), 0); - }; - - let exp_of_first = point_pos - 1 - first_sig as i32 + exp_part; - let last_sig = all.rfind(|c: char| c != '0').unwrap(); - let digits: String = all[first_sig..=last_sig].to_string(); - - (digits, exp_of_first) -} - -/// Renders fixed-point (`'f'`) form, `exp` = power of the first digit. -fn render_fixed(digits: &str, exp: i32) -> String { - let n = digits.len() as i32; - if exp >= 0 { - if exp + 1 >= n { - let mut s = digits.to_string(); - for _ in 0..(exp + 1 - n) { - s.push('0'); - } - s - } else { - let split = (exp + 1) as usize; - format!("{}.{}", &digits[..split], &digits[split..]) - } - } else { - let mut s = String::from("0."); - for _ in 0..(-exp - 1) { - s.push('0'); - } - s.push_str(digits); - s - } -} - -/// Renders exponential (`'e'`) form: `d.dddde±NN`. -fn render_exponential(digits: &str, exp: i32) -> String { - let mantissa = if digits.len() == 1 { - digits.to_string() - } else { - format!("{}.{}", &digits[..1], &digits[1..]) - }; - let sign = if exp < 0 { '-' } else { '+' }; - let mag = exp.unsigned_abs(); - format!("{mantissa}e{sign}{mag:02}") -} - -/// Encodes `metrics` as block-style YAML. -/// -/// NOTE: the production build MUST delegate to `cf_goyaml` (DESIGN §2.4, -/// highest residual risk). This reference implementation covers the cohesion -/// shape only and is intentionally minimal; it is not asserted byte-identical -/// here and exists so the YAML method compiles and round-trips structurally. -#[must_use] -pub fn encode_yaml(metrics: &ComputedMetrics) -> Vec<u8> { - let mut out = String::new(); - write_yaml_value(&mut out, &to_go_value(metrics), 0); - out.into_bytes() -} - -fn write_yaml_value(out: &mut String, v: &GoValue, indent: usize) { - match v { - GoValue::Int(n) => out.push_str(&n.to_string()), - GoValue::Float(f) => out.push_str(&go_float(*f)), - GoValue::Str(s) => out.push_str(&yaml_scalar(s)), - GoValue::Array(items) => { - if items.is_empty() { - out.push_str("[]"); - return; - } - for item in items { - out.push('\n'); - push_indent(out, indent); - out.push_str("- "); - write_yaml_value(out, item, indent + 1); - } - } - GoValue::Object { fields, sorted } => { - let mut ordered: Vec<&(String, GoValue)> = fields.iter().collect(); - if *sorted { - ordered.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); - } - for (k, val) in ordered.iter().map(|p| (&p.0, &p.1)) { - out.push('\n'); - push_indent(out, indent); - out.push_str(k); - out.push(':'); - match val { - GoValue::Object { .. } | GoValue::Array(_) => { - write_yaml_value(out, val, indent + 1); - } - _ => { - out.push(' '); - write_yaml_value(out, val, indent); - } - } - } - } - } -} - -fn push_indent(out: &mut String, indent: usize) { - for _ in 0..indent { - out.push_str(" "); - } -} - -fn yaml_scalar(s: &str) -> String { - if s.is_empty() { - "\"\"".to_string() - } else { - // Minimal: quote if it could be misparsed. Cosmetic-grade. - s.to_string() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn go_float_integers() { - assert_eq!(go_float(1.0), "1"); - assert_eq!(go_float(0.0), "0"); - assert_eq!(go_float(-0.0), "0"); - assert_eq!(go_float(100.0), "100"); - assert_eq!(go_float(2.0), "2"); - } - - #[test] - fn go_float_decimals() { - assert_eq!(go_float(0.5), "0.5"); - assert_eq!(go_float(0.25), "0.25"); - assert_eq!(go_float(0.75), "0.75"); - assert_eq!(go_float(0.1), "0.1"); - } - - #[test] - fn go_float_small_uses_exponent() { - // 1e-5 has first-digit exponent -5 < -4 -> exponential. - assert_eq!(go_float(0.00001), "1e-05"); - // 1e-4 -> exp -4, not < -4 -> fixed. - assert_eq!(go_float(0.0001), "0.0001"); - } - - #[test] - fn go_float_large_uses_exponent() { - // 1e21 -> exp 21 >= 21 -> exponential. - assert_eq!(go_float(1e21), "1e+21"); - // 1e20 -> exp 20 < 21 -> fixed (21 digits). - assert_eq!(go_float(1e20), "100000000000000000000"); - } - - #[test] - fn json_empty_metrics_shape() { - let m = ComputedMetrics::default(); - let bytes = encode_json(&m); - let s = String::from_utf8(bytes).unwrap(); - // function_cohesion [] , distribution {} , low_cohesion_functions [] , - // aggregate {...}. No trailing newline. - assert!(s.starts_with("{\n \"function_cohesion\": [],")); - assert!(!s.ends_with('\n')); - assert!(s.contains("\"distribution\": {}")); - } - - #[test] - fn binary_envelope_header() { - let m = ComputedMetrics::default(); - let bytes = encode_binary(&m); - assert_eq!(&bytes[0..4], b"CFB1"); - let len = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize; - assert_eq!(len, bytes.len() - 8); - // Payload is compact (no spaces after colon, no newlines). - let payload = String::from_utf8(bytes[8..].to_vec()).unwrap(); - assert!(!payload.contains('\n')); - assert!(payload.contains("\"function_cohesion\":[]")); - } - - #[test] - fn compact_json_no_space_after_colon() { - let m = ComputedMetrics::default(); - let s = String::from_utf8(encode_compact_json(&m)).unwrap(); - assert!(s.contains("\"distribution\":{}")); - assert!(!s.contains(": ")); - } - - #[test] - fn html_escaping_on() { - let mut m = ComputedMetrics::default(); - m.aggregate.message = "a<b>&c".to_string(); - let s = String::from_utf8(encode_compact_json(&m)).unwrap(); - assert!(s.contains("a\\u003cb\\u003e\\u0026c")); - } - - #[test] - fn distribution_keys_byte_sorted() { - let mut m = ComputedMetrics::default(); - m.distribution.insert("poor".into(), 1); - m.distribution.insert("excellent".into(), 2); - m.distribution.insert("good".into(), 3); - let s = String::from_utf8(encode_compact_json(&m)).unwrap(); - // byte order: excellent < good < poor - let pos_e = s.find("excellent").unwrap(); - let pos_g = s.find("good").unwrap(); - let pos_p = s.find("\"poor\"").unwrap(); - assert!(pos_e < pos_g && pos_g < pos_p); - } -} diff --git a/crates/cf-cohesion/src/uast.rs b/crates/cf-cohesion/src/uast.rs index 56d667bc..1da7cf96 100644 --- a/crates/cf-cohesion/src/uast.rs +++ b/crates/cf-cohesion/src/uast.rs @@ -1,14 +1,23 @@ -//! UAST node abstraction used by the cohesion analyzer. +//! UAST node inspection for the cohesion analyzer. //! -//! This module defines the **minimal trait surface** the cohesion analyzer -//! needs; the production implementation is provided for `cf_uast_node::Node` -//! below, and tests use [`TestNode`]. +//! Everything the analyzer needs to know about a [`Node`] lives here: which +//! nodes are functions, which are identifiers, how an identifier's name is +//! resolved, and the small structural predicates (type-role identifiers, +//! declarations, calls, class-like scopes) the variable rules are written in. //! -//! # Constants +//! # Name resolution //! -//! The string constants below are the canonical UAST type/role names. They -//! must match `cf-uast-node` exactly because they select which AST nodes count -//! as functions / variables, which in turn determines the report bytes. +//! Mappings differ in where an identifier's text ends up. Most grammars put it +//! in the node's `token`; some carry it only as `props["name"]`; and a few +//! (the Rust mapping in its current form) leave identifier nodes token-less +//! while an ancestor — the function, a field access — carries the full source +//! text of its span. The resolver tries, in order: `props["name"]`, the token, +//! and finally the identifier's source text sliced out of the nearest enclosing +//! [`Anchor`] by byte offsets. An anchor is only taken from a node whose token +//! is exactly its source span, so slicing can never produce text from +//! somewhere else. + +use cf_uast_node::Node; /// UAST node *type* names referenced by cohesion. pub mod ty { @@ -20,8 +29,20 @@ pub mod ty { pub const VARIABLE: &str = "Variable"; /// `Parameter` type. pub const PARAMETER: &str = "Parameter"; + /// `Field` type. + pub const FIELD: &str = "Field"; /// `Identifier` type. pub const IDENTIFIER: &str = "Identifier"; + /// `Call` type. + pub const CALL: &str = "Call"; + /// `Class` type. + pub const CLASS: &str = "Class"; + /// `Struct` type. + pub const STRUCT: &str = "Struct"; + /// `Interface` type. + pub const INTERFACE: &str = "Interface"; + /// `Enum` type. + pub const ENUM: &str = "Enum"; } /// UAST *role* names referenced by cohesion. @@ -34,181 +55,244 @@ pub mod role { pub const VARIABLE: &str = "Variable"; /// `Name` role. pub const NAME: &str = "Name"; + /// `Reference` role. + pub const REFERENCE: &str = "Reference"; + /// `Type` role. + pub const TYPE: &str = "Type"; + /// `Call` role. + pub const CALL: &str = "Call"; } -/// The subset of UAST node behavior the cohesion analyzer relies on. -/// -/// Implementations come from the UAST parser; tests use [`TestNode`]. -pub trait Node { - /// Child nodes, in source order. - fn children(&self) -> &[Self] - where - Self: Sized; - - /// True if the node's type is any of `types`. - fn has_any_type(&self, types: &[&str]) -> bool; - - /// True if the node has any of `roles`. - fn has_any_role(&self, roles: &[&str]) -> bool; - - /// True if the node has *all* of `roles`. - fn has_all_roles(&self, roles: &[&str]) -> bool; - - /// The entity name carried by this node, if any. Empty string means - /// "no name". - /// - /// Returns an owned `String` because the real UAST implementation derives - /// the name from props/token/child, which is not a borrow of any single - /// field. - fn entity_name(&self) -> String; - - /// Number of source lines spanned by the node (recursive; see the - /// `cf_uast_node::Node` impl). - fn count_lines(&self) -> i64; -} - -/// A simple in-memory [`Node`] implementation for unit tests, covering enough -/// of the UAST node shape to drive the cohesion algorithm. -#[derive(Debug, Clone, Default)] -pub struct TestNode { - /// Node type names. - pub types: Vec<String>, - /// Node role names. - pub roles: Vec<String>, - /// Entity name (empty = none). - pub name: String, - /// Source line span. - pub lines: i64, - /// Children. - pub children: Vec<TestNode>, -} - -impl TestNode { - /// Builds a function node of the given name with the given child nodes. - #[must_use] - pub fn function(name: &str, lines: i64, children: Vec<TestNode>) -> Self { - TestNode { - types: vec![ty::FUNCTION.to_string()], - roles: vec![role::FUNCTION.to_string()], - name: name.to_string(), - lines, - children, - } - } +/// The `props` key carrying a node's declared name. +pub const NAME_PROP: &str = "name"; + +/// Identifier spellings of the implicit method receiver. They are shared by +/// every method by construction, so counting them would inflate cohesion +/// uniformly; they are never variables. +pub const RECEIVER_KEYWORDS: [&str; 2] = ["self", "this"]; + +/// Source text of a node whose token is exactly its span, used to recover the +/// text of token-less identifiers below it by byte offsets. +#[derive(Debug, Clone, Copy)] +pub struct Anchor<'a> { + text: &'a str, + start: u64, +} - /// Builds a variable-declaration node. +impl<'a> Anchor<'a> { + /// An anchor over `n`, when `n` has a position and its token is exactly + /// as long as its byte span (i.e. the token *is* the source text). #[must_use] - pub fn variable(name: &str) -> Self { - TestNode { - types: vec![ty::VARIABLE.to_string()], - roles: vec![role::DECLARATION.to_string()], - name: name.to_string(), - lines: 1, - children: vec![], + pub fn of(n: &'a Node) -> Option<Self> { + let pos = n.pos.as_ref()?; + let span = pos.end_offset.checked_sub(pos.start_offset)?; + if n.token.is_empty() || n.token.len() as u64 != span { + return None; } + Some(Anchor { + text: &n.token, + start: pos.start_offset, + }) } - /// Builds a variable *identifier* node. + /// The source text of `n` inside this anchor, when `n` lies within it. #[must_use] - pub fn identifier(name: &str) -> Self { - TestNode { - types: vec![ty::IDENTIFIER.to_string()], - roles: vec![role::VARIABLE.to_string()], - name: name.to_string(), - lines: 1, - children: vec![], - } + pub fn slice(&self, n: &Node) -> Option<&'a str> { + let pos = n.pos.as_ref()?; + let lo = usize::try_from(pos.start_offset.checked_sub(self.start)?).ok()?; + let hi = usize::try_from(pos.end_offset.checked_sub(self.start)?).ok()?; + self.text.get(lo..hi).filter(|s| !s.is_empty()) } +} - /// Builds a plain container node with children. - #[must_use] - pub fn block(children: Vec<TestNode>) -> Self { - TestNode { - children, - ..Default::default() - } +/// Resolves the name an identifier node stands for: `props["name"]`, else +/// its token, else its source text under `anchor`. `None` when the node +/// carries no recoverable name. +#[must_use] +pub fn identifier_name(n: &Node, anchor: Option<Anchor<'_>>) -> Option<String> { + if let Some(name) = n.props.get(NAME_PROP) { + return Some(name.clone()); } + if !n.token.is_empty() { + return Some(n.token.clone()); + } + anchor.and_then(|a| a.slice(n)).map(str::to_owned) } -// === Real UAST node adapter === -// -// Implements the cohesion [`Node`] surface for the shared `cf_uast_node::Node`, -// so the static pipeline can drive the analyzer over parsed source. +/// The declared name a node carries in its props, if any. +#[must_use] +pub fn declared_name(n: &Node) -> Option<&str> { + n.props.get(NAME_PROP).map(String::as_str) +} -impl Node for cf_uast_node::Node { - fn children(&self) -> &[Self] { - &self.children - } +/// True for function nodes: type `Function`/`Method`, or the role pair +/// `{Function, Declaration}`. +#[must_use] +pub fn is_function(n: &Node) -> bool { + n.has_any_type(&[ty::FUNCTION, ty::METHOD]) + || n.has_all_roles(&[role::FUNCTION, role::DECLARATION]) +} - fn has_any_type(&self, types: &[&str]) -> bool { - cf_uast_node::Node::has_any_type(self, types) - } +/// True for `Method` nodes (functions that may carry a receiver). +#[must_use] +pub fn is_method(n: &Node) -> bool { + n.has_any_type(&[ty::METHOD]) +} - fn has_any_role(&self, roles: &[&str]) -> bool { - cf_uast_node::Node::has_any_role(self, roles) - } +/// True for `Identifier` nodes. +#[must_use] +pub fn is_identifier(n: &Node) -> bool { + n.has_any_type(&[ty::IDENTIFIER]) +} - fn has_all_roles(&self, roles: &[&str]) -> bool { - cf_uast_node::Node::has_all_roles(self, roles) - } +/// True for identifiers that name a type (role `Type`): `int`, `Config`, +/// `i32`, `Self`. +#[must_use] +pub fn is_type_identifier(n: &Node) -> bool { + is_identifier(n) && n.has_any_role(&[role::TYPE]) +} - fn entity_name(&self) -> String { - extract_entity_name(self).unwrap_or_default() - } +/// True for identifiers in a variable position: role `Variable`, `Reference` +/// or `Name`, excluding type identifiers. +#[must_use] +pub fn is_variable_identifier(n: &Node) -> bool { + is_identifier(n) + && !n.has_any_role(&[role::TYPE]) + && n.has_any_role(&[role::VARIABLE, role::REFERENCE, role::NAME]) +} + +/// True for `Parameter` nodes. +#[must_use] +pub fn is_parameter(n: &Node) -> bool { + n.has_any_type(&[ty::PARAMETER]) +} + +/// True for declaration nodes whose `props["name"]` names a variable: +/// `Variable`, `Parameter`, `Field`. +#[must_use] +pub fn is_named_declaration(n: &Node) -> bool { + n.has_any_type(&[ty::VARIABLE, ty::PARAMETER, ty::FIELD]) +} + +/// True for call nodes (type `Call` or role `Call`): the first child is the +/// callee expression. +#[must_use] +pub fn is_call(n: &Node) -> bool { + n.has_any_type(&[ty::CALL]) || n.has_any_role(&[role::CALL]) +} + +/// True for class-like scopes whose name qualifies the functions inside. +#[must_use] +pub fn is_scope(n: &Node) -> bool { + n.has_any_type(&[ty::CLASS, ty::STRUCT, ty::INTERFACE, ty::ENUM]) +} - fn count_lines(&self) -> i64 { - // (end_line - start_line + 1) for this node when a position is - // present, plus the recursive sum over children (report contract: - // nested spans are double-counted on purpose). - let mut total: i64 = 0; - if let Some(pos) = &self.pos { - total = pos.end_line as i64 - pos.start_line as i64 + 1; +/// The direct child carrying a node's own name: the first `Identifier` child +/// with role `Name` that is not a type identifier. +#[must_use] +pub fn name_child(n: &Node) -> Option<&Node> { + n.children + .iter() + .find(|c| is_identifier(c) && c.has_any_role(&[role::NAME]) && !is_type_identifier(c)) +} + +/// A node's own name: `props["name"]`, else its name child resolved under +/// `anchor`. +#[must_use] +pub fn own_name(n: &Node, anchor: Option<Anchor<'_>>) -> Option<String> { + declared_name(n) + .map(str::to_owned) + .or_else(|| name_child(n).and_then(|c| identifier_name(c, anchor))) +} + +/// The first descendant (preorder, `n` excluded) matching `pred`. +#[must_use] +pub fn find_descendant<'a>(n: &'a Node, pred: &dyn Fn(&Node) -> bool) -> Option<&'a Node> { + for child in &n.children { + if pred(child) { + return Some(child); } - for child in &self.children { - total += Node::count_lines(child); + if let Some(found) = find_descendant(child, pred) { + return Some(found); } - total } + None } -/// The shared entity-name chain: try the `name` prop, then a non-empty token, -/// then the first child's token / `name` prop. -fn extract_entity_name(n: &cf_uast_node::Node) -> Option<String> { - if let Some(name) = n.props.get("name") { - return Some(name.clone()); - } - if !n.token.is_empty() { - return Some(n.token.clone()); - } - let child = n.children.first()?; - if !child.token.is_empty() { - return Some(child.token.clone()); - } - child.props.get("name").cloned() +/// The 1-based `(start_line, end_line)` of a node; `(0, 0)` without position. +#[must_use] +pub fn line_span(n: &Node) -> (u64, u64) { + n.pos + .as_ref() + .map_or((0, 0), |p| (p.start_line, p.end_line)) } -impl Node for TestNode { - fn children(&self) -> &[Self] { - &self.children - } +#[cfg(test)] +mod tests { + use super::*; + use cf_uast_node::{Builder, Positions}; - fn has_any_type(&self, types: &[&str]) -> bool { - types.iter().any(|t| self.types.iter().any(|s| s == t)) + fn positioned(node_type: &str, token: &str, start: u64, end: u64) -> Node { + let mut n = Builder::new() + .with_type(node_type) + .with_token(token) + .with_roles(vec![role::NAME.to_string()]) + .build(); + n.pos = Some(Positions { + start_offset: start, + end_offset: end, + ..Positions::default() + }); + n } - fn has_any_role(&self, roles: &[&str]) -> bool { - roles.iter().any(|r| self.roles.iter().any(|s| s == r)) + #[test] + fn anchor_requires_token_equal_to_span() { + let exact = positioned("Field", "self.a", 10, 16); + assert!(Anchor::of(&exact).is_some()); + let shorter = positioned("Variable", "total", 10, 25); + assert!( + Anchor::of(&shorter).is_none(), + "token shorter than span must not anchor" + ); + let no_pos = Builder::new() + .with_type("Field") + .with_token("self.a") + .build(); + assert!(Anchor::of(&no_pos).is_none()); } - fn has_all_roles(&self, roles: &[&str]) -> bool { - roles.iter().all(|r| self.roles.iter().any(|s| s == r)) + #[test] + fn token_less_identifier_resolves_through_the_anchor() { + let field = positioned("Field", "self.a", 10, 16); + let anchor = Anchor::of(&field); + let ident = positioned(ty::IDENTIFIER, "", 15, 16); + assert_eq!(identifier_name(&ident, anchor), Some("a".to_string())); + let outside = positioned(ty::IDENTIFIER, "", 2, 4); + assert_eq!(identifier_name(&outside, anchor), None); } - fn entity_name(&self) -> String { - self.name.clone() + #[test] + fn props_name_wins_over_token_and_anchor() { + let mut ident = positioned(ty::IDENTIFIER, "tok", 0, 3); + ident.props.insert(NAME_PROP.into(), "declared".into()); + assert_eq!(identifier_name(&ident, None), Some("declared".to_string())); } - fn count_lines(&self) -> i64 { - self.lines + #[test] + fn type_role_identifiers_are_not_variables() { + let t = Builder::new() + .with_type(ty::IDENTIFIER) + .with_token("int") + .with_roles(vec![role::NAME.into(), role::TYPE.into()]) + .build(); + assert!(is_type_identifier(&t)); + assert!(!is_variable_identifier(&t)); + let v = Builder::new() + .with_type(ty::IDENTIFIER) + .with_token("x") + .with_roles(vec![role::REFERENCE.into()]) + .build(); + assert!(is_variable_identifier(&v)); } } diff --git a/crates/cf-cohesion/src/visitor.rs b/crates/cf-cohesion/src/visitor.rs index 8c794d07..b91fa2fe 100644 --- a/crates/cf-cohesion/src/visitor.rs +++ b/crates/cf-cohesion/src/visitor.rs @@ -1,113 +1,240 @@ -//! Cohesion visitor: the traversal the static pipeline actually runs. +//! Function discovery and variable attribution over a UAST. //! -//! The static pipeline drives the cohesion analyzer with a preorder DFS and a -//! context stack. This traversal is part of the report contract (pinned by the -//! differential gate). +//! One preorder walk with a context stack: every function node opens a +//! context, every variable use below it is attributed to the **innermost** +//! open function, and the function is emitted when its subtree is done. //! -//! Key differences from the -//! [`Analyzer::analyze`](crate::analyzer::Analyzer::analyze) path: +//! # What counts as a variable use //! -//! * Function detection is type Function/Method OR roles -//! {Function AND Declaration}, not role Function OR type Function/Method. -//! * Variables are attributed to the **innermost** enclosing function on the -//! context stack, via the variable-node processing on every visited node -//! while a context is active. A variable inside a nested function therefore -//! belongs only to that nested function, never to the outer one (the -//! `find_functions` path would attribute it to both). -//! * Functions are emitted in exit order (a function is appended when its -//! subtree walk completes). - -use crate::analyzer::{Analyzer, Function}; -use crate::uast::Node; - -/// One entry on the visitor's context stack: the function being built and the -/// variables collected so far for it. +//! A node contributes a name to the innermost function when it is +//! +//! 1. an `Identifier` with role `Variable`, `Reference` or `Name` (see +//! [`crate::uast::is_variable_identifier`]), or +//! 2. a `Variable` / `Parameter` / `Field` declaration carrying +//! `props["name"]` (the declaration's own token — `x := 1` — is never +//! used), +//! +//! and it is **not** +//! +//! * a type identifier (role `Type`: `int`, `Config`, `i32`, `Self`), +//! * a direct `Identifier` child of the function node (its own name and its +//! return-type annotation), +//! * a callee: the first child of a `Call` node when it is an identifier, or +//! the last identifier of a member chain in callee position +//! (`helper()`, `fmt.Println(x)` → only `fmt` and `x` count), +//! * the method receiver: the spellings in [`crate::uast::RECEIVER_KEYWORDS`] +//! (`self`, `this`) and a Go-style receiver parameter (`(s *S)`), +//! * a literal (literals are not identifiers). +//! +//! Names are resolved per [`crate::uast::identifier_name`]; identifiers with +//! no recoverable name are skipped. Nested function nodes open their own +//! context (their variables are theirs alone); lambdas do not, so a closure's +//! variables belong to the enclosing function. +//! +//! # Function names +//! +//! A function is named by `props["name"]` or its name child. Methods are +//! qualified `Type.name` when the UAST gives a receiver type (Go) or an +//! enclosing named `Class`/`Struct`/`Interface`/`Enum` (Python, Java, +//! TypeScript, …). A function whose name cannot be resolved is reported as +//! [`ANONYMOUS_FUNCTION_NAME`]. + +use std::collections::BTreeSet; + +use cf_uast_node::Node; + +use crate::analyzer::Function; +use crate::uast::{self, Anchor, RECEIVER_KEYWORDS}; + +/// Name reported for a function node without a resolvable name. +pub const ANONYMOUS_FUNCTION_NAME: &str = "<anonymous>"; + +/// Stack red zone for the growable-stack walk (64 KiB). +const STACK_RED_ZONE: usize = 64 * 1024; +/// Stack growth chunk for the growable-stack walk (4 MiB). +const STACK_CHUNK: usize = 4 * 1024 * 1024; + +/// Why a node, or one of its children, must not count as a variable. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Exclusion { + /// The node contributes normally. + None, + /// The node itself is never a variable (own name, callee, type). + Node, + /// The node is a member chain in callee position: its last direct + /// identifier child is the method name and never a variable. + LastIdentifier, +} + +/// One open function on the context stack. struct Ctx { function: Function, + /// Names that are never variables of this function (the receiver). + excluded: BTreeSet<String>, } -/// Collects functions from `root` with the context-stack preorder traversal. -/// -/// Cohesion of each returned [`Function`] is left at `0.0`; the caller fills it -/// via [`Analyzer::compute_per_function_cohesion`]. -pub(crate) fn collect_functions_via_visitor<N: Node>( - analyzer: &Analyzer, - root: &N, -) -> Vec<Function> { - let mut stack: Vec<Ctx> = Vec::new(); - let mut functions: Vec<Function> = Vec::new(); - walk(analyzer, root, &mut stack, &mut functions); - // Any unclosed contexts (cannot happen for a well-formed tree, since every - // entered function exits): flush them. - while let Some(ctx) = stack.pop() { - functions.push(ctx.function); - } - functions +struct Walker { + stack: Vec<Ctx>, + /// Names of the enclosing class-like scopes, innermost last. + scopes: Vec<String>, + out: Vec<Function>, + /// Function nodes deeper than this (root at depth 0) are not discovered. + max_depth: Option<usize>, } -fn walk<N: Node>(analyzer: &Analyzer, n: &N, stack: &mut Vec<Ctx>, functions: &mut Vec<Function>) { - // Depth-bounded only by the UAST's nesting, which generated sources push - // past 10k levels. The reference runtime grows its stacks automatically; - // `maybe_grow` gives this preorder walk the same growable-stack semantics - // without touching traversal order (64 KiB red zone, 4 MiB chunks). - stacker::maybe_grow(64 * 1024, 4 * 1024 * 1024, || { - walk_inner(analyzer, n, stack, functions); - }); +/// Collects every function below `root` (preorder, emitted on exit) with its +/// distinct variables. With `max_depth = Some(cap)` only function nodes at +/// depth `<= cap` open a context. +#[must_use] +pub fn collect_functions(root: &Node, max_depth: Option<usize>) -> Vec<Function> { + let mut walker = Walker { + stack: Vec::new(), + scopes: Vec::new(), + out: Vec::new(), + max_depth, + }; + walker.visit(root, 0, None, Exclusion::None); + walker.out } -fn walk_inner<N: Node>( - analyzer: &Analyzer, - n: &N, - stack: &mut Vec<Ctx>, - functions: &mut Vec<Function>, -) { - // On enter. - let is_fn = analyzer.is_visitor_function(n); - if is_fn { - // Push a context: capture name + line count at push time. - stack.push(Ctx { +impl Walker { + fn visit(&mut self, n: &Node, depth: usize, anchor: Option<Anchor<'_>>, excl: Exclusion) { + // Generated sources nest past 10k levels; grow the stack on demand + // instead of overflowing a fixed OS stack. + stacker::maybe_grow(STACK_RED_ZONE, STACK_CHUNK, || { + self.visit_inner(n, depth, anchor, excl); + }); + } + + fn visit_inner(&mut self, n: &Node, depth: usize, anchor: Option<Anchor<'_>>, excl: Exclusion) { + let anchor = Anchor::of(n).or(anchor); + let entered = self.enter_function(n, depth, anchor); + let scoped = self.enter_scope(n, anchor); + if excl != Exclusion::Node { + self.contribute(n, anchor); + } + + let last_identifier = (excl == Exclusion::LastIdentifier) + .then(|| n.children.iter().rposition(uast::is_identifier)) + .flatten(); + let is_call = uast::is_call(n); + for (i, child) in n.children.iter().enumerate() { + let child_excl = child_exclusion(entered, is_call, last_identifier, i, child); + self.visit(child, depth + 1, anchor, child_excl); + } + + if scoped { + self.scopes.pop(); + } + if entered { + if let Some(ctx) = self.stack.pop() { + self.out.push(ctx.function); + } + } + } + + /// Opens a context when `n` is a discoverable function; returns whether + /// it did. + fn enter_function(&mut self, n: &Node, depth: usize, anchor: Option<Anchor<'_>>) -> bool { + if !uast::is_function(n) || self.max_depth.is_some_and(|cap| depth > cap) { + return false; + } + let receiver = if uast::is_method(n) { + receiver_parameter(n) + } else { + None + }; + let receiver_name = receiver + .and_then(|r| uast::find_descendant(r, &uast::is_variable_identifier)) + .and_then(|id| uast::identifier_name(id, anchor)); + let receiver_type = receiver + .and_then(|r| uast::find_descendant(r, &uast::is_type_identifier)) + .and_then(|id| uast::identifier_name(id, anchor)); + + let base = uast::own_name(n, anchor).unwrap_or_else(|| ANONYMOUS_FUNCTION_NAME.to_string()); + let name = match receiver_type.or_else(|| self.scopes.last().cloned()) { + Some(qualifier) => format!("{qualifier}.{base}"), + None => base, + }; + let (start_line, end_line) = uast::line_span(n); + self.stack.push(Ctx { function: Function { - name: analyzer.extract_function_name(n), - line_count: n.count_lines(), - variables: Vec::new(), - cohesion: 0.0, + name, + variables: BTreeSet::new(), + start_line, + end_line, }, + excluded: receiver_name.into_iter().collect(), }); - } - // Process variables against the *innermost* (top) context, if any. The - // function node itself is processed under its own freshly-pushed context. - if let Some(ctx) = stack.last_mut() { - process_variable_node(analyzer, n, &mut ctx.function.variables); + true } - // Recurse (preorder DFS, source-order children). - for child in n.children() { - walk(analyzer, child, stack, functions); + /// Pushes the name of a class-like scope; returns whether it did. + fn enter_scope(&mut self, n: &Node, anchor: Option<Anchor<'_>>) -> bool { + if !uast::is_scope(n) { + return false; + } + match uast::own_name(n, anchor) { + Some(name) => { + self.scopes.push(name); + true + } + None => false, + } } - // On exit: pop and emit the function. - if is_fn { - if let Some(ctx) = stack.pop() { - functions.push(ctx.function); + /// Attributes `n`'s variable name, if it has one, to the innermost function. + fn contribute(&mut self, n: &Node, anchor: Option<Anchor<'_>>) { + let Some(ctx) = self.stack.last_mut() else { + return; + }; + let name = if uast::is_variable_identifier(n) { + uast::identifier_name(n, anchor) + } else if uast::is_named_declaration(n) { + uast::declared_name(n).map(str::to_owned) + } else { + None + }; + let Some(name) = name else { + return; + }; + if RECEIVER_KEYWORDS.contains(&name.as_str()) || ctx.excluded.contains(&name) { + return; } + ctx.function.variables.insert(name); } } -/// A node satisfying both the declaration and identifier predicates -/// contributes its name twice (two independent `if`s — report-contract quirk, -/// see [`Analyzer`] docs). -fn process_variable_node<N: Node>(analyzer: &Analyzer, n: &N, vars: &mut Vec<String>) { - if analyzer.is_variable_declaration_pub(n) { - add_variable_if_valid(n, vars); +/// The exclusion that applies to the `i`-th child of a node. +fn child_exclusion( + parent_is_function: bool, + parent_is_call: bool, + last_identifier: Option<usize>, + i: usize, + child: &Node, +) -> Exclusion { + if (parent_is_function && uast::is_identifier(child)) || last_identifier == Some(i) { + return Exclusion::Node; } - if analyzer.is_variable_identifier_pub(n) { - add_variable_if_valid(n, vars); + if parent_is_call && i == 0 { + return if uast::is_identifier(child) { + Exclusion::Node + } else { + Exclusion::LastIdentifier + }; } + Exclusion::None } -fn add_variable_if_valid<N: Node>(n: &N, vars: &mut Vec<String>) { - let name = n.entity_name(); - if !name.is_empty() { - vars.push(name); - } +/// A Go-style receiver: a `Parameter` child of a method that precedes the +/// method's own name child. +fn receiver_parameter(method: &Node) -> Option<&Node> { + let name_index = method + .children + .iter() + .position(|c| uast::name_child(method).is_some_and(|nc| std::ptr::eq(nc, c))) + .unwrap_or(method.children.len()); + method.children[..name_index] + .iter() + .find(|c| uast::is_parameter(c)) } diff --git a/crates/cf-cohesion/tests/real_trees.rs b/crates/cf-cohesion/tests/real_trees.rs new file mode 100644 index 00000000..26a142da --- /dev/null +++ b/crates/cf-cohesion/tests/real_trees.rs @@ -0,0 +1,379 @@ +//! The cohesion analyzer over REAL parsed trees (`cf_uast::Parser`), asserting +//! the hand-computed values of `site/explanation/cohesion.md` and the +//! variable-identification rules of `cf_cohesion::visitor`. + +use std::collections::BTreeSet; + +use cf_cohesion::{Analyzer, FileReport, ReportValue}; +use cf_uast_node::Node; + +const EPS: f64 = 1e-9; + +fn parse(name: &str, src: &str) -> Node { + cf_uast::Parser::new() + .parse(name, src.as_bytes()) + .unwrap_or_else(|e| panic!("parse {name}: {e}")) +} + +fn analyze(name: &str, src: &str) -> FileReport { + Analyzer::new().analyze_file(&parse(name, src)) +} + +fn cohesion_of(report: &FileReport, name: &str) -> f64 { + report + .functions + .iter() + .find(|f| f.name == name) + .unwrap_or_else(|| panic!("no function {name} in {report:?}")) + .cohesion +} + +fn variables_of(name: &str, src: &str, function: &str) -> BTreeSet<String> { + let root = parse(name, src); + cf_cohesion::visitor::collect_functions(&root, None) + .into_iter() + .find(|f| f.name == function) + .unwrap_or_else(|| panic!("no function {function}")) + .variables +} + +fn assert_close(got: f64, want: f64, what: &str) { + assert!((got - want).abs() < EPS, "{what}: got {got}, want {want}"); +} + +const GO_SHARE: &str = "package a + +func f() int { + x := 1 + y := 2 + return x + y +} + +func g() int { + x := 3 + return x +} +"; + +/// f uses {x, y}, g uses {x}: LCOM = 1 - 3/4 = 0.25, score 0.75; +/// f shares x only (0.5), g shares x (1.0); mean 0.75. +#[test] +fn go_share_matches_the_documented_formula() { + let report = analyze("a.go", GO_SHARE); + assert_eq!(report.total_functions, 2); + assert_close(report.lcom, 0.25, "lcom"); + assert_close(report.cohesion_score, 0.75, "cohesion_score"); + assert_close(cohesion_of(&report, "f"), 0.5, "f"); + assert_close(cohesion_of(&report, "g"), 1.0, "g"); + assert_close(report.function_cohesion, 0.75, "function_cohesion"); +} + +/// Variables are lexical identifier references only: no type names, no +/// function names, no whole declaration tokens. +#[test] +fn go_share_variables_are_identifiers_only() { + let f: Vec<String> = variables_of("a.go", GO_SHARE, "f").into_iter().collect(); + assert_eq!(f, ["x", "y"]); + let g: Vec<String> = variables_of("a.go", GO_SHARE, "g").into_iter().collect(); + assert_eq!(g, ["x"]); +} + +const GO_DISJOINT: &str = "package a + +func f() int { + x := 1 + x = x + 1 + return x +} + +func g() int { + y := 2 + return y +} +"; + +/// A variable repeated inside ONE function is not shared: both functions +/// score 0.0 and LCOM = 1 - 2/4 = 0.5. +#[test] +fn go_disjoint_scores_zero_sharing() { + let report = analyze("a.go", GO_DISJOINT); + assert_close(report.lcom, 0.5, "lcom"); + assert_close(cohesion_of(&report, "f"), 0.0, "f"); + assert_close(cohesion_of(&report, "g"), 0.0, "g"); + assert_eq!( + report + .functions + .iter() + .find(|f| f.name == "f") + .unwrap() + .variable_count, + 1, + "variable_count counts distinct variables" + ); +} + +/// Functions without variables score 1.0 and the file is perfectly cohesive. +#[test] +fn go_novars_scores_perfectly() { + let report = analyze("a.go", "package a\n\nfunc f() {\n}\n\nfunc g() {\n}\n"); + assert_eq!(report.total_functions, 2); + assert_close(report.lcom, 0.0, "lcom"); + assert_close(report.cohesion_score, 1.0, "cohesion_score"); + assert_close(cohesion_of(&report, "f"), 1.0, "f"); + assert_close(cohesion_of(&report, "g"), 1.0, "g"); + assert_close(report.function_cohesion, 1.0, "function_cohesion"); +} + +/// A single-function file is perfectly cohesive by definition. +#[test] +fn single_function_file_scores_perfectly() { + let report = analyze( + "a.go", + "package a\n\nfunc f() int {\n\tx := 1\n\treturn x\n}\n", + ); + assert_eq!(report.total_functions, 1); + assert_close(report.lcom, 0.0, "lcom"); + assert_close(report.cohesion_score, 1.0, "cohesion_score"); + assert_close(cohesion_of(&report, "f"), 1.0, "f"); +} + +/// Callees are not variables: `caller` calling `helper()` uses nothing. +#[test] +fn go_calls_exclude_callees() { + let src = "package a\n\nfunc helper() int {\n\treturn 1\n}\n\nfunc caller() int {\n\treturn helper()\n}\n"; + assert!(variables_of("a.go", src, "caller").is_empty()); + let report = analyze("a.go", src); + assert_close(report.lcom, 0.0, "lcom"); + assert_close(cohesion_of(&report, "caller"), 1.0, "caller"); +} + +/// Method calls through a member chain exclude only the method name: +/// `cfg.Validate()` uses `cfg`; `fmt.Println(n)` uses `fmt` and `n`. +#[test] +fn go_member_calls_keep_the_receiver_expression() { + let src = "package main + +import \"fmt\" + +func use(cfg *Config, items []string) error { + if cfg == nil { + return nil + } + cfg.Name = items[0] + fmt.Println(cfg) + return cfg.Validate() +} +"; + let vars: Vec<String> = variables_of("a.go", src, "use").into_iter().collect(); + assert_eq!(vars, ["Name", "cfg", "fmt", "items"]); +} + +/// Go receivers are the implicit `self`: excluded from the variables, and the +/// receiver type qualifies the method name. +#[test] +fn go_struct_receiver_is_excluded_and_qualifies_the_name() { + let src = "package a + +type S struct { + a int + b int +} + +func (s *S) M1() int { + return s.a +} + +func (s S) M2() int { + return s.a +} + +func (s *S) M3() int { + return s.b +} +"; + let m1: Vec<String> = variables_of("a.go", src, "S.M1").into_iter().collect(); + assert_eq!(m1, ["a"]); + let report = analyze("a.go", src); + // {a}, {a}, {b}: m = 3, a = 2, sum(mA) = 3 -> 1 - 3/6. + assert_close(report.lcom, 0.5, "lcom"); + assert_close(cohesion_of(&report, "S.M1"), 1.0, "S.M1"); + assert_close(cohesion_of(&report, "S.M3"), 0.0, "S.M3"); +} + +/// Same-named methods on different types are all kept, qualified by their +/// receiver type. +#[test] +fn go_dup_keeps_every_same_named_function() { + let src = "package a + +type A struct{ v int } +type B struct{ w int } + +func (a A) String() string { + v := a.v + return string(rune(v)) +} + +func (b B) String() string { + w := b.w + return string(rune(w)) +} + +func (a A) Len() int { + v := a.v + return v +} +"; + let report = analyze("a.go", src); + let names: Vec<&str> = report.functions.iter().map(|f| f.name.as_str()).collect(); + assert_eq!(names, ["A.String", "B.String", "A.Len"]); + assert_eq!(report.total_functions, 3); +} + +/// Rust: `m1` reads `self.a`, `m2` reads `self.b` — disjoint fields, `self` +/// excluded: LCOM = 0.5, both methods 0.0. +#[test] +fn rs_impl_resolves_token_less_identifiers() { + let src = "pub struct S { + a: i32, + b: i32, +} + +impl S { + pub fn m1(&self) -> i32 { + self.a + } + + pub fn m2(&self) -> i32 { + self.b + } +} +"; + let m1: Vec<String> = variables_of("a.rs", src, "m1").into_iter().collect(); + assert_eq!(m1, ["a"]); + let m2: Vec<String> = variables_of("a.rs", src, "m2").into_iter().collect(); + assert_eq!(m2, ["b"]); + let report = analyze("a.rs", src); + assert_eq!(report.total_functions, 2); + assert_close(report.lcom, 0.5, "lcom"); + assert_close(cohesion_of(&report, "m1"), 0.0, "m1"); + assert_close(cohesion_of(&report, "m2"), 0.0, "m2"); +} + +/// Python: `self` is excluded, attribute names count, methods are qualified +/// by their class. +#[test] +fn py_class_counts_attributes_not_self() { + let src = "class C: + def __init__(self): + self.x = 1 + self.y = 2 + + def m1(self): + return self.x + + def m2(self): + return self.y +"; + let init: Vec<String> = variables_of("a.py", src, "C.__init__") + .into_iter() + .collect(); + assert_eq!(init, ["x", "y"]); + let report = analyze("a.py", src); + // {x, y}, {x}, {y}: m = 3, a = 2, sum(mA) = 4 -> 1 - 4/6. + assert_close(report.lcom, 1.0 / 3.0, "lcom"); + assert_close(cohesion_of(&report, "C.__init__"), 1.0, "__init__"); +} + +/// Every supported language the docs name yields variables for a function +/// that uses some. +#[test] +fn variable_count_is_positive_across_languages() { + let cases: [(&str, &str, &str); 6] = [ + ("a.go", "package a\n\nfunc f(n int) int {\n\tm := n + 1\n\treturn m\n}\n", "f"), + ("a.py", "def f(n):\n m = n + 1\n return m\n", "f"), + ("a.js", "function f(n) {\n const m = n + 1;\n return m;\n}\n", "f"), + ("a.rs", "fn f(n: i32) -> i32 {\n let m = n + 1;\n m\n}\n", "f"), + ( + "A.java", + "public class A {\n public int f(int n) {\n int m = n + 1;\n return m;\n }\n}\n", + "A.f", + ), + ("a.ts", "function f(n: number): number {\n const m = n + 1;\n return m;\n}\n", "f"), + ]; + for (file, src, function) in cases { + let report = analyze(file, src); + let f = report + .functions + .iter() + .find(|f| f.name == function) + .unwrap_or_else(|| panic!("{file}: no function {function} in {report:?}")); + assert!(f.variable_count > 0, "{file}: {function} has no variables"); + let vars = variables_of(file, src, function); + assert!(vars.contains("n") && vars.contains("m"), "{file}: {vars:?}"); + assert!( + !vars.contains("int") && !vars.contains("i32") && !vars.contains("number"), + "{file}: type names leaked into {vars:?}" + ); + } +} + +/// `line_count` is the inclusive line span of the function node, never the +/// sum over its descendants. +#[test] +fn line_count_is_the_function_span() { + let report = analyze("a.go", GO_SHARE); + let f = report.functions.iter().find(|f| f.name == "f").unwrap(); + assert_eq!((f.start_line, f.line_count), (3, 5)); + let g = report.functions.iter().find(|f| f.name == "g").unwrap(); + assert_eq!((g.start_line, g.line_count), (9, 4)); +} + +/// A closure is not a function: its variables belong to the enclosing one. +#[test] +fn go_nested_closure_variables_belong_to_the_outer_function() { + let src = "package a + +func outer() int { + x := 1 + inner := func() int { + y := 2 + return y + } + return x + inner() +} +"; + let report = analyze("a.go", src); + assert_eq!(report.total_functions, 1); + let vars: Vec<String> = variables_of("a.go", src, "outer").into_iter().collect(); + assert_eq!(vars, ["inner", "x", "y"]); +} + +/// A file without functions is the empty report. +#[test] +fn struct_only_file_has_no_functions() { + let report = analyze( + "a.go", + "package a\n\ntype Config struct {\n\tName string\n\tPort int\n}\n", + ); + assert_eq!(report, FileReport::empty()); +} + +/// The discovery depth cap of the history `quality` consumer: functions below +/// the cap are not found. +#[test] +fn find_depth_caps_function_discovery() { + let root = parse("a.go", GO_SHARE); + let capped = Analyzer::new() + .analyze_file_capped(&root, Some(0)) + .to_report(); + assert_eq!(capped.get("total_functions"), Some(&ReportValue::Int(0))); + let found = Analyzer::new() + .analyze_file_capped(&root, Some(1)) + .to_report(); + assert_eq!(found.get("total_functions"), Some(&ReportValue::Int(2))); + assert_eq!( + found.get("cohesion_score").and_then(ReportValue::as_float), + Some(0.75) + ); +} diff --git a/crates/cf-commands/Cargo.toml b/crates/cf-commands/Cargo.toml index 0e94e31b..2de157ad 100644 --- a/crates/cf-commands/Cargo.toml +++ b/crates/cf-commands/Cargo.toml @@ -6,45 +6,18 @@ rust-version.workspace = true license.workspace = true repository.workspace = true publish.workspace = true -description = "Analyzer-registration + clap command construction for codefang: the run/render/version command tree and dynamic per-analyzer flags." +description = "Analyzer-registration + clap command construction for codefang: the run/version/completion command tree, the configuration-file layer, and dynamic per-analyzer flags." # cf-commands is the tier-8 analyzer-registration + clap command-construction -# aggregation point (DESIGN §1 tier 8, §4): it builds the run/render/version +# aggregation point (DESIGN §1 tier 8, §4): it builds the run/version/completion # clap commands, registers every analyzer and its dynamic per-analyzer flags, # and routes all report serialization through the shared report-format crates # (never raw serde). Output bytes are pinned against the reference binary by -# the differential gate in tests/compat. - -[features] -# Default builds only the parts that do NOT depend on unported sibling crates: -# the clap command tree, format validation, and the version command. -default = [] - -# `runtime` pulls in the remaining execution crates (framework runner, -# checkpoint/cache, observability, plot/store) for the not-yet-wired streaming -# pipeline paths. The core run pipeline + registry (this crate's `pipeline` and -# `handlers` modules) now depend on the analyzer + git crates unconditionally, -# so they are no longer behind this feature. -runtime = [ - "dep:cf-framework", - "dep:cf-config", - "dep:cf-checkpoint", - "dep:cf-budget", - "dep:cf-observability", - "dep:cf-storage", -] - -# The reference `mcp` command is not shipped by default (DESIGN §4.1). -# Mirror it as a non-default feature. -mcp = ["dep:cf-mcp"] +# the report e2e suites in bins/codefang/tests. [dependencies] clap = { workspace = true } thiserror = "1" -# On-demand stack growth for the recursive UAST deep-copy in the complexity -# handler — mirrors cf-uast's lowering walk, which needs a segmented stack on -# real >10k-deep sources. -stacker = "0.1.24" # SIGINT/SIGTERM → cooperative cancellation of the static folder walks. signal-hook = { workspace = true } # Generates the shell-completion scripts for the `completion` subcommand, the @@ -54,6 +27,8 @@ clap_complete = "4" # Real, compiling sibling crates depended on unconditionally. cf-version = { path = "../cf-version" } +# The `.codefang.yaml` / CODEFANG_* layer that defaults the run flags. +cf-config = { path = "../cf-config" } cf-pipeline = { path = "../cf-pipeline" } # --- Run-pipeline execution crates (the general run pipeline + registry) --- @@ -68,13 +43,30 @@ cf-uast-node = { path = "../cf-uast-node" } cf-pathpolicy = { path = "../cf-pathpolicy", features = ["default-deps"] } # libgit2-backed repository/commit access for the single history revwalk. cf-gitlib = { path = "../cf-gitlib" } +# Returns freed heap pages to the OS between history chunks. +cf-meminfo = { path = "../cf-meminfo" } +# The history memory budget: size parsing + RAM detection, chunk planning. +cf-framework = { path = "../cf-framework" } +cf-budget = { path = "../cf-budget" } +cf-streaming = { path = "../cf-streaming" } +cf-units = { path = "../cf-units" } +# Finished results spilled to disk while other walks run: the binary codec, +# serde for the records, and unlinked temporary files. +cf-persist = { path = "../cf-persist" } +# `codefang mcp`: the MCP server and its tool contracts. +cf-mcp = { path = "../cf-mcp", features = ["mcp"] } +# Reads report bytes back: the MCP tools hand the CLI's json reports to the +# client, and the cross-analyzer parity tests compare them. Report OUTPUT +# never routes through serde. +serde_json = { workspace = true } +serde = { workspace = true } +tempfile = { workspace = true } # Pinned RFC3339 formatting + metadata for history tick bounds / timeseries. cf-analyze = { path = "../cf-analyze" } cf-textutil = { path = "../cf-textutil" } cf-langpath = { path = "../cf-langpath" } # Line-mode diff (diffmatchpatch parity) for history per-file line stats. cf-godiff = { path = "../cf-godiff" } -cf-alg-bloom = { path = "../cf-alg-bloom" } cf-alg-levenshtein = { path = "../cf-alg-levenshtein" } cf-analyzers-plumbing = { path = "../cf-analyzers-plumbing" } cf-burndown-core = { path = "../cf-burndown-core" } @@ -101,18 +93,7 @@ cf-sentiment = { path = "../cf-sentiment" } cf-file-history = { path = "../cf-file-history" } cf-terminal = { path = "../cf-terminal" } -# Scaffold / not-yet-wired sibling crates -- optional, gated behind the -# `runtime` / `mcp` features (streaming framework runner, persistence, plot). -cf-framework = { path = "../cf-framework", optional = true } -cf-config = { path = "../cf-config", optional = true } -cf-checkpoint = { path = "../cf-checkpoint", optional = true } -cf-budget = { path = "../cf-budget", optional = true } -cf-observability = { path = "../cf-observability", optional = true } -cf-storage = { path = "../cf-storage", optional = true } -cf-mcp = { path = "../cf-mcp", optional = true } - [dev-dependencies] -tempfile = "3" # Test fixtures build tiny multi-language git repositories in memory-backed # tempdirs (history-filter oracle tests) — write access comes straight from # git2, the same libgit2 vendoring cf-gitlib is pinned to. diff --git a/crates/cf-commands/README.md b/crates/cf-commands/README.md index d1e90013..aafe8879 100644 --- a/crates/cf-commands/README.md +++ b/crates/cf-commands/README.md @@ -10,8 +10,8 @@ to the reference cobra surface), drives the analysis pipeline, and routes report serialization through the shared report-format crates (`cf-gojson` / `cf-goyaml`) — never raw `serde`. -Every machine-report surface this crate emits is pinned byte-for-byte against -the reference binary by the differential gate in `tests/compat`. +Every machine-report surface this crate emits is pinned byte-for-byte by the +CLI-level end-to-end tests under `bins/codefang/tests`. The major pieces: diff --git a/crates/cf-commands/src/flags.rs b/crates/cf-commands/src/flags.rs index 51172e8c..2fab4433 100644 --- a/crates/cf-commands/src/flags.rs +++ b/crates/cf-commands/src/flags.rs @@ -1,31 +1,28 @@ -//! The `run` and `render` clap command trees, reproducing the reference -//! binary's flag wiring. +//! The `run` and `completion` clap command trees. //! //! Built with clap's **builder API** (not derive) so every flag's long name, -//! short, default, and help string matches cobra byte-for-byte (DESIGN §4). +//! short, default, and help string is declared explicitly. The help text is a +//! contract: every flag it lists is read by the pipeline (an accepted-and- +//! ignored flag is a bug), and `site/guide/cli-reference.md` documents each +//! one. //! //! # `run` flags //! -//! [`build_run_command`] registers the reference `run` command's ~45 literal -//! flags (the main flag block + `registerPersistenceFlags` + `registerExclusionFlags`), -//! then the deprecated hidden flags ([`mark_deprecated_exclusion_flags`]), then -//! the dynamic per-analyzer flags ([`register_analyzer_flags`]). The help -//! strings are copied verbatim from the reference source. +//! [`build_run_command`] registers the `run` command's literal flags (the +//! selection / format / window / exclusion / configuration blocks), then the +//! dynamic per-analyzer flags ([`register_analyzer_flags`]). //! -//! # Tri-state `--checkpoint` / `--resume` +//! # Configuration defaults //! -//! Both default `true`, but the reference implementation reads them via `Flags().Changed(name)` so a -//! config-file default only applies when the CLI flag was *not* supplied -//! (`parseBoolFlag`). clap models this with [`clap::ArgAction::SetTrue`]/`SetFalse` -//! is not enough for tri-state; instead these two flags take an explicit -//! `bool` value (`--checkpoint=false`) and the caller checks -//! [`clap::ArgMatches::value_source`] for [`clap::parser::ValueSource::CommandLine`] -//! to decide whether the user set it — the Rust analogue of `Changed`. +//! [`CONFIG_KEY_FLAGS`] maps every `cf-config` key to the `run` flag it +//! defaults, and [`with_config_defaults`] installs the configured values as +//! those flags' defaults. clap then resolves the precedence itself: a value +//! given on the command line wins over the configured default, which wins +//! over the compiled one. //! //! # Dynamic per-analyzer flags //! -//! [`register_analyzer_flags`] mirrors the reference `registerAnalyzerFlags` / -//! `registerConfigFlag`: it walks the supplied +//! [`register_analyzer_flags`] walks the supplied //! [`crate::registry::ConfigOptionProvider`]s, dedupes by flag name, and adds one //! clap flag per [`cf_pipeline::ConfigurationOption`] keyed by its //! [`cf_pipeline::ConfigurationOptionType`]. @@ -33,26 +30,11 @@ use cf_pipeline::{ConfigurationOption, ConfigurationOptionType, DefaultValue}; use clap::{Arg, ArgAction, Command}; -use crate::formats::{FORMAT_JSON, INPUT_FORMAT_AUTO}; +use crate::formats::FORMAT_JSON; use crate::registry::ConfigOptionProvider; -/// Render-command constants, mirroring the reference implementation consts. -mod render_consts { - /// `render <store-dir>` — the cobra `Use` string. - pub const USE: &str = "render"; - /// Short help for the render command. - pub const SHORT: &str = "Render stored analysis results as multi-page HTML"; - /// `--output` long flag name. - pub const OUTPUT_FLAG: &str = "output"; - /// `-o` short flag. - pub const OUTPUT_SHORT: char = 'o'; - /// `--output` help text. - pub const OUTPUT_USAGE: &str = "output directory for HTML files"; -} - -/// Build the `run [path]` [`clap::Command`] with every literal flag, the -/// deprecated hidden flags, and the dynamic per-analyzer flags from -/// `providers`. Mirrors the reference `newRunCommandWithAllDeps`. +/// Build the `run [path]` [`clap::Command`] with every literal flag and the +/// dynamic per-analyzer flags from `providers`. /// /// `providers` supplies the analyzer configuration options (the reference implementation's /// `buildPipeline(nil)` Core + Leaves). In the default (non-`runtime`) build the @@ -73,9 +55,7 @@ pub fn build_run_command_with(providers: &[&dyn ConfigOptionProvider]) -> Comman ); cmd = add_literal_run_flags(cmd); - cmd = mark_deprecated_exclusion_flags(cmd); - cmd = register_analyzer_flags(cmd, providers); - cmd + register_analyzer_flags(cmd, providers) } /// The `run --help` trailer: the available analyzer ids (grouped by phase) plus @@ -123,9 +103,10 @@ pub fn build_run_command() -> Command { build_run_command_with(&[provider]) } -/// Adds the ~45 literal `run` flags in the reference implementation declaration order, with verbatim help -/// strings and defaults (the reference `run` flag block, plus `registerPersistenceFlags` -/// and `registerExclusionFlags`). +/// Adds the literal `run` flags in declaration order: analyzer selection and +/// output format, the commit window (`--limit`, `--first-parent`, `--head`, +/// `--since`), the exclusion flags, `--max-changes-per-commit`, `--chunk-size`, +/// `--config`, `--list-analyzers` and `--output`. #[allow(clippy::too_many_lines)] fn add_literal_run_flags(cmd: Command) -> Command { cmd.arg(str_slice_arg( @@ -143,52 +124,17 @@ fn add_literal_run_flags(cmd: Command) -> Command { "ndjson", "With --format timeseries: emit one JSON line per commit (NDJSON)", )) - .arg(str_arg( - "input", - None, - "", - "Input report path for cross-format conversion", - )) - .arg(str_arg( - "input-format", - None, - INPUT_FORMAT_AUTO, - "Input format: auto, json, bin", - )) - .arg(int_arg( - "gogc", - 0, - "GC percent for history pipeline (0 = auto, >0 = exact)", - )) - .arg(str_arg( - "ballast-size", - None, - "0", - "Optional GC ballast size for history pipeline (0 = disabled)", - )) - .arg(bool_arg("silent", "Disable progress output")) - .arg(bool_arg("no-color", "Disable colored static output")) + .arg(bool_arg("no-color", "Disable colored text/compact output")) .arg(str_arg( "path", Some('p'), ".", "Folder/repository path to analyze", )) - .arg(bool_arg( - "debug-trace", - "Enable 100% trace sampling for debugging", - )) - .arg(str_arg("cpuprofile", None, "", "Write CPU profile to file")) - .arg(str_arg( - "heapprofile", - None, - "", - "Write heap profile to file", - )) .arg(int_arg( "limit", 0, - "Limit number of commits to analyze (0 = no limit)", + "Analyze only the N newest commits (0 = no limit)", )) .arg(bool_arg( "first-parent", @@ -199,19 +145,9 @@ fn add_literal_run_flags(cmd: Command) -> Command { "since", None, "", - "Only analyze commits after this time (e.g., '24h', '2024-01-01', RFC3339)", - )) - .arg(int_arg( - "workers", - 0, - "Number of parallel workers (0 = use CPU count)", - )) - .arg(int_arg( - "static-workers", - 0, - "Number of parallel static analysis workers (0 = min(CPU count, 8))", + "Only analyze commits authored at/after this time or revision (e.g., '24h', '2024-01-01', RFC3339, 'v1.2.0')", )) - // registerExclusionFlags + // Exclusion flags .arg(bool_arg( "include-vendored", "Re-include vendored dependencies (detected by enry / Linguist) in analysis. \ @@ -236,153 +172,132 @@ first 512 bytes contain a generated-file marker (\"DO NOT EDIT\", \"Code generat ) .short('F'), ) - .arg(int_arg( - "buffer-size", - 0, - "Size of internal pipeline channels (0 = workers*2)", - )) - .arg(int_arg( - "commit-batch-size", - 0, - "Commits per processing batch (0 = default 100)", - )) - .arg(str_arg( - "blob-cache-size", - None, - "", - "Max blob cache size (e.g., '256MB', '1GB'; empty = default 1GB)", - )) - .arg(int_arg( - "diff-cache-size", - 0, - "Max diff cache entries (0 = default 10000)", - )) - .arg(str_arg( - "blob-arena-size", - None, - "", - "Memory arena size for blob loading (e.g., '4MB'; empty = default 4MB)", - )) - .arg(str_arg( - "memory-budget", - None, - "", - "Memory budget for auto-tuning (e.g., '512MB', '2GB')", - )) .arg(int_arg( "max-changes-per-commit", 0, "Skip commits whose tree diff exceeds this many changes (0 = default 10000). \ -Commits over the cap are silently dropped from history, which can desync \ -burndown's tracked state for affected files. Raise on monorepos with \ -legitimate large commits (Pods updates, generated code dumps).", - )) - // registerPersistenceFlags (tri-state checkpoint/resume default true) - .arg(tristate_bool_arg( - "checkpoint", - true, - "Enable checkpointing for crash recovery", - )) - .arg(str_arg( - "checkpoint-dir", - None, - "", - "Checkpoint directory (default: ~/.codefang/checkpoints)", - )) - .arg(tristate_bool_arg( - "resume", - true, - "Resume from checkpoint if available", - )) - .arg(bool_arg( - "clear-checkpoint", - "Clear existing checkpoint before run", - )) - .arg(str_arg( - "cache-dir", - None, - "", - "Incremental analysis cache directory (skip already-processed commits)", - )) - .arg(bool_arg( - "no-cache", - "Force full re-analysis, overwriting any existing cache", +Dropped commits are invisible to every analyzer; burndown re-inserts an affected \ +file whole the next time it is touched. Raise on monorepos with legitimate large \ +commits (Pods updates, generated code dumps).", )) + .arg( + Arg::new("chunk-size") + .long("chunk-size") + .help( + "Commits per history chunk; per-commit work for at most this many commits is \ +held in memory at once. Overrides the chunk size --memory-budget plans", + ) + .action(ArgAction::Set) + .value_parser(clap::value_parser!(i64)) + // cobra pflag last-wins on repeated flags. + .overrides_with("chunk-size"), + ) + .arg( + Arg::new("memory-budget") + .long("memory-budget") + .value_name("SIZE") + .help( + "Memory a history run plans its chunk sizes and worker counts against, e.g. \ +512MB, 2GB, 1.5GiB (default: half the physical RAM, at most 2 GiB; minimum 256 MiB)", + ) + .action(ArgAction::Set) + .overrides_with("memory-budget"), + ) .arg(str_arg( "config", None, "", - "Configuration file path (default: .codefang.yaml in CWD or $HOME)", + "Configuration file (default: ./.codefang.yaml, else $HOME/.codefang.yaml); \ +its keys default the matching flags", )) .arg(bool_arg( "list-analyzers", "List all available analyzer IDs and exit", )) - .arg(str_arg( - "diagnostics-addr", - None, - "", - "Start diagnostics HTTP server (health/metrics) at this address (e.g., :6060)", - )) .arg(str_arg( "output", Some('o'), "", - "Output directory for plot HTML files (required with --format plot)", - )) - .arg(bool_arg( - "keep-store", - "Keep temp ReportStore directory after rendering (with --format plot)", - )) - .arg(str_arg( - "tmp-dir", - None, - "", - "Directory for temporary spill files (default: system temp)", + "Write the report to this file instead of stdout; with --format plot, the \ +output directory for the HTML pages (required)", )) } -/// Marks the two legacy exclusion flags as deprecated and hidden, with the exact -/// reference deprecation messages. clap has no -/// first-class "deprecated" attribute, so we register them hidden and surface -/// the message through [`deprecated_flag_message`] for the caller to emit when -/// the flag is used (mirroring cobra's deprecation warning behavior). -#[must_use] -pub fn mark_deprecated_exclusion_flags(cmd: Command) -> Command { - cmd.arg(bool_arg("skip-blacklist", "DEPRECATED").hide(true)) - .arg(str_slice_arg("blacklisted-prefixes", None, "DEPRECATED").hide(true)) -} - -/// Returns the exact cobra deprecation message for a deprecated flag, or `None` -/// if the flag is not deprecated. Messages copied verbatim from -/// `markDeprecatedExclusionFlags`. +/// The `run` flag each `cf-config` key defaults, in [`cf_config::KEYS`] +/// order. Every key has exactly one flag (pinned by a test). +pub const CONFIG_KEY_FLAGS: &[(&str, &str)] = &[ + (cf_config::keys::ANALYZERS, "analyzers"), + ( + cf_config::keys::MAX_CHANGES_PER_COMMIT, + "max-changes-per-commit", + ), + (cf_config::keys::MEMORY_BUDGET, "memory-budget"), + (cf_config::keys::CHUNK_SIZE, "chunk-size"), + (cf_config::keys::TICK_SIZE, "tick-size"), + (cf_config::keys::BURNDOWN_GRANULARITY, "granularity"), + (cf_config::keys::BURNDOWN_SAMPLING, "sampling"), + (cf_config::keys::BURNDOWN_TRACK_FILES, "burndown-files"), + (cf_config::keys::BURNDOWN_TRACK_PEOPLE, "burndown-people"), + ( + cf_config::keys::DEVS_CONSIDER_EMPTY_COMMITS, + "empty-commits", + ), + (cf_config::keys::DEVS_ANONYMIZE, "anonymize"), + ( + cf_config::keys::SENTIMENT_MIN_COMMENT_LENGTH, + "min-comment-len", + ), + (cf_config::keys::SHOTNESS_DSL_STRUCT, "shotness-dsl-struct"), + (cf_config::keys::SHOTNESS_DSL_NAME, "shotness-dsl-name"), + (cf_config::keys::SHOTNESS_KEEP_ENDED, "shotness-keep-ended"), + (cf_config::keys::TYPOS_MAX_DISTANCE, "typos-max-distance"), + (cf_config::keys::ANOMALY_THRESHOLD, "anomaly-threshold"), + (cf_config::keys::ANOMALY_WINDOW_SIZE, "anomaly-window"), +]; + +/// The `run` flag the configuration key `key` defaults. /// /// ``` -/// use cf_commands::deprecated_flag_message; +/// use cf_commands::flags::flag_for_config_key; /// -/// // The two legacy exclusion flags carry migration guidance. -/// assert!(deprecated_flag_message("skip-blacklist") -/// .unwrap() -/// .contains("--include-vendored=false")); -/// assert!(deprecated_flag_message("blacklisted-prefixes") -/// .unwrap() -/// .contains("--extra-excluded-prefixes")); -/// // Any other flag is not deprecated. -/// assert_eq!(deprecated_flag_message("workers"), None); +/// assert_eq!(flag_for_config_key("history.anomaly.window_size"), Some("anomaly-window")); +/// assert_eq!(flag_for_config_key("pipeline.workers"), None); /// ``` #[must_use] -pub fn deprecated_flag_message(flag: &str) -> Option<&'static str> { - match flag { - "skip-blacklist" => Some( - "use --include-vendored=false and --include-generated=false \ -(the new defaults). See CHANGELOG for migration.", - ), - "blacklisted-prefixes" => Some( - "use --extra-excluded-prefixes; the old flag name is preserved \ -for back-compat but will be removed in the next minor release.", - ), - _ => None, +pub fn flag_for_config_key(key: &str) -> Option<&'static str> { + CONFIG_KEY_FLAGS + .iter() + .find(|(k, _)| *k == key) + .map(|(_, flag)| *flag) +} + +/// Installs every configured `setting` as the default of its `run` flag in +/// the root `codefang` command `cmd`. A value given on the command line still +/// wins: clap only falls back to a default when the flag is absent. +#[must_use] +pub fn with_config_defaults(cmd: Command, settings: &[cf_config::Setting]) -> Command { + let defaults: Vec<(&'static str, Vec<&'static str>)> = settings + .iter() + .filter_map(|s| { + let flag = flag_for_config_key(s.key)?; + let values = s + .value + .to_args() + .iter() + .map(|v| string_to_static_leak(v)) + .collect(); + Some((flag, values)) + }) + .collect(); + if defaults.is_empty() { + return cmd; } + cmd.mut_subcommand("run", |mut run| { + for (flag, values) in defaults { + run = run.mut_arg(flag, |a| a.default_values(values)); + } + run + }) } /// Registers one clap flag per configuration option exposed by `providers`, @@ -484,41 +399,15 @@ fn format_go_float_default(v: f64) -> String { } } -/// `runtime.NumCPU()` analogue — the per-machine goroutine-count default the reference implementation -/// analyzers derive their `*-goroutines` defaults from (`runtime.NumCPU()` and -/// `max(NumCPU()/4, 1)`). The live reference binary bakes the host CPU count into its -/// `--help` defaults, so the Rust surface must compute the same number for the -/// cli-surface comparison (and for runtime parity) to match. -fn num_cpu() -> i64 { - std::thread::available_parallelism() - .map(std::num::NonZeroUsize::get) - .unwrap_or(1) as i64 -} - -/// The default analyzer configuration options: the FULL reference analyzer option set -/// produced by `registerAnalyzerFlags` walking `buildPipeline(nil)` Core + -/// Leaves (every analyzer's `ListConfigurationOptions`). Each entry mirrors the -/// the reference `ConfigurationOption` (Name / Flag / Type / Default / Description) so the -/// dynamic clap registration in [`register_analyzer_flags`] reproduces cobra's -/// flag surface byte-for-byte, and every value lands in the parsed matches that -/// `cf_commands::run` threads into the handlers via [`crate::pipeline::RunContext`]. -/// -/// The `*-goroutines` defaults are derived from [`num_cpu`] exactly as the reference implementation derives -/// them from `runtime.NumCPU()`. +/// The default analyzer configuration options: every per-analyzer option +/// registered as a `run` flag (Name / Flag / Type / Default / Description). +/// Every value lands in the parsed matches that `cf_commands::run` threads +/// into the handlers via [`crate::pipeline::RunContext`]. #[must_use] #[allow(clippy::too_many_lines)] pub fn default_analyzer_options() -> Vec<ConfigurationOption> { - let ncpu = num_cpu(); - let uast_goroutines = (ncpu / 4).max(1); vec![ - // --- plumbing/tree_diff --- - ConfigurationOption { - name: "TreeDiff.FilteredRegexes".into(), - flag: "whitelist".into(), - description: "Whitelist regexp to determine which files to analyze.".into(), - option_type: ConfigurationOptionType::String, - default: DefaultValue::String(String::new()), - }, + // --- shared change filter --- ConfigurationOption { name: "TreeDiff.Languages".into(), flag: "languages".into(), @@ -526,7 +415,7 @@ pub fn default_analyzer_options() -> Vec<ConfigurationOption> { option_type: ConfigurationOptionType::Strings, default: DefaultValue::Strings(Vec::new()), }, - // --- plumbing/ticks --- + // --- shared ticks --- ConfigurationOption { name: "TicksSinceStart.TickSize".into(), flag: "tick-size".into(), @@ -534,7 +423,7 @@ pub fn default_analyzer_options() -> Vec<ConfigurationOption> { option_type: ConfigurationOptionType::Int, default: DefaultValue::Int(24), }, - // --- plumbing/identity --- + // --- shared identity --- ConfigurationOption { name: "IdentityDetector.PeopleDictPath".into(), flag: "people-dict".into(), @@ -549,58 +438,6 @@ pub fn default_analyzer_options() -> Vec<ConfigurationOption> { option_type: ConfigurationOptionType::Bool, default: DefaultValue::Bool(false), }, - // --- plumbing/blob_cache --- - ConfigurationOption { - name: "BlobCache.FailOnMissingSubmodules".into(), - flag: "fail-on-missing-submodules".into(), - description: "Specifies whether to panic if any referenced submodule does not exist in .gitmodules and thus the corresponding Git object cannot be loaded. Override this if you want to ensure that your repository is integral.".into(), - option_type: ConfigurationOptionType::Bool, - default: DefaultValue::Bool(false), - }, - ConfigurationOption { - name: "BlobCache.Goroutines".into(), - flag: "blob-cache-goroutines".into(), - description: "Number of goroutines to use for parallel blob loading.".into(), - option_type: ConfigurationOptionType::Int, - default: DefaultValue::Int(ncpu), - }, - // --- plumbing/file_diff --- - ConfigurationOption { - name: "FileDiff.NoCleanup".into(), - flag: "no-diff-cleanup".into(), - description: "Do not apply additional heuristics to improve diffs.".into(), - option_type: ConfigurationOptionType::Bool, - default: DefaultValue::Bool(false), - }, - ConfigurationOption { - name: "FileDiff.WhitespaceIgnore".into(), - flag: "no-diff-whitespace".into(), - description: "Ignore whitespace when computing diffs.".into(), - option_type: ConfigurationOptionType::Bool, - default: DefaultValue::Bool(false), - }, - ConfigurationOption { - name: "FileDiff.Timeout".into(), - flag: "diff-timeout".into(), - description: "Maximum time in milliseconds a single diff calculation may elapse.".into(), - option_type: ConfigurationOptionType::Int, - default: DefaultValue::Int(1000), - }, - ConfigurationOption { - name: "FileDiff.Goroutines".into(), - flag: "diff-goroutines".into(), - description: "Number of goroutines to use for diff calculation.".into(), - option_type: ConfigurationOptionType::Int, - default: DefaultValue::Int(ncpu), - }, - // --- plumbing/uast --- - ConfigurationOption { - name: "UASTChanges.Goroutines".into(), - flag: "uast-changes-goroutines".into(), - description: "Number of goroutines to use for parallel UAST parsing (fallback when pipeline is not available).".into(), - option_type: ConfigurationOptionType::Int, - default: DefaultValue::Int(uast_goroutines), - }, // --- burndown --- ConfigurationOption { name: "Burndown.Granularity".into(), @@ -630,49 +467,21 @@ pub fn default_analyzer_options() -> Vec<ConfigurationOption> { option_type: ConfigurationOptionType::Bool, default: DefaultValue::Bool(false), }, - ConfigurationOption { - name: "Burndown.HibernationThreshold".into(), - flag: "burndown-hibernation-threshold".into(), - description: "The minimum size for the allocated memory in each branch to be compressed.".into(), - option_type: ConfigurationOptionType::Int, - default: DefaultValue::Int(1000), - }, - ConfigurationOption { - name: "Burndown.HibernationOnDisk".into(), - flag: "burndown-hibernation-disk".into(), - description: "If true, save hibernated state to disk (no-op with default treap timeline).".into(), - option_type: ConfigurationOptionType::Bool, - default: DefaultValue::Bool(true), - }, - ConfigurationOption { - name: "Burndown.HibernationDirectory".into(), - flag: "burndown-hibernation-dir".into(), - description: "Temporary directory for hibernated state (no-op with default treap timeline).".into(), - option_type: ConfigurationOptionType::Path, - default: DefaultValue::Path(String::new()), - }, - ConfigurationOption { - name: "Burndown.Debug".into(), - flag: "burndown-debug".into(), - description: "Validate the trees at each step.".into(), - option_type: ConfigurationOptionType::Bool, - default: DefaultValue::Bool(false), - }, - ConfigurationOption { - name: "Burndown.Goroutines".into(), - flag: "burndown-goroutines".into(), - description: "Number of goroutines to use for parallel processing.".into(), - option_type: ConfigurationOptionType::Int, - default: DefaultValue::Int(ncpu), - }, // --- anomaly --- ConfigurationOption { name: "TemporalAnomaly.WindowSize".into(), flag: "anomaly-window".into(), - description: "Sliding window size in ticks for computing rolling statistics.".into(), + description: "Trailing window size in ticks for the rolling mean and standard deviation (>= 2).".into(), option_type: ConfigurationOptionType::Int, default: DefaultValue::Int(20), }, + ConfigurationOption { + name: "TemporalAnomaly.Threshold".into(), + flag: "anomaly-threshold".into(), + description: "Z-score threshold, in standard deviations, above which a tick is flagged (> 0).".into(), + option_type: ConfigurationOptionType::Float, + default: DefaultValue::Float(2.0), + }, // --- devs --- ConfigurationOption { name: "Devs.ConsiderEmptyCommits".into(), @@ -694,7 +503,7 @@ pub fn default_analyzer_options() -> Vec<ConfigurationOption> { flag: "shotness-dsl-struct".into(), description: "UAST DSL query to use for filtering the nodes.".into(), option_type: ConfigurationOptionType::String, - default: DefaultValue::String("filter(.roles has \"Function\")".into()), + default: DefaultValue::String("rfilter(.roles has \"Function\")".into()), }, ConfigurationOption { name: "Shotness.DSLName".into(), @@ -703,6 +512,15 @@ pub fn default_analyzer_options() -> Vec<ConfigurationOption> { option_type: ConfigurationOptionType::String, default: DefaultValue::String(".props.name".into()), }, + ConfigurationOption { + name: "Shotness.KeepEnded".into(), + flag: "shotness-keep-ended".into(), + description: "Keep entities whose declaration or file was removed in the report, \ +marked ended, with their counts and couplings (default: drop them)." + .into(), + option_type: ConfigurationOptionType::Bool, + default: DefaultValue::Bool(false), + }, // --- typos --- ConfigurationOption { name: "TyposDatasetBuilder.MaximumAllowedDistance".into(), @@ -722,29 +540,6 @@ pub fn default_analyzer_options() -> Vec<ConfigurationOption> { ] } -/// Build the `render <store-dir>` [`clap::Command`]. mirrors the reference implementation -/// `buildRenderCommand`: `Args = ExactArgs(1)` and the `--output`/`-o` flag. -#[must_use] -pub fn build_render_command() -> Command { - Command::new(render_consts::USE) - .about(render_consts::SHORT) - .arg( - Arg::new("store-dir") - .value_name("store-dir") - .required(true) - .num_args(1) - .help("Directory containing stored analysis results"), - ) - .arg( - Arg::new(render_consts::OUTPUT_FLAG) - .long(render_consts::OUTPUT_FLAG) - .short(render_consts::OUTPUT_SHORT) - .action(ArgAction::Set) - .default_value("") - .help(render_consts::OUTPUT_USAGE), - ) -} - /// Build the `completion` [`clap::Command`] with the four shell subcommands /// (`bash`, `fish`, `powershell`, `zsh`), mirroring the command cobra /// auto-registers (`rootCmd.AddCommand`'s implicit completion command). The help @@ -806,33 +601,6 @@ fn bool_arg(name: &'static str, help: &'static str) -> Arg { .overrides_with(name) } -/// A tri-state boolean flag, the Rust analogue of cobra's `Flags().Bool(..)` -/// read through `Changed` (`parseBoolFlag`): default `default`, the bare -/// `--flag` form sets `true`, `--flag=false` negates, and the caller -/// distinguishes "set by user" from "default" via -/// [`clap::ArgMatches::value_source`] (cobra `Changed`). -/// -/// `require_equals(true)` is load-bearing for surface parity: a cobra `Bool` -/// flag advertises NO value placeholder in `--help` (it is `takes_value=false` -/// in the cli-surface comparison), and `require_equals` makes clap render -/// `--flag[=<flag>]` — the `[=<` form the surface extractor reads as -/// value-less — instead of the `[<flag>]` (value-taking) form a plain -/// `num_args(0..=1)` would print. The explicit `--flag=false` value still -/// parses, matching cobra (the analyzer matrix passes `--checkpoint=false`). -fn tristate_bool_arg(name: &'static str, default: bool, help: &'static str) -> Arg { - Arg::new(name) - .long(name) - .help(help) - .action(ArgAction::Set) - .num_args(0..=1) - .require_equals(true) - .default_value(if default { "true" } else { "false" }) - .default_missing_value("true") - .value_parser(clap::value_parser!(bool)) - // cobra pflag last-wins on repeated flags. - .overrides_with(name) -} - /// A string `--flag` (optionally with a short), with the given default. Mirrors /// cobra `Flags().StringVar`/`StringVarP`. fn str_arg( @@ -901,37 +669,86 @@ mod tests { build_run_command() } - // --- TestRunCommandConfig_Defaults --- #[test] - fn run_defaults_format_gogc_ballast() { + fn run_defaults_format_json() { let m = run().try_get_matches_from(["run"]).expect("defaults parse"); assert_eq!(m.get_one::<String>("format").unwrap(), "json"); - assert_eq!(*m.get_one::<i64>("gogc").unwrap(), 0); - assert_eq!(m.get_one::<String>("ballast-size").unwrap(), "0"); } - // --- TestRunCommandConfig_PathFlag --- + /// Flags with no effect on any report are not accepted: an accepted-and- + /// ignored flag is a bug. + #[test] + fn inert_flags_are_rejected() { + for flag in [ + "--workers=1", + "--static-workers=1", + "--input=x", + "--input-format=json", + "--gogc=1", + "--ballast-size=1", + "--cpuprofile=x", + "--heapprofile=x", + "--buffer-size=1", + "--commit-batch-size=1", + "--blob-cache-size=1", + "--diff-cache-size=1", + "--blob-arena-size=1", + "--diagnostics-addr=:1", + "--keep-store", + "--tmp-dir=x", + "--whitelist=x", + "--fail-on-missing-submodules", + "--blob-cache-goroutines=1", + "--no-diff-cleanup", + "--no-diff-whitespace", + "--diff-timeout=1", + "--diff-goroutines=1", + "--uast-changes-goroutines=1", + "--burndown-hibernation-threshold=1", + "--burndown-hibernation-disk", + "--burndown-hibernation-dir=x", + "--burndown-debug", + "--burndown-goroutines=1", + // No checkpoint store, incremental cache or trace exporter is + // linked into the binary. + "--checkpoint", + "--checkpoint=false", + "--checkpoint-dir=x", + "--resume", + "--resume=false", + "--clear-checkpoint", + "--cache-dir=x", + "--no-cache", + "--debug-trace", + // The hidden deprecated spellings were accepted and ignored. + "--skip-blacklist", + "--blacklisted-prefixes=x", + ] { + assert!( + run().try_get_matches_from(["run", flag]).is_err(), + "{flag} must not be accepted" + ); + } + } + #[test] fn run_default_path_is_dot() { let m = run().try_get_matches_from(["run"]).unwrap(); assert_eq!(m.get_one::<String>("path").unwrap(), "."); } - // --- TestRunCommandConfig_OutputFlag --- #[test] fn run_default_output_is_empty() { let m = run().try_get_matches_from(["run"]).unwrap(); assert_eq!(m.get_one::<String>("output").unwrap(), ""); } - // --- TestRunCommandConfig_NDJSONFlag --- #[test] fn run_default_ndjson_false() { let m = run().try_get_matches_from(["run"]).unwrap(); assert!(!m.get_flag("ndjson")); } - // --- TestRunCommandConfig_PerFileFlag --- #[test] fn run_default_per_file_false_with_short_flag() { let m = run().try_get_matches_from(["run", "-F"]).unwrap(); @@ -940,7 +757,6 @@ mod tests { assert!(!m2.get_flag("per-file")); } - // --- TestRunCommandConfig_ExclusionFlags --- #[test] fn run_default_exclusion_flags_false() { let m = run().try_get_matches_from(["run"]).unwrap(); @@ -948,44 +764,14 @@ mod tests { assert!(!m.get_flag("include-generated")); } - // --- TestRunCommandConfig_PersistenceFlags (defaults true) --- - #[test] - fn run_persistence_defaults_true() { - let m = run().try_get_matches_from(["run"]).unwrap(); - assert!(*m.get_one::<bool>("checkpoint").unwrap()); - assert!(*m.get_one::<bool>("resume").unwrap()); - } - - // --- Tri-state: --checkpoint=false sets value AND marks as user-supplied --- - #[test] - fn checkpoint_tristate_distinguishes_user_set() { - let m = run() - .try_get_matches_from(["run", "--checkpoint=false"]) - .unwrap(); - assert!(!*m.get_one::<bool>("checkpoint").unwrap()); - assert_eq!( - m.value_source("checkpoint"), - Some(clap::parser::ValueSource::CommandLine), - "explicit --checkpoint should report CommandLine (reference: Changed==true)" - ); - // Not supplied -> DefaultValue (reference: Changed==false -> parseBoolFlag nil). - let m2 = run().try_get_matches_from(["run"]).unwrap(); - assert_eq!( - m2.value_source("checkpoint"), - Some(clap::parser::ValueSource::DefaultValue) - ); - } - - // --- TestRunCommandConfig_AnalyzerFlags (burndown --granularity registered) --- - // the reference implementation renamed --burndown-granularity to --granularity (the cobra flag is - // "granularity"); the dynamic registration must expose that name. + /// `--granularity` is the flag name (the Go-era `--burndown-granularity` + /// spelling is gone). #[test] fn run_registers_dynamic_burndown_flag() { let m = run() .try_get_matches_from(["run", "--granularity", "60"]) .unwrap(); assert_eq!(*m.get_one::<i64>("granularity").unwrap(), 60); - // The old name must be gone (reference: dropped it). assert!( run() .try_get_matches_from(["run", "--burndown-granularity", "60"]) @@ -994,52 +780,34 @@ mod tests { ); } - // The new analyzer flags are registered with reference-exact names/defaults. #[test] - fn run_registers_new_analyzer_flags() { + fn run_registers_analyzer_flags() { let m = run().try_get_matches_from(["run"]).unwrap(); assert_eq!(*m.get_one::<i64>("anomaly-window").unwrap(), 20); + assert!((*m.get_one::<f64>("anomaly-threshold").unwrap() - 2.0).abs() < f64::EPSILON); assert_eq!(*m.get_one::<i64>("sampling").unwrap(), 30); assert_eq!(*m.get_one::<i64>("tick-size").unwrap(), 24); assert_eq!(*m.get_one::<i64>("typos-max-distance").unwrap(), 4); assert_eq!(*m.get_one::<i64>("min-comment-len").unwrap(), 20); - assert_eq!(*m.get_one::<i64>("diff-timeout").unwrap(), 1000); + assert!(!m.get_flag("anonymize")); + assert!(!*m.get_one::<bool>("exact-signatures").unwrap()); assert_eq!( - *m.get_one::<i64>("burndown-hibernation-threshold").unwrap(), - 1000 + m.get_one::<String>("shotness-dsl-struct").unwrap(), + "rfilter(.roles has \"Function\")" ); - assert!(*m.get_one::<bool>("burndown-hibernation-disk").unwrap()); - assert!(!m.get_flag("anonymize")); assert_eq!( m.get_one::<String>("shotness-dsl-name").unwrap(), ".props.name" ); assert_eq!(m.get_one::<String>("people-dict").unwrap(), ""); - assert_eq!(m.get_one::<String>("whitelist").unwrap(), ""); } - // --checkpoint=false still parses (the analyzer matrix passes it) and reports - // a CommandLine source (reference: Changed==true) — surface is value-less. - #[test] - fn checkpoint_value_form_still_parses() { - let m = run() - .try_get_matches_from(["run", "--checkpoint=false"]) - .unwrap(); - assert!(!*m.get_one::<bool>("checkpoint").unwrap()); - assert_eq!( - m.value_source("checkpoint"), - Some(clap::parser::ValueSource::CommandLine) - ); - } - - // --- TestRunCommandConfig_DryRunOmitted --- #[test] fn run_has_no_dry_run_flag() { let parsed = run().try_get_matches_from(["run", "--dry-run"]); assert!(parsed.is_err(), "--dry-run must not exist"); } - // --- analyzers short -a, comma-split --- #[test] fn analyzers_short_and_comma_split() { let m = run() @@ -1049,7 +817,6 @@ mod tests { assert_eq!(vals, vec!["history/anomaly", "history/devs"]); } - // --- positional [path] (MaximumNArgs(1)) --- #[test] fn run_accepts_one_positional_path() { let m = run().try_get_matches_from(["run", "/repo"]).unwrap(); @@ -1062,38 +829,9 @@ mod tests { #[test] fn run_rejects_two_positionals() { let parsed = run().try_get_matches_from(["run", "/a", "/b"]); - assert!(parsed.is_err(), "MaximumNArgs(1) must reject 2 positionals"); + assert!(parsed.is_err(), "at most one positional path"); } - // --- deprecated flags exist (hidden) with exact messages --- - #[test] - fn deprecated_flags_present_and_hidden() { - let m = run() - .try_get_matches_from(["run", "--skip-blacklist"]) - .unwrap(); - assert!(m.get_flag("skip-blacklist")); - } - - #[test] - fn deprecated_messages_are_verbatim() { - assert_eq!( - deprecated_flag_message("skip-blacklist"), - Some( - "use --include-vendored=false and --include-generated=false \ -(the new defaults). See CHANGELOG for migration." - ) - ); - assert_eq!( - deprecated_flag_message("blacklisted-prefixes"), - Some( - "use --extra-excluded-prefixes; the old flag name is preserved \ -for back-compat but will be removed in the next minor release." - ) - ); - assert_eq!(deprecated_flag_message("nope"), None); - } - - // --- register_analyzer_flags dedupes by flag name --- #[test] fn dynamic_flags_dedupe_by_flag_name() { let a = vec![ConfigurationOption { @@ -1105,7 +843,7 @@ for back-compat but will be removed in the next minor release." }]; let b = vec![ConfigurationOption { name: "Other".into(), - flag: "g".into(), // duplicate flag -> ignored (reference: registeredFlags set) + flag: "g".into(), description: "second".into(), option_type: ConfigurationOptionType::Int, default: DefaultValue::Int(2), @@ -1118,7 +856,6 @@ for back-compat but will be removed in the next minor release." assert_eq!(*m.get_one::<i64>("g").unwrap(), 1); } - // --- kind/default mismatch is skipped --- #[test] fn dynamic_flag_kind_mismatch_skipped() { let bad = vec![ConfigurationOption { @@ -1130,38 +867,100 @@ for back-compat but will be removed in the next minor release." }]; let p: &dyn ConfigOptionProvider = &bad; let cmd = register_analyzer_flags(Command::new("t"), &[p]); - // The flag must not have been added. let parsed = cmd.try_get_matches_from(["t", "--x", "1"]); assert!(parsed.is_err()); } - // --- render command: ExactArgs(1) + --output/-o --- + /// Every supported configuration key defaults exactly one existing `run` + /// flag, in key order. #[test] - fn render_requires_store_dir_and_has_output() { - let m = build_render_command() - .try_get_matches_from(["render", "/store", "-o", "/out"]) - .unwrap(); - assert_eq!(m.get_one::<String>("store-dir").unwrap(), "/store"); - assert_eq!(m.get_one::<String>("output").unwrap(), "/out"); + fn every_config_key_maps_to_a_run_flag() { + let keys: Vec<&str> = CONFIG_KEY_FLAGS.iter().map(|(k, _)| *k).collect(); + assert_eq!(keys, cf_config::KEYS); + let cmd = run(); + for (key, flag) in CONFIG_KEY_FLAGS { + assert!( + cmd.get_arguments().any(|a| a.get_id() == *flag), + "{key} maps to the missing flag --{flag}" + ); + } } - #[test] - fn render_rejects_missing_store_dir() { - let parsed = build_render_command().try_get_matches_from(["render"]); - assert!(parsed.is_err(), "render requires exactly one store-dir arg"); + /// The root command with `settings` installed, parsed from `args`. + fn root_with(settings: &[cf_config::Setting], args: &[&str]) -> clap::ArgMatches { + let root = Command::new("codefang").subcommand(run()); + let m = with_config_defaults(root, settings) + .try_get_matches_from(args) + .unwrap(); + m.subcommand_matches("run").unwrap().clone() } #[test] - fn render_rejects_two_args() { - let parsed = build_render_command().try_get_matches_from(["render", "/a", "/b"]); - assert!(parsed.is_err(), "render ExactArgs(1)"); + fn configured_values_become_flag_defaults_and_explicit_flags_win() { + use cf_config::{keys, Setting, Value}; + let settings = [ + Setting { + key: keys::ANALYZERS, + value: Value::List(vec!["history/devs".into(), "history/anomaly".into()]), + }, + Setting { + key: keys::ANOMALY_WINDOW_SIZE, + value: Value::Int(3), + }, + Setting { + key: keys::ANOMALY_THRESHOLD, + value: Value::Float(1.5), + }, + Setting { + key: keys::DEVS_ANONYMIZE, + value: Value::Bool(true), + }, + Setting { + key: keys::SHOTNESS_DSL_NAME, + value: Value::Text(".props.id".into()), + }, + ]; + let m = root_with(&settings, &["codefang", "run"]); + let ids: Vec<&String> = m.get_many::<String>("analyzers").unwrap().collect(); + assert_eq!(ids, vec!["history/devs", "history/anomaly"]); + assert_eq!(*m.get_one::<i64>("anomaly-window").unwrap(), 3); + assert!((*m.get_one::<f64>("anomaly-threshold").unwrap() - 1.5).abs() < f64::EPSILON); + assert!(*m.get_one::<bool>("anonymize").unwrap()); + assert_eq!( + m.get_one::<String>("shotness-dsl-name").unwrap(), + ".props.id" + ); + assert_eq!( + m.value_source("anomaly-window"), + Some(clap::parser::ValueSource::DefaultValue) + ); + + let m = root_with( + &settings, + &[ + "codefang", + "run", + "-a", + "static/complexity", + "--anomaly-window", + "9", + "--anonymize=false", + ], + ); + let ids: Vec<&String> = m.get_many::<String>("analyzers").unwrap().collect(); + assert_eq!(ids, vec!["static/complexity"]); + assert_eq!(*m.get_one::<i64>("anomaly-window").unwrap(), 9); + assert!(!*m.get_one::<bool>("anonymize").unwrap()); + assert_eq!( + m.value_source("anomaly-window"), + Some(clap::parser::ValueSource::CommandLine) + ); } #[test] - fn render_default_output_empty() { - let m = build_render_command() - .try_get_matches_from(["render", "/store"]) - .unwrap(); - assert_eq!(m.get_one::<String>("output").unwrap(), ""); + fn no_settings_leave_the_command_unchanged() { + let m = root_with(&[], &["codefang", "run"]); + assert_eq!(*m.get_one::<i64>("anomaly-window").unwrap(), 20); + assert!(m.get_many::<String>("analyzers").is_none()); } } diff --git a/crates/cf-commands/src/formats.rs b/crates/cf-commands/src/formats.rs index f2840f4e..9f679548 100644 --- a/crates/cf-commands/src/formats.rs +++ b/crates/cf-commands/src/formats.rs @@ -47,14 +47,6 @@ pub const FORMAT_TIMESERIES_NDJSON: &str = "timeseries+ndjson"; /// `text` — human-readable table / plain output. pub const FORMAT_TEXT: &str = "text"; -/// `auto` — the sentinel that asks [`resolve_input_format`] to infer the input -/// format from the file extension. -pub const INPUT_FORMAT_AUTO: &str = "auto"; -/// `json` input format. -pub const INPUT_FORMAT_JSON: &str = "json"; -/// `binary` input format. -pub const INPUT_FORMAT_BINARY: &str = "binary"; - /// Error returned when a format string is not recognized. /// /// `Display` formats as the exact reference string `unsupported format: <fmt>`, where @@ -176,7 +168,7 @@ pub fn validate_universal_format(format: &str) -> Result<String, FormatError> { /// // Static-only accepts `compact` (a static output format)... /// assert_eq!(resolve_formats("compact", true, false).unwrap(), ("compact".to_string(), String::new())); /// // ...but a mixed run validates against the universal set, which excludes it. -/// assert!(resolve_formats("compact", true, true).is_err()); +/// assert!(resolve_formats("bogus", true, true).is_err()); /// ``` pub fn resolve_formats( format: &str, @@ -200,14 +192,16 @@ pub fn resolve_formats( return Ok((static_fmt, history_fmt)); } - // Mirror the reference `ResolveFormats` branching exactly: - // * mixed (static && history) -> ValidateUniversalFormat (text yes, compact no) - // * static-only -> ValidateFormat(staticOutputFormats) (text & compact yes) - // * history-only -> ValidateUniversalFormat (text yes, compact no) - // The earlier port validated EVERY non-plot path against the universal set, - // which wrongly rejected static-only `text` (reference: accepts it) and wrongly - // accepted history/mixed `compact` (reference: rejects it). + // Per-phase branching: + // * mixed (static && history) -> the universal set plus `compact`: text + // and compact render each phase in turn (`handlers::render_mixed`) + // * static-only -> the per-analyzer set (text & compact yes) + // * history-only -> the universal set plus `compact` (one + // compact section per leaf, see `history_formats::history_compact`) if has_static && has_history { + if normalized == FORMAT_COMPACT { + return Ok((normalized.clone(), normalized)); + } let validated = validate_universal_format(format)?; return Ok((validated.clone(), validated)); } @@ -216,6 +210,9 @@ pub fn resolve_formats( return Ok((validated, String::new())); } if has_history { + if normalized == FORMAT_COMPACT { + return Ok((String::new(), normalized)); + } let validated = validate_universal_format(format)?; return Ok((String::new(), validated)); } @@ -223,46 +220,6 @@ pub fn resolve_formats( Ok((String::new(), String::new())) } -/// Determines the input format from the path and explicit flag, mirror of the reference implementation -/// `ResolveInputFormat`. -/// -/// When `input_format` is not [`INPUT_FORMAT_AUTO`] it is normalized and -/// returned. Otherwise the file extension drives the choice: `.json` → `json`, -/// `.bin`/`.binary` → `binary`, anything else → `json` (the reference implementation's `default`). -/// -/// This never errors (the reference signature returns an error, but the body has no -/// failure path); the `Result` is kept for signature parity with the reference caller. -/// -/// # Errors -/// -/// Never returns `Err`; the `Result` mirrors the reference signature. -#[allow(clippy::missing_panics_doc)] -pub fn resolve_input_format(input_path: &str, input_format: &str) -> Result<String, FormatError> { - if input_format != INPUT_FORMAT_AUTO { - return Ok(normalize_format(input_format)); - } - - let ext = extension_lower(input_path); - match ext.as_str() { - ".json" => Ok(INPUT_FORMAT_JSON.to_string()), - ".bin" | ".binary" => Ok(INPUT_FORMAT_BINARY.to_string()), - _ => Ok(INPUT_FORMAT_JSON.to_string()), - } -} - -/// Returns the lower-cased file extension (including the leading dot), mirroring -/// the reference `strings.ToLower(filepath.Ext(path))`. the reference implementation's `filepath.Ext` returns the -/// suffix from the final dot of the final path element, or "" if none. -fn extension_lower(path: &str) -> String { - // Restrict to the final path element so dots in directory names are ignored, - // matching the reference implementation's filepath.Ext. - let base = path.rsplit(['/', '\\']).next().unwrap_or(path); - match base.rfind('.') { - Some(idx) => base[idx..].to_lowercase(), - None => String::new(), - } -} - /// Applies the `--ndjson` modifier to a resolved format: when set and the format /// is `timeseries`, composes it into `timeseries+ndjson`; otherwise returns the /// format unchanged. Mirrors the reference composition in @@ -378,10 +335,24 @@ mod tests { #[test] fn resolve_formats_propagates_unsupported_error() { - // `compact` is rejected on the MIXED path (reference `ResolveFormats` -> - // ValidateUniversalFormat, which excludes compact). - let err = resolve_formats("compact", true, true).unwrap_err(); - assert_eq!(err.to_string(), "unsupported format: compact"); + let err = resolve_formats("bogus", true, true).unwrap_err(); + assert_eq!(err.to_string(), "unsupported format: bogus"); + } + + #[test] + fn resolve_formats_mixed_accepts_text_and_compact() { + // A mixed selection renders text and compact phase by phase. + let (s, h) = resolve_formats("text", true, true).unwrap(); + assert_eq!((s.as_str(), h.as_str()), ("text", "text")); + let (s, h) = resolve_formats("compact", true, true).unwrap(); + assert_eq!((s.as_str(), h.as_str()), ("compact", "compact")); + assert!(resolve_formats("ndjson", true, true).is_ok()); + } + + #[test] + fn resolve_formats_history_only_accepts_compact() { + let (s, h) = resolve_formats("compact", false, true).unwrap(); + assert_eq!((s.as_str(), h.as_str()), ("", "compact")); } #[test] @@ -394,30 +365,6 @@ mod tests { assert_eq!((s.as_str(), h.as_str()), ("compact", "")); } - #[test] - fn resolve_input_format_explicit_is_normalized() { - assert_eq!(resolve_input_format("x.dat", "bin").unwrap(), "binary"); - assert_eq!(resolve_input_format("x.dat", "JSON").unwrap(), "json"); - } - - #[test] - fn resolve_input_format_auto_uses_extension() { - assert_eq!(resolve_input_format("report.json", "auto").unwrap(), "json"); - assert_eq!( - resolve_input_format("report.BIN", "auto").unwrap(), - "binary" - ); - assert_eq!( - resolve_input_format("report.binary", "auto").unwrap(), - "binary" - ); - // Unknown / no extension -> json. - assert_eq!(resolve_input_format("report.txt", "auto").unwrap(), "json"); - assert_eq!(resolve_input_format("report", "auto").unwrap(), "json"); - // Dot in directory, none in basename -> no extension -> json. - assert_eq!(resolve_input_format("a.b/report", "auto").unwrap(), "json"); - } - #[test] fn ndjson_modifier_composes_only_timeseries() { assert_eq!( diff --git a/crates/cf-commands/src/handlers.rs b/crates/cf-commands/src/handlers.rs index e670361a..412c9267 100644 --- a/crates/cf-commands/src/handlers.rs +++ b/crates/cf-commands/src/handlers.rs @@ -2,23 +2,24 @@ //! //! Each submodule is the crate-owned orchestration for one analyzer family //! (static folder walk + per-file UAST analysis, or one git revwalk + per-commit -//! analysis), moved verbatim out of the `codefang` binary's `main.rs` where it -//! had previously been reached through a 31-arm per-`(analyzer, format)` `if` -//! ladder. The analyzer MATH stays in the cf-* analyzer crates these call +//! analysis). The analyzer MATH stays in the cf-* analyzer crates these call //! (cf-complexity, cf-halstead, cf-comments, cf-imports, cf-couples, //! cf-shotness, cf-analyzer-burndown, …); this module owns only the //! pipeline-tier orchestration + serialization (cf-gojson / cf-goyaml / -//! cf-reportutil), exactly as the reference framework + run command do. +//! cf-reportutil). //! //! [`default_registry`] builds the single [`crate::pipeline::Registry`] mapping //! every analyzer id to ONE [`crate::pipeline::RunHandler`]. Each handler owns -//! its own `match format` (mirroring an analyzer's `FormatReport*` family in -//! the reference implementation); dispatch in [`crate::pipeline::run_pipeline`] is a keyed lookup by id, -//! NOT a per-format branch ladder. +//! its own `match format`; dispatch in [`crate::pipeline::run_pipeline`] is a +//! keyed lookup by id, NOT a per-format branch ladder. +//! +//! The shared run validation ([`preflight`]), the ONE static file walk +//! ([`walk_source_files`] / [`walk_all_files`]) and the two walk filters +//! ([`StaticFilter`], [`HistoryFilter`]) live here because every handler of +//! both phases reads them. pub mod burndown_ndjson; pub mod couples_run; -pub mod go_sort; pub mod history; pub mod history_formats; pub mod history_plot; @@ -27,6 +28,7 @@ pub mod plot; pub mod plot_sections; pub mod section_render; pub mod shotness_run; +pub(crate) mod spill; pub mod static_clones; pub mod static_cohesion; pub mod static_comments; @@ -38,7 +40,11 @@ pub mod static_imports; pub mod static_json; pub(crate) mod uast_walk; -use crate::pipeline::{AnalyzerEntry, Mode, Registry, RunContext}; +use std::path::{Path, PathBuf}; + +use cf_uast::Parser; + +use crate::pipeline::{AnalyzerEntry, HandlerError, Mode, Registry, RunContext}; /// Concurrency cap for independent per-analyzer tasks in the multi-analyzer /// dispatch paths (combined render, plot orchestrators, the per-id pipeline). @@ -104,57 +110,391 @@ pub(crate) fn run_cancelled() -> bool { cancel_flag().load(std::sync::atomic::Ordering::Relaxed) } +// --------------------------------------------------------------------------- +// The ONE static file walk. Every `static/*` handler consumes it, so the +// skip set, the read cap, the `--languages` globs, the vendor / generated +// path policy and the generated-content marker are applied in exactly one +// place; a handler owns only what it does with the bytes. +// --------------------------------------------------------------------------- + /// Upper bound for a single source file on the static path (64 MiB). No /// legitimate source file approaches this; without a bound, a pathological /// file is fully read, tree-sitter-parsed (tree several times the source /// size), lowered to a UAST, and — on the complexity path — deep-copied /// again, so peak RSS is a large multiple of the largest file encountered. /// Oversized files are skipped exactly like unreadable ones. -pub(crate) const MAX_STATIC_FILE_SIZE: u64 = 64 * 1024 * 1024; +pub const MAX_STATIC_FILE_SIZE: u64 = 64 * 1024 * 1024; + +/// Bytes read per file by [`walk_all_files`]: the classification header +/// (binary sniffing, generated markers, shebangs and modelines all live +/// there), so classifying a tree never reads whole binaries. +pub const ALL_FILES_HEADER_BYTES: usize = 8192; + +/// Why the static walk (or a handler's parse step) left a file out. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SkipReason { + /// The file (or its metadata) could not be read. + Unreadable, + /// The file is larger than [`MAX_STATIC_FILE_SIZE`]. + Oversized, + /// The parser has no usable grammar for the file. + Unparseable, +} + +impl SkipReason { + /// The reason as the stderr warning prints it. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Unreadable => "unreadable", + Self::Oversized => "larger than 64 MiB", + Self::Unparseable => "unparseable", + } + } +} -/// Count of files the static walks skipped (unreadable, oversized, or -/// unparseable). Skips used to be completely silent, so a permissions -/// failure across half a tree produced a plausible-looking but wrong report -/// with no signal; the run summary now surfaces the count on stderr. -static SKIPPED_FILES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); +/// Every path skipped in this run. Keyed by path so a file is counted ONCE +/// however many analyzers walk it (a `-a 'static/*'` run walks the tree once +/// per analyzer) and the unreadable / oversized warning is printed once. +/// Skips used to be completely silent, so a permissions failure across half +/// a tree produced a plausible-looking but wrong report with no signal. +static SKIPPED_FILES: std::sync::Mutex<std::collections::BTreeSet<PathBuf>> = + std::sync::Mutex::new(std::collections::BTreeSet::new()); -/// Records one skipped file for the end-of-run summary. -pub(crate) fn note_skipped_file() { - SKIPPED_FILES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); +/// Records a skipped file for the end-of-run summary. The first skip of a +/// path for an I/O reason (unreadable, oversized) prints +/// `warning: skipped <path>: <reason>` on stderr; a parse failure is only +/// counted, because the handlers that hit it either list the file as empty +/// or drop it by design. Returns `true` when this call recorded the path +/// (and so printed its warning), `false` for a repeat. +pub(crate) fn note_skipped(path: &Path, reason: SkipReason) -> bool { + let newly_recorded = SKIPPED_FILES + .lock() + .expect("skipped-file registry poisoned") + .insert(path.to_path_buf()); + if newly_recorded && reason != SkipReason::Unparseable { + eprintln!("warning: skipped {}: {}", path.display(), reason.as_str()); + } + newly_recorded } -/// Number of files skipped so far in this run. +/// Number of distinct files skipped so far in this run. pub(crate) fn skipped_file_count() -> u64 { - SKIPPED_FILES.load(std::sync::atomic::Ordering::Relaxed) + SKIPPED_FILES + .lock() + .expect("skipped-file registry poisoned") + .len() as u64 +} + +/// True once `path` has been recorded as skipped in this process. +#[cfg(test)] +fn was_skipped(path: &Path) -> bool { + SKIPPED_FILES + .lock() + .expect("skipped-file registry poisoned") + .contains(path) +} + +/// Reads a source file whole, refusing files over [`MAX_STATIC_FILE_SIZE`] +/// before any byte is read. +fn read_source_capped(path: &Path) -> Result<Vec<u8>, SkipReason> { + let meta = std::fs::metadata(path).map_err(|_| SkipReason::Unreadable)?; + if meta.len() > MAX_STATIC_FILE_SIZE { + return Err(SkipReason::Oversized); + } + std::fs::read(path).map_err(|_| SkipReason::Unreadable) +} + +/// Reads at most `limit` leading bytes of `path`; empty when the file cannot +/// be read (the all-files walk then classifies on the path alone). +fn read_header(path: &Path, limit: usize) -> Vec<u8> { + use std::io::Read as _; + let Ok(file) = std::fs::File::open(path) else { + return Vec::new(); + }; + let mut buf = Vec::new(); + if file.take(limit as u64).read_to_end(&mut buf).is_err() { + return Vec::new(); + } + buf +} + +/// One file admitted by the static walk. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceFile { + /// The file's path as the walk reached it (the root joined with the + /// relative components; the root itself for a FILE root). + pub path: PathBuf, + /// Slash-joined path relative to the walk root. A FILE root stamps the + /// file's name, so `codefang run -a static/comments src/a.go` reports + /// `a.go`, never `.`. + pub rel_path: String, + /// The file's bytes: the whole file under [`walk_source_files`], the + /// first [`ALL_FILES_HEADER_BYTES`] under [`walk_all_files`]. + pub bytes: Vec<u8>, + /// The parser's language name for the file (empty under + /// [`walk_all_files`], whose consumer classifies the language itself). + pub language: String, +} + +impl SourceFile { + /// Directory portion of [`Self::rel_path`] (`.` for a top-level file). + #[must_use] + pub fn directory(&self) -> String { + parent_dir(&self.rel_path) + } +} + +/// Which files a [`StaticWalk`] admits. +enum WalkMode<'a> { + /// Files the parser supports that pass the whole run filter — the + /// `--languages` globs, the vendor / generated path policy and the + /// generated-content marker — read whole. + Source(&'a Parser), + /// Every regular file that passes the `--languages` globs and + /// `--extra-excluded-prefixes`; vendored, generated and binary files are + /// admitted (composition exists to report them) and only the header is + /// read. + AllFiles, +} + +/// The lazy static file walk. The candidate paths are gathered up front +/// (lexical, depth-first); each [`Iterator::next`] reads, filters and stamps +/// one file, so one file's bytes are live at a time however large the tree. +pub struct StaticWalk<'a> { + root: PathBuf, + mode: WalkMode<'a>, + filter: StaticFilter, + pending: std::vec::IntoIter<PathBuf>, +} + +/// The source walk every UAST-backed static analyzer runs on. `root` is a +/// directory or a single file. The walk: +/// +/// 1. visits directories in lexical order, depth-first, skipping `.git`, +/// cache-tagged directories ([`should_skip_walk_dir`]) and symlinked +/// directories (never followed, so a symlink loop cannot recurse and the +/// walk cannot escape the root); symlinked regular files are read; +/// 2. admits only files the parser supports and the `--languages` basename +/// globs keep; +/// 3. applies the vendor / generated / `--extra-excluded-prefixes` policy to +/// the ROOT-RELATIVE path, so `--extra-excluded-prefixes gen/` works +/// whatever the root spelling; +/// 4. reads each file once, refusing files over [`MAX_STATIC_FILE_SIZE`]; +/// an unreadable or oversized file is skipped and reported once on +/// stderr for the whole run ([`note_skipped`]); +/// 5. applies the generated-content marker over the first 512 bytes +/// ([`StaticFilter::skips_with_header`]) unless `--include-generated`. +/// +/// # Errors +/// +/// [`HandlerError::PathNotFound`] when `root` does not exist. +pub fn walk_source_files<'a>( + root: &str, + parser: &'a Parser, + filter: &StaticFilter, +) -> Result<StaticWalk<'a>, HandlerError> { + StaticWalk::new(root, WalkMode::Source(parser), filter.clone()) +} + +/// The classification walk `static/composition` runs on: the same traversal +/// as [`walk_source_files`] but EVERY regular file is admitted — vendored, +/// generated, binary, unsupported — with only `--languages` and +/// `--extra-excluded-prefixes` narrowing it. Each file carries its first +/// [`ALL_FILES_HEADER_BYTES`]; an unreadable file carries no bytes and is +/// still admitted (it classifies on its path). +/// +/// # Errors +/// +/// [`HandlerError::PathNotFound`] when `root` does not exist. +pub fn walk_all_files( + root: &str, + filter: &StaticFilter, +) -> Result<StaticWalk<'static>, HandlerError> { + let relaxed = StaticFilter { + policy: cf_pathpolicy::Options { + include_vendored: true, + include_generated: true, + extra_excluded_prefixes: filter.policy.extra_excluded_prefixes.clone(), + }, + language_globs: filter.language_globs.clone(), + }; + StaticWalk::new(root, WalkMode::AllFiles, relaxed) } -/// Reads a source file for static analysis, refusing oversized files -/// (see [`MAX_STATIC_FILE_SIZE`]). `None` mirrors the walkers' existing -/// skip-on-unreadable behavior. -pub(crate) fn read_source_capped(path: &std::path::Path) -> Option<Vec<u8>> { - let read = || -> Option<Vec<u8>> { - let meta = std::fs::metadata(path).ok()?; - if meta.len() > MAX_STATIC_FILE_SIZE { +impl<'a> StaticWalk<'a> { + fn new(root: &str, mode: WalkMode<'a>, filter: StaticFilter) -> Result<Self, HandlerError> { + let root_path = PathBuf::from(root); + if !root_path.exists() { + return Err(HandlerError::PathNotFound(root.to_string())); + } + let mut pending = Vec::new(); + collect_candidates(&root_path, &mut pending); + Ok(StaticWalk { + root: root_path, + mode, + filter, + pending: pending.into_iter(), + }) + } + + /// Filters, reads and stamps one candidate; `None` when the run filter + /// drops it or it cannot be read. + fn admit(&self, path: &Path) -> Option<SourceFile> { + let rel_path = make_relative_path(path, &self.root); + match self.mode { + WalkMode::Source(parser) => self.admit_source(parser, path, rel_path), + WalkMode::AllFiles => self.admit_any(path, rel_path), + } + } + + fn admit_source(&self, parser: &Parser, path: &Path, rel_path: String) -> Option<SourceFile> { + if !parser.is_supported(&rel_path) || self.filter.skips(&rel_path) { + return None; + } + let bytes = match read_source_capped(path) { + Ok(bytes) => bytes, + Err(reason) => { + note_skipped(path, reason); + return None; + } + }; + if self.filter.skips_with_header(&rel_path, &bytes) { + return None; + } + Some(SourceFile { + path: path.to_path_buf(), + language: parser.get_language(&rel_path), + rel_path, + bytes, + }) + } + + fn admit_any(&self, path: &Path, rel_path: String) -> Option<SourceFile> { + if self.filter.skips(&rel_path) { return None; } - std::fs::read(path).ok() + Some(SourceFile { + path: path.to_path_buf(), + rel_path, + bytes: read_header(path, ALL_FILES_HEADER_BYTES), + language: String::new(), + }) + } +} + +impl Iterator for StaticWalk<'_> { + type Item = SourceFile; + + /// The next admitted file; the walk ends early (and stays ended) once + /// SIGINT/SIGTERM was received. + fn next(&mut self) -> Option<SourceFile> { + loop { + if run_cancelled() { + return None; + } + let path = self.pending.next()?; + if let Some(file) = self.admit(&path) { + return Some(file); + } + } + } +} + +/// What a directory entry contributes to the walk. +enum CandidateKind { + /// A directory to recurse into (unless skipped). + Dir, + /// A regular file (or a symlink resolving to one) to analyze. + File, + /// Anything else: a symlinked or dangling directory, a socket, a FIFO… + Other, +} + +/// Classifies a directory entry WITHOUT following directory symlinks: a +/// symlink to a regular file is a file; a symlink to a directory — or a +/// dangling one — contributes nothing. +fn candidate_kind(entry: &std::fs::DirEntry) -> CandidateKind { + let Ok(file_type) = entry.file_type() else { + return CandidateKind::Other; + }; + if file_type.is_dir() { + return CandidateKind::Dir; + } + if file_type.is_file() { + return CandidateKind::File; + } + if file_type.is_symlink() && std::fs::metadata(entry.path()).is_ok_and(|m| m.is_file()) { + return CandidateKind::File; + } + CandidateKind::Other +} + +/// Gathers the candidate files under `root` in lexical depth-first order: a +/// FILE root is the single candidate; directories are recursed except the +/// [`should_skip_walk_dir`] set; symlinked directories are never followed. +/// Stops promptly on SIGINT/SIGTERM. +fn collect_candidates(root: &Path, out: &mut Vec<PathBuf>) { + if root.is_file() { + out.push(root.to_path_buf()); + return; + } + let Ok(read) = std::fs::read_dir(root) else { + return; }; - let content = read(); - if content.is_none() { - note_skipped_file(); + let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + if run_cancelled() { + return; + } + let path = entry.path(); + match candidate_kind(&entry) { + CandidateKind::Dir => { + if !should_skip_walk_dir(&path, &entry.file_name()) { + collect_candidates(&path, out); + } + } + CandidateKind::File => out.push(path), + CandidateKind::Other => {} + } + } +} + +/// The slash-joined path of `path` relative to `root`. A FILE root (the +/// path IS the root) stamps the file's name, as does a path outside the +/// root, so a stamp is never `.` or empty. +#[must_use] +pub fn make_relative_path(path: &Path, root: &Path) -> String { + if let Ok(rel) = path.strip_prefix(root) { + if !rel.as_os_str().is_empty() { + return rel.to_string_lossy().into_owned(); + } + } + path.file_name().map_or_else( + || path.to_string_lossy().into_owned(), + |name| name.to_string_lossy().into_owned(), + ) +} + +/// Directory portion of a relative stamp (`.` for a bare filename, `/` for +/// a root-anchored one). +#[must_use] +pub fn parent_dir(relative: &str) -> String { + match relative.rfind('/') { + Some(0) => "/".to_string(), + Some(pos) => relative[..pos].to_string(), + None => ".".to_string(), } - content } -/// Directory-level skip predicate for the static folder walks. The reference -/// pipeline only does `filepath.SkipDir` on `.git`; this EXTENDS that to also -/// skip any directory carrying a `CACHEDIR.TAG` — the BSD cache-directory -/// convention Cargo writes into `target/` (and that ripgrep/fd/tar honor). A -/// bare `codefang run .` in a built Rust repo would otherwise descend into a +/// Directory-level skip predicate for the static folder walks: `.git` and any +/// directory carrying a `CACHEDIR.TAG` — the BSD cache-directory convention +/// Cargo writes into `target/` (and that ripgrep/fd/tar honor). A bare +/// `codefang run .` in a built Rust repo would otherwise descend into a /// multi-gigabyte `target/` tree and parse its generated sources, which looks /// like a hang. The tag check has effectively zero false positives (a real -/// source directory never carries the signature), so analysis is unchanged on -/// repos without such dirs — including the parity gate's kubernetes checkout. +/// source directory never carries the signature). pub(crate) fn should_skip_walk_dir(path: &std::path::Path, name: &std::ffi::OsStr) -> bool { name == ".git" || is_cache_tagged_dir(path) } @@ -172,13 +512,11 @@ fn is_cache_tagged_dir(dir: &std::path::Path) -> bool { } // --------------------------------------------------------------------------- -// Shared pipeline helpers (path resolution, tick floor, RFC3339 formatting). -// These mirror the reference implementation / plumbing helpers and are shared by the static -// and history handlers. +// Shared pipeline helpers (path resolution, commit window, RFC3339 formatting). // --------------------------------------------------------------------------- /// Resolves the repository path from `run`'s positional arg or `-p/--path` -/// (the reference implementation: the positional wins when present, else `--path`, default `.`). +/// (the positional wins when present, else `--path`, default `.`). #[must_use] pub fn run_repo_path(sub: &clap::ArgMatches) -> String { if let Some(p) = sub.get_one::<String>("path-positional") { @@ -191,41 +529,11 @@ pub fn run_repo_path(sub: &clap::ArgMatches) -> String { .unwrap_or_else(|| ".".to_string()) } -/// The effective first-parent mode for the shared history revwalk, mirroring -/// the reference `initHistoryPipeline`: when the resolved leaf set contains -/// `burndown` and `--first-parent` is off, first-parent is forced on. -/// -/// The reference implementation forces first-parent for the WHOLE history run (the single shared revwalk -/// that feeds every selected history analyzer) whenever `history/burndown` is in -/// the resolved leaf set, regardless of the `--first-parent` flag. Because every -/// history analyzer in one `run` shares that revwalk, the window selection — and -/// therefore each analyzer's tick assignment and commit set — must observe the -/// same forced flag. A handler that read only `--first-parent` would diverge from -/// the reference implementation whenever burndown is co-selected (e.g. `--analyzers history/devs,history/burndown`, -/// `history/*`, or `*`), even though it is not the burndown handler. -/// -/// The burndown membership is computed over the RESOLVED leaf set, so literal ids -/// and globs (`history/*`, `*`) that select burndown all force the flag, exactly -/// as the reference implementation's `slices.Contains(analyzerKeys, "burndown")` does after glob expansion. -#[must_use] -pub fn effective_first_parent(sub: &clap::ArgMatches) -> bool { - if sub.get_flag("first-parent") { - return true; - } - let patterns: Vec<String> = sub - .get_many::<String>("analyzers") - .map(|vals| vals.cloned().collect()) - .unwrap_or_default(); - let pats: Vec<&str> = patterns.iter().map(String::as_str).collect(); - let (_static_ids, history_ids) = expand_combined_ids(&pats); - history_ids.iter().any(|id| id == "history/burndown") -} - /// The `--since` state of a history run: raw spec text, resolved against the /// repository when the walk is built. /// /// Accepted forms, in precedence order: -/// 1. a Go-style duration (`24h`, `1h30m`) subtracted from "now" — units are +/// 1. a duration (`24h`, `1h30m`) subtracted from "now" — units are /// `h`/`m`/`s` only, so `m` means MINUTES and there is no `d`/`w` unit; /// "now" honors `CODEFANG_NOW` so duration-relative runs stay reproducible /// (DESIGN §2.8); @@ -241,7 +549,7 @@ pub fn effective_first_parent(sub: &clap::ArgMatches) -> bool { /// /// Parsing lives in [`cf_gitlib::helpers::parse_time`] / /// [`cf_gitlib::Repository::resolve_time`] — ONE parser shared by the CLI and -/// the library, instead of a second CLI-only copy. +/// the library. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SinceSpec { /// `--since` absent or empty: the walk is unfiltered. @@ -259,60 +567,63 @@ pub fn history_since_spec(sub: &clap::ArgMatches) -> SinceSpec { } } -/// The commit window selected by `--limit` / `--since` / `--first-parent`. +/// The commit window of a history run, as selected by `--head`, `--limit`, +/// `--since` and the walk's first-parent mode. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HistoryWindow { + /// `--head`: the window is exactly `[HEAD]`; the other bounds are ignored. + pub head: bool, + /// `--limit N`: the N **newest** reachable commits; `0` = the whole walk. + pub limit: usize, + /// Follow only the first parent of merge commits. + pub first_parent: bool, + /// `--since`: every commit with author time >= the cutoff. + pub since: SinceSpec, +} + +/// The commit window selected by a [`HistoryWindow`]. /// /// Returns hashes in the **oldest-first** order the history analyzers consume /// (tick assignment, burndown line survival and identity ids are all /// order-dependent). Window semantics: -/// - `--limit N` → the N **newest** reachable commits (the `git log -n` -/// convention); `limit <= 0` → the whole walk; -/// - `--since C` → every commit with author time >= C. A cutoff after HEAD +/// - `head` → exactly `[HEAD]`, the same single-commit window `--limit 1` +/// selects, so the two are byte-identical for every analyzer and format; +/// - `limit N` → the N **newest** reachable commits (the `git log -n` +/// convention); `0` → the whole walk; +/// - `since C` → every commit with author time >= C. A cutoff after HEAD /// yields an EMPTY window (analyzers emit their empty report); a cutoff at -/// or before the root commit filters nothing; -/// - both → the newest `N` commits *inside* the `--since` window. -/// -/// Both bounds come from ONE newest-first pass: the walk is sorted -/// `TIME|TOPOLOGICAL` newest-first, so [`cf_gitlib::CommitIter`]'s author-time -/// *stop* filter cuts exactly at the cutoff and `limit` caps that prefix. +/// or before the root commit filters nothing. Author time can be skewed +/// against the committer-time walk order, so the cutoff is a SKIP filter +/// over the whole walk, never a stop condition; +/// - both → the newest `N` commits *inside* the `since` window. /// -/// KNOWN RISK (inherited, not introduced here): the walk is ordered by -/// committer time while the filter compares AUTHOR time, so a history whose -/// author dates are skewed against its commit dates (bulk `git commit --date` -/// rewrites) can be cut short. Mitigation if it ever bites: turn -/// `CommitIter`'s stop filter into a skip filter, which costs a full walk. -/// -/// DIVERGENCE (deliberate — see `specs/frds/FRD-newest-commit-window.md`): the -/// retired Go reference selected the N OLDEST commits and aborted with empty -/// stdout on a *partial* `--since` window. Both are corrected here; captures -/// bounded by `--limit` against that binary no longer apply. -/// -/// Returns `None` (⇒ the run aborts with empty stdout) when the repository -/// cannot be walked or `--since` cannot be resolved against it — the CLI -/// pre-checks the latter, so this is the library-level contract. +/// Returns `None` (⇒ the run aborts) when the repository cannot be walked or +/// `since` cannot be resolved against it — the CLI preflight reports the +/// latter by name, so this is the library-level contract. #[must_use] pub fn load_history_commit_hashes( repo: &cf_gitlib::Repository, - limit: i64, - first_parent: bool, - since: SinceSpec, + window: &HistoryWindow, ) -> Option<Vec<cf_gitlib::Hash>> { use cf_gitlib::repository::{time_from_unix_secs, LogOptions}; - // Resolve the spec against THIS repository so revisions are usable and - // duration forms honor CODEFANG_NOW (shared parser; see [`SinceSpec`]). - let since_secs = match since { + if window.head { + return Some(vec![repo.head().ok()?]); + } + + let since_secs = match &window.since { SinceSpec::Inactive => None, - SinceSpec::Active(spec) => Some(repo.resolve_time(&spec).ok()?), + SinceSpec::Active(spec) => Some(repo.resolve_time(spec).ok()?), }; let log_opts = LogOptions { since: since_secs.map(time_from_unix_secs), - first_parent, + first_parent: window.first_parent, reverse: false, }; let mut iter = repo.log(&log_opts).ok()?; let mut hashes = Vec::new(); - while limit <= 0 || (hashes.len() as i64) < limit { + while window.limit == 0 || hashes.len() < window.limit { match iter.next_commit() { Some(c) => hashes.push(c.hash()), None => break, @@ -322,97 +633,8 @@ pub fn load_history_commit_hashes( Some(hashes) } -/// Returns the reference implementation's streaming-pipeline *consume* order for an oldest-first commit -/// window: the IDENTITY (oldest-first revwalk order). -/// -/// At `--workers 1` (the only config the differential gate exercises) every -/// stage of the reference coordinator pipeline preserves input order, so the leaf -/// analyzers consume commits in exactly the oldest-first order they are fed: -/// -/// * the reference `Stream` emits contiguous oldest-first -/// batches (`commits[i:end]`) from a single goroutine. -/// * the reference blob and diff pipelines use -/// `pipeline.RunPC`: a single producer emits jobs -/// in input order onto one FIFO channel, and a single consumer reads and -/// emits them in that same order — the parallel blob/diff prefetch only -/// shares one batched worker request, it never resequences commits. -/// * the reference `Process` is explicitly order-preserving -/// ("Output order matches input order via a slot-based approach"): the -/// `emit` goroutine waits on each slot's `done` in dispatch order. -/// * the reference coordinator `Process` and drain stage -/// `SignalOnDrain` each forward items one-for-one from a single worker. -/// * the reference `processCommitsSerial`/`hybridCommitLoop` range over -/// the coordinator's `dataChan` in arrival order, and the per-commit `Index` -/// carried through is the plain oldest-first revwalk index -/// (`batch.StartIndex + job.index`). -/// -/// So the order in which the COORDINATOR pipeline yields commits is the identity -/// of the oldest-first window. Earlier code reordered into round-robin -/// `PIPELINE_CHUNK` blocks here; that was incorrect — no pipeline stage performs -/// that reordering. -/// -/// This is the order the CORE (plumbing) analyzers consume — notably the -/// `IdentityDetector`, which assigns loose author ids strictly oldest-first. It -/// is NOT necessarily the order in which a LEAF analyzer's order-sensitive state -/// is updated: at the default `LeafWorkers = max(NumCPU / 3, 4)` (which -/// `--workers` does NOT override — that flag only sets the blob/diff `Workers` -/// pool), the reference implementation's hybrid leaf path (the reference `processCommitsHybrid`) forks the -/// leaf across workers, dispatching consume position `p` to worker `p % W`. The -/// effect is leaf-specific and handled at the leaf consumer, not here: -/// - couples: each fork has an INDEPENDENT seen-files Bloom; commits stay in -/// oldest-first order WITHIN a worker (see `couples_run`). -/// - file-history: forked TCs are drained worker-by-worker into one aggregator -/// whose insert handling resets a path's hash list, so its add-order is the -/// commits stably reordered by `(p % W, p)` (see `file_history_run`). -/// -/// Both use [`leaf_worker_count`] for `W`, reproducing the reference binary on -/// this machine. -#[must_use] -pub fn pipeline_consume_order(hashes: Vec<cf_gitlib::Hash>) -> Vec<cf_gitlib::Hash> { - hashes -} - -/// Reference leaf-worker divisor (`framework` `leafWorkerDivisor`): `LeafWorkers = -/// NumCPU / divisor`. -const LEAF_WORKER_DIVISOR: usize = 3; -/// Reference minimum leaf-worker count (`framework` `minLeafWorkers`). -const MIN_LEAF_WORKERS: usize = 4; - -/// Number of forked leaf-analyzer workers the reference implementation dispatches commits across, mirroring -/// `framework.DefaultCoordinatorConfig`: `max(NumCPU / 3, 4)`, where `NumCPU` is -/// the machine's logical CPU count. -/// -/// The reference implementation's hybrid leaf path (the reference `processCommitsHybrid`, taken for a single -/// non-`SequentialOnly` leaf when `0 < CoreCount < len(Analyzers)`) forks the -/// leaf across this many workers and dispatches consume position `p` to worker -/// `p % count`, each worker holding INDEPENDENT analyzer state (e.g. couples' -/// seen-files Bloom, file-history's per-path map). That makes the order-sensitive -/// parts of those analyzers depend on this count, so a byte-exact port must use -/// the same value as the live binary on this machine (which the oracle also runs -/// on). The `--workers` flag only overrides `Workers` (the blob/diff pool), never -/// `LeafWorkers`, so this is unaffected by `--workers 1`. -#[must_use] -pub fn leaf_worker_count() -> usize { - let num_cpu = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - (num_cpu / LEAF_WORKER_DIVISOR).max(MIN_LEAF_WORKERS) -} - -/// Rounds Unix `secs` down to the start of its 24-hour tick (reference: -/// `plumbing.FloorTime(when, 24h)`). `time.Round` rounds half away from zero; -/// the post-round correction yields the floor. -#[must_use] -pub fn floor_tick_secs(secs: i64) -> i64 { - const PERIOD: i64 = 86_400; - let rounded = ((secs + PERIOD / 2).div_euclid(PERIOD)) * PERIOD; - if rounded > secs { - rounded - PERIOD - } else { - rounded - } -} - -/// Formats Unix seconds as the reference `time.RFC3339` in the zone given by -/// `offset_minutes`; a zero offset prints the literal `Z`. +/// Formats Unix seconds as RFC3339 in the zone given by `offset_minutes`; a +/// zero offset prints the literal `Z`. #[must_use] pub fn format_rfc3339_offset(unix_secs: i64, offset_minutes: i32) -> String { let local = unix_secs + i64::from(offset_minutes) * 60; @@ -433,33 +655,36 @@ pub fn format_rfc3339_offset(unix_secs: i64, offset_minutes: i32) -> String { } /// Serializes a history analyzer's report value across the json/yaml/bin -/// machine formats, mirroring the reference `OutputHistoryResults` + -/// `BaseHistoryAnalyzer.Serialize` (`writeMetricsToFormat`): the *same* report -/// value is encoded each way, so a handler computes the value once and routes -/// it here rather than re-deriving per format. +/// machine formats: the *same* report value is encoded each way, so a handler +/// computes the value once and routes it here rather than re-deriving per +/// format. /// -/// - `json` (a "raw" format): `json.Marshal(metrics.ToJSON())` — `json_value`, -/// no header, no trailing newline (cf-gojson `marshal`). -/// - `binary` (a "raw" format): `reportutil.EncodeBinaryEnvelope(metrics)` — a -/// CFB1 envelope wrapping the same `json_value` bytes (no header). -/// - `yaml` (non-raw): `PrintHeader` (`codefang (v2):` / version / hash) then -/// `<analyzer_name>:\n` then `yaml.Marshal(metrics.ToYAML())` — `yaml_value`. +/// - `json`: `json_value`, no header, no trailing newline (cf-gojson `marshal`). +/// - `binary`: a CFB1 envelope wrapping the same `json_value` bytes. +/// - `yaml`: the `codefang (v2):` / version / hash header, then +/// `<analyzer_name>:\n`, then the yaml body of `yaml_value`. /// -/// `analyzer_name` is the history analyzer's `Name()` (the YAML section header, -/// e.g. `ImportsPerDeveloper`). Returns `None` for any non-machine format, so -/// the caller surfaces the same dispatch error the reference implementation does. -#[must_use] +/// `analyzer_name` is the history analyzer's display name (the YAML section +/// header); `analyzer_id` names the analyzer in the error. +/// +/// # Errors +/// +/// [`HandlerError::FormatNotSupported`] for any other format. pub fn serialize_history_metrics( format: &str, + analyzer_id: &str, analyzer_name: &str, json_value: &cf_gojson::GoValue, yaml_value: &cf_gojson::GoValue, -) -> Option<Vec<u8>> { +) -> Result<Vec<u8>, HandlerError> { match format { - // Raw formats: no version header, no per-analyzer section name. - "json" => Some(cf_gojson::marshal(json_value)), - "binary" | "bin" => cf_reportutil::encode_binary_envelope(json_value).ok(), - // Non-raw: PrintHeader + "<Name>:\n" + yaml body. + "json" => Ok(cf_gojson::marshal(json_value)), + "binary" | "bin" => { + cf_reportutil::encode_binary_envelope(json_value).map_err(|_| HandlerError::NoReport { + analyzer: analyzer_id.to_string(), + format: format.to_string(), + }) + } "yaml" => { let mut out = Vec::new(); out.extend_from_slice(b"codefang (v2):\n"); @@ -469,12 +694,32 @@ pub fn serialize_history_metrics( out.extend_from_slice(format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes()); out.extend_from_slice(format!("{analyzer_name}:\n").as_bytes()); out.extend_from_slice(&cf_goyaml::marshal(yaml_value)); - Some(out) + Ok(out) } - _ => None, + _ => Err(format_not_supported(format, analyzer_id)), + } +} + +/// The [`HandlerError::FormatNotSupported`] for `(format, analyzer)`. +fn format_not_supported(format: &str, analyzer: &str) -> HandlerError { + HandlerError::FormatNotSupported { + format: format.to_string(), + analyzer: analyzer.to_string(), } } +/// Maps a report builder's `None` to the generic [`HandlerError::NoReport`]. +fn report_or_err( + bytes: Option<Vec<u8>>, + analyzer: &str, + format: &str, +) -> Result<Vec<u8>, HandlerError> { + bytes.ok_or_else(|| HandlerError::NoReport { + analyzer: analyzer.to_string(), + format: format.to_string(), + }) +} + /// Civil date from a day count since the Unix epoch (Howard Hinnant's algorithm). #[must_use] pub fn civil_from_days(z: i64) -> (i64, u32, u32) { @@ -490,15 +735,134 @@ pub fn civil_from_days(z: i64) -> (i64, u32, u32) { (if m <= 2 { y + 1 } else { y }, m, d) } +// --------------------------------------------------------------------------- +// Run preflight: ONE place that validates the path, the repository, the +// flags and the format before any walk starts. +// --------------------------------------------------------------------------- + +/// Every `--format` value codefang knows, across all phases. +const KNOWN_FORMATS: &[&str] = &[ + crate::formats::FORMAT_JSON, + crate::formats::FORMAT_YAML, + crate::formats::FORMAT_PLOT, + crate::formats::FORMAT_BINARY, + crate::formats::FORMAT_TIMESERIES, + crate::formats::FORMAT_NDJSON, + crate::formats::FORMAT_COMPACT, + crate::formats::FORMAT_TEXT, + crate::formats::FORMAT_TIMESERIES_NDJSON, +]; + +/// Validates a run BEFORE any walk: the path exists; a history selection +/// opens the repository, resolves `--since`, reads `--people-dict` and +/// validates `--limit` / `--tick-size` / `--max-changes-per-commit`; both +/// phases validate `--languages`; and the `--format` value is one every +/// selected phase can render. `static_ids` / `history_ids` are the resolved +/// selection (either may be empty). +/// +/// # Errors +/// +/// The [`HandlerError`] naming the first failing input. +pub fn preflight( + ctx: &RunContext, + static_ids: &[String], + history_ids: &[String], +) -> Result<(), HandlerError> { + if !std::path::Path::new(&ctx.path).exists() { + return Err(HandlerError::PathNotFound(ctx.path.clone())); + } + if !static_ids.is_empty() { + static_filter(ctx).map_err(HandlerError::InvalidFlag)?; + } + if !history_ids.is_empty() { + history::limit_native_memory(ctx.matches); + history::validate_history_run(ctx.matches)?; + } + preflight_format(ctx, static_ids, history_ids)?; + if let Some(note) = per_file_ignored_note( + !static_ids.is_empty(), + ctx.matches.get_flag("per-file"), + &ctx.raw_format(), + ) { + warn_once(¬e); + } + Ok(()) +} + +/// The stderr note for a `--per-file` run whose format cannot carry per-file +/// entries: the flag is a `--format json` enrichment, and every other format +/// (`yaml`, `bin`, `text`, `compact`, `plot`) silently rendered without it. +/// `None` when no static analyzer is selected, the flag is off, or the +/// format is json. +#[must_use] +pub fn per_file_ignored_note( + static_selected: bool, + per_file: bool, + format: &str, +) -> Option<String> { + let is_json = crate::formats::normalize_format(format) == crate::formats::FORMAT_JSON; + (static_selected && per_file && !is_json).then(|| { + format!( + "--per-file applies to --format json only; ignored for {}", + format.trim() + ) + }) +} + +/// Prints `note` on stderr once per process: the preflight runs in the CLI +/// entry point AND inside the library pipeline, and a multi-analyzer run +/// must not repeat the note per analyzer. +fn warn_once(note: &str) { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| eprintln!("{note}")); +} + +/// The format leg of [`preflight`]: `--ndjson` modifies `timeseries` only; +/// `plot` is routed by the command itself; everything else must resolve for +/// the selected phases. +fn preflight_format( + ctx: &RunContext, + static_ids: &[String], + history_ids: &[String], +) -> Result<(), HandlerError> { + let raw = ctx.raw_format(); + let normalized = crate::formats::normalize_format(&raw); + if ctx.ndjson() && normalized != crate::formats::FORMAT_TIMESERIES { + return Err(HandlerError::InvalidFlag(format!( + "invalid --ndjson: requires --format timeseries, got --format {}", + raw.trim() + ))); + } + if normalized == crate::formats::FORMAT_PLOT { + return Ok(()); + } + if !KNOWN_FORMATS.contains(&normalized.as_str()) { + return Err(HandlerError::UnsupportedFormat(raw)); + } + let has_static = !static_ids.is_empty(); + let has_history = !history_ids.is_empty(); + if crate::formats::resolve_formats(&raw, has_static, has_history).is_ok() { + return Ok(()); + } + let analyzer = if has_static && has_history { + "a mixed static+history selection".to_string() + } else { + static_ids + .iter() + .chain(history_ids.iter()) + .next() + .cloned() + .unwrap_or_default() + }; + Err(format_not_supported(&raw, &analyzer)) +} + // --------------------------------------------------------------------------- // Static analyzer glob/bin helpers (registry-ordered multi-analyzer bin output). // --------------------------------------------------------------------------- -/// The static analyzers in registry order (reference `defaultUASTAnalyzers ++ -/// defaultRawFileAnalyzers`). `bin_ported` is true for the analyzers whose -/// `--format bin` payload is reproduced byte-for-byte; cohesion is not yet -/// ported. clones IS ported: its bin payload is the CFB1 envelope of -/// `computeMetricsFromReport` over the cross-file aggregate report. +/// The static analyzers in registry order. The `bool` records whether the +/// analyzer's `--format bin` payload is produced (all are). pub const STATIC_BIN_ANALYZERS: &[(&str, bool)] = &[ ("static/clones", true), ("static/complexity", true), @@ -526,25 +890,14 @@ pub fn is_static_id_or_glob(pat: &str) -> bool { /// True when the glob matches any known history analyzer ID. #[must_use] pub fn history_glob_matches(pat: &str) -> bool { - const HISTORY_IDS: &[&str] = &[ - "history/burndown", - "history/couples", - "history/devs", - "history/file-history", - "history/imports", - "history/shotness", - "history/typos", - "history/sentiment", - "history/quality", - "history/anomaly", - ]; - HISTORY_IDS.iter().any(|id| go_path_match(pat, id)) + HISTORY_COMBINED_ORDER + .iter() + .any(|id| go_path_match(pat, id)) } /// Expands the requested patterns over the registry-ordered static analyzers and -/// concatenates each selected analyzer's CFB1 bin envelope (reference: -/// `FormatPerAnalyzer(FormatBinary)`). Returns `None` if any selected analyzer -/// is not ported (clones/cohesion) or any folder walk fails. +/// concatenates each selected analyzer's CFB1 bin envelope. Returns `None` if +/// any selected analyzer cannot produce its envelope or any folder walk fails. #[must_use] pub fn static_multi_bin(patterns: &[&str], path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { let mut selected: Vec<(&str, bool)> = Vec::new(); @@ -575,27 +928,18 @@ pub fn static_multi_bin(patterns: &[&str], path: &str, filter: &StaticFilter) -> } // --------------------------------------------------------------------------- -// Static analyzer multi-analyzer JSON merge (the reference `renderer.SectionsToJSON` over -// several analyzers). For `codefang run --format json` with more than one static -// analyzer (or a glob that selects several), the reference implementation renders ONE `renderer.JSONReport` -// whose `sections` are the per-analyzer sections in REGISTRY order and whose -// `overall_score` is the executive-summary average of the scored sections -// (info-only sections, score < 0, are excluded; all-info ⇒ overall is -1 / Info). -// Each analyzer's section value comes from its own crate-owned report builder -// (the same GoValue the single-analyzer JSON path serializes), so the merge owns -// no analyzer math and every format follows the same report value. +// Static analyzer multi-analyzer JSON merge. For `codefang run --format json` +// with more than one static analyzer (or a glob that selects several), ONE +// merged report is rendered whose `sections` are the per-analyzer sections in +// REGISTRY order and whose `overall_score` is the executive-summary average of +// the scored sections (info-only sections, score < 0, are excluded; all-info ⇒ +// overall is -1 / Info). Each analyzer's section value comes from its own +// crate-owned report builder, so the merge owns no analyzer math. // --------------------------------------------------------------------------- -/// Registry-ordered map of -/// static analyzer id → the crate-owned builder of that analyzer's single-section -/// `renderer.JSONReport` GoValue. Used by [`static_multi_json`] to merge several -/// analyzers' sections; the merge never branches per format — the same GoValue -/// feeds the serializer. The `bool` is the run's `--per-file` flag: every -/// builder owns its section's `files` enrichment (the reference -/// `EnrichWithPerFileData` gives EVERY section at least an empty `files` array -/// under the flag, so no post-processing happens in the merge). The -/// [`StaticFilter`] is the run's shared walk filter (`--languages` + path -/// policy), applied by every builder's folder walk. +/// Registry-ordered map of static analyzer id → the crate-owned builder of +/// that analyzer's single-section report GoValue. The `bool` is the run's +/// `--per-file` flag; the [`StaticFilter`] is the run's shared walk filter. type ReportValueFn = fn(&str, &StaticFilter, bool) -> Option<GoValue>; use cf_gojson::{GoMap, GoValue, MapOrigin}; @@ -625,13 +969,8 @@ const STATIC_JSON_VALUE_BUILDERS: &[(&str, ReportValueFn)] = &[ ), ]; -/// Pulls the `sections` array and `overall_score` out of a single-analyzer -/// `renderer.JSONReport` GoValue (`{overall_score_label, sections, overall_score}`). -/// Returns the section GoValues and the contained `overall_score` (each section's -/// own `score` field is what the merge re-averages, but the single-analyzer -/// `overall_score` equals that section's score for one section, so we read the -/// per-section `score` directly for robustness against future multi-section -/// analyzers). +/// Pulls the `sections` array out of a single-analyzer report GoValue +/// (`{overall_score_label, sections, overall_score}`). fn extract_sections(report: &GoValue) -> Vec<GoValue> { report .as_map() @@ -643,8 +982,8 @@ fn extract_sections(report: &GoValue) -> Vec<GoValue> { .unwrap_or_default() } -/// Reads a section's numeric `score` field, defaulting -/// to the info-only sentinel `-1.0` when absent. +/// Reads a section's numeric `score` field, defaulting to the info-only +/// sentinel `-1.0` when absent. fn section_score(section: &GoValue) -> f64 { match section.as_map().and_then(|m| m.get("score")) { Some(GoValue::Float(f)) => *f, @@ -653,8 +992,8 @@ fn section_score(section: &GoValue) -> f64 { } } -/// The reference `terminal.FormatScore`: `round(score*10)/10` → `"N/10"`; a negative score -/// (info-only) renders `"Info"`. +/// `round(score*10)/10` → `"N/10"`; a negative score (info-only) renders +/// `"Info"`. fn overall_score_label(score: f64) -> String { if score < 0.0 { return "Info".to_string(); @@ -664,11 +1003,10 @@ fn overall_score_label(score: f64) -> String { } /// Expands `patterns` over the registry-ordered static analyzers and renders ONE -/// merged `renderer.JSONReport`: sections in -/// registry order, `overall_score` the average of the scored (`score >= 0`) -/// sections (or `-1` when none are scored). `None` if no static analyzer is -/// selected or any selected analyzer cannot produce a report (the caller then -/// falls through to the same error path the reference implementation takes). +/// merged report: sections in registry order, `overall_score` the average of +/// the scored (`score >= 0`) sections (or `-1` when none are scored). `None` +/// if no static analyzer is selected or any selected analyzer cannot produce a +/// report. #[must_use] pub fn static_multi_json( patterns: &[&str], @@ -683,18 +1021,15 @@ pub fn static_multi_json( Some(bytes) } -/// The merged multi-static-analyzer `--format text` render (the reference -/// `FormatText` over ALL selected sections at once): a ≥2-section selection -/// carries the executive-summary header (`CODE ANALYSIS REPORT` + per-analyzer -/// score table) followed by each analyzer's section rendered EXACTLY as its -/// solo `--format text` body — the sections come from the same per-analyzer -/// TEXT-shaped section values the solo text handlers feed the renderer (NOT -/// the JSON report values, whose extra `distribution`/`issues` data the -/// reference text sections for e.g. complexity do not carry). +/// The merged multi-static-analyzer `--format text` render: a ≥2-section +/// selection carries the executive-summary header followed by each analyzer's +/// section rendered EXACTLY as its solo `--format text` body — the sections +/// come from the same per-analyzer TEXT-shaped section values the solo text +/// handlers feed the renderer. #[must_use] pub fn static_multi_text(patterns: &[&str], path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { type TextValueFn = fn(&str, &StaticFilter) -> Option<GoValue>; - // Registry order (the reference registry / summary-table order). + // Registry order (the summary-table order). const TEXT_VALUE_BUILDERS: &[(&str, TextValueFn)] = &[ ("static/clones", |p, f| { static_clones::clones_report_value_flags(p, f, false) @@ -748,10 +1083,9 @@ pub fn static_multi_text(patterns: &[&str], path: &str, filter: &StaticFilter) - Some(section_render::render_text_report(&GoValue::Map(root))) } -/// Builds the ONE merged `renderer.JSONReport` GoValue for a multi-static -/// selection: sections in registry order, `overall_score` the executive-summary -/// average of the scored sections. Every output format renders from this same -/// value (rule: one report value, many encodings). +/// Builds the ONE merged report GoValue for a multi-static selection: sections +/// in registry order, `overall_score` the executive-summary average of the +/// scored sections. Every output format renders from this same value. #[must_use] fn static_multi_report_value( patterns: &[&str], @@ -806,49 +1140,6 @@ fn static_multi_report_value( Some(GoValue::Map(root)) } -/// The reference `EnrichWithPerFileData` INITIALIZATION step: under -/// `--per-file` every section gets at least an EMPTY `files` array (inserted -/// between `issues` and `score`, the renderer.JSONSection field order), even -/// when its analyzer retains no per-file snapshots (clones is not -/// `PerFileModeEnabled`). Sections that already carry `files` are unchanged. -pub(crate) fn ensure_sections_files_key(report: GoValue) -> GoValue { - let GoValue::Map(root) = report else { - return report; - }; - let mut new_root = GoMap::new(MapOrigin::Struct); - for (key, value) in root.iter() { - if key != "sections" { - new_root.push(key, value.clone()); - continue; - } - let GoValue::Array(sections) = value else { - new_root.push(key, value.clone()); - continue; - }; - let enriched: Vec<GoValue> = sections - .iter() - .map(|section| { - let GoValue::Map(m) = section else { - return section.clone(); - }; - if m.get("files").is_some() { - return section.clone(); - } - let mut out = GoMap::new(MapOrigin::Struct); - for (k, v) in m.iter() { - if k == "score" { - out.push("files", GoValue::Array(Vec::new())); - } - out.push(k, v.clone()); - } - GoValue::Map(out) - }) - .collect(); - new_root.push(key, GoValue::Array(enriched)); - } - GoValue::Map(new_root) -} - /// True when `patterns` select MORE THAN ONE static analyzer (a literal multi-id /// list or a glob matching several), so the JSON path must merge sections rather /// than emit a single-analyzer document. @@ -888,9 +1179,8 @@ pub fn static_single_bin(id: &str, path: &str, filter: &StaticFilter) -> Option< } } -/// The history analyzers in the reference implementation phase/registry order (the reference `defaultHistoryLeaves` -/// as emitted by the combined unified-model path). Used to expand `*`/globs and -/// to order the history phase of the combined static+history render. +/// The history analyzers in combined-model order (the order the combined +/// static+history render lists them). Used to expand `*`/globs. pub const HISTORY_COMBINED_ORDER: &[&str] = &[ "history/typos", "history/file-history", @@ -904,14 +1194,9 @@ pub const HISTORY_COMBINED_ORDER: &[&str] = &[ "history/sentiment", ]; -/// The history analyzers in the reference implementation's *separate-phase* per-analyzer emit order — the -/// order `runHistoryPhase` writes each leaf's standalone report when the run is -/// NOT a mixed static+history combined render (i.e. a history-only selection, -/// literal list or glob, in a machine format). This is the pipeline leaf order -/// (`pl.Leaves` → `selectLeaves`), which differs from both the registry id sort -/// and [`HISTORY_COMBINED_ORDER`]. Verified against the live reference binary -/// (`--analyzers history/* --format json`): the concatenated per-analyzer reports -/// appear in exactly this sequence. +/// The history analyzers in the *separate-phase* per-analyzer emit order — the +/// order a history-only selection (literal list or glob) writes each leaf's +/// standalone report. pub const HISTORY_PHASE_EMIT_ORDER: &[&str] = &[ "history/quality", "history/sentiment", @@ -925,20 +1210,10 @@ pub const HISTORY_PHASE_EMIT_ORDER: &[&str] = &[ "history/file-history", ]; -/// Expands a requested pattern list into the concrete history leaf ids it -/// selects, in the reference implementation's separate-phase emit order ([`HISTORY_PHASE_EMIT_ORDER`]). -/// Literal ids match exactly; globs use the reference `path.Match` semantics. Used by the -/// history-only-glob per-analyzer concatenation path (the reference `runHistoryPhase` over a -/// glob-expanded selection), so a `history/*` or multi-id history selection emits -/// each leaf's standalone report in the same order the reference implementation does. -/// Whether any requested pattern selects the analyzer `id`, mirroring the reference -/// `Registry.resolvePattern`: a bare `*` matches EVERY id (reference: -/// special-cases `pattern == "*"` to `allIDs()` BEFORE `path.Match`, because -/// `path.Match("*", "history/typos")` is false — `*` does not cross `/`); other -/// globs use the reference `path.Match` semantics ([`go_path_match`]); a literal id matches -/// exactly. Without the `*` special case, `--analyzers '*'` would select nothing, -/// while `--analyzers 'history/*'` would still work (the literal `history/` -/// prefix anchors the match). +/// Whether any requested pattern selects the analyzer `id`: a bare `*` +/// matches EVERY id (`path.Match("*", "history/typos")` is false — `*` does +/// not cross `/`); other globs use [`go_path_match`]; a literal id matches +/// exactly. #[must_use] fn pattern_selects_id(patterns: &[&str], id: &str) -> bool { let is_glob = |p: &str| p.contains(['*', '?', '[']); @@ -954,7 +1229,7 @@ fn pattern_selects_id(patterns: &[&str], id: &str) -> bool { } /// Expands a requested pattern list into the concrete history leaf ids it -/// selects, in the reference implementation's separate-phase emit order ([`HISTORY_PHASE_EMIT_ORDER`]). +/// selects, in the separate-phase emit order ([`HISTORY_PHASE_EMIT_ORDER`]). #[must_use] pub fn expand_history_phase_ids(patterns: &[&str]) -> Vec<String> { let selected = |id: &str| pattern_selects_id(patterns, id); @@ -966,12 +1241,10 @@ pub fn expand_history_phase_ids(patterns: &[&str]) -> Vec<String> { } /// Expands the requested analyzer patterns into concrete (static, history) id -/// lists in the reference implementation SELECTION order — the `registry.ExpandPatterns` semantics the -/// run command applies before `registry.Split`: patterns -/// resolve IN ORDER (a literal id to itself, a glob to the matching registry -/// ids in registration order), duplicates dropped first-wins, then ids are -/// divided by mode preserving that order. The plot path needs this order: -/// The reference implementation renders pages (and the index cards) in the resolved id order. +/// lists in SELECTION order: patterns resolve IN ORDER (a literal id to +/// itself, a glob to the matching registry ids in registration order), +/// duplicates dropped first-wins, then ids are divided by mode preserving that +/// order. The plot path renders pages (and the index cards) in this order. #[must_use] pub fn expand_selection_ids(patterns: &[&str]) -> (Vec<String>, Vec<String>) { let is_glob = |p: &str| p.contains(['*', '?', '[']); @@ -984,8 +1257,6 @@ pub fn expand_selection_ids(patterns: &[&str]) -> (Vec<String>, Vec<String>) { }; for pat in patterns { if is_glob(pat) { - // Glob: the full registry in registration order (statics, then - // history leaves), filtered by the reference `path.Match` semantics. for (id, _) in STATIC_BIN_ANALYZERS { if *pat == "*" || go_path_match(pat, id) { push_unique(&mut statics, id); @@ -999,7 +1270,7 @@ pub fn expand_selection_ids(patterns: &[&str]) -> (Vec<String>, Vec<String>) { continue; } // Literal id: itself, in its pattern position (unknown ids are kept - // out; the caller surfaces the dispatch diagnostic). + // out; the caller surfaces the diagnostic). if STATIC_BIN_ANALYZERS.iter().any(|(id, _)| id == pat) { push_unique(&mut statics, pat); } else if HISTORY_COMBINED_ORDER.iter().any(|id| id == pat) { @@ -1010,11 +1281,8 @@ pub fn expand_selection_ids(patterns: &[&str]) -> (Vec<String>, Vec<String>) { } /// Expands the requested analyzer patterns into concrete (static, history) id -/// lists in the reference implementation combined-model order: static analyzers in [`STATIC_BIN_ANALYZERS`] -/// registry order, then history analyzers in [`HISTORY_COMBINED_ORDER`]. Literal -/// (non-glob) ids are matched exactly; globs use the reference `path.Match` semantics. This -/// mirrors the reference `registry.Split` + `combinedIDsAndModes` ordering used by the -/// combined render. +/// lists in combined-model order: static analyzers in [`STATIC_BIN_ANALYZERS`] +/// registry order, then history analyzers in [`HISTORY_COMBINED_ORDER`]. #[must_use] pub fn expand_combined_ids(patterns: &[&str]) -> (Vec<String>, Vec<String>) { let matches = |id: &str| pattern_selects_id(patterns, id); @@ -1031,55 +1299,61 @@ pub fn expand_combined_ids(patterns: &[&str]) -> (Vec<String>, Vec<String>) { (statics, history) } -/// Renders the combined static+history run as the single `codefang.run.v1` -/// unified-model envelope, the Rust analogue of the reference `renderCombinedDirect` -///. Each selected analyzer is dispatched through its registry -/// handler with `--format bin`, producing a CFB1 envelope whose payload is the +/// Renders a multi-analyzer run as the single `codefang.run.v1` unified-model +/// document. Each selected analyzer is dispatched through its registry handler +/// with `--format bin`, producing a CFB1 envelope whose payload is the /// analyzer's raw report JSON. The concatenated envelopes are decoded into a -/// [`cf_analyze::conversion::UnifiedModel`], -/// stamped with run metadata, and re-serialized in the -/// requested `output_format` via [`cf_analyze::conversion::write_converted_output`] -/// so every machine format (json/yaml/bin/ndjson/timeseries) follows from the -/// one model value. +/// [`cf_analyze::conversion::UnifiedModel`], stamped with run metadata, and +/// re-serialized in the requested `output_format` via +/// [`cf_analyze::conversion::write_converted_output`] so every machine format +/// (json/yaml/bin/ndjson/timeseries) follows from the one model value. /// -/// Returns `None` if any selected analyzer cannot produce its bin payload (e.g. -/// an unported history analyzer), so the caller can fall back to the per-analyzer -/// pipeline rather than emit a partial envelope. -#[must_use] +/// Used for the mixed static+history selection AND for a history-only +/// selection of several analyzers in `json`/`yaml`, so a multi-analyzer run +/// always emits ONE document (`{version, metadata, analyzers: [{id, mode, +/// report}]}`), never a concatenation. +/// +/// # Errors +/// +/// The first failing analyzer's [`HandlerError`], or a [`HandlerError::NoReport`] +/// when the model cannot be decoded / encoded. pub fn render_combined( ctx: &RunContext, static_ids: &[String], history_ids: &[String], output_format: &str, -) -> Option<Vec<u8>> { +) -> Result<Vec<u8>, HandlerError> { let registry = default_registry(); - let mut raw: Vec<u8> = Vec::new(); let mut ids: Vec<String> = Vec::new(); let mut modes: Vec<cf_analyze::AnalyzerMode> = Vec::new(); // Pre-compute the ONE shared UAST walk for the co-selected heavy history - // analyzers (imports/quality/sentiment/shotness/typos) before fanning out, - // so it runs as a single task instead of N tasks blocking on its store. + // analyzers before fanning out, so it runs as a single task. uast_walk::prewarm(ctx.matches); - // Static phase, then history phase — the reference `renderCombinedDirect` order. Each - // handler is dispatched with the literal "bin" format; its CFB1 envelope is - // appended to the combined buffer (reference: staticExec/historyExec into &raw). - // Each leaf's raw report is gathered via its CFB1 bin envelope. Handlers - // match on the NORMALIZED format name (the reference `ValidateFormat` maps the `bin` - // alias to `binary`), so pass the normalized name here — passing the bare - // `bin` alias would miss any handler that only accepts `binary` (e.g. - // static/halstead), aborting the whole combined render. - // - // The per-analyzer envelopes are independent (each handler owns its own - // repository handle and parsers), so they are COMPUTED concurrently and - // then appended in the same deterministic static-then-history order. + // Static phase, then history phase. Each handler is dispatched with the + // normalized `binary` format; its CFB1 envelope is appended to the + // combined buffer. The per-analyzer envelopes are independent, so they + // are COMPUTED concurrently and appended in the deterministic + // static-then-history order. let all_ids: Vec<&String> = static_ids.iter().chain(history_ids.iter()).collect(); - let envelopes: Vec<Option<Vec<u8>>> = + let envelopes: Vec<Result<Vec<u8>, HandlerError>> = run_concurrent(all_ids.len(), ANALYZER_CONCURRENCY, |i| { - let entry = registry.lookup(all_ids[i])?; - (entry.run)(ctx, "binary") + let entry = registry + .lookup(all_ids[i]) + .ok_or_else(|| HandlerError::NoReport { + analyzer: all_ids[i].clone(), + format: crate::formats::FORMAT_BINARY.to_string(), + })?; + (entry.run)(ctx, crate::formats::FORMAT_BINARY) }); + // Sized once: a large report (538 MB of json for kubernetes history/*) + // must not be copied while the buffer doubles. + let total = envelopes + .iter() + .map(|env| env.as_ref().map_or(0, Vec::len)) + .sum(); + let mut raw: Vec<u8> = Vec::with_capacity(total); for (i, (id, env)) in all_ids.iter().zip(envelopes).enumerate() { raw.extend_from_slice(&env?); ids.push((*id).clone()); @@ -1090,27 +1364,88 @@ pub fn render_combined( }); } - let mut model = - cf_analyze::conversion::decode_combined_binary_reports(&raw, &ids, &modes).ok()?; + let no_report = || HandlerError::NoReport { + analyzer: ids.join(","), + format: output_format.to_string(), + }; + let mut model = cf_analyze::conversion::decode_combined_binary_reports(&raw, &ids, &modes) + .map_err(|_| no_report())?; + drop(raw); model.metadata = Some(cf_analyze::metadata::new_analysis_metadata(&ctx.path)); - // Normalize the requested format to the canonical name the conversion - // serializer matches on (reference `ValidateUniversalFormat`: "bin" -> "binary", - // case-folded), then apply the --ndjson modifier on timeseries exactly as - // the reference `renderCombinedDirect` does. let normalized = crate::formats::normalize_format(output_format); - let render_format = if ctx.ndjson() && normalized == "timeseries" { - "timeseries+ndjson".to_string() - } else { - normalized - }; + let render_format = crate::formats::apply_ndjson_modifier(&normalized, ctx.ndjson()); - let mut out: Vec<u8> = Vec::new(); - cf_analyze::conversion::write_converted_output(&model, &render_format, &mut out, None).ok()?; - Some(out) + // The model is not needed after rendering: render it in place. + cf_analyze::conversion::render_converted_output(model, &render_format, None) + .map_err(|_| no_report()) +} + +/// Renders a MIXED static+history selection. The formats whose content is +/// per-phase render each phase with its own renderer: `text` is the static +/// text sections followed by the history text, `compact` is every analyzer's +/// compact output (static first), and `timeseries` (with or without +/// `--ndjson`) is the history time series — static analyzers have no +/// per-commit data, so they are named once on stderr. Every other format is +/// the unified `codefang.run.v1` document ([`render_combined`]). +/// +/// # Errors +/// +/// Propagates the first phase renderer's [`HandlerError`]. +pub fn render_mixed( + ctx: &RunContext, + static_ids: &[String], + history_ids: &[String], + output_format: &str, +) -> Result<Vec<u8>, HandlerError> { + use crate::formats::{ + FORMAT_COMPACT, FORMAT_TEXT, FORMAT_TIMESERIES, FORMAT_TIMESERIES_NDJSON, + }; + let normalized = crate::formats::normalize_format(output_format); + let format = crate::formats::apply_ndjson_modifier(&normalized, ctx.ndjson()); + match format.as_str() { + FORMAT_TEXT => { + let patterns: Vec<&str> = static_ids.iter().map(String::as_str).collect(); + let filter = static_filter(ctx).map_err(HandlerError::InvalidFlag)?; + let mut out = static_multi_text(&patterns, &ctx.path, &filter).ok_or_else(|| { + HandlerError::NoReport { + analyzer: static_ids.join(","), + format: format.clone(), + } + })?; + out.extend(history_formats::history_text(ctx, history_ids)?); + Ok(out) + } + FORMAT_COMPACT => { + let registry = default_registry(); + let mut out = Vec::new(); + for id in static_ids { + let entry = registry.lookup(id).ok_or_else(|| HandlerError::NoReport { + analyzer: id.clone(), + format: format.clone(), + })?; + out.extend((entry.run)(ctx, FORMAT_COMPACT)?); + } + out.extend(history_formats::history_compact(ctx, history_ids)?); + Ok(out) + } + FORMAT_TIMESERIES | FORMAT_TIMESERIES_NDJSON => { + eprintln!( + "warning: static analyzers have no per-commit time series; omitted: {}", + static_ids.join(", ") + ); + history_formats::history_timeseries( + ctx, + history_ids, + format == FORMAT_TIMESERIES_NDJSON, + ) + } + _ => render_combined(ctx, static_ids, history_ids, output_format), + } } -/// The reference `path.Match` semantics over an analyzer ID (`*`, `?`, `[...]`). +/// `path.Match` semantics over an analyzer ID (`*`, `?`, `[...]`; `*` does not +/// cross `/`). #[must_use] pub fn go_path_match(pattern: &str, name: &str) -> bool { go_path_match_inner(pattern.as_bytes(), name.as_bytes()) @@ -1197,62 +1532,53 @@ fn match_class(pat: &[u8], ch: u8) -> (bool, &[u8]) { } // --------------------------------------------------------------------------- -// Per-analyzer registry handlers. Each owns its own `match format`; this is the -// ONE place that knows how to format a given analyzer (reference: each analyzer's -// FormatReport* family). One handler per analyzer id — NOT one per (id,format). +// Walk filters shared by both phases. // --------------------------------------------------------------------------- -fn h_static_clones(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - let path = &ctx.path; - let filter = static_filter(ctx).ok()?; - // `format` is the resolved/normalized value: `--format bin` arrives here as - // `"binary"` (the reference `ValidateFormat` maps the `bin` alias to `binary`); accept - // both spellings for robustness. - match format { - "json" => { - static_clones::clones_report_json_flags(path, &filter, ctx.matches.get_flag("per-file")) - } - "yaml" => static_clones::clones_report_yaml(path, &filter), - "binary" | "bin" => static_clones::clones_report_bin(path, &filter), - "compact" => static_clones::clones_report_compact(path, &filter), - "text" => Some(section_render::render_text_report( - &static_clones::clones_report_value_flags(path, &filter, false)?, - )), - _ => None, +/// The vendor / generated path policy from the run flags +/// (`--include-vendored`, `--include-generated`, `--extra-excluded-prefixes`), +/// applied identically by both phases. +#[must_use] +pub fn path_policy_from_flags(sub: &clap::ArgMatches) -> cf_pathpolicy::Options { + let extra: Vec<String> = sub + .get_many::<String>("extra-excluded-prefixes") + .map(|vals| vals.filter(|p| !p.is_empty()).cloned().collect()) + .unwrap_or_default(); + cf_pathpolicy::Options { + include_vendored: sub.get_flag("include-vendored"), + include_generated: sub.get_flag("include-generated"), + extra_excluded_prefixes: extra, } } -/// The shared static-phase path-policy options from the run flags -/// (the reference `pathPolicyFromFlags`: `--include-vendored` / -/// `--include-generated`; `--extra-excluded-prefixes` is not exposed on `run`). +/// The static-phase path-policy options from the run flags. pub(crate) fn static_path_policy(ctx: &RunContext) -> cf_pathpolicy::Options { - cf_pathpolicy::Options { - include_vendored: ctx.matches.get_flag("include-vendored"), - include_generated: ctx.matches.get_flag("include-generated"), - ..cf_pathpolicy::Options::default() - } + path_policy_from_flags(ctx.matches) } +/// Number of leading bytes scanned for a generated-file marker (`DO NOT +/// EDIT`, `Code generated`, `@generated`, …). +pub const GENERATED_MARKER_HEADER_BYTES: usize = 512; + /// The static-phase file filter shared by EVERY output format of every static -/// analyzer, mirroring the reference `StaticService` walk callback: +/// analyzer: /// -/// 1. `matchesLanguageGlobs(path, svc.LanguageGlobs)` — the `--languages` -/// restriction, fnmatch globs over the file BASENAME (built from the flag -/// via `langpath.Globs`; `None` ⇔ empty/`all` → no restriction); -/// 2. `pathpolicy.Exclude(path, nil, svc.PathPolicy)` — the -/// `--include-vendored` / `--include-generated` policy. +/// 1. the `--languages` restriction — fnmatch globs over the file BASENAME +/// (`None` ⇔ empty/`all` → no restriction); +/// 2. the `--include-vendored` / `--include-generated` / +/// `--extra-excluded-prefixes` policy over the path and, through +/// [`StaticFilter::skips_with_header`], over the file's first +/// [`GENERATED_MARKER_HEADER_BYTES`] bytes. #[derive(Debug, Clone, Default)] pub struct StaticFilter { - /// Vendor / generated exclusion policy (`pathPolicyFromFlags`). + /// Vendor / generated exclusion policy. pub policy: cf_pathpolicy::Options, - /// Basename globs from `--languages`; `None` disables the filter - /// (the reference `LanguageGlobs == nil`). + /// Basename globs from `--languages`; `None` disables the filter. pub language_globs: Option<Vec<String>>, } impl StaticFilter { - /// Filter with the given path policy and no language restriction (the - /// pre-`--languages` behavior; used by legacy no-flag wrappers). + /// Filter with the given path policy and no language restriction. #[must_use] pub fn from_policy(policy: cf_pathpolicy::Options) -> Self { StaticFilter { @@ -1261,12 +1587,12 @@ impl StaticFilter { } } - /// Reports whether the walk must skip `path` — the reference walk callback's - /// `!matchesLanguageGlobs(...) || pathpolicy.Exclude(...)`. + /// Reports whether the walk must skip `path` on path evidence alone (the + /// language globs, then the path policy). #[must_use] pub fn skips(&self, path: &str) -> bool { if let Some(globs) = &self.language_globs { - // filepath.Base: the final path element. + // The final path element. let base = path.rsplit('/').next().unwrap_or(path); if !globs.iter().any(|g| go_path_match(g, base)) { return true; @@ -1274,16 +1600,30 @@ impl StaticFilter { } cf_pathpolicy::exclude(path, None, &self.policy) } + + /// [`StaticFilter::skips`] plus the generated-content marker over the + /// file's leading bytes (`header`, at most + /// [`GENERATED_MARKER_HEADER_BYTES`] are consulted): a file whose header + /// carries a generated-file marker is skipped unless + /// `--include-generated`. This is the check every static walk must apply + /// once it has read the file. + #[must_use] + pub fn skips_with_header(&self, path: &str, header: &[u8]) -> bool { + if self.skips(path) { + return true; + } + let head = &header[..header.len().min(GENERATED_MARKER_HEADER_BYTES)]; + cf_pathpolicy::exclude(path, Some(head), &self.policy) + } } -/// Builds the [`StaticFilter`] from the run flags: the path policy -/// (`static_path_policy`) plus the `--languages` globs (the reference -/// `applyStaticLanguageFilter` → `langpath.Globs`). +/// Builds the [`StaticFilter`] from the run flags: the path policy plus the +/// `--languages` globs. /// /// # Errors /// -/// Returns the reference error message (`static --languages: unknown language: -/// "<token>"`) when a `--languages` token resolves to no Linguist language. +/// Returns `static --languages: unknown language: "<token>"` when a +/// `--languages` token resolves to no Linguist language. pub(crate) fn static_filter(ctx: &RunContext) -> Result<StaticFilter, String> { let policy = static_path_policy(ctx); let tokens = language_tokens(ctx.matches); @@ -1298,34 +1638,21 @@ pub(crate) fn static_filter(ctx: &RunContext) -> Result<StaticFilter, String> { }) } -/// The history-phase change filter shared by EVERY history analyzer, -/// mirroring the reference `TreeDiffAnalyzer.shouldIncludeChange`: -/// -/// 1. `pathpolicy.Exclude(name, nil, t.PathPolicy)` — vendor / generated -/// exclusion, ALWAYS with the DEFAULT options: although the reference run -/// command wires `rc.buildPathPolicy()` into `TreeDiff.PathPolicy`, the -/// `--include-vendored` / `--include-generated` flags are empirically -/// INERT for the history run (oracle-verified: `.gitignore` + vendor-tree -/// changes on hercules/kubernetes produce byte-identical output with and -/// without the flags — the flag channel never reaches the pipeline); -/// 2. `t.checkLanguage(name, hash)` — the `--languages` restriction (enry -/// language detection on the BASENAME, with a blob-content fallback when -/// the path alone is undetermined), applied only when the set restricts -/// (the reference `!t.Languages[allLanguages]` gate). +/// The history-phase change filter shared by EVERY history analyzer: /// -/// The reference blacklist (`--blacklisted-prefixes`) and whitelist -/// (`--whitelist` regex) are not exposed on `codefang run`, so this struct -/// has no corresponding legs. +/// 1. the vendor / generated / extra-prefix policy from the run flags, over +/// the change's path and over the blob's leading +/// [`GENERATED_MARKER_HEADER_BYTES`] bytes (the generated-file marker); +/// 2. the `--languages` restriction — the full enry cascade over the basename +/// AND the blob content (so `.h` resolves to C/C++/Objective-C by content +/// and an extensionless script by its shebang), applied only when the set +/// restricts. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct HistoryFilter { - /// Vendor / generated exclusion policy — always the default (see the - /// struct doc: the reference history run ignores the include flags). + /// Vendor / generated exclusion policy from the run flags. pub policy: cf_pathpolicy::Options, /// Lowercased canonical Linguist names from `--languages`; `None` disables - /// the filter (flag absent / the `all` token — the reference - /// `Languages[allLanguages]`). An EMPTY set restricts everything away - /// (the reference `--languages ""` → an empty `Languages` map, so every - /// change's detected language misses the set). + /// the filter (flag absent, empty, or the `all` token). pub languages: Option<std::collections::BTreeSet<String>>, } @@ -1339,14 +1666,8 @@ impl HistoryFilter { } } - /// Reports whether one tree-diff change survives - /// `shouldIncludeChange`. `name`/`hash` are the `changeNameHash` pair the - /// caller derived (Delete → `from`, otherwise `to`). - /// - /// Language detection (`checkLanguage` parity): enry on the basename with - /// NO content first; only when that yields no language is the blob looked - /// up and enry re-run over its contents. An undetermined language - /// excludes the change. + /// Reports whether one tree-diff change survives the filter. `name`/`hash` + /// are the change's path and blob (Delete → `from`, otherwise `to`). #[must_use] pub fn includes_change( &self, @@ -1354,33 +1675,36 @@ impl HistoryFilter { name: &str, hash: cf_gitlib::hash::Hash, ) -> bool { - // shouldIncludeChange step 1: shared vendor / generated policy. + // 1. path policy on the path alone. if cf_pathpolicy::exclude(name, None, &self.policy) { return false; } - - // step 2: `!t.Languages[allLanguages]` → checkLanguage. - let Some(languages) = &self.languages else { + // The blob is read only when a content check is needed. + let needs_content = !self.policy.include_generated || self.languages.is_some(); + if !needs_content || hash.is_zero() { return true; + } + let Ok(blob) = repo.lookup_blob(hash) else { + return self.languages.is_none(); }; - // path.Base(fileName). - let base = name.rsplit('/').next().unwrap_or(name); - let mut lang = cf_langpath::language_by_path(base); - if lang.is_none() { - // Content detection: LookupBlob failure (incl. a zero hash) and an - // EMPTY blob both leave the language undetermined, exactly as the - // reference `len(contents) > 0` guard does. - if let Ok(blob) = cf_gitlib::blob::CachedBlob::from_repo(repo, hash) { - if !blob.data.is_empty() { - lang = cf_langpath::language_by_path_with_content(base, &blob.data); - } + let content = blob.content(); + if !self.policy.include_generated { + let head = &content[..content.len().min(GENERATED_MARKER_HEADER_BYTES)]; + if cf_pathpolicy::exclude(name, Some(head), &self.policy) { + return false; } } - lang.is_some_and(|l| languages.contains(l.to_lowercase().as_str())) + // 2. the `--languages` set, by basename + content. + let Some(languages) = &self.languages else { + return true; + }; + let base = name.rsplit('/').next().unwrap_or(name); + cf_langpath::language_by_path_with_content(base, content) + .is_some_and(|l| languages.contains(l.to_lowercase().as_str())) } - /// Retains only the changes `shouldIncludeChange` admits, deriving the - /// `changeNameHash` pair per change (Delete → `from`, otherwise `to`). + /// Retains only the changes the filter admits, deriving the path/blob per + /// change (Delete → `from`, otherwise `to`). pub fn retain_changes( &self, repo: &cf_gitlib::Repository, @@ -1397,30 +1721,22 @@ impl HistoryFilter { } } -/// Reads the `--languages` flag, applying the STATIC-phase collapse-to-empty -/// parity rule. -/// -/// cobra's `GetStringSlice` runs the raw value through encoding/csv, and -/// csv.Read on an empty string yields an EMPTY slice (no filter) — while -/// clap's value_delimiter yields one empty token, which would error as an -/// unknown language. A value like ",Go" still carries a non-empty token -/// alongside the empty one and errors on the empty token in BOTH -/// implementations, so only the all-empty case collapses. (The HISTORY phase -/// does NOT use this helper: for TreeDiff the explicit empty value configures -/// an EMPTY language set — restrict-everything — not no-filter.) -fn language_tokens(sub: &clap::ArgMatches) -> Vec<String> { +/// Reads the `--languages` tokens — the ONE parser both phases share: an +/// absent flag, an explicit empty value (`--languages ""`) and the `all` +/// token all mean "no restriction"; every other token is validated against +/// the Linguist tables by the caller. +#[must_use] +pub fn language_tokens(sub: &clap::ArgMatches) -> Vec<String> { let mut tokens: Vec<String> = sub .get_many::<String>("languages") .map(|vals| vals.cloned().collect()) .unwrap_or_default(); - if tokens.iter().all(String::is_empty) { - tokens.clear(); - } + tokens.retain(|t| !t.trim().is_empty()); tokens } /// The canonical lowercase language set for the validated `tokens`, or `None` -/// when the tokens do not restrict (empty / `all` — `wants_all`). +/// when the tokens do not restrict (empty / `all`). fn language_token_set(tokens: &[String]) -> Option<std::collections::BTreeSet<String>> { if tokens.is_empty() { return None; @@ -1429,8 +1745,7 @@ fn language_token_set(tokens: &[String]) -> Option<std::collections::BTreeSet<St for raw in tokens { let token = raw.trim(); if token.eq_ignore_ascii_case("all") { - // The reference `t.Languages[allLanguages] = true` disables the - // filter even alongside other tokens. + // `all` disables the filter even alongside other tokens. return None; } // globs() validated every token before this is called. @@ -1440,46 +1755,17 @@ fn language_token_set(tokens: &[String]) -> Option<std::collections::BTreeSet<St Some(set) } -/// Builds the [`HistoryFilter`] from the run flags: the DEFAULT path policy -/// (the reference history run ignores `--include-vendored` / -/// `--include-generated` — empirically byte-verified on hercules/kubernetes; -/// see the [`HistoryFilter`] docs) plus the `--languages` canonical-name set -/// (the reference `TreeDiffAnalyzer.Configure` → `applyLanguageConfig` → -/// `langpath.Globs`). -/// -/// Differences from the static [`static_filter`] semantics (both oracle- -/// verified): the flag being ABSENT means no restriction, while an explicit -/// EMPTY value (`--languages ""`) restricts everything away — cobra's csv -/// read of `""` yields an empty slice, so the reference `Languages` map is -/// configured EMPTY (no `all` key) and `checkLanguage` rejects every change. +/// Builds the [`HistoryFilter`] from the run flags: the shared path policy +/// plus the `--languages` canonical-name set. /// /// # Errors /// -/// Returns the reference error message (`failed to configure TreeDiff: -/// tree-diff pathspec: unknown language: "<token>"`) when a `--languages` -/// token resolves to no Linguist language. +/// Returns `history --languages: unknown language: "<token>"` when a +/// `--languages` token resolves to no Linguist language. pub(crate) fn history_filter(sub: &clap::ArgMatches) -> Result<HistoryFilter, String> { - let policy = cf_pathpolicy::Options::default(); - let Some(vals) = sub.get_many::<String>("languages") else { - // Flag absent: Languages == {"all"} → no restriction. - return Ok(HistoryFilter { - policy, - languages: None, - }); - }; - let tokens: Vec<String> = vals.cloned().collect(); - if tokens.iter().all(String::is_empty) { - // Explicit empty value: cobra csv.Read("") → EMPTY slice → the config - // EXISTS with no tokens → Languages = {} (no `all`) → every change's - // detected language misses the set. clap yields one empty token here, - // so construct the empty-set restriction explicitly. - return Ok(HistoryFilter { - policy, - languages: Some(std::collections::BTreeSet::new()), - }); - } - let globs = cf_langpath::globs(&tokens) - .map_err(|e| format!("failed to configure TreeDiff: tree-diff pathspec: {e}"))?; + let policy = path_policy_from_flags(sub); + let tokens = language_tokens(sub); + let globs = cf_langpath::globs(&tokens).map_err(|e| format!("history --languages: {e}"))?; Ok(HistoryFilter { policy, languages: if globs.wants_all { @@ -1490,354 +1776,295 @@ pub(crate) fn history_filter(sub: &clap::ArgMatches) -> Result<HistoryFilter, St }) } -fn h_static_complexity(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - let path = &ctx.path; - // `format` is the resolved/normalized format from `resolve_formats`, where the - // `bin` CLI alias has already been normalized to `binary` (formats.rs - // `normalize_format`). Match the normalized name so `--format bin` dispatches - // to the CFB1 envelope builder rather than falling through to `None`. - let filter = static_filter(ctx).ok()?; - match format { - "json" => static_complexity::complexity_report_flags( - path, - &filter, - ctx.matches.get_flag("per-file"), - ), - "yaml" => static_complexity_yaml::complexity_report_yaml(path, &filter), - "binary" | "bin" => static_complexity_bin::complexity_report_bin(path, &filter), - "compact" => Some(section_render::render_compact_report( - &static_complexity::complexity_report_value_summary(path, &filter)?, - )), - "text" => Some(section_render::render_text_report( - &static_complexity::complexity_report_value_summary(path, &filter)?, - )), - _ => None, +// --------------------------------------------------------------------------- +// Per-analyzer registry handlers. Each owns its own `match format`; this is the +// ONE place that knows how to format a given analyzer. One handler per +// analyzer id — NOT one per (id,format). +// --------------------------------------------------------------------------- + +/// The shared static dispatch shape: build the walk filter, run the +/// per-format report builder, map a missing report to [`HandlerError`]. +fn static_dispatch( + ctx: &RunContext, + id: &str, + format: &str, + build: impl FnOnce(&str, &StaticFilter) -> Option<Option<Vec<u8>>>, +) -> Result<Vec<u8>, HandlerError> { + let filter = static_filter(ctx).map_err(HandlerError::InvalidFlag)?; + match build(&ctx.path, &filter) { + Some(bytes) => report_or_err(bytes, id, format), + None => Err(format_not_supported(format, id)), } } -fn h_static_cohesion(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - let path = &ctx.path; - // `format` is the resolved/normalized format from `resolve_formats`, where the - // `bin` CLI alias has already been normalized to `binary` (formats.rs - // `normalize_format`). Match the normalized name (accept the raw alias too). - let filter = static_filter(ctx).ok()?; - match format { - "json" => static_cohesion::cohesion_report_json_flags( - path, - &filter, - ctx.matches.get_flag("per-file"), - ), - "yaml" => static_cohesion::cohesion_report_yaml(path, &filter), - "binary" | "bin" => static_cohesion::cohesion_report_bin(path, &filter), - "compact" => Some(section_render::render_compact_report( - &static_cohesion::cohesion_report_value_summary(path, &filter)?, - )), - "text" => Some(section_render::render_text_report( - &static_cohesion::cohesion_report_value_summary(path, &filter)?, +fn h_static_clones(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "static/clones"; + let per_file = ctx.matches.get_flag("per-file"); + static_dispatch(ctx, ID, format, |path, filter| match format { + "json" => Some(static_clones::clones_report_json_flags( + path, filter, per_file, )), + "yaml" => Some(static_clones::clones_report_yaml(path, filter)), + "binary" | "bin" => Some(static_clones::clones_report_bin(path, filter)), + "compact" => Some(static_clones::clones_report_compact(path, filter)), + "text" => Some( + static_clones::clones_report_value_flags(path, filter, false) + .map(|v| section_render::render_text_report(&v)), + ), _ => None, - } + }) } -fn h_static_composition(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - let path = &ctx.path; - // The reference static walk filters files through the language globs + - // pathpolicy.Exclude with the RUN flags (`--languages`, - // `--include-vendored` / `--include-generated`), so every encoding of the - // aggregated report must honor them. - let filter = static_filter(ctx).ok()?; - match format { - "json" => static_json::composition_report_opts_flags( - path, - &filter, - ctx.matches.get_flag("per-file"), - ), - "yaml" => static_json::composition_yaml_opts(path, &filter), - // The pipeline resolves the `bin` alias to the canonical `binary` - // (formats::normalize_format) before dispatch, so match that; accept the - // raw alias too for direct callers. - "binary" | "bin" => static_json::composition_bin_opts(path, &filter), - "compact" => Some(section_render::render_compact_report( - &static_json::composition_report_value_opts(path, &filter)?, - )), - "text" => Some(section_render::render_text_report( - &static_json::composition_report_value_opts(path, &filter)?, +fn h_static_complexity(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "static/complexity"; + let per_file = ctx.matches.get_flag("per-file"); + static_dispatch(ctx, ID, format, |path, filter| match format { + "json" => Some(static_complexity::complexity_report_flags( + path, filter, per_file, )), + "yaml" => Some(static_complexity_yaml::complexity_report_yaml(path, filter)), + "binary" | "bin" => Some(static_complexity_bin::complexity_report_bin(path, filter)), + "compact" => Some( + static_complexity::complexity_report_value_summary(path, filter) + .map(|v| section_render::render_compact_report(&v)), + ), + "text" => Some( + static_complexity::complexity_report_value_summary(path, filter) + .map(|v| section_render::render_text_report(&v)), + ), _ => None, - } + }) } -fn h_static_halstead(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - let path = &ctx.path; - // `format` is the resolved/normalized format from `resolve_formats`, where the - // `bin` CLI alias has already been normalized to `binary` (formats.rs - // `normalize_format`). Match the normalized name so `--format bin` dispatches - // to the CFB1 envelope builder rather than falling through to `None`. - let filter = static_filter(ctx).ok()?; - match format { - "json" => static_halstead::halstead_json_report_flags( - path, - &filter, - ctx.matches.get_flag("per-file"), - ), - "yaml" => static_halstead::halstead_yaml_report(path, &filter), - "binary" => static_halstead::halstead_bin_report(path, &filter), - "compact" => Some(section_render::render_compact_report( - &static_halstead::halstead_report_value_summary(path, &filter)?, - )), - "text" => Some(section_render::render_text_report( - &static_halstead::halstead_report_value_summary(path, &filter)?, +fn h_static_cohesion(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "static/cohesion"; + let per_file = ctx.matches.get_flag("per-file"); + static_dispatch(ctx, ID, format, |path, filter| match format { + "json" => Some(static_cohesion::cohesion_report_json_flags( + path, filter, per_file, )), + "yaml" => Some(static_cohesion::cohesion_report_yaml(path, filter)), + "binary" | "bin" => Some(static_cohesion::cohesion_report_bin(path, filter)), + "compact" => Some( + static_cohesion::cohesion_report_value_summary(path, filter) + .map(|v| section_render::render_compact_report(&v)), + ), + "text" => Some( + static_cohesion::cohesion_report_value_summary(path, filter) + .map(|v| section_render::render_text_report(&v)), + ), _ => None, - } + }) } -fn h_static_imports(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - let path = &ctx.path; - // `format` is the resolved/normalized format from `resolve_formats`, where the - // `bin` CLI alias has already been normalized to `binary` (formats.rs - // `normalize_format`). Match the normalized name so `--format bin` dispatches - // to the CFB1 envelope builder rather than falling through to `None`. - let filter = static_filter(ctx).ok()?; - match format { - "json" => static_imports::imports_report_json_flags( - path, - &filter, - ctx.matches.get_flag("per-file"), - ), - "yaml" => static_imports::imports_report_yaml(path, &filter), - "binary" => static_imports::imports_report_bin(path, &filter), - "compact" => Some(section_render::render_compact_report( - &static_imports::imports_report_value_flags(path, &filter, false)?, - )), - "text" => Some(section_render::render_text_report( - &static_imports::imports_report_value_flags(path, &filter, false)?, +fn h_static_composition(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "static/composition"; + let per_file = ctx.matches.get_flag("per-file"); + static_dispatch(ctx, ID, format, |path, filter| match format { + "json" => Some(static_json::composition_report_opts_flags( + path, filter, per_file, )), + "yaml" => Some(static_json::composition_yaml_opts(path, filter)), + "binary" | "bin" => Some(static_json::composition_bin_opts(path, filter)), + "compact" => Some( + static_json::composition_report_value_opts(path, filter) + .map(|v| section_render::render_compact_report(&v)), + ), + "text" => Some( + static_json::composition_report_value_opts(path, filter) + .map(|v| section_render::render_text_report(&v)), + ), _ => None, - } + }) } -fn h_static_comments(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - let path = &ctx.path; - let filter = static_filter(ctx).ok()?; - match format { - "json" => static_comments::comments_report_json_flags( - path, - &filter, - ctx.matches.get_flag("per-file"), +fn h_static_halstead(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "static/halstead"; + let per_file = ctx.matches.get_flag("per-file"); + static_dispatch(ctx, ID, format, |path, filter| match format { + "json" => Some(static_halstead::halstead_json_report_flags( + path, filter, per_file, + )), + "yaml" => Some(static_halstead::halstead_yaml_report(path, filter)), + "binary" | "bin" => Some(static_halstead::halstead_bin_report(path, filter)), + "compact" => Some( + static_halstead::halstead_report_value_summary(path, filter) + .map(|v| section_render::render_compact_report(&v)), ), - "yaml" => static_comments::comments_report_yaml(path, &filter), - // The pipeline resolves the `bin` alias to the canonical `binary` - // (formats::normalize_format) before dispatch, so match that; accept the - // raw alias too for direct callers. - "binary" | "bin" => static_comments::comments_report_bin(path, &filter), - "compact" => Some(section_render::render_compact_report( - &static_comments::comments_report_value_summary(path, &filter)?, + "text" => Some( + static_halstead::halstead_report_value_summary(path, filter) + .map(|v| section_render::render_text_report(&v)), + ), + _ => None, + }) +} + +fn h_static_imports(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "static/imports"; + let per_file = ctx.matches.get_flag("per-file"); + static_dispatch(ctx, ID, format, |path, filter| match format { + "json" => Some(static_imports::imports_report_json_flags( + path, filter, per_file, )), - "text" => Some(section_render::render_text_report( - &static_comments::comments_report_value_summary(path, &filter)?, + "yaml" => Some(static_imports::imports_report_yaml(path, filter)), + "binary" | "bin" => Some(static_imports::imports_report_bin(path, filter)), + "compact" => Some( + static_imports::imports_report_value_flags(path, filter, false) + .map(|v| section_render::render_compact_report(&v)), + ), + "text" => Some( + static_imports::imports_report_value_flags(path, filter, false) + .map(|v| section_render::render_text_report(&v)), + ), + _ => None, + }) +} + +fn h_static_comments(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "static/comments"; + let per_file = ctx.matches.get_flag("per-file"); + static_dispatch(ctx, ID, format, |path, filter| match format { + "json" => Some(static_comments::comments_report_json_flags( + path, filter, per_file, )), + "yaml" => Some(static_comments::comments_report_yaml(path, filter)), + "binary" | "bin" => Some(static_comments::comments_report_bin(path, filter)), + "compact" => Some( + static_comments::comments_report_value_summary(path, filter) + .map(|v| section_render::render_compact_report(&v)), + ), + "text" => Some( + static_comments::comments_report_value_summary(path, filter) + .map(|v| section_render::render_text_report(&v)), + ), _ => None, - } + }) } -fn h_history_imports(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - // `timeseries+ndjson` (the `--ndjson` modifier) is NOT an encoding of the - // report value: the reference implementation streams per-commit lines - // through the per-chunk TimeSeriesChunkFlusher (DrainCommitStats), so it - // has its own emitter over the same memoized walk. - if format == "timeseries+ndjson" { - return history::imports_timeseries_ndjson(ctx.matches); - } - // One report value, encoded per format by the shared history serializer - //. The YAML section header is the reference implementation - // analyzer Name() (`imports.HistoryAnalyzer.Name` == "ImportsPerDeveloper"). - let metrics = history::imports_run_metrics(ctx.matches)?; +fn h_history_imports(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/imports"; + let metrics = + history::imports_run_metrics(ctx.matches).ok_or_else(|| HandlerError::NoReport { + analyzer: ID.to_string(), + format: format.to_string(), + })?; serialize_history_metrics( format, + ID, "ImportsPerDeveloper", &metrics.to_go_value(), &metrics.to_go_value_yaml(), ) } -fn h_history_typos(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - match format { +fn h_history_typos(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/typos"; + let bytes = match format { "json" => history::typos_run_report(ctx.matches), "yaml" => history::typos_run_report_yaml(ctx.matches), - "binary" => history::typos_run_report_bin(ctx.matches), - _ => None, - } -} - -fn h_history_couples(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - // One report value (the reference `ComputedMetrics`, behind ToJSON/ToYAML); every - // machine format is a serializer over it. ToJSON == ToYAML for couples, so - // json_value and yaml_value share the same tree. The YAML section name is - // the analyzer's the reference `Name()` ("Couples"). - let value = couples_run::couples_run_value(ctx.matches)?; - serialize_history_metrics(format, "Couples", &value, &value) -} - -fn h_history_shotness(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - // One report value (the reference `ComputedMetrics`, the value behind ToJSON/ToYAML); - // every machine format is just a serializer over it. ToJSON == ToYAML for - // shotness, so json_value and yaml_value share `to_go_value()`. The YAML - // section name is the analyzer's the reference `Name()` ("Shotness"). - let metrics = shotness_run::shotness_run_metrics(ctx.matches)?; + "binary" | "bin" => history::typos_run_report_bin(ctx.matches), + _ => return Err(format_not_supported(format, ID)), + }; + report_or_err(bytes, ID, format) +} + +fn h_history_couples(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/couples"; + let value = + couples_run::couples_run_value(ctx.matches).ok_or_else(|| HandlerError::NoReport { + analyzer: ID.to_string(), + format: format.to_string(), + })?; + serialize_history_metrics(format, ID, "Couples", &value, &value) +} + +fn h_history_shotness(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/shotness"; + let metrics = + shotness_run::shotness_run_metrics(ctx.matches).ok_or_else(|| HandlerError::NoReport { + analyzer: ID.to_string(), + format: format.to_string(), + })?; let value = metrics.to_go_value(); - serialize_history_metrics(format, "Shotness", &value, &value) + serialize_history_metrics(format, ID, "Shotness", &value, &value) } -fn h_history_devs(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { +fn h_history_devs(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/devs"; let sub = ctx.matches; - let head = ctx.head(); - match (format, head) { - ("json", true) => history::devs_head_report(sub), - ("json", false) => history::devs_run_report(sub), - ("yaml", true) => history::devs_head_report_yaml(sub), - ("yaml", false) => history::devs_run_report_yaml(sub), - ("timeseries+ndjson", false) => history::devs_run_timeseries_ndjson(sub), - // The pipeline resolves the `bin` alias to canonical `binary` - // (formats::normalize_format) before dispatch; accept the raw alias too - // for direct callers. - ("binary" | "bin", false) => history::devs_run_report_bin(sub), - ("binary" | "bin", true) => { - let metrics = history::devs_head_metrics(sub)?; - let payload = cf_devs::serialize::computed_metrics_to_go(&metrics); - cf_reportutil::encode_binary_envelope(&payload).ok() - } - _ => None, - } + let bytes = match format { + "json" => history::devs_run_report(sub), + "yaml" => history::devs_run_report_yaml(sub), + "binary" | "bin" => history::devs_run_report_bin(sub), + _ => return Err(format_not_supported(format, ID)), + }; + report_or_err(bytes, ID, format) } -fn h_history_anomaly(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { +fn h_history_anomaly(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { use cf_anomaly::model::ToGoValue; - if ctx.head() { - // Closed-form merge-HEAD path (analyzer's deterministic head case): ONE - // report value, every machine format an encoding of it via the shared - // serializer — so the combined `*` model can request `binary` here too. - let metrics = history::anomaly_head_report(ctx.matches)?; - let value = metrics.to_go_value(); - return serialize_history_metrics(format, "TemporalAnomaly", &value, &value); - } - // Full revwalk (no --head): one report value (reference `ComputeAllMetrics` → - // ComputedMetrics), every machine format an encoding of it via the shared - // history serializer. ToJSON == ToYAML for anomaly, so json/yaml share the - // same GoValue. The YAML section name is the analyzer's the reference `Name()` - // ("TemporalAnomaly"). - let metrics = history::anomaly_run_metrics(ctx.matches)?; + const ID: &str = "history/anomaly"; + let metrics = + history::anomaly_run_metrics(ctx.matches).ok_or_else(|| HandlerError::NoReport { + analyzer: ID.to_string(), + format: format.to_string(), + })?; let value = metrics.to_go_value(); - serialize_history_metrics(format, "TemporalAnomaly", &value, &value) -} - -fn h_history_quality(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - // `timeseries+ndjson` (the `--ndjson` modifier) is NOT an encoding of the - // report value: the reference implementation streams per-commit lines - // through the per-chunk TimeSeriesChunkFlusher (DrainCommitStats), so it - // has its own emitter over the same memoized walk. - if format == "timeseries+ndjson" { - return history::quality_timeseries_ndjson(ctx.matches); - } - // `--head` is handled inside `quality_metrics` (single HEAD-commit window), - // so every format is the same encoding of one computed value — including the - // `binary` payload the combined `*` model gathers. - // One computed report value, three encodings routed - // through the shared serializer (reference: FormatReportJSON/YAML/Binary): json/bin - // marshal the encoding/json value tree; yaml wraps the same struct-origin - // value tree in the `codefang (v2)` envelope under `history/quality:`. - let metrics = history::quality_metrics(ctx.matches)?; + serialize_history_metrics(format, ID, "TemporalAnomaly", &value, &value) +} + +fn h_history_quality(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/quality"; + let metrics = history::quality_metrics(ctx.matches).ok_or_else(|| HandlerError::NoReport { + analyzer: ID.to_string(), + format: format.to_string(), + })?; let value = cf_quality::serialize::computed_metrics_value(&metrics); - serialize_history_metrics(format, "history/quality", &value, &value) + serialize_history_metrics(format, ID, ID, &value, &value) } -fn h_history_sentiment(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { +fn h_history_sentiment(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { use cf_sentiment::ToGoValue; - // `timeseries+ndjson` (the `--ndjson` modifier) is NOT an encoding of the - // report value: the reference implementation streams per-commit lines - // through the per-chunk TimeSeriesChunkFlusher (DrainCommitStats), so it - // has its own emitter over the same memoized walk. - if format == "timeseries+ndjson" { - return history::sentiment_timeseries_ndjson(ctx.matches); - } - // `--head` is handled inside `sentiment_metrics` (single HEAD-commit window), - // so every format (including the combined `*` model's `binary`) is one - // encoding of the same computed value. - // One computed report value, three encodings (reference `ComputeAllMetrics` → - // FormatReportJSON/YAML/Binary): json/bin marshal the encoding/json value - // tree (nil slice → null); yaml wraps the yaml.v3 value tree (nil → []) in - // the `codefang (v2)` envelope. Routed through the shared serializer so every - // format follows the one computation (same path as the other history leaves). - let metrics = history::sentiment_metrics(ctx.matches)?; + const ID: &str = "history/sentiment"; + let metrics = + history::sentiment_metrics(ctx.matches).ok_or_else(|| HandlerError::NoReport { + analyzer: ID.to_string(), + format: format.to_string(), + })?; serialize_history_metrics( format, - "history/sentiment", + ID, + ID, &metrics.to_go_value(), &metrics.to_go_value_yaml(), ) } -fn h_history_file_history(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { - // One computed report value, three machine - // encodings (json/bin/yaml). The crate's `computed_metrics_to_go` is the - // single `ToJSON`/`ToYAML` value tree (file_history's ToJSON == ToYAML); - // route it through the shared history-metrics serializer so all formats are - // encodings of THE SAME value. The YAML - // section header is the analyzer's Name(): `FileHistoryAnalysis`. - // `timeseries+ndjson` (the `--ndjson` modifier) is NOT an encoding of the - // report value: the reference implementation streams per-commit lines - // through the per-chunk TimeSeriesChunkFlusher (DrainCommitStats), so it - // has its own emitter over the same walk (`--head` loads the single HEAD - // commit inside `file_history_run`, matching the reference head window). - if format == "timeseries+ndjson" { - return history::file_history_timeseries_ndjson(ctx.matches); - } - let value = history::file_history_report_value(ctx.matches)?; - serialize_history_metrics(format, "FileHistoryAnalysis", &value, &value) +fn h_history_file_history(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/file-history"; + let value = + history::file_history_report_value(ctx.matches).ok_or_else(|| HandlerError::NoReport { + analyzer: ID.to_string(), + format: format.to_string(), + })?; + serialize_history_metrics(format, ID, "FileHistoryAnalysis", &value, &value) } -fn h_history_burndown(ctx: &RunContext, format: &str) -> Option<Vec<u8>> { +fn h_history_burndown(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError> { + const ID: &str = "history/burndown"; let sub = ctx.matches; - let head = ctx.head(); - let ndjson = ctx.ndjson(); - match (format, head, ndjson) { - ("timeseries", true, false) => history::burndown_head_timeseries(sub), - ("timeseries+ndjson", false, _) => burndown_ndjson::burndown_timeseries_ndjson(sub), - ("ndjson", false, _) => burndown_ndjson::burndown_record_ndjson(sub), - ("json", false, _) => burndown_ndjson::burndown_run_report(sub), - ("yaml", false, _) => burndown_ndjson::burndown_run_report_yaml(sub), - ("binary" | "bin", false, _) => burndown_ndjson::burndown_run_report_bin(sub), - ("json" | "yaml" | "binary" | "bin", true, _) => { - let metrics = history::burndown_head_metrics(sub)?; - let bytes = match format { - "json" => cf_gojson::marshal(&metrics.to_go_value()), - "binary" | "bin" => { - cf_reportutil::encode_binary_envelope(&metrics.to_go_value()).ok()? - } - _ => { - let mut out = Vec::new(); - out.extend_from_slice(b"codefang (v2):\n"); - out.extend_from_slice( - format!(" version: {}\n", cf_version::DEFAULT_BINARY).as_bytes(), - ); - out.extend_from_slice( - format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes(), - ); - out.extend_from_slice(b"history/burndown:\n"); - out.extend_from_slice(&cf_goyaml::marshal(&metrics.to_go_value_yaml())); - out - } - }; - Some(bytes) - } - _ => None, - } + let bytes = match format { + "json" => burndown_ndjson::burndown_run_report(sub), + "yaml" => burndown_ndjson::burndown_run_report_yaml(sub), + "binary" | "bin" => burndown_ndjson::burndown_run_report_bin(sub), + _ => return Err(format_not_supported(format, ID)), + }; + report_or_err(bytes, ID, format) } -/// Builds the single default analyzer [`Registry`] — the Rust analogue of the reference implementation -/// `defaultRegistry()` (`analyze.NewRegistry(defaultUASTAnalyzers, -/// defaultRawFileAnalyzers, defaultHistoryLeaves)`). One registry insertion per -/// analyzer; dispatch is a keyed lookup by id, not a per-format match ladder. +/// Builds the single default analyzer [`Registry`]. One registry insertion +/// per analyzer; dispatch is a keyed lookup by id, not a per-format match +/// ladder. #[must_use] pub fn default_registry() -> Registry { let mut r = Registry::new(); @@ -1909,8 +2136,7 @@ mod static_filter_tests { #[test] fn skips_matches_language_globs_on_basename() { - // The reference matchesLanguageGlobs runs fnmatch over filepath.Base, - // so a `*.go` glob must match a NESTED path's basename. + // A `*.go` glob must match a NESTED path's basename. let filter = StaticFilter { policy: permissive_policy(), language_globs: Some(vec!["*.go".to_string()]), @@ -1921,9 +2147,6 @@ mod static_filter_tests { #[test] fn skips_language_filter_applies_before_path_policy() { - // A language-mismatched path is skipped even under the fully - // permissive policy: the glob leg decides first, independent of the - // vendored/generated leg. let filter = StaticFilter { policy: permissive_policy(), language_globs: Some(vec!["*.py".to_string()]), @@ -1940,8 +2163,6 @@ mod static_filter_tests { #[test] fn skips_none_globs_means_no_language_restriction() { - // `LanguageGlobs == nil` (flag absent / `all`): only the path policy - // decides. let filter = StaticFilter { policy: permissive_policy(), language_globs: None, @@ -1962,6 +2183,19 @@ mod static_filter_tests { assert!(filter.skips("a.py")); } + /// The generated-content marker in the file header excludes the file + /// under the default policy and is ignored with `--include-generated`. + #[test] + fn skips_with_header_applies_the_generated_marker() { + let marked = b"// Code generated by protoc-gen-go. DO NOT EDIT.\npackage main\n"; + let plain = b"package main\n"; + let default = StaticFilter::default(); + assert!(default.skips_with_header("a.go", marked)); + assert!(!default.skips_with_header("a.go", plain)); + let permissive = StaticFilter::from_policy(permissive_policy()); + assert!(!permissive.skips_with_header("a.go", marked)); + } + #[test] fn static_filter_parses_languages_into_basename_globs() { let filter = filter_for(&["--languages", "Go"]).expect("Go resolves"); @@ -1976,8 +2210,6 @@ mod static_filter_tests { #[test] fn static_filter_all_token_disables_the_restriction() { - // The `all` sentinel (langpath wants_all) maps to `None` — the same - // no-restriction state as an absent flag. let filter = filter_for(&["--languages", "all"]).expect("all resolves"); assert!(filter.language_globs.is_none()); let filter = filter_for(&[]).expect("absent flag resolves"); @@ -1993,6 +2225,18 @@ mod static_filter_tests { "error must carry the CLI contract wording, got {err:?}" ); } + + /// `--extra-excluded-prefixes` reaches the path policy of both phases. + #[test] + fn extra_excluded_prefixes_reach_the_policy() { + let filter = filter_for(&["--extra-excluded-prefixes", "gen/,build/"]).expect("parses"); + assert_eq!( + filter.policy.extra_excluded_prefixes, + vec!["gen/".to_string(), "build/".to_string()] + ); + assert!(filter.skips("gen/a.go")); + assert!(!filter.skips("src/a.go")); + } } #[cfg(test)] @@ -2023,8 +2267,6 @@ mod per_file_multi_tests { ) .unwrap(); let json = String::from_utf8(bytes).unwrap(); - // Every merged section gets a files key — the retaining analyzers with - // real entries, clones with the initialized empty array. let sections = json.matches("\"title\"").count(); let files_keys = json.matches("\"files\"").count(); assert_eq!( @@ -2053,16 +2295,388 @@ mod per_file_multi_tests { "files key must be omitted:\n{json}" ); } + + /// The merged `overall_score` reads a report's top-level score. + fn overall_score(report: &GoValue) -> f64 { + match report.as_map().and_then(|m| m.get("overall_score")) { + Some(GoValue::Float(f)) => *f, + other => panic!("overall_score missing: {other:?}"), + } + } + + /// Info-only sections (imports, composition: score `-1`) are left out of + /// the executive-summary average instead of dragging it toward `-1`; an + /// all-info selection is `-1` / `Info`. + #[test] + fn info_only_sections_do_not_drag_the_overall_score() { + let dir = fixture(); + let path = dir.path().to_str().unwrap(); + let filter = StaticFilter::default(); + let scored = static_multi_report_value(&["static/complexity"], path, &filter, false) + .expect("complexity report"); + let mixed = static_multi_report_value( + &["static/complexity", "static/imports", "static/composition"], + path, + &filter, + false, + ) + .expect("mixed report"); + assert!( + overall_score(&scored) >= 0.0, + "complexity is a scored section" + ); + assert!( + (overall_score(&mixed) - overall_score(&scored)).abs() < f64::EPSILON, + "info-only sections changed the average: {} vs {}", + overall_score(&mixed), + overall_score(&scored) + ); + let info_only = static_multi_report_value( + &["static/imports", "static/composition"], + path, + &filter, + false, + ) + .expect("info report"); + assert!((overall_score(&info_only) - (-1.0)).abs() < f64::EPSILON); + assert_eq!( + info_only + .as_map() + .and_then(|m| m.get("overall_score_label")) + .and_then(GoValue::as_str), + Some("Info") + ); + } +} + +#[cfg(test)] +mod per_file_note_tests { + use super::per_file_ignored_note; + + #[test] + fn note_names_every_non_json_format_and_is_silent_for_json() { + for format in ["yaml", "bin", "binary", "text", "compact", "plot"] { + assert_eq!( + per_file_ignored_note(true, true, format), + Some(format!( + "--per-file applies to --format json only; ignored for {format}" + )) + ); + } + assert_eq!(per_file_ignored_note(true, true, "json"), None); + assert_eq!(per_file_ignored_note(true, true, " JSON "), None); + } + + #[test] + fn note_needs_the_flag_and_a_static_selection() { + assert_eq!(per_file_ignored_note(true, false, "yaml"), None); + assert_eq!(per_file_ignored_note(false, true, "yaml"), None); + } +} + +#[cfg(test)] +mod static_walk_tests { + //! The ONE static walk: file selection, reading, stamping and the skip + //! registry, exercised on real tempdir trees. + + use super::*; + + const PLAIN_GO: &str = "package main\n\nfunc f() {}\n"; + const MARKED_GO: &str = + "// Code generated by protoc-gen-go. DO NOT EDIT.\npackage main\n\nfunc g() {}\n"; + + fn write(dir: &Path, rel: &str, content: &[u8]) { + let path = dir.join(rel); + std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir"); + std::fs::write(path, content).expect("write fixture"); + } + + fn permissive() -> StaticFilter { + StaticFilter::from_policy(cf_pathpolicy::Options { + include_vendored: true, + include_generated: true, + ..cf_pathpolicy::Options::default() + }) + } + + /// The admitted root-relative paths of the source walk, in walk order. + fn source_paths(root: &Path, filter: &StaticFilter) -> Vec<String> { + let parser = Parser::new(); + walk_source_files(root.to_str().unwrap(), &parser, filter) + .expect("root exists") + .map(|f| f.rel_path) + .collect() + } + + /// The admitted root-relative paths of the all-files walk, in walk order. + fn all_paths(root: &Path, filter: &StaticFilter) -> Vec<String> { + walk_all_files(root.to_str().unwrap(), filter) + .expect("root exists") + .map(|f| f.rel_path) + .collect() + } + + #[test] + fn file_root_yields_the_file_stamped_with_its_name() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "pkg/a.go", PLAIN_GO.as_bytes()); + let file = dir.path().join("pkg/a.go"); + let parser = Parser::new(); + let files: Vec<SourceFile> = + walk_source_files(file.to_str().unwrap(), &parser, &permissive()) + .expect("file root") + .collect(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].rel_path, "a.go"); + assert_eq!(files[0].path, file); + assert_eq!(files[0].bytes, PLAIN_GO.as_bytes()); + assert_eq!(files[0].language, "go"); + assert_eq!(files[0].directory(), "."); + } + + #[test] + fn nested_directories_walk_lexically_and_skip_git_and_cache_dirs() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "b.go", PLAIN_GO.as_bytes()); + write(dir.path(), "a/z.go", PLAIN_GO.as_bytes()); + write(dir.path(), "a/y.go", PLAIN_GO.as_bytes()); + write(dir.path(), ".git/x.go", PLAIN_GO.as_bytes()); + write( + dir.path(), + "target/CACHEDIR.TAG", + b"Signature: 8a477f597d28d172789f06886806bc55\n", + ); + write(dir.path(), "target/t.go", PLAIN_GO.as_bytes()); + write(dir.path(), "notes.txt", b"not source\n"); + assert_eq!( + source_paths(dir.path(), &permissive()), + vec!["a/y.go", "a/z.go", "b.go"] + ); + let parser = Parser::new(); + let nested = walk_source_files(dir.path().to_str().unwrap(), &parser, &permissive()) + .unwrap() + .next() + .unwrap(); + assert_eq!(nested.directory(), "a"); + // The all-files walk skips the same directories. + assert_eq!( + all_paths(dir.path(), &StaticFilter::default()), + vec!["a/y.go", "a/z.go", "b.go", "notes.txt"] + ); + } + + #[cfg(unix)] + #[test] + fn symlink_loop_terminates_and_symlinked_dirs_are_never_files() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "sub/c.go", PLAIN_GO.as_bytes()); + // sub/loop -> .. (the root): following it would recurse forever. + std::os::unix::fs::symlink("..", dir.path().join("sub/loop")).unwrap(); + // A symlink to a regular file is read through. + std::os::unix::fs::symlink("sub/c.go", dir.path().join("link.go")).unwrap(); + // A dangling symlink contributes nothing. + std::os::unix::fs::symlink("nowhere.go", dir.path().join("dangling.go")).unwrap(); + assert_eq!( + source_paths(dir.path(), &permissive()), + vec!["link.go", "sub/c.go"] + ); + assert_eq!( + all_paths(dir.path(), &StaticFilter::default()), + vec!["link.go", "sub/c.go"], + "a symlinked directory must not be counted as a file" + ); + } + + #[test] + fn oversized_file_is_skipped_and_recorded_once() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", PLAIN_GO.as_bytes()); + let big = dir.path().join("big.go"); + // A sparse file: the size is what the cap reads, no bytes are written. + std::fs::File::create(&big) + .unwrap() + .set_len(MAX_STATIC_FILE_SIZE + 1) + .unwrap(); + assert!(!was_skipped(&big)); + assert_eq!(source_paths(dir.path(), &permissive()), vec!["a.go"]); + assert!(was_skipped(&big), "the oversized file must be recorded"); + // A second walk skips it again without re-recording (or re-printing) + // it: the registry already holds the path. + assert_eq!(source_paths(dir.path(), &permissive()), vec!["a.go"]); + assert!( + !note_skipped(&big, SkipReason::Oversized), + "a repeat skip must not be recorded (or warned about) twice" + ); + // The all-files walk still classifies it (header only). + let all = walk_all_files(dir.path().to_str().unwrap(), &StaticFilter::default()) + .unwrap() + .find(|f| f.rel_path == "big.go") + .expect("composition sees the oversized file"); + assert_eq!(all.bytes.len(), ALL_FILES_HEADER_BYTES); + } + + #[cfg(unix)] + #[test] + fn unreadable_file_is_skipped_and_recorded() { + use std::os::unix::fs::PermissionsExt; + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", PLAIN_GO.as_bytes()); + let secret = dir.path().join("secret.go"); + write(dir.path(), "secret.go", PLAIN_GO.as_bytes()); + std::fs::set_permissions(&secret, std::fs::Permissions::from_mode(0o000)).unwrap(); + if std::fs::read(&secret).is_ok() { + // Running as root: nothing is unreadable. + return; + } + assert_eq!(source_paths(dir.path(), &permissive()), vec!["a.go"]); + assert!(was_skipped(&secret)); + // Composition still classifies it, on its path alone. + let all = walk_all_files(dir.path().to_str().unwrap(), &StaticFilter::default()) + .unwrap() + .find(|f| f.rel_path == "secret.go") + .expect("composition sees the unreadable file"); + assert!(all.bytes.is_empty()); + } + + #[test] + fn generated_marker_excludes_by_default_and_include_generated_admits() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", PLAIN_GO.as_bytes()); + write(dir.path(), "gen.go", MARKED_GO.as_bytes()); + write(dir.path(), "gen.pb.go", PLAIN_GO.as_bytes()); + assert_eq!( + source_paths(dir.path(), &StaticFilter::default()), + vec!["a.go"] + ); + assert_eq!( + source_paths(dir.path(), &permissive()), + vec!["a.go", "gen.go", "gen.pb.go"] + ); + } + + #[test] + fn vendored_paths_follow_the_policy() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", PLAIN_GO.as_bytes()); + write(dir.path(), "vendor/dep.go", PLAIN_GO.as_bytes()); + write(dir.path(), "node_modules/m/i.js", b"export const x = 1;\n"); + assert_eq!( + source_paths(dir.path(), &StaticFilter::default()), + vec!["a.go"] + ); + assert_eq!( + source_paths(dir.path(), &permissive()), + vec!["a.go", "node_modules/m/i.js", "vendor/dep.go"] + ); + } + + /// `--extra-excluded-prefixes` matches the ROOT-RELATIVE path, so an + /// absolute (tempdir) root does not defeat a `gen/` prefix. + #[test] + fn extra_excluded_prefixes_apply_to_the_root_relative_path() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", PLAIN_GO.as_bytes()); + write(dir.path(), "gen/a.go", PLAIN_GO.as_bytes()); + assert!(dir.path().is_absolute()); + let filter = StaticFilter::from_policy(cf_pathpolicy::Options { + extra_excluded_prefixes: vec!["gen/".to_string()], + ..cf_pathpolicy::Options::default() + }); + assert_eq!(source_paths(dir.path(), &filter), vec!["a.go"]); + assert_eq!(all_paths(dir.path(), &filter), vec!["a.go"]); + } + + #[test] + fn languages_globs_restrict_by_basename() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", PLAIN_GO.as_bytes()); + write(dir.path(), "deep/b.py", b"def f():\n return 1\n"); + let py_only = StaticFilter { + language_globs: Some(vec!["*.py".to_string()]), + ..permissive() + }; + assert_eq!(source_paths(dir.path(), &py_only), vec!["deep/b.py"]); + assert_eq!(all_paths(dir.path(), &py_only), vec!["deep/b.py"]); + assert_eq!( + source_paths(dir.path(), &permissive()), + vec!["a.go", "deep/b.py"] + ); + } + + /// Composition's walk admits vendored, generated, binary and unsupported + /// files with their header bytes; only the language globs narrow it. + #[test] + fn all_files_walk_sees_everything_with_header_bytes() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", PLAIN_GO.as_bytes()); + write(dir.path(), "vendor/v.go", PLAIN_GO.as_bytes()); + write(dir.path(), "gen.go", MARKED_GO.as_bytes()); + write(dir.path(), "data.bin", b"\x00\x01\x02\xff"); + write(dir.path(), "notes.txt", b"plain text\n"); + let long = vec![b'x'; ALL_FILES_HEADER_BYTES + 100]; + write(dir.path(), "long.txt", &long); + let files: Vec<SourceFile> = + walk_all_files(dir.path().to_str().unwrap(), &StaticFilter::default()) + .unwrap() + .collect(); + let paths: Vec<&str> = files.iter().map(|f| f.rel_path.as_str()).collect(); + assert_eq!( + paths, + vec![ + "a.go", + "data.bin", + "gen.go", + "long.txt", + "notes.txt", + "vendor/v.go" + ] + ); + let bin = files.iter().find(|f| f.rel_path == "data.bin").unwrap(); + assert_eq!(bin.bytes, b"\x00\x01\x02\xff"); + assert!(bin.language.is_empty()); + let capped = files.iter().find(|f| f.rel_path == "long.txt").unwrap(); + assert_eq!(capped.bytes.len(), ALL_FILES_HEADER_BYTES); + // The source walk over the same tree admits only the plain Go file. + assert_eq!( + source_paths(dir.path(), &StaticFilter::default()), + vec!["a.go"] + ); + } + + #[test] + fn missing_root_is_path_not_found() { + let parser = Parser::new(); + let missing = "/nonexistent/codefang-static-walk"; + assert_eq!( + walk_source_files(missing, &parser, &StaticFilter::default()) + .err() + .map(|e| e.to_string()), + Some(format!("path not found: {missing}")) + ); + assert!(walk_all_files(missing, &StaticFilter::default()).is_err()); + } + + #[test] + fn relative_paths_and_parent_dirs() { + let rel = |p: &str, r: &str| make_relative_path(Path::new(p), Path::new(r)); + assert_eq!(rel("/r/a/b.go", "/r"), "a/b.go"); + assert_eq!(rel("/r/b.go", "/r/"), "b.go"); + assert_eq!(rel("./b.go", "."), "b.go"); + // A FILE root stamps the file's name, never `.`. + assert_eq!(rel("/r/b.go", "/r/b.go"), "b.go"); + // A path outside the root stamps its name too. + assert_eq!(rel("/x/b.go", "/r"), "b.go"); + assert_eq!(parent_dir("a/b.go"), "a"); + assert_eq!(parent_dir("b.go"), "."); + assert_eq!(parent_dir("/b.go"), "/"); + } } #[cfg(test)] mod dispatch_flag_tests { //! Walk-filter flags exercised through the REAL registry dispatch //! (`default_registry().lookup(id).run`), NOT by calling the report - //! builders with a hand-made filter. This is the layer the original - //! regression lived in: `h_static_*` built the filter for the json arm but - //! passed a default filter on the text/compact arms — a bug the - //! builder-level tests cannot see. + //! builders with a hand-made filter. use super::*; @@ -2139,9 +2753,6 @@ mod dispatch_flag_tests { &["--include-generated", "--include-vendored"], dir.path(), )); - // Default policy: a.go + b.py = 2; permissive adds gen.pb.go + - // vendor/dep/d.go = 4. `>` (not the exact figures) is the - // regression guard: the flags must REACH the text-format walk. assert!( permissive_total > default_total, "{id}: text dispatch dropped the include flags \ @@ -2173,9 +2784,6 @@ mod dispatch_flag_tests { #[test] fn compact_dispatch_applies_language_filter() { let dir = fixture(); - // Ruby matches no fixture file, so the compact render must collapse to - // the empty-report section — byte-different from the unfiltered one. - // Under a dispatch that drops the filter both renders are identical. let unfiltered = dispatch("static/complexity", "compact", &[], dir.path()); let ruby_only = dispatch( "static/complexity", @@ -2191,9 +2799,6 @@ mod dispatch_flag_tests { #[test] fn empty_languages_value_means_no_filter() { - // `--languages ""` parity with the reference CLI: cobra's csv reader - // yields an empty slice for an explicit empty value, so the run - // proceeds unfiltered instead of failing on an unknown "" language. let dir = fixture(); let empty = text_total_functions(&dispatch( "static/complexity", @@ -2204,24 +2809,43 @@ mod dispatch_flag_tests { let absent = text_total_functions(&dispatch("static/complexity", "text", &[], dir.path())); assert_eq!(empty, absent, "--languages \"\" must behave as no filter"); } + + /// An unsupported format for a static analyzer names the format and the + /// analyzer instead of silently producing nothing. + #[test] + fn unsupported_static_format_is_a_named_error() { + let dir = fixture(); + let root = dir.path().to_str().unwrap(); + let matches = crate::build_codefang_command() + .try_get_matches_from(["codefang", "run", "-a", "static/complexity", root]) + .expect("parse"); + let sub = matches.subcommand_matches("run").expect("run"); + let ctx = RunContext::from_matches(sub); + let registry = default_registry(); + let entry = registry.lookup("static/complexity").expect("registered"); + let err = (entry.run)(&ctx, "ndjson").expect_err("ndjson is not a static format"); + assert_eq!( + err.to_string(), + "format ndjson is not supported for static/complexity" + ); + } } #[cfg(test)] mod history_filter_tests { - //! The history-phase `--languages` filter (TreeDiffAnalyzer - //! `shouldIncludeChange` → `checkLanguage`) — exercised both at the + //! The history-phase change filter (`--languages`, the vendor/generated + //! policy and the generated-content marker) — exercised both at the //! filter level on a real multi-language git fixture and through the REAL - //! registry dispatch (the bug this guards: history walks consumed - //! `--languages` NOWHERE, and the error path silently succeeded). + //! registry dispatch. use super::*; /// A two-commit repo exercising every leg of the filter: a Go file /// (insert + modify), a Python file (insert + delete — the delete-side /// name leg), an EXTENSIONLESS script whose language is detectable only - /// from blob content (the `checkLanguage` content fallback), a - /// vendor-classified dotfile (the always-default policy leg), and a - /// Markdown doc (neither set member). + /// from blob content, a vendor-classified dotfile (the policy leg), a + /// Markdown doc (neither set member), and a Go file carrying the + /// generated-content marker (the header leg). fn history_fixture() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("tempdir"); let repo = git2::Repository::init(dir.path()).expect("git init"); @@ -2260,7 +2884,8 @@ mod history_filter_tests { .expect("commit"); }; // main.go: 5 lines; app.py: 2; deploy (extensionless python): 2; - // .gitignore (vendor-classified): 2; README.md: 1. + // .gitignore (vendor-classified): 2; README.md: 1; marked.go: 3 + // (generated marker in the header). commit( "init", &[ @@ -2272,6 +2897,10 @@ mod history_filter_tests { ("deploy", Some("#!/usr/bin/env python3\nprint('d')\n")), (".gitignore", Some("target/\nbuild/\n")), ("README.md", Some("# docs\n")), + ( + "marked.go", + Some("// Code generated by gen. DO NOT EDIT.\npackage main\n\nfunc g() {}\n"), + ), ], ); // main.go grows by one line; app.py is deleted. @@ -2323,28 +2952,34 @@ mod history_filter_tests { ); } + /// `--languages ""` means NO filter in the history phase, exactly as in + /// the static phase (one shared token parser). #[test] - fn explicit_empty_languages_restricts_everything() { - // The reference: cobra csv.Read("") → EMPTY slice → TreeDiff.Configure - // runs with a present-but-empty token list → Languages = {} (no - // "all") → checkLanguage rejects every change. NOT no-filter (that is - // the STATIC phase's semantics for the same flag value). + fn explicit_empty_languages_means_no_restriction() { let filter = filter_from_run_args(&["--languages", ""]).expect("parses"); - let set = filter.languages.expect("empty set still restricts"); - assert!(set.is_empty()); + assert_eq!(filter.languages, None); } #[test] - fn unknown_language_errors_with_tree_diff_message() { + fn unknown_language_errors_with_history_message() { let err = filter_from_run_args(&["--languages", "notalang"]).expect_err("unknown token errors"); - assert_eq!( - err, - r#"failed to configure TreeDiff: tree-diff pathspec: unknown language: "notalang""# - ); + assert_eq!(err, r#"history --languages: unknown language: "notalang""#); + } + + /// The include flags reach the history policy (they were inert before). + #[test] + fn include_flags_reach_the_history_policy() { + let filter = + filter_from_run_args(&["--include-vendored", "--include-generated"]).expect("parses"); + assert!(filter.policy.include_vendored); + assert!(filter.policy.include_generated); + let default = filter_from_run_args(&[]).expect("parses"); + assert!(!default.policy.include_vendored); + assert!(!default.policy.include_generated); } - /// Walks the fixture and returns the retained change (name, action-debug) + /// Walks the fixture and returns the retained change (name, action) /// pairs under `filter` — the exact `retain_changes` path every history /// walk shares. fn retained_names( @@ -2352,18 +2987,20 @@ mod history_filter_tests { filter: &HistoryFilter, ) -> Vec<(String, cf_gitlib::changes::ChangeAction)> { let repo = cf_gitlib::Repository::open(dir.to_str().unwrap()).expect("open"); - let hashes = crate::handlers::load_history_commit_hashes( + let hashes = load_history_commit_hashes( &repo, - 0, - false, - crate::handlers::SinceSpec::Inactive, + &HistoryWindow { + head: false, + limit: 0, + first_parent: false, + since: SinceSpec::Inactive, + }, ) .expect("history"); let mut out = Vec::new(); for hash in hashes { let commit = repo.lookup_commit(hash).expect("commit"); - let mut changes = - crate::handlers::history::commit_tree_changes(&repo, &commit).expect("diff"); + let mut changes = history::commit_tree_changes(&repo, &commit).expect("diff"); filter.retain_changes(&repo, &mut changes); out.extend(changes.into_iter().map(|c| { let name = if matches!(c.action, cf_gitlib::changes::ChangeAction::Delete) { @@ -2382,9 +3019,9 @@ mod history_filter_tests { let dir = history_fixture(); let filter = filter_from_run_args(&["--languages", "Go"]).expect("parses"); let retained = retained_names(dir.path(), &filter); - // Two main.go changes survive; the Python files, README.md, and the - // vendor-classified .gitignore do NOT (policy is ALWAYS the default — - // the reference history run never exposes the include flags). + // Two main.go changes survive; the Python files, README.md, the + // vendor-classified .gitignore and the generated-marked marked.go do + // NOT. assert_eq!( retained, vec![ @@ -2406,9 +3043,8 @@ mod history_filter_tests { let filter = filter_from_run_args(&["--languages", "Python"]).expect("parses"); let retained = retained_names(dir.path(), &filter); let names: Vec<&str> = retained.iter().map(|(n, _)| n.as_str()).collect(); - // app.py by extension; `deploy` by SHEBANG content detection (the - // reference GetLanguage(basename, nil) → blob-content fallback leg); - // the app.py delete keeps the FROM-side name (changeNameHash). + // app.py by extension; `deploy` by SHEBANG content detection; the + // app.py delete keeps the FROM-side name. assert_eq!(names, vec!["app.py", "deploy", "app.py"]); assert!(matches!( retained[2].1, @@ -2416,16 +3052,47 @@ mod history_filter_tests { )); } + /// An empty language SET (constructible only in code) retains nothing — + /// the filter-level invariant behind `languages: None` meaning "no + /// filter". #[test] fn empty_language_set_retains_nothing() { let dir = history_fixture(); - let filter = filter_from_run_args(&["--languages", ""]).expect("parses"); + let filter = HistoryFilter { + policy: cf_pathpolicy::Options::default(), + languages: Some(std::collections::BTreeSet::new()), + }; assert!(retained_names(dir.path(), &filter).is_empty()); } + /// The generated-content marker (first 512 bytes) excludes a change under + /// the default policy; `--include-generated` keeps it. + #[test] + fn generated_marker_in_blob_header_is_applied() { + let dir = history_fixture(); + let default = filter_from_run_args(&[]).expect("parses"); + let names: Vec<String> = retained_names(dir.path(), &default) + .into_iter() + .map(|(n, _)| n) + .collect(); + assert!( + !names.iter().any(|n| n == "marked.go"), + "the generated marker must exclude marked.go by default, got {names:?}" + ); + let permissive = filter_from_run_args(&["--include-generated"]).expect("parses"); + let names: Vec<String> = retained_names(dir.path(), &permissive) + .into_iter() + .map(|(n, _)| n) + .collect(); + assert!( + names.iter().any(|n| n == "marked.go"), + "--include-generated must keep marked.go, got {names:?}" + ); + } + /// Runs `codefang run --analyzers <id> --format json <extra..> <root>` - /// through the real argv parse + registry handler (same harness as - /// `dispatch_flag_tests`), returning the parsed json aggregate. + /// through the real argv parse + registry handler, returning the parsed + /// json aggregate. fn dispatch_aggregate(id: &str, extra: &[&str], root: &std::path::Path, key: &str) -> i64 { let root = root.to_str().unwrap(); let mut argv = vec!["codefang", "run", "--analyzers", id, "--format", "json"]; @@ -2453,9 +3120,6 @@ mod history_filter_tests { #[test] fn dispatch_history_devs_applies_language_filter() { - // The exact regression class of the static fix, on the history side: - // dispatch through the registry, not the filter helpers — if a walk - // silently lost the filter, totals would equal the unfiltered run. let dir = history_fixture(); let root = dir.path(); @@ -2465,7 +3129,8 @@ mod history_filter_tests { root, "total_lines_added", ); - // main.go insert (+5) + modify (+1); everything else filtered away. + // main.go insert (+5) + modify (+1); everything else filtered away + // (marked.go carries the generated marker). assert_eq!(added_go, 6); let (added_py, removed_py) = ( @@ -2487,14 +3152,23 @@ mod history_filter_tests { let all_added = dispatch_aggregate("history/devs", &[], root, "total_lines_added"); // Unfiltered: 6 (Go) + 2 (app.py) + 2 (deploy) + 1 (README.md); - // .gitignore is vendor-classified and excluded even unfiltered. + // .gitignore is vendor-classified and marked.go generated — both + // excluded even unfiltered. assert_eq!(all_added, 11); - // The TreeDiff restrict-everything case: no commit survives, so devs - // attributes zero commits (reference-observed empty devs report). + // `--include-generated` adds marked.go's 4 lines. + let with_generated = dispatch_aggregate( + "history/devs", + &["--include-generated"], + root, + "total_lines_added", + ); + assert_eq!(with_generated, 15); + + // `--languages ""` is no filter: both commits are attributed. let empty_commits = dispatch_aggregate("history/devs", &["--languages", ""], root, "total_commits"); - assert_eq!(empty_commits, 0); + assert_eq!(empty_commits, 2); } } @@ -2507,15 +3181,13 @@ mod history_window_tests { /// Base instant of the fixture history: 2021-01-01T00:00:00Z. const T0: i64 = 1_609_459_200; - /// Spacing between fixture commits (one hour), so author-time order is - /// strictly increasing and every cutoff can land between two commits. + /// Spacing between fixture commits (one hour). const STEP: i64 = 3_600; /// Fixture length: five linear commits; `hashes[0]` is the oldest. const N_COMMITS: usize = 5; - /// The 3rd commit's instant as an RFC3339 cutoff (T0 + 2*STEP), i.e. - /// `2021-01-01T02:00:00Z`; excluded from a window whose cutoff equals it. + /// The 3rd commit's instant as an RFC3339 cutoff (T0 + 2*STEP). const CUTOFF_3RD: &str = "2021-01-01T02:00:00Z"; - /// The newest commit's instant as a UTC date (2021-01-01T04:00:00Z). + /// A date after the newest commit. const DATE_AFTER_HEAD: &str = "2021-01-02"; /// A date at/before the fixture root, so `--since` filters nothing. const DATE_BEFORE_ROOT: &str = "2020-01-01"; @@ -2547,36 +3219,56 @@ mod history_window_tests { (test, hashes) } + fn window(limit: usize, since: SinceSpec) -> HistoryWindow { + HistoryWindow { + head: false, + limit, + first_parent: false, + since, + } + } + /// `--limit N` selects the N NEWEST commits, delivered oldest-first. #[test] fn limit_selects_the_newest_commits_oldest_first() { let (test, hashes) = linear_repo(); let got = - load_history_commit_hashes(&test.repo, 2, false, SinceSpec::Inactive).expect("loads"); - assert_eq!( - got.as_slice(), - &hashes[3..], - "--limit 2 must be the two NEWEST commits, oldest-first" - ); + load_history_commit_hashes(&test.repo, &window(2, SinceSpec::Inactive)).expect("loads"); + assert_eq!(got.as_slice(), &hashes[3..]); } - /// `--limit 0` (unset) and a negative limit are both "no limit". + /// `--limit 0` (unset) is "no limit". #[test] - fn limit_zero_or_negative_loads_whole_history() { + fn limit_zero_loads_whole_history() { let (test, hashes) = linear_repo(); - for limit in [0, -1] { - let got = load_history_commit_hashes(&test.repo, limit, false, SinceSpec::Inactive) - .unwrap_or_else(|| panic!("limit {limit} loads")); - assert_eq!( - got.as_slice(), - &hashes[..], - "limit {limit} must load all commits" - ); - } + let got = + load_history_commit_hashes(&test.repo, &window(0, SinceSpec::Inactive)).expect("loads"); + assert_eq!(got.as_slice(), &hashes[..]); + } + + /// `--head` is exactly `[HEAD]`, whatever the other bounds say. + #[test] + fn head_is_exactly_the_head_commit() { + let (test, hashes) = linear_repo(); + let got = load_history_commit_hashes( + &test.repo, + &HistoryWindow { + head: true, + limit: 3, + first_parent: false, + since: SinceSpec::Active(DATE_BEFORE_ROOT.to_owned()), + }, + ) + .expect("loads"); + assert_eq!(got, vec![hashes[4]]); + assert_eq!( + got, + load_history_commit_hashes(&test.repo, &window(1, SinceSpec::Inactive)).expect("loads"), + "--head and --limit 1 select the same window" + ); } - /// The cutoff literal must mean exactly the 3rd fixture instant, or every - /// cutoff-based expectation below drifts silently. + /// The cutoff literal must mean exactly the 3rd fixture instant. #[test] fn cutoff_literal_matches_the_fixture_clock() { assert_eq!( @@ -2586,128 +3278,250 @@ mod history_window_tests { ); } - /// A `--since` cutoff that excludes only part of the history yields exactly - /// that window oldest-first. (The reference-parity loader aborted here.) #[test] fn since_partial_window_yields_the_window() { let (test, hashes) = linear_repo(); let got = load_history_commit_hashes( &test.repo, - 0, - false, - SinceSpec::Active(CUTOFF_3RD.to_owned()), + &window(0, SinceSpec::Active(CUTOFF_3RD.to_owned())), ) .expect("a partial --since window must load, not abort"); - assert_eq!( - got.as_slice(), - &hashes[2..], - "--since must yield the in-window commits, oldest-first" - ); + assert_eq!(got.as_slice(), &hashes[2..]); } - /// A cutoff newer than every commit plans an EMPTY window (analyzers then - /// emit their empty report) rather than aborting. #[test] fn since_cutoff_after_head_yields_empty_window() { let (test, _hashes) = linear_repo(); let got = load_history_commit_hashes( &test.repo, - 0, - false, - SinceSpec::Active(DATE_AFTER_HEAD.to_owned()), + &window(0, SinceSpec::Active(DATE_AFTER_HEAD.to_owned())), ) .expect("a fully-excluded window is empty, not an error"); assert!(got.is_empty(), "window must be empty, got {got:?}"); } - /// A cutoff at/before the root commit filters nothing: same window, same - /// order as an unfiltered run. #[test] fn since_cutoff_at_or_before_root_yields_whole_history() { let (test, hashes) = linear_repo(); let got = load_history_commit_hashes( &test.repo, - 0, - false, - SinceSpec::Active(DATE_BEFORE_ROOT.to_owned()), + &window(0, SinceSpec::Active(DATE_BEFORE_ROOT.to_owned())), ) .expect("loads"); - assert_eq!( - got.as_slice(), - &hashes[..], - "cutoff before root filters nothing" - ); + assert_eq!(got.as_slice(), &hashes[..]); } - /// `--limit` caps the NEWEST end of the `--since` window: the window is - /// `hashes[1..]`, so the newest two of those are `hashes[3..]`. + /// `--limit` caps the NEWEST end of the `--since` window. #[test] fn limit_caps_the_newest_end_of_the_since_window() { let (test, hashes) = linear_repo(); let cutoff = CUTOFF_3RD.to_owned(); let unbounded = - load_history_commit_hashes(&test.repo, 0, false, SinceSpec::Active(cutoff.clone())) + load_history_commit_hashes(&test.repo, &window(0, SinceSpec::Active(cutoff.clone()))) .expect("window loads"); assert_eq!(unbounded.as_slice(), &hashes[2..]); - let capped = load_history_commit_hashes(&test.repo, 2, false, SinceSpec::Active(cutoff)) + let capped = load_history_commit_hashes(&test.repo, &window(2, SinceSpec::Active(cutoff))) .expect("window loads"); - assert_eq!( - capped.as_slice(), - &hashes[3..], - "--limit inside a --since window keeps its newest commits" - ); + assert_eq!(capped.as_slice(), &hashes[3..]); } - /// `--since` accepts a git revision, using that commit's author time. #[test] fn since_accepts_a_git_revision() { let (test, hashes) = linear_repo(); let got = load_history_commit_hashes( &test.repo, - 0, - false, - SinceSpec::Active("HEAD~2".to_owned()), + &window(0, SinceSpec::Active("HEAD~2".to_owned())), ) .expect("--since must resolve a revision like `HEAD~2`"); - assert_eq!( - got.as_slice(), - &hashes[2..], - "`--since HEAD~2` must start the window at that commit" - ); + assert_eq!(got.as_slice(), &hashes[2..]); } /// Duration forms resolve against the injectable clock (DESIGN §2.8), not - /// the wall clock. The fixture dates are years behind the real clock, so - /// this window can only appear if `CODEFANG_NOW` is honored. + /// the wall clock. #[test] fn duration_since_uses_the_injected_clock() { let _guard = NowGuard; - // Now == the newest-but-one commit's instant; `1h` back == CUTOFF_3RD. std::env::set_var("CODEFANG_NOW", (T0 + 3 * STEP).to_string()); let (test, hashes) = linear_repo(); let got = - load_history_commit_hashes(&test.repo, 0, false, SinceSpec::Active("1h".to_owned())) + load_history_commit_hashes(&test.repo, &window(0, SinceSpec::Active("1h".to_owned()))) .expect("`--since 1h` must resolve against CODEFANG_NOW"); - assert_eq!( - got.as_slice(), - &hashes[2..], - "`--since 1h` must cut at CODEFANG_NOW minus one hour" - ); + assert_eq!(got.as_slice(), &hashes[2..]); } - /// A `--since` that is neither a time form nor a revision aborts the run. #[test] fn unresolvable_since_aborts_the_run() { let (test, _hashes) = linear_repo(); assert!( load_history_commit_hashes( &test.repo, - 0, - false, - SinceSpec::Active("not-a-time-or-ref".to_owned()) + &window(0, SinceSpec::Active("not-a-time-or-ref".to_owned())) ) .is_none(), - "an unresolvable --since must yield None (empty stdout, exit 1)" + "an unresolvable --since must yield None" + ); + } +} + +#[cfg(test)] +mod preflight_tests { + //! The shared run validation: every user error has a named message. + + use super::*; + + fn ctx_err(args: &[&str], static_ids: &[&str], history_ids: &[&str]) -> Option<String> { + let mut argv = vec!["codefang", "run"]; + argv.extend_from_slice(args); + let matches = crate::build_codefang_command() + .try_get_matches_from(argv) + .expect("run args parse"); + let sub = matches.subcommand_matches("run").expect("run subcommand"); + let ctx = RunContext::from_matches(sub); + let s: Vec<String> = static_ids.iter().map(|s| (*s).to_string()).collect(); + let h: Vec<String> = history_ids.iter().map(|s| (*s).to_string()).collect(); + preflight(&ctx, &s, &h).err().map(|e| e.to_string()) + } + + #[test] + fn missing_path_is_named() { + assert_eq!( + ctx_err(&["/no/such/dir"], &["static/complexity"], &[]), + Some("path not found: /no/such/dir".to_string()) + ); + } + + #[test] + fn non_repository_is_named_for_history_only() { + let dir = tempfile::tempdir().expect("tempdir"); + let p = dir.path().to_str().unwrap(); + assert_eq!( + ctx_err(&[p], &[], &["history/devs"]), + Some(format!("cannot open repository: {p}")) + ); + // A static selection never needs a repository. + assert_eq!(ctx_err(&[p], &["static/complexity"], &[]), None); + } + + #[test] + fn formats_are_validated_per_phase() { + let dir = tempfile::tempdir().expect("tempdir"); + git2::Repository::init(dir.path()).expect("git init"); + let p = dir.path().to_str().unwrap(); + assert_eq!( + ctx_err(&["--format", "bogus", p], &["static/complexity"], &[]), + Some("unsupported format: bogus".to_string()) + ); + assert_eq!( + ctx_err(&["--format", "ndjson", p], &["static/complexity"], &[]), + Some("format ndjson is not supported for static/complexity".to_string()) + ); + for format in ["compact", "text"] { + assert_eq!( + ctx_err( + &["--format", format, p], + &["static/complexity"], + &["history/devs"] + ), + None, + "a mixed selection renders --format {format} phase by phase" + ); + } + assert_eq!( + ctx_err(&["--format", "compact", p], &[], &["history/devs"]), + None + ); + assert_eq!( + ctx_err(&["--format", "plot", p], &[], &["history/devs"]), + None + ); + } + + #[test] + fn numeric_history_flags_are_validated() { + let dir = tempfile::tempdir().expect("tempdir"); + git2::Repository::init(dir.path()).expect("git init"); + let p = dir.path().to_str().unwrap(); + assert_eq!( + ctx_err(&["--limit=-5", p], &[], &["history/devs"]), + Some("invalid --limit: must be >= 0, got -5".to_string()) + ); + assert_eq!( + ctx_err(&["--tick-size", "0", p], &[], &["history/devs"]), + Some("invalid --tick-size: must be > 0, got 0".to_string()) + ); + assert_eq!( + ctx_err(&["--max-changes-per-commit=-1", p], &[], &["history/devs"]), + Some("invalid --max-changes-per-commit: must be >= 0, got -1".to_string()) + ); + assert_eq!( + ctx_err(&["--people-dict", "/no/such/people.txt", p], &[], &["history/devs"]), + Some( + "cannot read --people-dict /no/such/people.txt: No such file or directory (os error 2)" + .to_string() + ) + ); + // A static selection ignores the history flags entirely. + assert_eq!( + ctx_err(&["--limit=-5", p], &["static/complexity"], &[]), + None + ); + } + + /// A non-positive band, sample or typo distance was silently replaced by + /// the default; it is an error now. + #[test] + fn non_positive_analyzer_sizes_are_rejected() { + let dir = tempfile::tempdir().expect("tempdir"); + git2::Repository::init(dir.path()).expect("git init"); + let p = dir.path().to_str().unwrap(); + for (flag, value) in [ + ("--granularity", "0"), + ("--sampling", "0"), + ("--granularity", "-3"), + ("--typos-max-distance", "0"), + ] { + let arg = format!("{flag}={value}"); + assert_eq!( + ctx_err(&[&arg, p], &[], &["history/burndown"]), + Some(format!("invalid {flag}: must be > 0, got {value}")) + ); + } + assert_eq!( + ctx_err( + &["--granularity", "1", "--sampling", "1", p], + &[], + &["history/burndown"] + ), + None + ); + } + + /// `--ndjson` only modifies `--format timeseries`; with any other format + /// it was accepted and ignored. + #[test] + fn ndjson_requires_the_timeseries_format() { + let dir = tempfile::tempdir().expect("tempdir"); + git2::Repository::init(dir.path()).expect("git init"); + let p = dir.path().to_str().unwrap(); + assert_eq!( + ctx_err(&["--ndjson", p], &[], &["history/devs"]), + Some("invalid --ndjson: requires --format timeseries, got --format json".to_string()) + ); + assert_eq!( + ctx_err( + &["--ndjson", "--format", "ndjson", p], + &[], + &["history/devs"] + ), + Some("invalid --ndjson: requires --format timeseries, got --format ndjson".to_string()) + ); + assert_eq!( + ctx_err( + &["--ndjson", "--format", "timeseries", p], + &[], + &["history/devs"] + ), + None ); } } diff --git a/crates/cf-commands/src/handlers/burndown_ndjson.rs b/crates/cf-commands/src/handlers/burndown_ndjson.rs index 45404223..3e996d2a 100644 --- a/crates/cf-commands/src/handlers/burndown_ndjson.rs +++ b/crates/cf-commands/src/handlers/burndown_ndjson.rs @@ -1,230 +1,515 @@ -//! `run --analyzers history/burndown --format timeseries --ndjson` closed form. +//! `history/burndown` over the shared history walk. //! -//! Reproduces the reference streaming burndown time-series NDJSON pipeline for the -//! newest `--limit` commits as a deterministic, single-pass computation: +//! Burndown is the one analyzer whose walk ALWAYS follows the first parent +//! ([`HistoryRun::open_first_parent`]): its line-survival treaps track the +//! main line of development, so a merge is consumed as the ordinary +//! single-parent change set it introduces on that line — the lines a merge +//! deletes on the main line are debited at the merge's own tick, like any +//! other commit's deletions. //! -//! - **commit set / order** — `handlers::load_history_commit_hashes`: the -//! `--limit` NEWEST commits, delivered oldest-first (burndown forces -//! `--first-parent` for the whole run). -//! - **tick assignment** (`plumbing.TicksSinceStart`, 24 h default tick) — -//! `tick0 = FloorTime(when0, 24h)`; `tick = max(floor((when-tick0)/24h), -//! previousTick)` over the committer time. With no identity provider -//! (`PeopleNumber == 0`) the burndown "packed person+tick" value collapses to -//! the raw tick, so the treap time keys are plain ticks. -//! - **per-commit deltas** (`burndown.HistoryAnalyzer.Consume` → -//! `computeCommitLineStats`): each tracked path is a [`File`] line-survival -//! treap. Inserts create a `File` at the commit tick; deletions update the -//! file to length 0; modifications drive `File::update` from the -//! `cf-godiff` line diff via the same `diffApplier` pending/flush logic as the reference implementation. -//! Every `File::update` notifies an updater that records `(curTick, prevTick, -//! delta)` into the per-commit global deltas. `lines_added` sums positive -//! deltas at `(tick, tick)`; `lines_removed` sums the absolute negative deltas -//! in the commit-tick row — exactly `computeCommitLineStats(globalDeltas[tick])`. -//! Merge commits apply pure deletions at tick 0 (`applyDeletionUpdate`'s -//! `if isMerge && !isDeletion { tick = 0 }`), so those negative deltas land -//! outside the commit-tick row and are excluded from `lines_removed`. -//! - **NDJSON framing** (`analyze.WriteTimeSeriesNDJSON`): one -//! `MergedCommitData` per commit, compact `json.Encoder.Encode` (sorted map -//! keys `author, burndown, hash, tick, timestamp`, newline-terminated). Author -//! is `""` (no identity provider); timestamp is the committer time as the reference implementation -//! `time.RFC3339` in the commit's original zone offset. +//! The walk: +//! - **window base** — in a `--limit` / `--since` / `--head` window the +//! tracked state is seeded from the base tree (the first in-window commit's +//! parent) at tick 0 before the first commit is applied, so a commit's +//! `LinesAdded` / `LinesRemoved` are the lines it really changed; the base +//! lines form band 0 of the global curve and have no author; +//! - **cap** — commits whose raw tree diff exceeds `--max-changes-per-commit` +//! are dropped before the stamps are assigned (the same rule devs applies), +//! so burndown and devs see the same commit set; a dropped commit's edits +//! are never applied, and the next commit touching an affected file +//! re-inserts it at that commit's tick; +//! - **ticks and authors** — the shared [`HistoryRun::stamps`] assignment; +//! each line's segment value packs `(person, tick)` through a +//! [`ValuePacking`] (ticks only without `--burndown-people`); +//! - **deltas** — each tracked path is a [`File`] treap whose updater reports +//! `(current, previous, delta)` into one per-commit sink; the reduce turns +//! the sink into the commit's global / per-file / per-developer sparse +//! deltas and the interaction matrix cells. Every re-insertion of a path +//! first debits whatever was tracked under it, so no line is counted twice +//! or leaked. //! -//! All output bytes route through `cf-gojson` for the reference `encoding/json` parity. +//! All output bytes route through `cf-gojson`. -use cf_burndown_core::{File, Updater}; -use cf_gitlib::blob::CachedBlob; -use cf_gitlib::changes::{initial_tree_changes, tree_diff, Change, ChangeAction}; -use cf_godiff::Op; -use cf_gojson::value::{GoMap, GoValue, MapOrigin}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; use std::rc::Rc; -use crate::handlers::{floor_tick_secs, format_rfc3339_offset, run_repo_path}; +use cf_analyzer_burndown::{ + compute_metrics, ComputedMetrics, DenseHistory, Grid, MetricsInput, SparseHistory, +}; +use cf_burndown_core::{File, Updater, ValuePacking}; +use cf_gitlib::blob::CachedBlob; +use cf_gitlib::changes::{Change, ChangeAction}; +use cf_godiff::Op; +use cf_gojson::value::{GoMap, GoValue, MapOrigin}; -/// 24-hour tick period in seconds (`plumbing.DefaultTicksSinceStartTickSize`). -const TICK_PERIOD: i64 = 86_400; +use crate::handlers::history::{ + commit_tree_changes, fold_planned, parallel_prepare, ChunkPlan, CommitStamp, HistoryRun, + Stamper, WalkCost, +}; +use crate::handlers::{format_rfc3339_offset, HistoryFilter}; + +/// Burndown band granularity in ticks (the `--granularity` default). +const DEFAULT_GRANULARITY: i64 = 30; +/// Burndown sampling period in ticks (the `--sampling` default). +const DEFAULT_SAMPLING: i64 = 30; +/// Nanoseconds per hour. +const NANOS_PER_HOUR: i64 = 3_600 * 1_000_000_000; +/// Nanoseconds per second. +const NANOS_PER_SECOND: i64 = 1_000_000_000; +/// The tick the lines of a window's base tree are credited to. +const BASE_TICK: i64 = 0; + +/// The burndown flags: the sampling grid and the two tracking switches. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BurndownFlags { + /// `--granularity`: ticks per age band. + pub granularity: i64, + /// `--sampling`: ticks per sample. + pub sampling: i64, + /// `--burndown-files`: per-file survival. + pub track_files: bool, + /// `--burndown-people`: per-developer survival and interactions. + pub track_people: bool, +} -/// Shared sink the per-file treap updaters push `(current, previous, delta)` -/// reports into. Drained per commit to compute that commit's line stats. -type DeltaSink = Rc<RefCell<Vec<(i64, i64, i64)>>>; +impl BurndownFlags { + /// Reads the flags. The preflight (`validate_history_run`) rejects a + /// non-positive `--granularity` / `--sampling`; an absent flag means the + /// default (30 ticks). + fn from_matches(sub: &clap::ArgMatches) -> Self { + let int = |name: &str, default: i64| { + sub.try_get_one::<i64>(name) + .ok() + .flatten() + .copied() + .filter(|v| *v > 0) + .unwrap_or(default) + }; + let flag = |name: &str| { + sub.try_get_one::<bool>(name) + .ok() + .flatten() + .copied() + .unwrap_or(false) + }; + Self { + granularity: int("granularity", DEFAULT_GRANULARITY), + sampling: int("sampling", DEFAULT_SAMPLING), + track_files: flag("burndown-files"), + track_people: flag("burndown-people"), + } + } +} -/// Builds the burndown timeseries NDJSON bytes for the newest `--limit` commits, -/// or `None` if the repository cannot be opened/walked. -pub fn burndown_timeseries_ndjson(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; +/// One `(current, previous, delta)` report of a tracked file's treap, tagged +/// with the file it came from. `current` / `previous` are packed values. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Report { + file: usize, + current: i64, + previous: i64, + delta: i64, +} - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); +/// The sink every treap updater pushes into; drained once per commit. +type ReportSink = Rc<RefCell<Vec<Report>>>; - // Burndown forces --first-parent (reference: `if slices.Contains(analyzerKeys, - // "burndown") && !opts.FirstParent { opts.FirstParent = true }`), so the walk - // follows only the first parent of merge commits (simplify_first_parent). - // The window is the `limit` NEWEST commits processed oldest-first. - let hashes = crate::handlers::load_history_commit_hashes( - &repo, - limit, - true, - crate::handlers::history_since_spec(sub), - )?; +/// A tracked path: its stable file id and its line-survival treap. +struct TrackedFile { + id: usize, + file: File, +} - let filter = crate::handlers::history_filter(sub).unwrap_or_default(); - let sink: DeltaSink = Rc::new(RefCell::new(Vec::new())); - // Persistent burndown state: per-path line-survival treaps (shard.filesByID). - let mut tracked: HashMap<String, File> = HashMap::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - let mut out: Vec<u8> = Vec::new(); - - for hash in &hashes { - let commit = repo.lookup_commit(*hash).ok()?; - let new_tree = commit.tree().ok()?; - - // Tick assignment (committer time vs. tick0 floor, monotonic). - let committer = commit.committer(); - let when = committer.when.seconds(); - let t0 = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw = (when - t0).div_euclid(TICK_PERIOD); - let tick = raw.max(previous_tick); - previous_tick = tick; - - // Tree diff vs. first parent (full initial tree for a root commit). - let num_parents = commit.num_parents(); - let changes = if num_parents > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(&repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(&repo, Some(&new_tree)).ok()? - }; - let is_merge = num_parents > 1; - - sink.borrow_mut().clear(); - process_commit_changes( - &repo, - &changes, - &filter, - &mut tracked, - &sink, - tick, - is_merge, - ); - let (added, removed) = commit_line_stats(&sink.borrow(), tick); +/// The persistent line-survival state of one walk. +struct Tracker { + packing: ValuePacking, + sink: ReportSink, + files: HashMap<String, TrackedFile>, + /// The current path of every file id ever tracked (renames update it). + paths: Vec<String>, +} - // burndown ExtractCommitTimeSeries map: sorted keys lines_added, - // lines_removed. - let mut burndown = GoMap::new_map(); - burndown.insert("lines_added", GoValue::Int(added)); - burndown.insert("lines_removed", GoValue::Int(removed)); +impl Tracker { + fn new(packing: ValuePacking) -> Self { + Self { + packing, + sink: Rc::new(RefCell::new(Vec::new())), + files: HashMap::new(), + paths: Vec::new(), + } + } - let timestamp = - format_rfc3339_offset(committer.when.seconds(), committer.when.offset_minutes()); + /// The updater of file `id`: every report lands in the shared sink. + fn updater(&self, id: usize) -> Updater { + let sink = Rc::clone(&self.sink); + Box::new(move |current, previous, delta| { + sink.borrow_mut().push(Report { + file: id, + current, + previous, + delta, + }); + }) + } - // MergedCommitData.MarshalJSON flat map (json.Marshal(map) → sorted keys: - // author, burndown, hash, tick, timestamp). - let mut commit_obj = GoMap::new_map(); - commit_obj.insert("author", GoValue::Str(String::new())); - commit_obj.insert("burndown", GoValue::Map(burndown)); - commit_obj.insert("hash", GoValue::Str(hash.to_string())); - commit_obj.insert("tick", GoValue::Int(tick)); - commit_obj.insert("timestamp", GoValue::Str(timestamp)); + /// Takes the reports accumulated since the last drain. + fn drain(&self) -> Vec<Report> { + std::mem::take(&mut *self.sink.borrow_mut()) + } - // json.Encoder.Encode (compact, newline-terminated) per commit. - out.extend_from_slice(&cf_gojson::marshal(&GoValue::Map(commit_obj))); - out.push(b'\n'); + /// The tracked line count of `path`, `None` when untracked. + fn len_of(&self, path: &str) -> Option<i64> { + self.files.get(path).map(|t| t.file.len()) } - Some(out) + /// Debits every tracked line of `path` at `value` and stops tracking it. + fn remove(&mut self, path: &str, value: i64) { + if let Some(mut tracked) = self.files.remove(path) { + let len = tracked.file.len(); + if len > 0 { + tracked.file.update(value, 0, 0, len); + } + } + } + + /// Tracks `path` as `lines` lines written at `value`, debiting whatever + /// was tracked under the path first. + fn insert(&mut self, path: &str, lines: i64, value: i64) { + self.remove(path, value); + let id = self.paths.len(); + self.paths.push(path.to_string()); + let file = File::new(value, lines, vec![self.updater(id)]); + self.files + .insert(path.to_string(), TrackedFile { id, file }); + } + + /// Moves the tracked file `from` to `to` (a rename), debiting whatever + /// was tracked under `to`. + fn rename(&mut self, from: &str, to: &str, value: i64) { + if from == to { + return; + } + if let Some(tracked) = self.files.remove(from) { + self.remove(to, value); + self.paths[tracked.id] = to.to_string(); + self.files.insert(to.to_string(), tracked); + } + } + + /// Drives the treap of `path` through a line diff (see [`apply_diffs`]). + fn apply_diffs(&mut self, path: &str, diffs: &[(Op, i64)], value: i64) { + if let Some(tracked) = self.files.get_mut(path) { + apply_diffs(&mut tracked.file, diffs, value); + } + } + + /// The live lines of every tracked file per known author + /// (`path -> person -> lines`); lines with no author are left out. + fn ownership(&self) -> BTreeMap<String, BTreeMap<i64, i64>> { + let unknown = self.packing.unknown_person(); + self.files + .iter() + .map(|(path, tracked)| { + let mut by_person: BTreeMap<i64, i64> = BTreeMap::new(); + for seg in tracked.file.segments() { + let (person, _) = self.packing.unpack(i64::from(seg.value)); + if person != unknown { + *by_person.entry(person).or_default() += seg.length; + } + } + (path.clone(), by_person) + }) + .collect() + } } -/// The burndown contribution to the merged `--format timeseries` document (reference: -/// `burndown.ExtractCommitTimeSeries` over `report["commit_stats"]`): per -/// commit `{"lines_added", "lines_removed"}`; burndown is `Sequential: true`, -/// so `commits_by_tick` appends in plain walk order with the real ticks. -pub fn burndown_timeseries_contribution( - sub: &clap::ArgMatches, -) -> Option<crate::handlers::history_formats::TimeSeriesContribution> { - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; +/// One commit's (or the window base's) line deltas. +#[derive(Debug, Default, PartialEq)] +struct CommitDeltas { + /// `curTick -> prevTick -> delta`. + global: SparseHistory, + /// Per file id (`--burndown-files`). + files: BTreeMap<usize, SparseHistory>, + /// Per author of the affected lines (`--burndown-people`). + people: BTreeMap<i64, SparseHistory>, + /// `author -> modifier -> lines of the author the modifier deleted or + /// replaced` (`--burndown-people`). + matrix: BTreeMap<i64, BTreeMap<i64, i64>>, + /// Lines written by the commit. + added: i64, + /// Lines deleted or replaced by the commit. + removed: i64, +} - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); +/// Adds one cell to a sparse history. +fn add_cell(history: &mut SparseHistory, current: i64, previous: i64, delta: i64) { + *history + .entry(current) + .or_default() + .entry(previous) + .or_default() += delta; +} - // Burndown forces --first-parent, exactly as the sibling paths. - let hashes = crate::handlers::load_history_commit_hashes( - &repo, - limit, - true, - crate::handlers::history_since_spec(sub), - )?; +/// Reduces one commit's reports into its deltas. `added` sums every positive +/// report, `removed` every negative one, so a line added and another removed +/// in the same tick never cancel. +fn reduce_reports(reports: &[Report], packing: ValuePacking, flags: BurndownFlags) -> CommitDeltas { + let unknown = packing.unknown_person(); + let mut d = CommitDeltas::default(); + for r in reports { + let (cur_person, cur_tick) = packing.unpack(r.current); + let (prev_person, prev_tick) = packing.unpack(r.previous); + add_cell(&mut d.global, cur_tick, prev_tick, r.delta); + if r.delta > 0 { + d.added += r.delta; + } else { + d.removed -= r.delta; + } + if flags.track_files { + add_cell( + d.files.entry(r.file).or_default(), + cur_tick, + prev_tick, + r.delta, + ); + } + if flags.track_people && prev_person != unknown { + add_cell( + d.people.entry(prev_person).or_default(), + cur_tick, + prev_tick, + r.delta, + ); + if r.delta < 0 { + *d.matrix + .entry(prev_person) + .or_default() + .entry(cur_person) + .or_default() -= r.delta; + } + } + } + d +} - let filter = crate::handlers::history_filter(sub).unwrap_or_default(); - let sink: DeltaSink = Rc::new(RefCell::new(Vec::new())); - let mut tracked: HashMap<String, File> = HashMap::new(); +/// One walked commit's burndown products plus the shared stamps. +struct BurndownCommit { + /// The shared identity/tick stamp. + stamp: CommitStamp, + /// This commit's deltas. + deltas: CommitDeltas, +} - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; +/// The full products of one burndown walk. +struct BurndownWalk { + /// Per-commit products, oldest-first. + commits: Vec<BurndownCommit>, + /// The window base tree's lines (tick 0, no author); empty for a walk + /// starting at a root commit. + base: CommitDeltas, + /// The tick of the newest kept commit. + last_tick: i64, + /// The run's tick size in hours. + tick_size_hours: i64, + /// The repository path as given. + path: String, + /// The burndown flags. + flags: BurndownFlags, + /// The final path of every file id. + paths: Vec<String>, + /// Final line ownership per path (both trackings on). + ownership: BTreeMap<String, BTreeMap<i64, i64>>, + /// Display name per person id (`--burndown-people`). + names: Vec<String>, +} - let mut per_commit = Vec::new(); - let mut commit_meta = Vec::new(); +/// The person slot of a stamp: the people-dict `<unmatched>` slot for an +/// unmatched author, else the author id. +fn person_slot(author_id: i64, unmatched: i64) -> i64 { + if author_id == i64::from(cf_analyzers_plumbing::AUTHOR_MISSING) { + unmatched + } else { + author_id + } +} - for hash in &hashes { - let commit = repo.lookup_commit(*hash).ok()?; - let new_tree = commit.tree().ok()?; +/// The display name of every person id (the same resolver devs uses). +fn display_names(names: &[String]) -> Vec<String> { + (0..names.len() as i64) + .map(|id| cf_devs::dev_name_and_email(id, names).0) + .collect() +} - let committer = commit.committer(); - let when = committer.when.seconds(); - let t0 = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw = (when - t0).div_euclid(TICK_PERIOD); - let tick = raw.max(previous_tick); - previous_tick = tick; +/// Seeds the tracker with the window's base tree — the first in-window +/// commit's parent — at tick 0 with no author. A window starting at a root +/// commit has no base. +fn seed_base_tree(run: &HistoryRun, tracker: &mut Tracker, value: i64) -> Option<()> { + let Some(first) = run.hashes.first() else { + return Some(()); + }; + let commit = run.repo.lookup_commit(*first).ok()?; + if commit.num_parents() == 0 { + return Some(()); + } + let tree = commit.parent(0).ok()?.tree().ok()?; + let changes = cf_gitlib::changes::initial_tree_changes(&run.repo, Some(&tree)).ok()?; + apply_changes(&run.repo, &run.filter, tracker, &changes, value); + Some(()) +} - let num_parents = commit.num_parents(); - let changes = if num_parents > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(&repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(&repo, Some(&new_tree)).ok()? - }; - let is_merge = num_parents > 1; - - sink.borrow_mut().clear(); - process_commit_changes( - &repo, - &changes, - &filter, - &mut tracked, - &sink, - tick, - is_merge, - ); - let (added, removed) = commit_line_stats(&sink.borrow(), tick); - - let mut entry = GoMap::new_map(); - entry.insert("lines_added", GoValue::Int(added)); - entry.insert("lines_removed", GoValue::Int(removed)); - per_commit.push((hash.to_string(), GoValue::Map(entry))); - commit_meta.push(( - hash.to_string(), - tick, - format_rfc3339_offset(when, committer.when.offset_minutes()), - String::new(), - )); - } - - // Burndown's serial-leaf TCs reach the aggregator at END-of-chunk drain, - // when the runner's TicksSinceStart already points at the FINAL tick — so - // every commits_by_tick entry lands under that one tick (oracle-observed: - // all 50 hercules commits report tick 181). the reference implementation's order within the single - // tick is map-iteration random; we keep deterministic walk order and let - // the oracle's measured-variance canonicalization compare the sets. - let last_tick = previous_tick; - for meta in &mut commit_meta { - meta.1 = last_tick; +/// Commits with at least this many raw changes keep their tree diff from the +/// raw-count pre-pass for the main pass: rename detection on such commits +/// (vendoring, mass moves) dominates the walk, and computing it twice put +/// each of them on the critical path twice. +const REUSED_DIFF_CHANGES: usize = 256; + +/// The burndown walk's measured cost. Kubernetes, 31,923 commits: one +/// worker peaks at 377 MB, 24 workers at 2058 MB (71 MiB per added worker: +/// each holds a commit's blobs and line diffs); the whole window holds 23 +/// bytes more per commit than 50-commit chunks. +const BURNDOWN_COST: WalkCost = WalkCost { + worker_bytes: 80 * cf_units::MIB, + product_bytes: cf_units::KIB, +}; + +/// Runs the burndown walk: the forced-first-parent window, the cap, the +/// shared stamps, the base seed, and the per-path treaps driven in +/// chronological order. +fn burndown_walk(sub: &clap::ArgMatches) -> Option<BurndownWalk> { + let run = HistoryRun::open_first_parent(sub)?; + let flags = BurndownFlags::from_matches(sub); + let max_changes = run.max_changes; + let raw = parallel_prepare( + &run.path, + &run.hashes, + HistoryRun::workers(), + |repo, hash| { + let commit = repo.lookup_commit(hash).ok()?; + let changes = commit_tree_changes(repo, &commit)?; + let count = changes.len(); + let keep = (REUSED_DIFF_CHANGES..=max_changes).contains(&count); + Some((count, keep.then_some(changes))) + }, + )?; + // Stamp every kept commit first: the value packing needs the last tick + // before the first line is attributed. + let mut stamper = Stamper::new(&run); + let mut stamps = Vec::with_capacity(run.hashes.len()); + let mut reused: HashMap<cf_gitlib::Hash, Vec<Change>> = HashMap::new(); + for (pos, (hash, (count, changes))) in run.hashes.iter().zip(raw).enumerate() { + if count <= max_changes { + stamps.push(stamper.stamp(&run.repo, pos, *hash)?); + if let Some(changes) = changes { + reused.insert(*hash, changes); + } + } } + let last_tick = stamper.last_tick(); + let summary = stamper.finish(); + let unmatched = summary.unmatched_slot as i64; + let packing = if flags.track_people { + ValuePacking::for_people(last_tick) + } else { + ValuePacking::ticks_only() + }; + let mut tracker = Tracker::new(packing); + + seed_base_tree( + &run, + &mut tracker, + packing.pack(packing.unknown_person(), BASE_TICK), + )?; + let base = reduce_reports(&tracker.drain(), packing, flags); + + // The per-change blob, filter and line-diff work runs on the worker + // pool; the treap updates fold in commit order. + let kept: Vec<cf_gitlib::Hash> = stamps.iter().map(|s| s.hash).collect(); + let plan = ChunkPlan::for_walk(sub, "history/burndown", kept.len(), BURNDOWN_COST).ok()?; + let filter = &run.filter; + let mut stamps = stamps.into_iter(); + let mut commits = Vec::with_capacity(kept.len()); + fold_planned( + &run.path, + &kept, + plan, + |repo, hash| { + if let Some(changes) = reused.get(&hash) { + return Some(prepare_change_facts(repo, filter, changes)); + } + let commit = repo.lookup_commit(hash).ok()?; + let changes = commit_tree_changes(repo, &commit)?; + Some(prepare_change_facts(repo, filter, &changes)) + }, + |_, facts| { + let stamp = stamps.next()?; + let value = packing.pack(person_slot(stamp.author_id, unmatched), stamp.tick); + fold_change_facts(&mut tracker, facts, value); + commits.push(BurndownCommit { + stamp, + deltas: reduce_reports(&tracker.drain(), packing, flags), + }); + Some(()) + }, + )?; + let ownership = if flags.track_files && flags.track_people { + tracker.ownership() + } else { + BTreeMap::new() + }; + let names = if flags.track_people { + display_names(&summary.names) + } else { + Vec::new() + }; + Some(BurndownWalk { + commits, + base, + last_tick, + tick_size_hours: run.tick_size_hours(), + path: run.path, + flags, + paths: tracker.paths, + ownership, + names, + }) +} + +/// The per-commit `{lines_added, lines_removed}` map of the timeseries +/// formats. +fn line_stats_map(c: &BurndownCommit) -> GoValue { + let mut burndown = GoMap::new_map(); + burndown.insert("lines_added", GoValue::Int(c.deltas.added)); + burndown.insert("lines_removed", GoValue::Int(c.deltas.removed)); + GoValue::Map(burndown) +} + +/// The burndown contribution to the merged `--format timeseries` document: +/// per commit `{"lines_added", "lines_removed"}` in walk order, each commit +/// under its own tick. +#[must_use] +pub fn burndown_timeseries_contribution( + sub: &clap::ArgMatches, +) -> Option<crate::handlers::history_formats::TimeSeriesContribution> { + let walk = burndown_walk(sub)?; + let per_commit = walk + .commits + .iter() + .map(|c| (c.stamp.hex.clone(), line_stats_map(c))) + .collect(); + let commit_meta = walk + .commits + .iter() + .map(|c| { + ( + c.stamp.hex.clone(), + c.stamp.tick, + format_rfc3339_offset(c.stamp.when, c.stamp.offset_min), + String::new(), + ) + }) + .collect(); Some(crate::handlers::history_formats::TimeSeriesContribution { flag: "burndown", per_commit, @@ -232,324 +517,188 @@ pub fn burndown_timeseries_contribution( }) } -/// Builds the burndown **record** NDJSON bytes for the newest `--limit` commits -/// (`run --analyzers history/burndown --format ndjson`, no `--timeseries`, -/// no `--head`), or `None` if the repository cannot be opened/walked. -/// -/// Reproduces the reference streaming NDJSON sink (`analyze.StreamingSink.WriteTC`): -/// one `NDJSONLine{hash, tick, author_id, timestamp, analyzer, data}` per commit, -/// where `data` is the burndown `CommitResult` carried as `TC.Data`. Unlike the -/// time-series sibling this emits the full per-commit sparse `GlobalDeltas` -/// (`curTick -> prevTick -> lineCountDelta`) plus the derived `LinesAdded` / -/// `LinesRemoved`; the people/matrix/file/ownership fields are `null` because the -/// streaming pipeline runs with `PeopleNumber == 0` and no file tracking. -/// -/// `author_id` is the identity assigned by `IdentityDetector` (default loose -/// signature matching, dict built incrementally in walk order): each commit's -/// lowercased author email/name resolves to (or registers) a sequential id. -/// `timestamp` is the committer time in the commit's original zone offset -/// (`time.RFC3339`). Tick assignment matches the time-series path -/// (`TicksSinceStart`, monotonic floor of committer time against `tick0`). -/// -/// Both the top-level `NDJSONLine` object (`hash, tick, author_id, timestamp, -/// analyzer, data`) and the nested `data` `CommitResult` (`GlobalDeltas, -/// PeopleDeltas, MatrixDeltas, FileDeltas, FileOwnership, LinesAdded, -/// LinesRemoved`) preserve reference struct-declaration field order via struct-origin -/// maps, exactly as `json.Marshal` of those structs. All bytes route through -/// `cf-gojson`. -pub fn burndown_record_ndjson(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let records = burndown_ndjson_records(sub)?; - let mut out: Vec<u8> = Vec::new(); - for r in records { - // NDJSONLine: json.Marshal of a struct → field-declaration order - // (hash, tick, author_id, timestamp, analyzer, data). - let mut line = GoMap::new(MapOrigin::Struct); - line.insert("hash", GoValue::Str(r.hash)); - line.insert("tick", GoValue::Int(r.tick)); - line.insert("author_id", GoValue::Int(r.author_id)); - line.insert( - "timestamp", - GoValue::Str(format_rfc3339_offset(r.time_secs, r.tz_offset_min)), - ); - line.insert("analyzer", GoValue::Str("burndown".to_string())); - line.insert("data", r.data); - out.extend_from_slice(&cf_gojson::marshal(&GoValue::Map(line))); - out.push(b'\n'); - } - Some(out) +/// The per-commit `data` payload: `GlobalDeltas` (`curTick -> prevTick -> +/// delta`), `FileDeltas` (per final path; `null` without `--burndown-files`), +/// `PeopleDeltas` (per author of the affected lines) and `MatrixDeltas` +/// (`author -> modifier -> lines`; both `null` without `--burndown-people`), +/// `LinesAdded`, `LinesRemoved`. +fn commit_data(c: &BurndownCommit, walk: &BurndownWalk) -> GoValue { + let d = &c.deltas; + let mut data = GoMap::new(MapOrigin::Struct); + data.insert("GlobalDeltas", sparse_to_value(&d.global)); + data.insert( + "FileDeltas", + if walk.flags.track_files { + let mut files = GoMap::new_map(); + for (id, history) in &d.files { + files.insert(walk.paths[*id].clone(), sparse_to_value(history)); + } + GoValue::Map(files) + } else { + GoValue::Null + }, + ); + data.insert( + "PeopleDeltas", + if walk.flags.track_people { + let mut people = GoMap::new_int_map(); + for (person, history) in &d.people { + people.insert(person.to_string(), sparse_to_value(history)); + } + GoValue::Map(people) + } else { + GoValue::Null + }, + ); + data.insert( + "MatrixDeltas", + if walk.flags.track_people { + int_matrix_to_value(&d.matrix) + } else { + GoValue::Null + }, + ); + data.insert("LinesAdded", GoValue::Int(d.added)); + data.insert("LinesRemoved", GoValue::Int(d.removed)); + GoValue::Map(data) } -/// The per-commit burndown ndjson records (`CommitResult` payloads), the -/// [`burndown_record_ndjson`] walk factored to the shared +/// The per-commit burndown ndjson records, in the shared /// [`crate::handlers::history_formats::NdjsonRecord`] shape so the centralized -/// multi-analyzer `--format ndjson` history path (`history_ndjson`) can -/// interleave burndown with the other sequential leaf (devs) per commit, -/// exactly as the reference streaming sink emits them. +/// multi-analyzer `--format ndjson` path can interleave burndown with the +/// other leaves per commit. The window base is not a commit and has no +/// record. +#[must_use] pub fn burndown_ndjson_records( sub: &clap::ArgMatches, ) -> Option<Vec<crate::handlers::history_formats::NdjsonRecord>> { - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - - // Window: `limit` NEWEST commits oldest-first, first-parent (burndown - // forces --first-parent). - let hashes = crate::handlers::load_history_commit_hashes( - &repo, - limit, - true, - crate::handlers::history_since_spec(sub), - )?; - - let filter = crate::handlers::history_filter(sub).unwrap_or_default(); - let sink: DeltaSink = Rc::new(RefCell::new(Vec::new())); - let mut tracked: HashMap<String, File> = HashMap::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - let mut identity = LooseIdentity::default(); - - let mut records: Vec<crate::handlers::history_formats::NdjsonRecord> = Vec::new(); - - for (pos, hash) in hashes.iter().enumerate() { - let commit = repo.lookup_commit(*hash).ok()?; - let new_tree = commit.tree().ok()?; - - // author_id: loose identity over the commit's author signature. - let author = commit.author(); - let author_id = identity.resolve(&author.name, &author.email); - - let committer = commit.committer(); - let when = committer.when.seconds(); - let t0 = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw = (when - t0).div_euclid(TICK_PERIOD); - let tick = raw.max(previous_tick); - previous_tick = tick; - - let num_parents = commit.num_parents(); - let changes = if num_parents > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(&repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(&repo, Some(&new_tree)).ok()? - }; - let is_merge = num_parents > 1; - - sink.borrow_mut().clear(); - process_commit_changes( - &repo, - &changes, - &filter, - &mut tracked, - &sink, - tick, - is_merge, - ); - let (global, added, removed) = commit_sparse_stats(&sink.borrow(), tick); - - // CommitResult struct (struct-declaration field order, GlobalDeltas full - // sparse map; people/matrix/file/ownership null). - let mut data = GoMap::new(MapOrigin::Struct); - data.insert("GlobalDeltas", sparse_to_value(&global)); - data.insert("PeopleDeltas", GoValue::Null); - data.insert("MatrixDeltas", GoValue::Null); - data.insert("FileDeltas", GoValue::Null); - data.insert("FileOwnership", GoValue::Null); - data.insert("LinesAdded", GoValue::Int(added)); - data.insert("LinesRemoved", GoValue::Int(removed)); - - records.push(crate::handlers::history_formats::NdjsonRecord { - pos, - hash: hash.to_string(), - tick, - author_id, - time_secs: committer.when.seconds(), - tz_offset_min: committer.when.offset_minutes(), - data: GoValue::Map(data), - }); - } - + let walk = burndown_walk(sub)?; + let records = walk + .commits + .iter() + .map(|c| crate::handlers::history_formats::NdjsonRecord { + pos: c.stamp.pos, + hash: c.stamp.hex.clone(), + tick: c.stamp.tick, + author_id: c.stamp.author_id, + time_secs: c.stamp.when, + tz_offset_min: c.stamp.offset_min, + data: commit_data(c, &walk), + }) + .collect(); Some(records) } -/// `history/burndown --format json` over the general history pipeline (streaming, -/// e.g. `--limit N --workers 1`): the line-survival "burndown" report over the -/// newest N commits. -/// -/// This is the REAL port of the reference streaming pipeline -/// (`the reference `initHistoryPipeline` Reverse+FirstParent+Limit → RunStreaming → -/// burndown.HistoryAnalyzer.Consume → Aggregator.Add (MergeNestedAdditive of -/// per-commit GlobalDeltas) → ticksToReport (groupSparseHistory) → -/// ComputeAllMetrics`). The per-commit consume machinery is shared with the -/// NDJSON paths (`process_commit_changes`): every commit's filtered tree changes -/// drive the per-file burndown treaps, whose `(curTick, prevTick, delta)` updater -/// reports are folded into the running global sparse history `curTick -> prevTick -/// -> lineCountDelta` (additive, matching `mapx.MergeNestedAdditive`). At the end -/// the sparse history is densified with `groupSparseHistory` (Sampling = -/// Granularity = 30, both clamped equal) and `compute_global_metrics` runs the -/// global-survival + aggregate computation. -/// -/// With the default config (`PeopleNumber == 0`, `TrackFiles == false`) the -/// report carries only `GlobalHistory`, so `file_survival` / `developer_survival` -/// are empty and `interactions` is nil — exactly the reference output shape. Bytes route -/// through `cf-gojson` (compact, no trailing newline) byte-identically to -/// `run/history_burndown.json`. +/// `history/burndown --format json`: the line-survival report over the window. +#[must_use] pub fn burndown_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { let metrics = burndown_run_metrics(sub)?; Some(cf_gojson::marshal(&metrics.to_go_value())) } -/// Shared full-revwalk `history/burndown` report value (no `--head`): runs the -/// general per-commit streaming pipeline once and returns the aggregated -/// [`cf_analyzer_burndown::ComputedMetrics`], so every output format -/// (json/yaml/bin) is an encoding of the SAME report value (reference: -/// `analyze.OutputHistoryResults`, which computes the per-leaf `ComputedMetrics` -/// once and then marshals it per format). The json/yaml/bin wrappers -/// ([`burndown_run_report`], [`burndown_run_report_yaml`], -/// [`burndown_run_report_bin`]) all route this one value through the serializer -/// layer. -pub fn burndown_run_metrics( - sub: &clap::ArgMatches, -) -> Option<cf_analyzer_burndown::ComputedMetrics> { +/// The shared `history/burndown` report value: the aggregated +/// [`ComputedMetrics`] every output format (json/yaml/bin/text) encodes. +#[must_use] +pub fn burndown_run_metrics(sub: &clap::ArgMatches) -> Option<ComputedMetrics> { Some(burndown_run_aggregate(sub)?.metrics) } -/// The aggregated products of one full-revwalk burndown run: the computed -/// metrics plus the chart-rendering state the plot path needs (reference store -/// `ChartData`: the dense global history with rendering metadata). +/// The aggregated products of one burndown run: the computed metrics plus the +/// chart-rendering state the plot and text paths need. pub struct BurndownRunAggregate { /// The computed report metrics (json/yaml/bin source). - pub metrics: cf_analyzer_burndown::ComputedMetrics, + pub metrics: ComputedMetrics, /// Dense global history (negatives NOT yet clamped — the chart-data - /// builder clamps, mirroring `buildChartData`). - pub global_dense: cf_analyzer_burndown::DenseHistory, - /// Sampling (ticks per sample). - pub sampling: i64, - /// Granularity (ticks per band). - pub granularity: i64, - /// Tick size, nanoseconds (24 h). + /// builder clamps). + pub global_dense: DenseHistory, + /// The sampling grid the report was laid out on. + pub grid: Grid, + /// Tick size, nanoseconds. pub tick_size_ns: i64, - /// Aggregator end time (max TC timestamp = max committer time), Unix - /// nanoseconds. + /// Aggregator end time (max committer time), Unix nanoseconds. pub end_time_ns: i64, - /// Repository base name (the reference `repoName()`: `filepath.Base(repo.Path())`, - /// empty for `""`/`"."`). + /// Repository base name (empty for `""`/`"."`). pub project_name: String, } -/// Runs the full-revwalk burndown aggregation once, returning both the report -/// metrics and the chart-data inputs (shared by [`burndown_run_metrics`] and -/// the plot path). -pub fn burndown_run_aggregate(sub: &clap::ArgMatches) -> Option<BurndownRunAggregate> { - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - - // Burndown defaults (Initialize): Granularity = Sampling = 30 (clamped equal - // since Sampling is not > Granularity), TickSize = 24h. These config keys are - // not exposed on the `run` subcommand, so the defaults always apply here. - let granularity = 30i64; - let sampling = 30i64; - let tick_size_hours = 24i64; - - // Window: `limit` NEWEST commits oldest-first, first-parent (burndown - // forces --first-parent). - let hashes = crate::handlers::load_history_commit_hashes( - &repo, - limit, - true, - crate::handlers::history_since_spec(sub), - )?; - - let filter = crate::handlers::history_filter(sub).unwrap_or_default(); - let sink: DeltaSink = Rc::new(RefCell::new(Vec::new())); - let mut tracked: HashMap<String, File> = HashMap::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - // Accumulated global sparse history (Aggregator.globalHistory); the last - // tick is derived from its KEY ticks after the walk (reference - // `findLastTick`: max curTick present in the merged GlobalHistory — ticks - // where deltas were RECORDED, so commits with zero surviving changes do - // not extend the window). - let mut global_history: cf_analyzer_burndown::SparseHistory = std::collections::BTreeMap::new(); - // Aggregator endTime: the max TC timestamp (committer time) seen. - let mut end_time_secs: i64 = 0; +/// Adds every cell of `src` into `dst`. +fn merge_sparse(dst: &mut SparseHistory, src: &SparseHistory) { + for (cur, row) in src { + for (prev, delta) in row { + add_cell(dst, *cur, *prev, *delta); + } + } +} - for hash in &hashes { - let commit = repo.lookup_commit(*hash).ok()?; - let new_tree = commit.tree().ok()?; - - let committer = commit.committer(); - let when = committer.when.seconds(); - let t0 = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw = (when - t0).div_euclid(TICK_PERIOD); - let tick = raw.max(previous_tick); - previous_tick = tick; - - let num_parents = commit.num_parents(); - let changes = if num_parents > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(&repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(&repo, Some(&new_tree)).ok()? - }; - let is_merge = num_parents > 1; - - sink.borrow_mut().clear(); - process_commit_changes( - &repo, - &changes, - &filter, - &mut tracked, - &sink, - tick, - is_merge, - ); - let (global, _added, _removed) = commit_sparse_stats(&sink.borrow(), tick); +/// The walk-level accumulation of every commit's deltas (plus the base). +#[derive(Default)] +struct WalkTotals { + global: SparseHistory, + files: BTreeMap<String, SparseHistory>, + people: BTreeMap<i64, SparseHistory>, + matrix: BTreeMap<i64, BTreeMap<i64, i64>>, +} - // Aggregator.Add: MergeNestedAdditive(globalHistory, cr.GlobalDeltas). - for (cur, row) in &global { - let dst = global_history.entry(*cur).or_default(); - for (prev, delta) in row { - *dst.entry(*prev).or_default() += *delta; +impl WalkTotals { + fn merge(&mut self, d: &CommitDeltas, paths: &[String]) { + merge_sparse(&mut self.global, &d.global); + for (id, history) in &d.files { + merge_sparse(self.files.entry(paths[*id].clone()).or_default(), history); + } + for (person, history) in &d.people { + merge_sparse(self.people.entry(*person).or_default(), history); + } + for (author, row) in &d.matrix { + let dst = self.matrix.entry(*author).or_default(); + for (modifier, lines) in row { + *dst.entry(*modifier).or_default() += *lines; } } + } +} - if when > end_time_secs { - end_time_secs = when; - } +/// Runs the burndown aggregation once, returning both the report metrics and +/// the chart-data inputs (shared by [`burndown_run_metrics`] and the plot +/// path). +#[must_use] +pub fn burndown_run_aggregate(sub: &clap::ArgMatches) -> Option<BurndownRunAggregate> { + let walk = burndown_walk(sub)?; + let mut totals = WalkTotals::default(); + totals.merge(&walk.base, &walk.paths); + let mut end_time_secs: i64 = 0; + for c in &walk.commits { + totals.merge(&c.deltas, &walk.paths); + end_time_secs = end_time_secs.max(c.stamp.when); } - // ticksToReport: findLastTick scans the merged GlobalHistory tick keys - // (zero when nothing was recorded), NOT the max walked commit tick. - let last_tick = global_history.keys().next_back().copied().unwrap_or(0); - let dense = cf_analyzer_burndown::group_sparse_history( - &global_history, - sampling, - granularity, - last_tick, - ); - let metrics = cf_analyzer_burndown::compute_global_metrics(&dense, sampling, tick_size_hours); + let grid = Grid { + sampling: walk.flags.sampling, + granularity: walk.flags.granularity, + last_tick: walk.last_tick, + tick_size_hours: walk.tick_size_hours, + }; + let metrics = compute_metrics(&MetricsInput { + global: &totals.global, + files: walk.flags.track_files.then_some(&totals.files), + people: walk.flags.track_people.then_some(&totals.people), + ownership: &walk.ownership, + matrix: &totals.matrix, + names: &walk.names, + grid, + }); Some(BurndownRunAggregate { metrics, - global_dense: dense, - sampling, - granularity, - tick_size_ns: tick_size_hours * 3600 * 1_000_000_000, - end_time_ns: end_time_secs.saturating_mul(1_000_000_000), - project_name: repo_base_name(&path), + global_dense: grid.densify(&totals.global), + grid, + tick_size_ns: walk.tick_size_hours * NANOS_PER_HOUR, + end_time_ns: end_time_secs.saturating_mul(NANOS_PER_SECOND), + project_name: repo_base_name(&walk.path), }) } -/// reference burndown `repoName()`: the repository path's base name, or empty when -/// the path is `""` or `"."`. +/// The repository path's base name, or empty when the path is `""` or `"."`. +#[must_use] pub fn repo_base_name(path: &str) -> String { if path.is_empty() || path == "." { return String::new(); @@ -564,10 +713,9 @@ pub fn repo_base_name(path: &str) -> String { } } -/// Full-revwalk `history/burndown --format yaml` report bytes (no `--head`), -/// reusing the shared [`burndown_run_metrics`] report value wrapped in the -/// `codefang (v2)` YAML envelope (the reference `analyze.OutputHistoryResults` YAML branch). -/// One report value, encoded by the serializer layer. +/// `history/burndown --format yaml`: the `codefang (v2)` header, the +/// `history/burndown:` section name and the metrics. +#[must_use] pub fn burndown_run_report_yaml(sub: &clap::ArgMatches) -> Option<Vec<u8>> { let metrics = burndown_run_metrics(sub)?; let mut out = Vec::new(); @@ -575,307 +723,203 @@ pub fn burndown_run_report_yaml(sub: &clap::ArgMatches) -> Option<Vec<u8>> { out.extend_from_slice(format!(" version: {}\n", cf_version::DEFAULT_BINARY).as_bytes()); out.extend_from_slice(format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes()); out.extend_from_slice(b"history/burndown:\n"); - out.extend_from_slice(&cf_goyaml::marshal(&metrics.to_go_value_yaml())); + out.extend_from_slice(&cf_goyaml::marshal(&metrics.to_go_value())); Some(out) } -/// Full-revwalk `history/burndown --format bin` report bytes (no `--head`), -/// reusing the shared [`burndown_run_metrics`] report value wrapped in the CFB1 -/// binary envelope (the reference `analyze.OutputHistoryResults` raw/binary branch). One -/// report value, encoded by the serializer layer. +/// `history/burndown --format bin`: the CFB1 envelope over the json value. +#[must_use] pub fn burndown_run_report_bin(sub: &clap::ArgMatches) -> Option<Vec<u8>> { let metrics = burndown_run_metrics(sub)?; cf_reportutil::encode_binary_envelope(&metrics.to_go_value()).ok() } -/// `IdentityDetector` with default loose signature matching, built incrementally -/// in commit-walk order (`registerLooseIdentity`). Maps lowercased author -/// email/name to a sequential id, registering new identities on first sight. -#[derive(Default)] -struct LooseIdentity { - dict: HashMap<String, i64>, - size: i64, -} - -impl LooseIdentity { - fn resolve(&mut self, name: &str, email: &str) -> i64 { - let email = email.to_lowercase(); - let name = name.to_lowercase(); - - if let Some(&id) = self.dict.get(&email) { - self.dict.entry(name).or_insert(id); - return id; - } - if let Some(&id) = self.dict.get(&name) { - self.dict.insert(email, id); - return id; - } - let id = self.size; - self.dict.insert(email, id); - self.dict.insert(name, id); - self.size += 1; - id - } -} - -/// `computeCommitLineStats` plus the full sparse `globalDeltas[cur][prev]` map. -/// Reports `(cur, prev, delta)` aggregate into numerically-sorted nested -/// `BTreeMap`s (so canceling `(tick,tick,+n)`/`(tick,tick,-n)` cells vanish, and -/// the int map keys emit in the reference implementation's numeric order). `lines_added` sums positive -/// `(tick, tick)` cells; `lines_removed` sums the magnitudes of negative cells in -/// the commit-tick row. -fn commit_sparse_stats( - reports: &[(i64, i64, i64)], - tick: i64, -) -> (BTreeMap<i64, BTreeMap<i64, i64>>, i64, i64) { - let mut global: BTreeMap<i64, BTreeMap<i64, i64>> = BTreeMap::new(); - for &(cur, prev, delta) in reports { - *global.entry(cur).or_default().entry(prev).or_default() += delta; - } - let mut added = 0i64; - let mut removed = 0i64; - if let Some(row) = global.get(&tick) { - for (&prev, &delta) in row { - if prev == tick && delta > 0 { - added += delta; - } else if delta < 0 { - removed += -delta; - } +/// Serializes a sparse `curTick -> prevTick -> delta` map as an int-keyed +/// object of int-keyed objects. +fn sparse_to_value(history: &SparseHistory) -> GoValue { + let mut outer = GoMap::new_int_map(); + for (cur, row) in history { + let mut inner = GoMap::new_int_map(); + for (prev, delta) in row { + inner.insert(prev.to_string(), GoValue::Int(*delta)); } - } - (global, added, removed) -} - -/// Serializes the sparse `curTick -> prevTick -> delta` map as a struct-origin -/// `GoValue` whose insertion order is the keys' STRING-LEXICOGRAPHIC order — -/// matching the reference implementation's `json.Marshal` of `map[int]map[int]int64`, which renders int -/// keys as strings and sorts those strings lexicographically (encoding/json -/// `mapEncoder`: `sv[i].ks < sv[j].ks`), e.g. `"130" < "2" < "7"`. Numeric -/// BTreeMap order diverges whenever a row mixes keys of different digit counts -/// (the burndown[ndjson]@hercules tick-130 row). -fn sparse_to_value(global: &BTreeMap<i64, BTreeMap<i64, i64>>) -> GoValue { - let lex = |m: &BTreeMap<i64, i64>| -> Vec<(String, i64)> { - let mut v: Vec<(String, i64)> = m.iter().map(|(k, d)| (k.to_string(), *d)).collect(); - v.sort_by(|a, b| a.0.cmp(&b.0)); - v - }; - let mut outer_keys: Vec<(String, &BTreeMap<i64, i64>)> = - global.iter().map(|(k, row)| (k.to_string(), row)).collect(); - outer_keys.sort_by(|a, b| a.0.cmp(&b.0)); - - let mut outer = GoMap::new(MapOrigin::Struct); - for (cur, row) in outer_keys { - let mut inner = GoMap::new(MapOrigin::Struct); - for (prev, delta) in lex(row) { - inner.insert(prev, GoValue::Int(delta)); - } - outer.insert(cur, GoValue::Map(inner)); + outer.insert(cur.to_string(), GoValue::Map(inner)); } GoValue::Map(outer) } -/// `computeCommitLineStats(cr, curTick)`: the `(cur, prev, delta)` reports are -/// first aggregated into the sparse `globalDeltas[cur][prev]` cells (matching the reference implementation's -/// `incrementSparseHistory`), so a `(tick, tick, +n)` insert and a -/// `(tick, tick, -n)` delete in the same cell cancel. Then, over the current -/// tick's row, positive `(tick, tick)` cells sum into `lines_added` and negative -/// cells' magnitudes sum into `lines_removed`. -fn commit_line_stats(reports: &[(i64, i64, i64)], tick: i64) -> (i64, i64) { - use std::collections::HashMap; - // globalDeltas[cur][prev] += delta. - let mut global: HashMap<i64, HashMap<i64, i64>> = HashMap::new(); - for &(cur, prev, delta) in reports { - *global.entry(cur).or_default().entry(prev).or_default() += delta; - } - let mut added = 0i64; - let mut removed = 0i64; - if let Some(row) = global.get(&tick) { - for (&prev, &delta) in row { - if prev == tick && delta > 0 { - added += delta; - } else if delta < 0 { - removed += -delta; - } +/// Serializes an `author -> modifier -> lines` matrix as an int-keyed object +/// of int-keyed objects. +fn int_matrix_to_value(matrix: &BTreeMap<i64, BTreeMap<i64, i64>>) -> GoValue { + let mut outer = GoMap::new_int_map(); + for (author, row) in matrix { + let mut inner = GoMap::new_int_map(); + for (modifier, lines) in row { + inner.insert(modifier.to_string(), GoValue::Int(*lines)); } + outer.insert(author.to_string(), GoValue::Map(inner)); } - (added, removed) -} - -/// Builds the treap updater for a newly tracked file: every `File::update` -/// report `(current, previous, delta)` is pushed to the shared sink. With no -/// identity provider the treap time keys are raw ticks, so `current`/`previous` -/// are already `curTick`/`prevTick`. -fn make_updater(sink: &DeltaSink) -> Vec<Updater> { - let s = sink.clone(); - let updater: Updater = Box::new(move |cur, prev, delta| { - s.borrow_mut().push((cur, prev, delta)); - }); - vec![updater] + GoValue::Map(outer) } -/// Applies one commit's filtered changes to the per-path treaps, recording all -/// line deltas into `sink`. -#[allow(clippy::too_many_arguments)] -fn process_commit_changes( +/// Applies one change set's filtered changes to the tracker at `value` (the +/// window base seed): [`prepare_change_facts`] then [`fold_change_facts`]. +fn apply_changes( repo: &cf_gitlib::Repository, + filter: &HistoryFilter, + tracker: &mut Tracker, changes: &[Change], - filter: &crate::handlers::HistoryFilter, - tracked: &mut HashMap<String, File>, - sink: &DeltaSink, - tick: i64, - is_merge: bool, + value: i64, ) { - for change in changes { - // TreeDiff.filterChanges: path policy, then the --languages check, by - // the change's (name, hash) (To for insert/modify, From for delete). - let (filter_name, filter_hash) = match change.action { - ChangeAction::Delete => (&change.from.name, change.from.hash), - _ => (&change.to.name, change.to.hash), - }; - if !filter.includes_change(repo, filter_name, filter_hash) { - continue; - } - - match change.action { - ChangeAction::Insert => apply_insertion(repo, change, tracked, sink, tick), - ChangeAction::Delete => apply_deletion(repo, change, tracked, tick, is_merge), - ChangeAction::Modify => apply_modification(repo, change, tracked, sink, tick, is_merge), - } - } + fold_change_facts(tracker, prepare_change_facts(repo, filter, changes), value); } -/// `handleInsertion`: create a `File` at the commit tick of the To-blob's line -/// length (binaries skipped). `File::new` fires the updater with -/// `(tick, tick, +lines)`. -fn apply_insertion( - repo: &cf_gitlib::Repository, - change: &Change, - tracked: &mut HashMap<String, File>, - sink: &DeltaSink, - tick: i64, -) { - if change.to.hash.is_zero() { - return; - } - let Ok(blob) = CachedBlob::from_repo(repo, change.to.hash) else { - return; - }; - let Ok(lines) = blob.count_lines() else { - return; // binary: CountLines → ErrBinary; handleInsertion skips. - }; - let file = File::new(tick, lines as i64, make_updater(sink)); - tracked.insert(change.to.name.clone(), file); +/// One change's effect on the line tracker, computed without the tracker so +/// it can run on the worker pool; [`fold_change_facts`] applies it in commit +/// order with the sequential walk's rules. +#[derive(Debug, Clone, PartialEq, Eq)] +enum ChangeFacts { + /// `path` (re)written whole: its line count, `None` for a binary or + /// unreadable blob. + Insert { path: String, lines: Option<i64> }, + /// `path` deleted. + Delete { path: String }, + /// A modification, possibly with a rename. + Modify { + from: String, + to: String, + /// The To blob's line count, `None` for a binary or unreadable blob: + /// the whole-file fallback. + to_lines: Option<i64>, + /// `None` when either blob is unreadable: the change is skipped. + diff: Option<ModifyDiff>, + }, } -/// `handleDeletion`: when the tracked treap length matches the From-blob's line -/// count, update the file to length 0 (recording the survival debits), else -/// force-remove with no delta. On a merge the deletion time is tick 0 so its -/// negative deltas fall outside the commit-tick row. The file is always dropped. -fn apply_deletion( - repo: &cf_gitlib::Repository, - change: &Change, - tracked: &mut HashMap<String, File>, - tick: i64, - is_merge: bool, -) { - let name = if !change.to.hash.is_zero() { - change.to.name.clone() - } else { - change.from.name.clone() - }; - let Some(mut file) = tracked.remove(&name) else { - return; - }; - let Ok(blob) = CachedBlob::from_repo(repo, change.from.hash) else { - return; - }; - let Ok(lines) = blob.count_lines() else { - return; // binary From: countDeletionLines errors (not in supported window). - }; - if file.len() != lines as i64 { - // forceRemoveFile: drop without recording a delta (already removed). - return; - } - // applyDeletionUpdate: tick 0 on a merge (non-deletion), else the commit tick. - let del_time = if is_merge { 0 } else { tick }; - let len = file.len(); - file.update(del_time, 0, 0, len); - // file.Delete() drops tracking — already removed from the map. +/// The line diff of a readable modification; it applies only while the From +/// path's tracked length equals `from_lines` and the To blob is text. +#[derive(Debug, Clone, PartialEq, Eq)] +struct ModifyDiff { + /// The From blob's line count, `None` when it cannot be counted. + from_lines: Option<i64>, + /// The To blob is binary. + to_binary: bool, + /// The line ops from From to To (empty when the diff cannot apply). + ops: Vec<(Op, i64)>, } -/// `handleModification`: tracked text file → drive `File::update` from the line -/// diff; untracked, binary-from, or length-mismatched → fall back to insertion. -fn apply_modification( +/// The tracker-independent facts of one commit's changes that pass the +/// change filter (path policy, then `--languages`, by the change's name and +/// hash: To for insert and modify, From for delete). +fn prepare_change_facts( repo: &cf_gitlib::Repository, - change: &Change, - tracked: &mut HashMap<String, File>, - sink: &DeltaSink, - tick: i64, - is_merge: bool, -) { - // Not currently tracked → handleInsertion. - if !tracked.contains_key(&change.from.name) { - apply_insertion(repo, change, tracked, sink, tick); - return; - } + filter: &HistoryFilter, + changes: &[Change], +) -> Vec<ChangeFacts> { + changes + .iter() + .filter(|change| { + let (name, hash) = match change.action { + ChangeAction::Delete => (&change.from.name, change.from.hash), + _ => (&change.to.name, change.to.hash), + }; + filter.includes_change(repo, name, hash) + }) + .map(|change| match change.action { + ChangeAction::Insert => ChangeFacts::Insert { + path: change.to.name.clone(), + lines: text_lines(repo, change.to.hash), + }, + ChangeAction::Delete => ChangeFacts::Delete { + path: change.from.name.clone(), + }, + ChangeAction::Modify => ChangeFacts::Modify { + from: change.from.name.clone(), + to: change.to.name.clone(), + to_lines: text_lines(repo, change.to.hash), + diff: modify_diff(repo, change), + }, + }) + .collect() +} +/// The line diff of `change`, `None` when either blob cannot be read. +fn modify_diff(repo: &cf_gitlib::Repository, change: &Change) -> Option<ModifyDiff> { let (Ok(blob_from), Ok(blob_to)) = ( CachedBlob::from_repo(repo, change.from.hash), CachedBlob::from_repo(repo, change.to.hash), ) else { - return; + return None; }; - - let from_binary = blob_from.is_binary(); + let from_lines = blob_from.count_lines().ok().map(|n| n as i64); let to_binary = blob_to.is_binary(); + let ops = match from_lines { + Some(old_lines) if !to_binary => { + compute_line_diff(repo, change, &blob_from, &blob_to, old_lines) + } + _ => Vec::new(), + }; + Some(ModifyDiff { + from_lines, + to_binary, + ops, + }) +} + +/// Applies one commit's prepared change facts to the tracker at `value`, in +/// change order. A modification follows its line diff only when the From +/// path's tracked length matches the From blob and the To blob is text; +/// otherwise the tracked lines are debited and the To blob is (re)inserted +/// whole. +fn fold_change_facts(tracker: &mut Tracker, facts: Vec<ChangeFacts>, value: i64) { + for fact in facts { + match fact { + ChangeFacts::Insert { path, lines } => insert_lines(tracker, &path, lines, value), + ChangeFacts::Delete { path } => tracker.remove(&path, value), + ChangeFacts::Modify { + from, + to, + to_lines, + diff, + } => { + let Some(tracked_len) = tracker.len_of(&from) else { + insert_lines(tracker, &to, to_lines, value); + continue; + }; + let Some(diff) = diff else { + continue; + }; + if diff.from_lines != Some(tracked_len) || diff.to_binary { + tracker.remove(&from, value); + insert_lines(tracker, &to, to_lines, value); + continue; + } + tracker.apply_diffs(&from, &diff.ops, value); + tracker.rename(&from, &to, value); + } + } + } +} + +/// Tracks `path` as `lines` text lines written at `value`; a binary or +/// unreadable blob (`None`) is not tracked. Whatever was tracked under the +/// path is debited first. +fn insert_lines(tracker: &mut Tracker, path: &str, lines: Option<i64>, value: i64) { + match lines { + Some(lines) => tracker.insert(path, lines, value), + None => tracker.remove(path, value), + } +} - // classifyBlobErrors: from binary,to text → insertion; from text,to binary → - // deletion; both binary → skip. - if from_binary && to_binary { - return; - } - if from_binary && !to_binary { - apply_insertion(repo, change, tracked, sink, tick); - return; - } - if !from_binary && to_binary { - apply_deletion(repo, change, tracked, tick, is_merge); - return; - } - - // Both valid: file.Len() must equal the diff's OldLinesOfCode (= From blob - // line count); on mismatch, resetAndReinsert (full insertion). - let old_lines = blob_from.count_lines().unwrap_or(0) as i64; - let tracked_len = tracked.get(&change.from.name).map_or(0, File::len); - if tracked_len != old_lines { - tracked.remove(&change.from.name); - apply_insertion(repo, change, tracked, sink, tick); - return; - } - - // applyDiffs: drive File::update from the line diff via the diffApplier - // pending/flush logic. The file may move to change.To.Name (here From == To). - // - // The runtime history pipeline (the reference diff pipeline → - // `cf_batch_diff_blobs` → `git_diff_blobs`) computes the line diff with - // libgit2's Myers xdiff, NOT diffmatchpatch — `FileDiffAnalyzer.Consume` - // takes the pre-computed `ac.FileDiffs` when the runtime pipeline provides - // them, and the streaming `run` command always does. The dmp engine only - // runs as the `fileDiffFromGoDiff` fallback on a libgit2 error. The two - // engines align edit boundaries differently (xdiff shift heuristics vs. - // dmp semantic-lossless cleanup), which changes per-line tick attribution - // in the survival treap, so burndown must consume the SAME libgit2 op - // stream as the reference. - let diffs = compute_line_diff(repo, change, &blob_from, &blob_to, old_lines); - let mut file = tracked.remove(&change.from.name).unwrap(); - apply_diffs(&mut file, &diffs, tick); - tracked.insert(change.to.name.clone(), file); -} - -/// The line-diff op stream the reference runtime pipeline feeds burndown: libgit2 -/// `git_diff_blobs` line ops (`diff_blob_line_ops`, the `cf_batch_diff_blobs` -/// port), falling back to the diffmatchpatch engine (`fileDiffFromGoDiff`) only -/// when libgit2 errors — mirroring `DiffPipeline.processDiffResponse`. +/// The line count of a text blob; `None` for a binary or unreadable blob. +fn text_lines(repo: &cf_gitlib::Repository, hash: cf_gitlib::Hash) -> Option<i64> { + let blob = CachedBlob::from_repo(repo, hash).ok()?; + blob.count_lines().ok().map(|n| n as i64) +} + +/// The line-diff op stream: libgit2 `git_diff_blobs` line ops +/// (`diff_blob_line_ops`), falling back to the diff-match-patch engine only +/// when libgit2 errors. fn compute_line_diff( repo: &cf_gitlib::Repository, change: &Change, @@ -900,25 +944,23 @@ fn compute_line_diff( } } -/// Port of burndown's `diffApplier` (`applyDiffs`): walks the line diff and -/// issues `File::update(packValue=tick, position, insLen, delLen)` calls. A -/// pending delete merges with a following insert into a single replace; equal -/// runs flush any pending delete and advance the position. -fn apply_diffs(file: &mut File, diffs: &[(Op, i64)], tick: i64) { +/// Walks a line diff and issues `File::update(value, position, insLen, +/// delLen)` calls: a pending delete merges with a following insert into a +/// single replace; equal runs flush any pending edit and advance the +/// position. +fn apply_diffs(file: &mut File, diffs: &[(Op, i64)], value: i64) { let mut position: i64 = 0; - // pending mirrors the reference implementation's `pending diffmatchpatch.Diff` (its op + line length). - // An empty pending is represented by `None`. let mut pending: Option<(Op, i64)> = None; - // flushPending: apply a deferred edit (insert advances position; delete does + // Apply a deferred edit (an insert advances the position; a delete does // not) and clear it. - fn flush(file: &mut File, pending: &mut Option<(Op, i64)>, position: &mut i64, tick: i64) { + fn flush(file: &mut File, pending: &mut Option<(Op, i64)>, position: &mut i64, value: i64) { if let Some((op, len)) = pending.take() { if op == Op::Insert { - file.update(tick, *position, len, 0); + file.update(value, *position, len, 0); *position += len; } else { - file.update(tick, *position, 0, len); + file.update(value, *position, 0, len); } } } @@ -926,14 +968,12 @@ fn apply_diffs(file: &mut File, diffs: &[(Op, i64)], tick: i64) { for &(op, len) in diffs { match op { Op::Equal => { - flush(file, &mut pending, &mut position, tick); + flush(file, &mut pending, &mut position, value); position += len; } Op::Insert => { - // handleInsert: if a pending (delete) exists, emit a combined - // replace and clear it; otherwise defer this insert as pending. if let Some((_, del_len)) = pending { - file.update(tick, position, len, del_len); + file.update(value, position, len, del_len); position += len; pending = None; } else { @@ -941,12 +981,388 @@ fn apply_diffs(file: &mut File, diffs: &[(Op, i64)], tick: i64) { } } Op::Delete => { - // The reference implementation overwrites pending with the delete (no flush); after - // DiffCleanupMerge a delete is always followed by its insert or - // an equality, so a pending insert never precedes a delete. + // A delete is always followed by its insert or an equality, + // so a pending insert never precedes a delete. pending = Some((Op::Delete, len)); } } } - flush(file, &mut pending, &mut position, tick); + flush(file, &mut pending, &mut position, value); +} + +#[cfg(test)] +mod tests { + use super::*; + + const DAILY_PEOPLE: BurndownFlags = BurndownFlags { + granularity: 30, + sampling: 30, + track_files: true, + track_people: true, + }; + const GLOBAL_ONLY: BurndownFlags = BurndownFlags { + granularity: 30, + sampling: 30, + track_files: false, + track_people: false, + }; + + /// The sequential walk's change application, kept as the reference the + /// prepared facts are checked against. + fn reference_apply_changes( + repo: &cf_gitlib::Repository, + filter: &HistoryFilter, + tracker: &mut Tracker, + changes: &[Change], + value: i64, + ) { + for change in changes { + let (filter_name, filter_hash) = match change.action { + ChangeAction::Delete => (&change.from.name, change.from.hash), + _ => (&change.to.name, change.to.hash), + }; + if !filter.includes_change(repo, filter_name, filter_hash) { + continue; + } + match change.action { + ChangeAction::Insert => { + insert_blob(repo, tracker, &change.to.name, change.to.hash, value); + } + ChangeAction::Delete => tracker.remove(&change.from.name, value), + ChangeAction::Modify => apply_modification(repo, tracker, change, value), + } + } + } + + /// Tracks `path` as the text blob `hash` written at `value`; a binary blob + /// is not tracked. Whatever was tracked under the path is debited first. + fn insert_blob( + repo: &cf_gitlib::Repository, + tracker: &mut Tracker, + path: &str, + hash: cf_gitlib::Hash, + value: i64, + ) { + match text_lines(repo, hash) { + Some(lines) => tracker.insert(path, lines, value), + None => tracker.remove(path, value), + } + } + + /// A modification (including a rename: `from.name != to.name`). When the + /// tracked state matches the From blob and the To blob is text, the treap + /// follows the line diff and moves to the To path; otherwise the tracked + /// lines are debited and the To blob is (re)inserted whole. + fn apply_modification( + repo: &cf_gitlib::Repository, + tracker: &mut Tracker, + change: &Change, + value: i64, + ) { + let (from, to) = (change.from.name.as_str(), change.to.name.as_str()); + let Some(tracked_len) = tracker.len_of(from) else { + insert_blob(repo, tracker, to, change.to.hash, value); + return; + }; + let (Ok(blob_from), Ok(blob_to)) = ( + CachedBlob::from_repo(repo, change.from.hash), + CachedBlob::from_repo(repo, change.to.hash), + ) else { + return; + }; + let in_sync = + blob_from.count_lines().ok() == Some(tracked_len as usize) && !blob_to.is_binary(); + if !in_sync { + tracker.remove(from, value); + insert_blob(repo, tracker, to, change.to.hash, value); + return; + } + let diffs = compute_line_diff(repo, change, &blob_from, &blob_to, tracked_len); + tracker.apply_diffs(from, &diffs, value); + tracker.rename(from, to, value); + } + + /// `n` numbered text lines tagged `tag`. + fn numbered(n: usize, tag: &str) -> Vec<u8> { + (0..n) + .map(|i| format!("{tag} {i}\n")) + .collect::<String>() + .into_bytes() + } + + /// Per change, the facts prepared off the tracker (on the worker pool) + /// fold into exactly the reports and paths the sequential walk produces: + /// through inserts, a modification, a file whose tracked length diverged + /// from its blob (its previous edit was dropped, like a commit over the + /// cap), a rename, an edit after the rename, a text-to-binary switch and + /// a delete. + #[test] + fn prepared_changes_fold_like_the_sequential_walk() { + use cf_gitlib::testutil::{init_repo, CommitBuilder}; + const T0: i64 = 1_609_459_200; + const DROPPED: usize = 2; + let test = init_repo().expect("init"); + let at = |i: i64| T0 + i; + let hashes = [ + CommitBuilder::new(&test, "add", at(0)) + .write("a.txt", &numbered(5, "a")) + .write("b.txt", &numbered(3, "b")) + .commit(), + CommitBuilder::new(&test, "modify", at(1)) + .write("a.txt", &[numbered(5, "a"), numbered(1, "more")].concat()) + .commit(), + CommitBuilder::new(&test, "dropped", at(2)) + .write("b.txt", &numbered(9, "b")) + .commit(), + CommitBuilder::new(&test, "diverged", at(3)) + .write("b.txt", &[numbered(9, "b"), numbered(1, "x")].concat()) + .commit(), + CommitBuilder::new(&test, "rename", at(4)) + .rename("a.txt", "c.txt") + .commit(), + CommitBuilder::new(&test, "edit renamed", at(5)) + .write("c.txt", &numbered(4, "a")) + .commit(), + CommitBuilder::new(&test, "binary", at(6)) + .write("c.txt", b"\x00\x01\x02binary") + .commit(), + CommitBuilder::new(&test, "delete", at(7)) + .delete("b.txt") + .commit(), + ] + .map(|h| h.expect("commit")); + let repo = cf_gitlib::Repository::open(test.path().to_str().expect("utf-8")).expect("open"); + let filter = HistoryFilter::from_policy(cf_pathpolicy::Options::default()); + let packing = ValuePacking::ticks_only(); + let (mut sequential, mut prepared) = (Tracker::new(packing), Tracker::new(packing)); + for (i, hash) in hashes.iter().enumerate() { + if i == DROPPED { + continue; + } + let commit = repo.lookup_commit(*hash).expect("lookup"); + let changes = commit_tree_changes(&repo, &commit).expect("changes"); + let value = i as i64 + 1; + reference_apply_changes(&repo, &filter, &mut sequential, &changes, value); + fold_change_facts( + &mut prepared, + prepare_change_facts(&repo, &filter, &changes), + value, + ); + assert_eq!(prepared.drain(), sequential.drain(), "commit {i}"); + assert_eq!(prepared.paths, sequential.paths, "commit {i}"); + } + } + + /// Commits big enough to reuse their pre-pass tree diff (an add and an + /// edit of more than [`REUSED_DIFF_CHANGES`] files) fold into the same + /// per-commit deltas as the sequential reference. + #[test] + fn heavy_commits_reuse_their_diff_and_fold_like_the_reference() { + use cf_gitlib::testutil::{init_repo, CommitBuilder}; + const T0: i64 = 1_609_459_200; + const DAY: i64 = 86_400; + let files = REUSED_DIFF_CHANGES + 1; + let test = init_repo().expect("init"); + let mut add = CommitBuilder::new(&test, "add", T0); + let mut edit = CommitBuilder::new(&test, "edit", T0 + DAY); + for i in 0..files { + let name = format!("f{i}.txt"); + add = add.write(&name, &numbered(3, "a")); + edit = edit.write(&name, &[numbered(2, "a"), numbered(2, "b")].concat()); + } + let first = add.commit().expect("add"); + let second = edit.commit().expect("edit"); + let path = test.path().to_str().expect("utf-8"); + let sub = crate::build_codefang_command() + .try_get_matches_from(["codefang", "run", "-a", "history/burndown", path]) + .expect("args") + .subcommand_matches("run") + .expect("run") + .clone(); + let walk = burndown_walk(&sub).expect("walk"); + + let repo = cf_gitlib::Repository::open(path).expect("open"); + let filter = HistoryFilter::from_policy(cf_pathpolicy::Options::default()); + let packing = ValuePacking::ticks_only(); + let mut tracker = Tracker::new(packing); + assert_eq!(walk.commits.len(), 2); + for (commit, hash) in walk.commits.iter().zip([first, second]) { + let changes = commit_tree_changes(&repo, &repo.lookup_commit(hash).expect("lookup")) + .expect("changes"); + assert!(changes.len() > REUSED_DIFF_CHANGES); + let value = packing.pack(packing.unknown_person(), commit.stamp.tick); + reference_apply_changes(&repo, &filter, &mut tracker, &changes, value); + let expected = reduce_reports(&tracker.drain(), packing, walk.flags); + assert_eq!( + commit.deltas, expected, + "commit at tick {}", + commit.stamp.tick + ); + } + assert!( + walk.commits[1].deltas.removed > 0, + "the edit replaced lines" + ); + } + + /// `insert` / `remove` / `rename` keep the per-path state and report + /// every line exactly once. + #[test] + fn tracker_debits_before_reinsert_and_follows_renames() { + let packing = ValuePacking::ticks_only(); + let mut t = Tracker::new(packing); + t.insert("a.go", 10, 0); + // Re-inserting a tracked path debits its 10 lines first. + t.insert("a.go", 4, 5); + // A rename keeps the treap and updates the id's path. + t.rename("a.go", "b.go", 6); + assert_eq!(t.len_of("b.go"), Some(4)); + assert_eq!(t.len_of("a.go"), None); + assert_eq!( + t.paths, + vec!["a.go", "b.go"], + "a dead id keeps its last path" + ); + // Renaming onto a tracked path debits that path's lines. + t.insert("c.go", 2, 7); + t.rename("b.go", "c.go", 8); + t.remove("c.go", 9); + assert!(t.files.is_empty()); + let d = reduce_reports(&t.drain(), packing, GLOBAL_ONLY); + assert_eq!(d.added, 10 + 4 + 2); + assert_eq!(d.removed, 10 + 2 + 4); + assert_eq!(d.global[&5][&0], -10); + assert_eq!(d.global[&8][&7], -2); + assert_eq!(d.global[&9][&5], -4); + } + + /// Added and removed never cancel within a commit, even in one tick. + #[test] + fn reduce_counts_gross_added_and_removed() { + let packing = ValuePacking::ticks_only(); + let reports = [ + Report { + file: 0, + current: 3, + previous: 3, + delta: 6, + }, + Report { + file: 1, + current: 3, + previous: 3, + delta: -2, + }, + ]; + let d = reduce_reports(&reports, packing, GLOBAL_ONLY); + assert_eq!((d.added, d.removed), (6, 2)); + assert_eq!(d.global[&3][&3], 4); + assert!(d.files.is_empty() && d.people.is_empty() && d.matrix.is_empty()); + } + + /// With people tracking the reduce attributes deltas to the author of + /// the affected lines, fills the matrix on deletions, and ignores lines + /// with no author. + #[test] + fn reduce_attributes_people_and_matrix() { + let packing = ValuePacking::for_people(100); + let unknown = packing.unknown_person(); + let alice_t0 = packing.pack(0, 0); + let bob_t5 = packing.pack(1, 5); + let base = packing.pack(unknown, 0); + let reports = [ + Report { + file: 0, + current: bob_t5, + previous: bob_t5, + delta: 3, + }, + Report { + file: 0, + current: bob_t5, + previous: alice_t0, + delta: -2, + }, + Report { + file: 1, + current: bob_t5, + previous: base, + delta: -4, + }, + ]; + let d = reduce_reports(&reports, packing, DAILY_PEOPLE); + assert_eq!(d.people[&1][&5][&5], 3); + assert_eq!(d.people[&0][&5][&0], -2); + assert!(!d.people.contains_key(&unknown)); + assert_eq!(d.matrix[&0][&1], 2); + assert!(!d.matrix.contains_key(&unknown)); + assert_eq!(d.global[&5][&0], -6, "the global curve keeps base lines"); + assert_eq!(d.files[&1][&5][&0], -4); + assert_eq!((d.added, d.removed), (3, 6)); + } + + /// Ownership reads the live segments per author and skips base lines. + #[test] + fn ownership_counts_live_lines_per_author() { + let packing = ValuePacking::for_people(100); + let mut t = Tracker::new(packing); + t.insert("f.py", 4, packing.pack(packing.unknown_person(), 0)); + let bob = packing.pack(1, 5); + t.apply_diffs( + "f.py", + &[ + (Op::Equal, 1), + (Op::Delete, 1), + (Op::Insert, 1), + (Op::Equal, 2), + (Op::Insert, 2), + ], + bob, + ); + let alice = packing.pack(0, 9); + t.apply_diffs("f.py", &[(Op::Equal, 6), (Op::Insert, 1)], alice); + assert_eq!(t.len_of("f.py"), Some(7)); + let own = t.ownership(); + assert_eq!(own["f.py"], BTreeMap::from([(0, 1), (1, 3)])); + } + + /// The diff applier merges a pending delete with its insert into one + /// replace and flushes a trailing insert. + #[test] + fn apply_diffs_pending_delete_merges_with_insert() { + let reports = Rc::new(RefCell::new(Vec::new())); + let sink = Rc::clone(&reports); + let updater: Updater = Box::new(move |c, p, d| sink.borrow_mut().push((c, p, d))); + let mut file = File::new(0, 4, vec![updater]); + reports.borrow_mut().clear(); + apply_diffs( + &mut file, + &[ + (Op::Equal, 1), + (Op::Delete, 1), + (Op::Insert, 1), + (Op::Equal, 2), + (Op::Insert, 2), + ], + 7, + ); + assert_eq!(file.len(), 6); + assert_eq!(*reports.borrow(), vec![(7, 7, 1), (7, 0, -1), (7, 7, 2)]); + } + + #[test] + fn repo_base_name_cases() { + assert_eq!(repo_base_name(""), ""); + assert_eq!(repo_base_name("."), ""); + assert_eq!(repo_base_name("/"), "/"); + assert_eq!(repo_base_name("/tmp/repo/"), "repo"); + assert_eq!(repo_base_name("repo"), "repo"); + } + + #[test] + fn person_slot_maps_unmatched_authors_to_the_dict_slot() { + let missing = i64::from(cf_analyzers_plumbing::AUTHOR_MISSING); + assert_eq!(person_slot(missing, 4), 4); + assert_eq!(person_slot(2, 4), 2); + } } diff --git a/crates/cf-commands/src/handlers/couples_run.rs b/crates/cf-commands/src/handlers/couples_run.rs index dd2e1846..528a94ee 100644 --- a/crates/cf-commands/src/handlers/couples_run.rs +++ b/crates/cf-commands/src/handlers/couples_run.rs @@ -1,69 +1,67 @@ -//! Real `history/couples` over the general history pipeline. +//! `history/couples` over the shared history walk. //! -//! Port of the reference streaming couples pipeline (the reference `initHistoryPipeline` / -//! initHeadOnly → Runner → couples `HistoryAnalyzer.Consume` (`processChange`, -//! seen-files Bloom merge dedup, oversized-changeset skip) → `Aggregator.Add` -//! → `ticksToReport` (`buildReport`: collect current files from the last -//! commit's tree, reduce to current files, byte-sorted file index, -//! per-file newline counts, people/files matrices) → `ComputeAllMetrics`). +//! A commit whose raw tree diff exceeds `--max-changes-per-commit` is dropped +//! before the walk, as for every other history analyzer. Per commit +//! (chronological order): the filtered tree diff against the first parent +//! feeds `process_change` — the coupling context (files co-changed in +//! the commit), the per-file author touch, rename pairs — and the aggregator +//! sums the file co-occurrence and people matrices, applying each rename as +//! it happens. Every change is counted ONCE over the walk: a merge commit +//! contributes only the files no earlier commit of the window changed +//! (`seen_files`, ONE exact set over the whole walk) and records no +//! deletions. A commit above the changeset cap contributes no coupling +//! context but still owns every file it touched. The report is reduced to +//! the files present in the NEWEST commit of the window, with their line +//! counts. //! -//! The tree diff base is always `parent(0)` (first parent), exactly as the reference implementation -//! `TreeDiffAnalyzer` (`ensurePreviousTree` → `Parent(0)`); merge handling is -//! controlled by the `IsMerge` flag (`NumParents() > 1 && !--first-parent`). -//! -//! Output bytes route through `cf-gojson` (the reference `encoding/json` parity); never -//! `serde_json`. +//! Output bytes route through `cf-gojson`; never `serde_json`. + +use std::collections::{BTreeMap, HashSet}; -use cf_analyzers_plumbing::identity_detector::IdentityDetector; +use cf_analyzers_plumbing::author_missing_id; use cf_couples::aggregator::Aggregator; use cf_couples::tc::{CommitData, RenamePair}; use cf_couples::{compute_all_metrics, report}; -use cf_gitlib::changes::{initial_tree_changes, tree_diff, ChangeAction}; -use std::collections::{BTreeMap, HashSet}; +use cf_gitlib::changes::ChangeAction; + +use cf_units::{KIB, MIB}; + +use crate::handlers::history::{ + commit_tree_changes, is_merge, walk_chunks, ChunkPlan, CommitStamp, HistoryRun, StampSummary, + Stamper, WalkCost, +}; -/// Expected unique file count for the seen-files Bloom filter. -const SEEN_FILES_BLOOM_EXPECTED: u64 = 100_000; -/// Target false-positive rate for the seen-files Bloom filter. -const SEEN_FILES_BLOOM_FP: f64 = 0.01; -/// Maximum coupling-context size. +/// The changeset cap: a commit touching more files feeds no coupling context. const MAX_MEANINGFUL_CONTEXT: usize = cf_couples::COUPLES_MAXIMUM_MEANINGFUL_CONTEXT_SIZE; -/// Sentinel author id for unidentified authors: `(1 << 18) - 1`. -const AUTHOR_MISSING: i64 = (1 << 18) - 1; - -/// `gitlib.Hash{}.String()` — the zero hash the reference implementation stamps on TCs whose Consume -/// early-returned without setting `CommitHash` (oversized couples changesets). -const ZERO_HASH_HEX: &str = "0000000000000000000000000000000000000000"; - -/// Builds the `history/couples` report value (the reference `ComputedMetrics`, the single -/// value behind `ToJSON`/`ToYAML`) for either the HEAD commit (`--head`) or the -/// newest `--limit` commits (streaming walk). Returns `None` if the -/// repository cannot be opened/walked. The caller serializes this one value -/// across json/yaml/bin uniformly (`serialize_history_metrics`), so every -/// format follows from the same report value (no per-format branch). + +/// Builds the `history/couples` report value (the single value behind +/// json/yaml/bin) over the commit window. Returns `None` if the repository +/// cannot be walked. +#[must_use] pub fn couples_run_value(sub: &clap::ArgMatches) -> Option<cf_gojson::GoValue> { couples_run(sub).map(|r| r.report_value) } /// The TYPED `ComputedMetrics` behind [`couples_run_value`] (same walk, same -/// `compute_all_metrics` product) — the text serializer reads struct fields -/// directly (the reference implementation `generateText` calls `ComputeAllMetrics` on -/// the report), so it must see the identical metrics the json/yaml bytes -/// encode. +/// `compute_all_metrics` product) for the text serializer. +#[must_use] pub fn couples_run_metrics(sub: &clap::ArgMatches) -> Option<cf_couples::ComputedMetrics> { couples_run(sub).map(|r| r.metrics) } -/// One walked commit's couples products (the reference `couples.Consume` TC + runner -/// stamps). `data` is `None` for a dedup-skipped merge (the reference implementation returns an EMPTY -/// `CommitData` with a zero commit hash, which the aggregator ignores). +/// One walked commit's couples payload plus the shared stamps. +#[derive(Debug, PartialEq, Eq)] pub(crate) struct CouplesCommit { + /// Position in the commit window (the ndjson merge key; a dropped + /// oversized commit keeps its slot). + pub pos: usize, /// Full hex hash. pub hash: String, - /// The per-commit TC payload; `None` ⇔ dedup-skipped merge. - pub data: Option<CommitData>, - /// Runner-stamped tick (TicksSinceStart). + /// The per-commit payload. + pub data: CommitData, + /// Tick. pub tick: i64, - /// Loose-identity author id. + /// Author id. pub author_id: i64, /// Committer time, Unix seconds. pub when: i64, @@ -72,355 +70,194 @@ pub(crate) struct CouplesCommit { } /// The full products of one couples walk: the aggregated report value plus the -/// per-commit TC stream (one entry per walked commit, walk order). +/// per-commit stream (one entry per walked commit, walk order). pub(crate) struct CouplesRun { /// The aggregated `ComputedMetrics` GoValue (json/yaml/bin source). pub report_value: cf_gojson::GoValue, /// The typed metrics `report_value` was rendered from (text source). pub metrics: cf_couples::ComputedMetrics, - /// Per-commit TC products, walk order. + /// Per-commit products, walk order. pub commits: Vec<CouplesCommit>, - /// Bounded store-path file-coupling records (the reference `writeFileCoupling`: - /// `computeSparseCoupling` over the reduced sparse map, min edge weight 2, - /// co-change-descending, top 100) — the `file_coupling` store kind the plot - /// sections consume. - pub store_file_coupling: Vec<cf_couples::FileCouplingData>, } -/// The data the `history/couples` plot sections consume — the Rust analogue of -/// the analyzer's structured store kinds. +/// The data the `history/couples` plot sections consume. pub struct CouplesPlotData { - /// `file_coupling` records (bounded sparse pairs, co-change-descending). + /// `file_coupling` records (co-change-descending, the report's list). pub file_coupling: Vec<cf_couples::FileCouplingData>, - /// `dev_matrix` names after `FilterTopDevs` — EMPTY on every `run` - /// pipeline: the aggregator's `reversedNames` is populated only from a - /// preloaded people dict, which run streaming never configures, so the - /// dev-coupling heatmap section is always skipped (matches the live reference - /// pages). - pub dev_names: Vec<String>, - /// `ownership` records, `filesSequence` order (identical inputs to the - /// dense `FileOwnershipMetric.Compute`, so the metric product is reused). + /// `ownership` records. pub ownership: Vec<cf_couples::FileOwnershipData>, } /// Builds the `history/couples` plot-section data over the shared couples walk. +#[must_use] pub fn couples_plot_data(sub: &clap::ArgMatches) -> Option<CouplesPlotData> { let run = couples_run(sub)?; Some(CouplesPlotData { - file_coupling: run.store_file_coupling, - dev_names: Vec::new(), + file_coupling: run.metrics.file_coupling, ownership: run.metrics.file_ownership, }) } -pub(crate) fn couples_run(sub: &clap::ArgMatches) -> Option<CouplesRun> { - let path = crate::handlers::run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let head_only = sub.get_flag("head"); - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - let first_parent = crate::handlers::effective_first_parent(sub); - - // Commit window: HEAD-only loads the single HEAD commit; streaming selects - // the `limit` NEWEST commits (newest-first walk, then reversed to - // oldest-first). With `limit <= 0` this is the full oldest-first history. - let hashes: Vec<cf_gitlib::Hash> = if head_only { - vec![repo.head().ok()?] - } else { - let v = crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )?; - // reference consume order at `--workers 1`: the streaming pipeline (commit - // streamer + blob/diff/uast prefetch) preserves input order end-to-end, - // so the couples leaf consumes commits in the oldest-first revwalk order - // they are fed (see `crate::handlers::pipeline_consume_order` for the - // per-stage source citation). This is the order in which the couples - // seen-files Bloom is populated, so a merge commit's merge-mode coupling - // gate (`!seenFiles.Test(name)`) depends on it. The additive - // coupling/people matrices are order-independent, but the Bloom gate and - // loose-identity id assignment are not, so we reproduce this exact order. - crate::handlers::pipeline_consume_order(v) - }; +/// The per-commit, order-independent product: the raw tree-diff size and the +/// filtered changes (none for a commit above `max_changes`, which the walk +/// drops). +struct CouplesPrepared { + /// Size of the raw tree diff (before the change filter), for the + /// `--max-changes-per-commit` gate. + raw_change_count: usize, + /// The filtered tree-diff changes, in diff order. + changes: Vec<cf_gitlib::changes::Change>, +} - let change_filter = crate::handlers::history_filter(sub).unwrap_or_default(); - // Loose identity detection (run streaming never preloads a people dict). - // Identity is a CORE (plumbing) analyzer: it runs sequentially on the main - // goroutine in oldest-first order BEFORE the leaf workers, and the resolved - // AuthorID is handed to the forked leaves via the per-commit plumbing - // snapshot. So author resolution is global oldest-first, NOT per-worker. - let mut identity = IdentityDetector::new(); - - // The reference implementation runs the couples leaf through `processCommitsHybrid`: with a - // single non-SequentialOnly leaf and CoreCount(8) < len(Analyzers)(9), the - // leaf is FORKED across `LeafWorkers` workers, each with an INDEPENDENT - // seen-files Bloom and merge-dedup tracker (couples `Fork`: fresh - // `newSeenFilesFilter()` + `NewMergeTracker()`; `Merge` deliberately does NOT - // combine them). Commits are dispatched round-robin by consume position - // (`workers[commitIdx % numWorkers]`, hybridCommitLoop), so worker `w` - // processes consume positions `p` with `p % numWorkers == w`, in oldest-first - // order within that worker. The per-commit coupling/people data each worker - // produces is then merged additively into the shared aggregator, so only the - // merge-mode Bloom gate (`!seenFiles.Test(name)`) is worker-partition - // sensitive. `LeafWorkers` is `max(NumCPU / 3, 4)` (coordinator default), - // making the merge-mode coupling count CPU-count dependent — we reproduce the - // live binary on THIS machine, which the oracle also runs on. - let num_workers = crate::handlers::leaf_worker_count(); - // One forked seen-files Bloom + merge-dedup tracker per worker. - let mut seen_files: Vec<cf_alg_bloom::Filter> = Vec::with_capacity(num_workers); - for _ in 0..num_workers { - seen_files.push( - cf_alg_bloom::Filter::new_with_estimates( - SEEN_FILES_BLOOM_EXPECTED, - SEEN_FILES_BLOOM_FP, - ) - .ok()?, - ); - } - let mut seen_merges: Vec<HashSet<cf_gitlib::Hash>> = vec![HashSet::new(); num_workers]; - - // The aggregator grows its people slices on demand (ensure_capacity), so the - // initial PeopleNumber is 0 (loose detection discovers authors incrementally). - let mut agg = Aggregator::new(0); - - let mut last_commit_hash: Option<cf_gitlib::Hash> = None; - - // ---- parallel pure-compute stage ----------------------------------------- - // ONLY the per-commit-independent work is parallelized — the tree diff (vs - // parent(0)) + path-policy filter — which dominates the per-commit cost. Each - // worker thread opens its OWN libgit2 Repository handle (the handle is !Send; - // per-thread handles also avoid shared-ODB-cache contention). The author - // signature is read here too (pure per commit) so the reduce needs no commit - // lookup. The order-SENSITIVE reduce below is UNCHANGED: the per-worker - // seen-files Bloom (partitioned by `pos % leaf_worker_count()`), the per-worker - // merge-dedup tracker, the loose-identity consume order (oldest-first), and the - // additive aggregator all run sequentially in the exact same order — only now - // reading `prepared[pos]` instead of recomputing the diff inline. The Bloom - // partition width (`num_workers`) is the modeled reference leaf-worker count and is - // INDEPENDENT of the parallel-compute worker count; neither is changed here. - struct CouplesPrepared { - num_parents: usize, - sig_name: String, - sig_email: String, - sig_when: i64, - committer_when: i64, - committer_offset: i32, - changes: Vec<cf_gitlib::changes::Change>, +/// The per-commit pure product of `hash`: its raw tree-diff size and its +/// filtered changes (none above `max_changes`). `None` on a fatal git error. +fn couples_commit_product( + repo: &cf_gitlib::Repository, + hash: cf_gitlib::Hash, + filter: &crate::handlers::HistoryFilter, + max_changes: usize, +) -> Option<CouplesPrepared> { + let commit = repo.lookup_commit(hash).ok()?; + let mut changes = commit_tree_changes(repo, &commit)?; + let raw_change_count = changes.len(); + if raw_change_count > max_changes { + changes.clear(); + } else { + filter.retain_changes(repo, &mut changes); } - let compute_workers = - std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let filter_ref = &change_filter; - let prepared = crate::handlers::history::parallel_prepare( - &path, - &hashes, - compute_workers, - move |repo, hash| { - let commit = repo.lookup_commit(hash).ok()?; - let num_parents = commit.num_parents(); - let gsig = commit.author(); - - // Tree diff against the first parent (root → full initial tree). - let new_tree = commit.tree().ok()?; - let raw_changes = if num_parents > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(repo, Some(&new_tree)).ok()? - }; - - // TreeDiffAnalyzer.shouldIncludeChange: path-policy exclusion, then - // the --languages check (no-op for the default all-languages case). - let mut changes = raw_changes; - filter_ref.retain_changes(repo, &mut changes); - - let cw = commit.committer().when; - Some(CouplesPrepared { - num_parents, - sig_name: gsig.name.clone(), - sig_email: gsig.email.clone(), - sig_when: gsig.when.seconds(), - committer_when: cw.seconds(), - committer_offset: cw.offset_minutes(), - changes, - }) - }, - )?; + Some(CouplesPrepared { + raw_change_count, + changes, + }) +} - // ---- sequential ordered-reduce stage (UNCHANGED order) ------------------- - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - let mut commits: Vec<CouplesCommit> = Vec::with_capacity(hashes.len()); - - for (pos, hash) in hashes.iter().enumerate() { - let prep = &prepared[pos]; - // Worker that consumes this commit in the reference implementation's hybrid leaf dispatch. - let worker = pos % num_workers; - - // Runner tick stamping (TicksSinceStart over the committer time). - let base = - *tick0.get_or_insert_with(|| crate::handlers::floor_tick_secs(prep.committer_when)); - let raw_tick = (prep.committer_when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - // Merge dedup: a merge commit (NumParents > 1) seen twice by the SAME - // worker contributes an empty CommitData (reference: SeenOrAdd → return empty). - // With unique hashes in a single window this never triggers, but mirror - // reference faithfully — and the tracker is per-worker (Fork/Merge above). - let is_multi_parent = prep.num_parents > 1; - if is_multi_parent && !seen_merges[worker].insert(*hash) { - // Already seen: empty CommitData with a ZERO commit hash in the reference implementation — - // the aggregator ignores it; record no per-commit entry. - commits.push(CouplesCommit { - hash: hash.to_string(), - data: None, - tick, - author_id: 0, - when: prep.committer_when, - offset_min: prep.committer_offset, - }); - continue; - } +/// The couples pre-pass over `run`'s change filter and flags: the pure +/// per-commit product a walk computes in parallel. Captures only the +/// shareable pieces of the run (its repository handle is not `Sync`). +fn couples_prepare( + run: &HistoryRun, +) -> impl Fn(&cf_gitlib::Repository, cf_gitlib::Hash) -> Option<CouplesPrepared> + Sync + '_ { + let filter = &run.filter; + let max_changes = run.max_changes; + move |repo, hash| couples_commit_product(repo, hash, filter, max_changes) +} - // IsMerge: NumParents > 1, unless - // --first-parent forces single-parent semantics. - let merge_mode = is_multi_parent && !first_parent; +/// The couples reduce state: the co-change and ownership aggregates, the +/// files changed so far (ONE exact set over the whole walk, so the output is +/// a pure function of the repository and the flags) and the per-commit +/// records. Each kept commit's changes are folded in window order and +/// dropped; only its record (what the per-commit formats emit) is kept. +struct CouplesFold { + /// The walk's first-parent mode (a merge is an ordinary commit when set). + first_parent: bool, + /// The people slot an unmatched author is counted under. + unmatched_slot: usize, + /// Every file changed by a commit folded so far. + seen_files: HashSet<String>, + /// The co-change and per-developer touch sums. + agg: Aggregator, + /// The per-commit records, walk order. + commits: Vec<CouplesCommit>, +} - // lastCommit is set on every (non-dedup-skipped) consume. - last_commit_hash = Some(*hash); +impl CouplesFold { + /// An empty fold for `run`. The unmatched slot is fixed before the + /// walk: only a run with a loaded `--people-dict` stamps an unmatched + /// author, and that dictionary never changes while commits are stamped, + /// so an unstamped identity state already names the slot the finished + /// one does. + fn new(run: &HistoryRun) -> Self { + Self { + first_parent: run.first_parent, + unmatched_slot: Stamper::new(run).finish().unmatched_slot, + seen_files: HashSet::new(), + // The aggregator grows its people slices on demand, so the + // initial PeopleNumber is 0 (identities are discovered + // incrementally). + agg: Aggregator::new(0), + commits: Vec::new(), + } + } - // Identity: resolve this commit's author id (loose signature). Consumed in - // oldest-first order (CORE analyzer), independent of the worker partition. - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: prep.sig_name.clone(), - email: prep.sig_email.clone(), - when_unix: prep.sig_when, - }); - // Reference: author = Identity.AuthorID; if AuthorMissing → PeopleNumber. - // With loose detection author_id is always a real id, never AuthorMissing. - let author = if author_id == AUTHOR_MISSING { - 0 + /// Folds one kept commit, in window order: its changes become the + /// commit's coupling context, author touches and renames, which the + /// aggregator sums before the record is kept. + fn consume(&mut self, stamp: &CommitStamp, prep: CouplesPrepared) { + let merge_mode = is_merge(stamp.num_parents, self.first_parent); + let author = if stamp.author_id == author_missing_id() { + self.unmatched_slot } else { - author_id as usize + stamp.author_id as usize }; - // The tree diff + path-policy filter for this commit was computed in the - // parallel pre-pass; the reduce only reads it. - let changes = &prep.changes; - - // Build this commit's CommitData. let mut data = CommitData { commit_counted: true, ..CommitData::default() }; - - // Oversized changeset: skip coupling/ownership extraction, but the commit - // is still counted (CommitCounted = true) — the reference implementation returns `&data` early - // WITHOUT setting TC.CommitHash, so the streamed ndjson line carries the - // ZERO hash and the aggregator's `!tc.CommitHash.IsZero()` guard drops - // the commit from commit_stats/commits_by_tick (no timeseries entry). - let oversized = changes.len() > MAX_MEANINGFUL_CONTEXT; - if !oversized { - for change in changes { - process_change( - change, - merge_mode, - author, - &mut data, - &mut seen_files[worker], - ); - } + let couple = prep.changes.len() <= MAX_MEANINGFUL_CONTEXT; + for change in &prep.changes { + process_change(change, merge_mode, couple, &mut data, &mut self.seen_files); } - agg.add(author, &data); - commits.push(CouplesCommit { - hash: if oversized { - ZERO_HASH_HEX.to_string() - } else { - hash.to_string() - }, - data: Some(data), - tick, - author_id, - when: prep.committer_when, - offset_min: prep.committer_offset, + self.agg.add(author, &data); + self.commits.push(CouplesCommit { + pos: stamp.pos, + hash: stamp.hex.clone(), + data, + tick: stamp.tick, + author_id: stamp.author_id, + when: stamp.when, + offset_min: stamp.offset_min, }); } - // buildReport (reference: ticksToReport → buildReport → collectCurrentFiles). - // - // The two code paths behave differently because of libgit2 object lifetimes: - // - // * --head: the single HEAD commit object is still LIVE when buildReport runs, - // so `lastCommit.Tree()` succeeds. `collectCurrentFiles` returns the HEAD - // tree's files, the report is reduced to those, and - // `computeFilesLinesFromCommit` reads each blob and counts newlines (real - // `FilesLines`). This matches the `--head --limit 5` golden (real line - // counts, files restricted to the HEAD tree). - // - // * streaming (--limit N, no --head): the commit objects consumed during the - // walk are FREED once their chunk completes, so the analyzer's `lastCommit` - // points at a freed commit and `lastCommit.Tree()` FAILS. `collectCurrentFiles` - // takes its fallback branch (return *all* accumulated raw-file keys) and - // every `commit.File(name)` in `computeFilesLinesFromCommit` likewise fails, - // so every `FilesLines` entry is 0. We reproduce that observed behavior: the - // current set is the accumulated raw-file keys (identity for the files map, - // but it still prunes PEOPLE entries for files that never entered the - // coupling map — e.g. delete-only author touches — which feeds the - // people-matrix diagonal and thus `developer_coupling.coupling_strength`) - // and zero line counts. - let (current_files, files_lines): (Option<HashSet<String>>, BTreeMap<String, i32>) = - if head_only { - collect_current_and_lines(&repo, last_commit_hash) - } else { - ( - Some(agg.raw_files().keys().cloned().collect()), - BTreeMap::new(), - ) - }; + /// The finished walk: the report reduced to the files present in the + /// NEWEST commit of `run`'s window, with their line counts and the + /// developer names of `summary`, plus the per-commit records. + fn finish(self, summary: StampSummary, run: &HistoryRun) -> CouplesRun { + let (current_files, files_lines) = + collect_current_and_lines(&run.repo, run.hashes.last().copied()); + let mut report_data = self.agg.build_report(current_files.as_ref(), &files_lines); + report_data.reversed_people_dict = summary.names; + let metrics = compute_all_metrics(&report_data); + CouplesRun { + report_value: report::computed_metrics_to_value(&metrics), + metrics, + commits: self.commits, + } + } +} - let mut report_data = agg.build_report(current_files.as_ref(), &files_lines); - - // Reversed people dict from loose identity detection (reference: - // GetReversedPeopleDict()). FinalizeDict builds the reverse entries from the - // incrementally-collected names/emails (the streaming pipeline finalizes the - // loose dict before report rendering). - identity.finalize_dict(); - report_data.reversed_people_dict = identity.reversed_people_dict.clone(); - - // Store-path file coupling (the reference implementation `writeFileCoupling`): - // sparse pairs over the SAME reduce the report uses (the live binary's - // plot pipeline observably takes `collectUnfiltered` — couples' lastCommit - // object is freed by store-finalize time, so the filtered prune/cap path - // never runs), min edge weight 2 (`DefaultMinEdgeWeight`; the - // `Couples.MinEdgeWeight` config key has no run flag), descending - // co-changes, top 100 (`DefaultTopKPerFile`). - let (reduced_files, _reduced_people) = agg.reduced(current_files.as_ref()); - let (files_sequence, files_index) = cf_couples::matrix::build_files_index(&reduced_files); - let sparse_pairs = cf_couples::store::compute_sparse_coupling( - &reduced_files, - &files_sequence, - &files_index, - 2, - ); - let store_file_coupling = cf_couples::store::top_k_file_coupling(sparse_pairs, 100); - - let metrics = compute_all_metrics(&report_data); - Some(CouplesRun { - report_value: report::computed_metrics_to_value(&metrics), - metrics, - commits, - store_file_coupling, - }) +/// The couples walk's measured cost. Kubernetes, 31,923 commits: one worker +/// peaks at 1345 MB (the co-change state), 24 workers at 1591 MB (10 MiB per +/// added worker); the whole window and 50-commit chunks peak alike. +const COUPLES_COST: WalkCost = WalkCost { + worker_bytes: 16 * MIB, + product_bytes: KIB, +}; + +/// Runs the `history/couples` walk over the run's chunk plan: the +/// per-commit changes are computed in parallel one chunk at a time, +/// oversized commits dropped, and folded in window order ([`walk_chunks`]). +/// `None` when the repository cannot be walked. +pub(crate) fn couples_run(sub: &clap::ArgMatches) -> Option<CouplesRun> { + let run = HistoryRun::open(sub)?; + let plan = ChunkPlan::for_walk(sub, "history/couples", run.hashes.len(), COUPLES_COST).ok()?; + let max_changes = run.max_changes; + let mut fold = CouplesFold::new(&run); + let summary = walk_chunks( + &run, + plan, + couples_prepare(&run), + |prep| prep.raw_change_count > max_changes, + |stamp, prep| fold.consume(stamp, prep), + )?; + Some(fold.finish(summary, &run)) } -/// Per-commit couples NDJSON records (forked leaf): every non-dedup-skipped -/// commit emits a line; `data` is the reference implementation's `*CommitData` struct — `CouplingFiles` -/// (initialized slice), `AuthorFiles` (map, key-sorted), `Renames` (initialized -/// slice of `{FromName, ToName}`), `CommitCounted` bool. +/// Per-commit couples NDJSON records: every walked commit emits a line; +/// `data` is `{CouplingFiles, AuthorFiles, Renames, CommitCounted}`. +#[must_use] pub fn couples_ndjson_records( sub: &clap::ArgMatches, ) -> Option<Vec<crate::handlers::history_formats::NdjsonRecord>> { @@ -428,8 +265,8 @@ pub fn couples_ndjson_records( let run = couples_run(sub)?; let mut records = Vec::new(); - for (pos, c) in run.commits.iter().enumerate() { - let Some(cd) = &c.data else { continue }; + for c in &run.commits { + let cd = &c.data; let mut data = GoMap::new_struct(); data.insert( "CouplingFiles".to_string(), @@ -464,7 +301,7 @@ pub fn couples_ndjson_records( GoValue::Bool(cd.commit_counted), ); records.push(crate::handlers::history_formats::NdjsonRecord { - pos, + pos: c.pos, hash: c.hash.clone(), tick: c.tick, author_id: c.author_id, @@ -476,11 +313,10 @@ pub fn couples_ndjson_records( Some(records) } -/// The couples contribution to the merged `--format timeseries` document (reference: -/// `couples.ExtractCommitTimeSeries` over `report["commit_stats"]`): per -/// commit `{"files_touched": len(CouplingFiles), "author_id": id}`. The couples -/// aggregator is NOT tick-bucketed — its single TICK carries tick index 0, so -/// every merged commit reports `"tick": 0`. +/// The couples contribution to the merged `--format timeseries` document: per +/// commit `{"files_touched": <files the author touched>, "author_id": id}` +/// under the commit's tick. +#[must_use] pub fn couples_timeseries_contribution( sub: &clap::ArgMatches, ) -> Option<crate::handlers::history_formats::TimeSeriesContribution> { @@ -490,22 +326,16 @@ pub fn couples_timeseries_contribution( let mut per_commit = Vec::new(); let mut commit_meta = Vec::new(); for c in &run.commits { - let Some(cd) = &c.data else { continue }; - if c.hash == ZERO_HASH_HEX { - // Aggregator.Add: `if !tc.CommitHash.IsZero()` — oversized commits - // never reach commit_stats, so the merged timeseries omits them. - continue; - } let mut entry = GoMap::new_map(); entry.insert( "files_touched".to_string(), - GoValue::Int(cd.coupling_files.len() as i64), + GoValue::Int(c.data.author_files.len() as i64), ); entry.insert("author_id".to_string(), GoValue::Int(c.author_id)); per_commit.push((c.hash.clone(), GoValue::Map(entry))); commit_meta.push(( c.hash.clone(), - 0, // aggregator TICK.Tick is 0 (single un-bucketed TICK). + c.tick, crate::handlers::format_rfc3339_offset(c.when, c.offset_min), String::new(), )); @@ -517,16 +347,15 @@ pub fn couples_timeseries_contribution( }) } -/// Collects the current-file set and per-file newline counts from the live -/// commit at `last_commit_hash` (reference: `collectCurrentFiles` + -/// `computeFilesLinesFromCommit`, used on the `--head` path where the commit -/// object is still live). Returns `(None, empty)` if the tree cannot be read, -/// matching the reference implementation's fallback (all raw files, zero lines). +/// Collects the current-file set and per-file newline counts from the commit +/// at `newest`. Returns `(None, empty)` when there is no commit or its tree +/// cannot be read (the report then keeps every accumulated file with zero +/// lines). fn collect_current_and_lines( repo: &cf_gitlib::Repository, - last_commit_hash: Option<cf_gitlib::Hash>, + newest: Option<cf_gitlib::Hash>, ) -> (Option<HashSet<String>>, BTreeMap<String, i32>) { - let Some(lc) = last_commit_hash else { + let Some(lc) = newest else { return (None, BTreeMap::new()); }; let Ok(commit) = repo.lookup_commit(lc) else { @@ -540,7 +369,6 @@ fn collect_current_and_lines( let mut lines: BTreeMap<String, i32> = BTreeMap::new(); let mut iter = tree.files(); while let Some(f) = iter.next_file() { - // newline count of the blob (reference: countFileLinesAt over the file blob). let n = match f.contents() { Ok(data) => data.iter().filter(|&&b| b == b'\n').count() as i32, Err(_) => 0, @@ -551,60 +379,286 @@ fn collect_current_and_lines( (Some(set), lines) } -/// Per-commit change processing. +/// Per-commit change processing: renames, the author touch and (when +/// `couple`, i.e. the commit is within the changeset cap) the coupling +/// context. A merge records no deletions and skips every file already +/// changed earlier in the walk — that change was counted at its own commit. fn process_change( change: &cf_gitlib::changes::Change, merge_mode: bool, - author: usize, + couple: bool, data: &mut CommitData, - seen_files: &mut cf_alg_bloom::Filter, + seen_files: &mut HashSet<String>, ) { - let action = change.action; - - let mut name = if action == ChangeAction::Delete { - change.from.name.clone() - } else { - change.to.name.clone() - }; + if change.action == ChangeAction::Delete { + if merge_mode { + return; + } + let name = change.from.name.clone(); + seen_files.insert(name.clone()); + data.author_files.insert(name, 1); + return; + } - if action == ChangeAction::Modify && change.to.name != change.from.name { + let name = change.to.name.clone(); + if change.action == ChangeAction::Modify && change.from.name != name { data.renames.push(RenamePair { from_name: change.from.name.clone(), - to_name: change.to.name.clone(), + to_name: name.clone(), }); - name = change.to.name.clone(); } - - if merge_mode && action == ChangeAction::Delete { + if merge_mode && seen_files.contains(&name) { return; } + seen_files.insert(name.clone()); + if couple { + data.coupling_files.push(name.clone()); + } + data.author_files.insert(name, 1); +} - if !merge_mode { - if action != ChangeAction::Delete { - data.coupling_files.push(name.clone()); +#[cfg(test)] +mod tests { + use super::*; + use crate::handlers::history::fold_testutil::{ + assert_chunk_invariant, chunk_fixture, open_run, run_matches, OVERSIZED_POS, + }; + use cf_gitlib::changes::{Change, ChangeEntry}; + use cf_gitlib::Hash; + + fn change(action: ChangeAction, from: &str, to: &str) -> Change { + Change { + action, + from: ChangeEntry { + name: from.to_string(), + hash: Hash::ZERO, + size: 0, + mode: 0, + }, + to: ChangeEntry { + name: to.to_string(), + hash: Hash::ZERO, + size: 0, + mode: 0, + }, } - seen_files.add(name.as_bytes()); - // Reference: if author != AuthorMissing { AuthorFiles[name] = 1 }. - if author != AUTHOR_MISSING_IDX { - data.author_files.insert(name, 1); + } + + fn run(changes: &[Change], merge_mode: bool, couple: bool, seen: &[&str]) -> CommitData { + let mut seen_files: HashSet<String> = seen.iter().map(|s| (*s).to_string()).collect(); + let mut data = CommitData::default(); + for c in changes { + process_change(c, merge_mode, couple, &mut data, &mut seen_files); } - return; + data } - // Merge mode. Only add the file to the - // coupling context if it was NOT seen on the first-parent line already - // (`!seenFiles.Test(name)`): a file changed on a merged-in branch must not - // be double-counted against files it already coupled with on mainline. - // The author touch is always recorded (coupling dedup != ownership dedup). - if !seen_files.test(name.as_bytes()) { - data.coupling_files.push(name.clone()); + #[test] + fn a_regular_commit_couples_and_owns_every_non_deleted_file() { + let data = run( + &[ + change(ChangeAction::Insert, "", "a"), + change(ChangeAction::Modify, "b", "b"), + change(ChangeAction::Delete, "c", ""), + ], + false, + true, + &[], + ); + assert_eq!(data.coupling_files, vec!["a", "b"]); + assert_eq!( + data.author_files.keys().collect::<Vec<_>>(), + vec!["a", "b", "c"] + ); + assert!(data.renames.is_empty()); } - if author != AUTHOR_MISSING_IDX { - data.author_files.insert(name, 1); + + #[test] + fn a_rename_is_recorded_and_counted_on_the_new_path() { + let data = run(&[change(ChangeAction::Modify, "b", "d")], false, true, &[]); + assert_eq!( + data.renames, + vec![RenamePair { + from_name: "b".into(), + to_name: "d".into() + }] + ); + assert_eq!(data.coupling_files, vec!["d"]); + assert_eq!(data.author_files.keys().collect::<Vec<_>>(), vec!["d"]); } -} -/// `identity.AuthorMissing` as a `usize` index (the reference implementation uses it both as the sentinel -/// id and, when present, as the `PeopleNumber` slot). Under loose detection the -/// resolved author is never this sentinel. -const AUTHOR_MISSING_IDX: usize = (1 << 18) - 1; + #[test] + fn a_merge_skips_files_already_changed_in_the_walk_and_deletions() { + let data = run( + &[ + change(ChangeAction::Insert, "", "b"), + change(ChangeAction::Modify, "c", "c"), + change(ChangeAction::Insert, "", "fresh"), + change(ChangeAction::Delete, "gone", ""), + ], + true, + true, + &["b", "c"], + ); + assert_eq!(data.coupling_files, vec!["fresh"]); + assert_eq!(data.author_files.keys().collect::<Vec<_>>(), vec!["fresh"]); + } + + /// `--max-changes-per-commit 2` over `c1 (alice: a, b) ; c2 (bob: a, b, + /// c) ; c3 (alice: a)`: Bob's three-change commit is dropped before the + /// walk, as devs and burndown drop it — no co-change, no ownership, no + /// developer. + #[test] + fn max_changes_per_commit_drops_the_oversized_commit() { + use cf_gitlib::testutil::{commit_files_as, init_repo}; + const T0: i64 = 1_609_459_200; + const DAY: i64 = 86_400; + let test = init_repo().expect("init"); + commit_files_as( + &test, + "alice", + "a@x", + "c1", + T0, + &[("a", b"1\n"), ("b", b"1\n")], + ) + .expect("c1"); + commit_files_as( + &test, + "bob", + "b@x", + "c2", + T0 + DAY, + &[("a", b"2\n"), ("b", b"2\n"), ("c", b"2\n")], + ) + .expect("c2"); + commit_files_as(&test, "alice", "a@x", "c3", T0 + 2 * DAY, &[("a", b"3\n")]).expect("c3"); + let path = test.path().to_str().expect("utf-8"); + let metrics = |extra: &[&str]| { + let argv: Vec<&str> = ["codefang", "run", "-a", "history/couples"] + .into_iter() + .chain(extra.iter().copied()) + .chain([path]) + .collect(); + let matches = crate::build_codefang_command() + .try_get_matches_from(argv) + .expect("run args parse"); + couples_run_metrics(matches.subcommand_matches("run").expect("run")) + .expect("couples report") + }; + let co_changes = |m: &cf_couples::ComputedMetrics| -> Vec<(String, String, i64)> { + m.file_coupling + .iter() + .map(|c| (c.file1.clone(), c.file2.clone(), c.co_changes)) + .collect() + }; + + let all = metrics(&[]); + assert!( + co_changes(&all).contains(&("a".into(), "b".into(), 2)), + "{all:?}" + ); + assert_eq!(all.aggregate.total_developers, 2, "{all:?}"); + + let capped = metrics(&["--max-changes-per-commit", "2"]); + assert_eq!(co_changes(&capped), [("a".to_string(), "b".to_string(), 1)]); + assert_eq!(capped.aggregate.total_developers, 1, "{capped:?}"); + let owned: Vec<&str> = capped + .file_ownership + .iter() + .map(|o| o.file.as_str()) + .collect(); + assert!( + !owned.contains(&"c"), + "c was only touched by the dropped commit: {owned:?}" + ); + } + + #[test] + fn an_oversized_commit_owns_without_coupling() { + let data = run( + &[ + change(ChangeAction::Insert, "", "a"), + change(ChangeAction::Modify, "b", "d"), + ], + false, + false, + &[], + ); + assert!(data.coupling_files.is_empty()); + assert_eq!(data.author_files.keys().collect::<Vec<_>>(), vec!["a", "d"]); + assert_eq!(data.renames.len(), 1); + } + + /// The analyzer id the fold tests run. + const ID: &str = "history/couples"; + + /// The comparable value of a finished couples walk: the report value and + /// the per-commit records. + fn comparable(run: CouplesRun) -> (cf_gojson::GoValue, Vec<CouplesCommit>) { + (run.report_value, run.commits) + } + + /// Folds the chunk fixture opened with `extra` in chunks of every size, + /// asserts it finishes to the one-pass walk (the oversized commit + /// dropped) and that the walk returns it for any `--chunk-size`. + fn assert_couples_chunk_invariant(extra: &[&str]) { + let test = chunk_fixture(); + let run = open_run(&test, ID, extra); + let max_changes = run.max_changes; + let whole = assert_chunk_invariant( + &run, + couples_prepare(&run), + |prep| prep.raw_change_count > max_changes, + || CouplesFold::new(&run), + CouplesFold::consume, + |fold, summary| comparable(fold.finish(summary, &run)), + ); + let positions: Vec<usize> = whole.1.iter().map(|c| c.pos).collect(); + assert_eq!(positions.len(), run.hashes.len() - 1, "{positions:?}"); + assert!(!positions.contains(&OVERSIZED_POS), "{positions:?}"); + for size in ["1", "4"] { + let args: Vec<&str> = extra + .iter() + .copied() + .chain(["--chunk-size", size]) + .collect(); + let chunked = couples_run(&run_matches(&test, ID, &args)).map(comparable); + assert_eq!(chunked.as_ref(), Some(&whole), "--chunk-size {size}"); + } + } + + /// The couples walk folds per chunk: the rename, the delete and the + /// merge's already-seen files carry across every chunk boundary. + #[test] + fn the_walk_folds_in_chunks_to_the_one_pass_value() { + assert_couples_chunk_invariant(&[]); + } + + /// With a `--people-dict` that misses Bob, his touches are counted under + /// the dictionary's `<unmatched>` slot in every chunking, so Alice + /// couples with `<unmatched>` — the slot is fixed before the first + /// commit is folded. + #[test] + fn an_unmatched_author_folds_into_the_unmatched_slot_in_every_chunking() { + let dict = tempfile::NamedTempFile::new().expect("dict file"); + std::fs::write(dict.path(), "Alice|alice@example.com|alice@elsewhere.org\n") + .expect("write dict"); + let dict_path = dict.path().to_str().expect("utf-8 path"); + assert_couples_chunk_invariant(&["--people-dict", dict_path]); + + let test = chunk_fixture(); + let sub = run_matches( + &test, + ID, + &["--people-dict", dict_path, "--chunk-size", "1"], + ); + let metrics = couples_run_metrics(&sub).expect("couples report"); + let pairs: Vec<(&str, &str)> = metrics + .developer_coupling + .iter() + .map(|d| (d.developer1.as_str(), d.developer2.as_str())) + .collect(); + assert_eq!(pairs, [("Alice", "<unmatched>")], "{metrics:?}"); + } +} diff --git a/crates/cf-commands/src/handlers/go_sort.rs b/crates/cf-commands/src/handlers/go_sort.rs deleted file mode 100644 index c0b2f660..00000000 --- a/crates/cf-commands/src/handlers/go_sort.rs +++ /dev/null @@ -1,418 +0,0 @@ -//! Faithful port of the reference implementation's `sort.Slice` (pattern-defeating quicksort, `pdqsort`). -//! -//! The reference `sort.Slice` is **not** a stable sort; its exact element -//! ordering for equal keys is an artifact of the pdqsort algorithm. Several -//! codefang report metrics -//! (`complexity.FunctionComplexityMetric.Compute`, `HighRiskFunctionMetric`) -//! call `sort.Slice` with comparators that have ties, so reproducing the -//! emitted byte order requires reproducing pdqsort exactly rather than using a -//! stable sort. This module is a line-for-line port operating on a slice of `T` -//! via a caller-supplied `less(&T, &T) -> bool`. - -/// Sorts `data` using the same algorithm and pivot choices as the reference implementation's -/// `sort.Slice(data, |i, j| less(data[i], data[j]))`. -pub fn slice<T, F>(data: &mut [T], mut less: F) -where - F: FnMut(&T, &T) -> bool, -{ - let n = data.len(); - // limit := bits.Len(uint(length)) - let limit = bits_len(n as u64); - pdqsort(data, 0, n, limit, &mut less); -} - -/// `bits.Len(uint)` — number of bits needed to represent `x` (0 for x==0). -fn bits_len(x: u64) -> i32 { - (64 - x.leading_zeros()) as i32 -} - -const MAX_INSERTION: usize = 12; - -// sortedHint values. -const UNKNOWN_HINT: i32 = 0; -const INCREASING_HINT: i32 = 1; -const DECREASING_HINT: i32 = 2; - -fn insertion_sort<T, F: FnMut(&T, &T) -> bool>(data: &mut [T], a: usize, b: usize, less: &mut F) { - for i in (a + 1)..b { - let mut j = i; - while j > a && less(&data[j], &data[j - 1]) { - data.swap(j, j - 1); - j -= 1; - } - } -} - -fn sift_down<T, F: FnMut(&T, &T) -> bool>( - data: &mut [T], - lo: usize, - hi: usize, - first: usize, - less: &mut F, -) { - let mut root = lo; - loop { - let mut child = 2 * root + 1; - if child >= hi { - break; - } - if child + 1 < hi && less(&data[first + child], &data[first + child + 1]) { - child += 1; - } - if !less(&data[first + root], &data[first + child]) { - return; - } - data.swap(first + root, first + child); - root = child; - } -} - -fn heap_sort<T, F: FnMut(&T, &T) -> bool>(data: &mut [T], a: usize, b: usize, less: &mut F) { - let first = a; - let lo = 0usize; - let hi = b - a; - - let mut i = (hi as isize - 1) / 2; - while i >= 0 { - sift_down(data, i as usize, hi, first, less); - i -= 1; - } - - let mut i = hi as isize - 1; - while i >= 0 { - data.swap(first, first + i as usize); - sift_down(data, lo, i as usize, first, less); - i -= 1; - } -} - -#[allow(clippy::too_many_lines)] -fn pdqsort<T, F: FnMut(&T, &T) -> bool>( - data: &mut [T], - mut a: usize, - mut b: usize, - mut limit: i32, - less: &mut F, -) { - let mut was_balanced = true; - let mut was_partitioned = true; - - loop { - let length = b - a; - - if length <= MAX_INSERTION { - insertion_sort(data, a, b, less); - return; - } - - if limit == 0 { - heap_sort(data, a, b, less); - return; - } - - if !was_balanced { - break_patterns(data, a, b); - limit -= 1; - } - - let (mut pivot, mut hint) = choose_pivot(data, a, b, less); - if hint == DECREASING_HINT { - reverse_range(data, a, b); - pivot = (b - 1) - (pivot - a); - hint = INCREASING_HINT; - } - - if was_balanced - && was_partitioned - && hint == INCREASING_HINT - && partial_insertion_sort(data, a, b, less) - { - return; - } - - // a > 0 && !data.Less(a-1, pivot) - if a > 0 && !less_idx(data, a - 1, pivot, less) { - let mid = partition_equal(data, a, b, pivot, less); - a = mid; - continue; - } - - let (mid, already_partitioned) = partition(data, a, b, pivot, less); - was_partitioned = already_partitioned; - - let left_len = mid - a; - let right_len = b - mid; - let balance_threshold = length / 8; - if left_len < right_len { - was_balanced = left_len >= balance_threshold; - pdqsort(data, a, mid, limit, less); - a = mid + 1; - } else { - was_balanced = right_len >= balance_threshold; - pdqsort(data, mid + 1, b, limit, less); - b = mid; - } - } -} - -#[inline] -fn less_idx<T, F: FnMut(&T, &T) -> bool>(data: &[T], i: usize, j: usize, less: &mut F) -> bool { - less(&data[i], &data[j]) -} - -fn partition<T, F: FnMut(&T, &T) -> bool>( - data: &mut [T], - a: usize, - b: usize, - pivot: usize, - less: &mut F, -) -> (usize, bool) { - data.swap(a, pivot); - let mut i = a + 1; - let mut j = b - 1; - - while i <= j && less_idx(data, i, a, less) { - i += 1; - } - while i <= j && !less_idx(data, j, a, less) { - j -= 1; - } - if i > j { - data.swap(j, a); - return (j, true); - } - data.swap(i, j); - i += 1; - j -= 1; - - loop { - while i <= j && less_idx(data, i, a, less) { - i += 1; - } - while i <= j && !less_idx(data, j, a, less) { - j -= 1; - } - if i > j { - break; - } - data.swap(i, j); - i += 1; - j -= 1; - } - data.swap(j, a); - (j, false) -} - -fn partition_equal<T, F: FnMut(&T, &T) -> bool>( - data: &mut [T], - a: usize, - b: usize, - pivot: usize, - less: &mut F, -) -> usize { - data.swap(a, pivot); - let mut i = a + 1; - let mut j = b - 1; - - loop { - while i <= j && !less_idx(data, a, i, less) { - i += 1; - } - while i <= j && less_idx(data, a, j, less) { - j -= 1; - } - if i > j { - break; - } - data.swap(i, j); - i += 1; - j -= 1; - } - i -} - -fn partial_insertion_sort<T, F: FnMut(&T, &T) -> bool>( - data: &mut [T], - a: usize, - b: usize, - less: &mut F, -) -> bool { - const MAX_STEPS: usize = 5; - const SHORTEST_SHIFTING: usize = 50; - let mut i = a + 1; - for _ in 0..MAX_STEPS { - while i < b && !less_idx(data, i, i - 1, less) { - i += 1; - } - - if i == b { - return true; - } - - if b - a < SHORTEST_SHIFTING { - return false; - } - - data.swap(i, i - 1); - - if i - a >= 2 { - let mut j = i - 1; - while j >= 1 { - if !less_idx(data, j, j - 1, less) { - break; - } - data.swap(j, j - 1); - j -= 1; - } - } - if b - i >= 2 { - let mut j = i + 1; - while j < b { - if !less_idx(data, j, j - 1, less) { - break; - } - data.swap(j, j - 1); - j += 1; - } - } - } - false -} - -fn break_patterns<T>(data: &mut [T], a: usize, b: usize) { - let length = b - a; - if length >= 8 { - let mut random = Xorshift(length as u64); - let modulus = next_power_of_two(length); - - let base = a + (length / 4) * 2; - // idx in [base-1, base+1] - for idx in (base - 1)..=(base + 1) { - let mut other = (random.next() as usize) & (modulus - 1); - if other >= length { - other -= length; - } - data.swap(idx, a + other); - } - } -} - -fn choose_pivot<T, F: FnMut(&T, &T) -> bool>( - data: &[T], - a: usize, - b: usize, - less: &mut F, -) -> (usize, i32) { - const SHORTEST_NINTHER: usize = 50; - const MAX_SWAPS: i32 = 4 * 3; - - let l = b - a; - - let mut swaps: i32 = 0; - let mut i = a + l / 4; - let mut j = a + l / 4 * 2; - let mut k = a + l / 4 * 3; - - if l >= 8 { - if l >= SHORTEST_NINTHER { - i = median_adjacent(data, i, &mut swaps, less); - j = median_adjacent(data, j, &mut swaps, less); - k = median_adjacent(data, k, &mut swaps, less); - } - j = median(data, i, j, k, &mut swaps, less); - } - - match swaps { - 0 => (j, INCREASING_HINT), - MAX_SWAPS => (j, DECREASING_HINT), - _ => (j, UNKNOWN_HINT), - } -} - -fn order2<T, F: FnMut(&T, &T) -> bool>( - data: &[T], - a: usize, - b: usize, - swaps: &mut i32, - less: &mut F, -) -> (usize, usize) { - if less(&data[b], &data[a]) { - *swaps += 1; - (b, a) - } else { - (a, b) - } -} - -fn median<T, F: FnMut(&T, &T) -> bool>( - data: &[T], - a: usize, - b: usize, - c: usize, - swaps: &mut i32, - less: &mut F, -) -> usize { - let (a, b) = order2(data, a, b, swaps, less); - let (b, c) = order2(data, b, c, swaps, less); - let (_a, b) = order2(data, a, b, swaps, less); - let _ = c; - b -} - -fn median_adjacent<T, F: FnMut(&T, &T) -> bool>( - data: &[T], - a: usize, - swaps: &mut i32, - less: &mut F, -) -> usize { - median(data, a - 1, a, a + 1, swaps, less) -} - -fn reverse_range<T>(data: &mut [T], a: usize, b: usize) { - let mut i = a; - let mut j = b - 1; - while i < j { - data.swap(i, j); - i += 1; - j -= 1; - } -} - -fn next_power_of_two(length: usize) -> usize { - let shift = bits_len(length as u64) as u32; - 1usize << shift -} - -/// The reference implementation's `xorshift uint64` PRNG used by `breakPatterns`. -struct Xorshift(u64); - -impl Xorshift { - fn next(&mut self) -> u64 { - self.0 ^= self.0 << 13; - self.0 ^= self.0 >> 7; - self.0 ^= self.0 << 17; - self.0 - } -} - -#[cfg(test)] -mod tests { - use super::slice; - - #[test] - fn matches_basic_descending() { - let mut v = vec![3, 1, 2, 3, 1]; - slice(&mut v, |a, b| a > b); - assert_eq!(v, vec![3, 3, 2, 1, 1]); - } - - #[test] - fn empty_and_single() { - let mut v: Vec<i32> = vec![]; - slice(&mut v, |a, b| a < b); - assert!(v.is_empty()); - let mut v = vec![42]; - slice(&mut v, |a, b| a < b); - assert_eq!(v, vec![42]); - } -} diff --git a/crates/cf-commands/src/handlers/history.rs b/crates/cf-commands/src/handlers/history.rs deleted file mode 100644 index 1988e2c8..00000000 --- a/crates/cf-commands/src/handlers/history.rs +++ /dev/null @@ -1,4583 +0,0 @@ -//! History-phase analyzer report builders, moved verbatim from the codefang -//! binary `main.rs` (the per-analyzer history report functions behind the old -//! per-(analyzer,format) dispatch ladder). The analyzer MATH lives in the -//! cf-* crates these call; this module owns only the shared history-pipeline -//! orchestration (one revwalk → per-commit tree diff → per-commit analyzer -//! feed → serialize) that the reference implementation `runHistoryPhase` + framework own. -//! Report bytes route through cf-gojson / cf-goyaml / cf-reportutil. - -use crate::handlers::{floor_tick_secs, format_rfc3339_offset, run_repo_path}; - -thread_local! { - /// One [`cf_uast::Parser`] per worker thread, reused across every commit that - /// thread processes in a [`parallel_prepare`] UAST walk. `Parser::new()` - /// registers a lazy parser for every embedded language mapping (hundreds of - /// bloom inserts), so constructing it per commit dominated the parallel - /// runtime — making the parallel UAST walks slower than sequential. A - /// thread-local amortizes that construction to once per thread while keeping - /// each thread its OWN parser (tree-sitter parsers are not thread-safe, so the - /// parser is never shared ACROSS threads). - static UAST_PARSER: cf_uast::Parser = cf_uast::Parser::new(); -} - -/// Runs `f` with this worker thread's reusable [`cf_uast::Parser`] (see -/// [`UAST_PARSER`]). Used by the per-commit compute closures of the parallel -/// UAST history walks (imports / quality / sentiment) so the parser is built once -/// per thread, not once per commit. -pub(crate) fn with_uast_parser<R>(f: impl FnOnce(&cf_uast::Parser) -> R) -> R { - UAST_PARSER.with(|p| f(p)) -} - -/// Closed-form `history/burndown --head` metrics (single HEAD-commit window), -/// shared by the json/yaml/bin head formats. -pub fn burndown_head_metrics( - sub: &clap::ArgMatches, -) -> Option<cf_analyzer_burndown::ComputedMetrics> { - use cf_analyzer_burndown::metrics::{AggregateData, SurvivalData}; - use cf_gitlib::blob::CachedBlob; - use cf_gitlib::changes::{initial_tree_changes, tree_diff, ChangeAction}; - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - let head = repo.head().ok()?; - let commit = repo.lookup_commit(head).ok()?; - let new_tree = commit.tree().ok()?; - - // Diff base: first parent's tree, or the empty tree for a root commit. - let changes = if commit.num_parents() > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(&repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(&repo, Some(&new_tree)).ok()? - }; - - let filter = super::history_filter(sub).unwrap_or_default(); - let mut total_lines: i64 = 0; - for change in &changes { - // handleInsertion uses To.Name; every surviving non-deletion change is - // counted as a full insertion. - if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { - continue; - } - if !filter.includes_change(&repo, &change.to.name, change.to.hash) { - continue; - } - let Ok(blob) = CachedBlob::from_repo(&repo, change.to.hash) else { - continue; - }; - // CountLines → ErrBinary for binary blobs (handleInsertion skips them). - if let Ok(lines) = blob.count_lines() { - total_lines += lines as i64; - } - } - - Some(cf_analyzer_burndown::ComputedMetrics { - aggregate: AggregateData { - total_current_lines: total_lines, - total_peak_lines: total_lines, - overall_survival_rate: if total_lines > 0 { 1.0 } else { 0.0 }, - analysis_period_days: 0, - num_bands: 1, - num_samples: 1, - tracked_files: 0, - tracked_developers: 0, - }, - global_survival: vec![SurvivalData { - sample_index: 0, - total_lines, - survival_rate: if total_lines > 0 { 1.0 } else { 0.0 }, - band_breakdown: vec![total_lines], - }], - // computeFileSurvival/computeDeveloperSurvivalList return empty (non-nil) - // slices → JSON `[]`; computeInteraction returns nil → JSON `null`. - file_survival: Some(Vec::new()), - developer_survival: Some(Vec::new()), - interactions: None, - }) -} - -/// Builds the `history/burndown --head --format timeseries` report bytes for the -/// HEAD commit, or `None` if HEAD has no resolvable tree. -/// -/// Reproduces analyze.MergedTimeSeries for the single HEAD commit: the top-level -/// struct (`version`, `tick_size_hours`, `analyzers`, `commits`) holds one commit -/// whose `MarshalJSON`-flattened object carries the sorted-key metadata + the -/// burndown ExtractCommitTimeSeries map `{lines_added, lines_removed}`. The -/// commit insertion-line count is the same closed form as [`burndown_head_metrics`] -/// (every surviving non-deletion change is a full insertion; binaries skipped). -/// Timestamp is the committer time `RFC3339`-formatted in the commit's -/// ORIGINAL zone offset (runner.recordCommitMeta: `tc.Timestamp.Format(RFC3339)`, -/// `tc.Timestamp == ac.Time == commit.Committer().When`). Author is "" (burndown -/// registers no identity provider, so `authorName` resolves the missing author to -/// the empty string). tick_size_hours defaults to 24 (no --tick-size on run). -pub fn burndown_head_timeseries(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - use cf_gitlib::blob::CachedBlob; - use cf_gitlib::changes::{initial_tree_changes, tree_diff, ChangeAction}; - use cf_gojson::value::{GoMap, GoValue}; - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - let head = repo.head().ok()?; - let commit = repo.lookup_commit(head).ok()?; - let new_tree = commit.tree().ok()?; - - let changes = if commit.num_parents() > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(&repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(&repo, Some(&new_tree)).ok()? - }; - - let filter = super::history_filter(sub).unwrap_or_default(); - let mut total_lines: i64 = 0; - for change in &changes { - if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { - continue; - } - if !filter.includes_change(&repo, &change.to.name, change.to.hash) { - continue; - } - let Ok(blob) = CachedBlob::from_repo(&repo, change.to.hash) else { - continue; - }; - if let Ok(lines) = blob.count_lines() { - total_lines += lines as i64; - } - } - - let committer = commit.committer(); - let timestamp = - format_rfc3339_offset(committer.when.seconds(), committer.when.offset_minutes()); - let hash = commit.hash().to_string(); - - // burndown ExtractCommitTimeSeries map: sorted keys lines_added, lines_removed. - let mut burndown = GoMap::new_map(); - burndown.insert("lines_added", GoValue::Int(total_lines)); - burndown.insert("lines_removed", GoValue::Int(0)); - - // MergedCommitData.MarshalJSON flat map (json.Marshal(map) → sorted keys: - // author, burndown, hash, tick, timestamp). - let mut commit_obj = GoMap::new_map(); - commit_obj.insert("author", GoValue::Str(String::new())); - commit_obj.insert("burndown", GoValue::Map(burndown)); - commit_obj.insert("hash", GoValue::Str(hash)); - commit_obj.insert("tick", GoValue::Int(0)); - commit_obj.insert("timestamp", GoValue::Str(timestamp)); - - // MergedTimeSeries struct: declaration order version, tick_size_hours, - // analyzers, commits. - let mut root = GoMap::new_struct(); - root.insert("version", GoValue::Str("codefang.timeseries.v1".into())); - root.insert("tick_size_hours", GoValue::Int(24)); - root.insert( - "analyzers", - GoValue::Array(vec![GoValue::Str("burndown".into())]), - ); - root.insert("commits", GoValue::Array(vec![GoValue::Map(commit_obj)])); - - // json.Encoder.SetIndent("", " ").Encode → 2-space indent + trailing newline. - let mut bytes = cf_gojson::marshal_indent(&GoValue::Map(root)); - bytes.push(b'\n'); - Some(bytes) -} - -/// Formats Unix seconds as the reference `time.RFC3339` (`2006-01-02T15:04:05Z07:00`) in the -/// zone given by `offset_minutes` (libgit2 `git2::Time::offset_minutes`). A zero -/// offset prints the literal `Z`; otherwise `±HH:MM`. Mirrors the reference implementation's behavior where -/// a non-UTC `time.Time` formats its numeric offset and only UTC prints `Z`. -/// Builds the `history/anomaly --head --format json` report bytes for the HEAD -/// commit, or `None` if HEAD is not the closed-form case this path reproduces. -/// -/// Reproduces the reference head-only pipeline for `history/anomaly`: -/// - tree diff: HEAD's tree vs its **first parent's** tree -/// (`TreeDiffAnalyzer.ensurePreviousTree` uses `Parent(0)`), then filtered -/// through the shared vendor / generated path policy -/// (`filterChanges -> pathpolicy.Exclude(name, nil, opts)`, content `nil`, -/// default opts: exclude vendor + generated paths). `files_changed` is the -/// surviving change count; -/// - per-change language detection (`LanguagesDetectionAnalyzer.Languages` + -/// `accumulateLanguagesAndAuthors`): each filtered change contributes its -/// extension-mapped language; `language_diversity` is the distinct count; -/// - a **merge** HEAD (`NumParents()>1`) skips `accumulateLineStats` -/// (reference behavior), so lines added/removed and net churn are 0 — the -/// deterministic, language-free-of-blob-content closed form. For a non-merge -/// HEAD the reference pipeline computes diff-match-patch line stats this closed form -/// does not reproduce; we return `None` so the caller surfaces the dispatch -/// sentinel rather than emitting subtly-divergent bytes; -/// - identity: a single HEAD commit yields author id 0 -/// (`IdentityDetector` loose dict over `[head]`), so `author_count` is 1; -/// - tick assignment: the single HEAD commit lands in tick 0; tick bounds -/// start == end == HEAD's **committer** time, `RFC3339`-formatted UTC. -/// -/// The typed report (`commit_metrics`/`commits_by_tick`/`tick_bounds`) is fed to -/// `cf_anomaly::build_report_data` → `compute_all_metrics`, whose -/// `ComputedMetrics::to_go_value` is serialized through cf-gojson (reference: -/// encoding/json parity: declaration-order keys, byte-sorted map keys, the reference implementation -/// shortest-float, `anomalies` nil slice → `null`, no trailing newline). -pub fn anomaly_head_report(sub: &clap::ArgMatches) -> Option<cf_anomaly::model::ComputedMetrics> { - use std::collections::BTreeMap; - - use cf_anomaly::metrics::{build_report_data, TickBounds}; - use cf_anomaly::model::CommitAnomalyData; - use cf_gitlib::blob::CachedBlob; - use cf_gitlib::changes::{tree_diff, ChangeAction}; - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - let head = repo.head().ok()?; - let commit = repo.lookup_commit(head).ok()?; - - // The HEAD commit is Consume'd exactly once (single tick). A MERGE HEAD - // (NumParents > 1) skips accumulateLineStats (the reference implementation's LineStats plumbing emits - // nothing for merges) so its line stats are 0; a regular HEAD computes them - // from the HEAD-vs-first-parent diff. A root HEAD (no parent) has no diff - // contract reproduced here. - if commit.num_parents() == 0 { - return None; - } - let is_merge = commit.num_parents() > 1; - - let committer_when = commit.committer().when.seconds(); // ac.Time == committer When. - let commit_hash = commit.hash().to_string(); - - // Tree diff HEAD vs first parent, then the shared vendor/generated filter. - let new_tree = commit.tree().ok()?; - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - let changes = tree_diff(&repo, Some(&old_tree), Some(&new_tree)).ok()?; - - let filter = super::history_filter(sub).unwrap_or_default(); - let mut files_changed: i64 = 0; - let mut languages: BTreeMap<String, i64> = BTreeMap::new(); - let mut lines_added: i64 = 0; - let mut lines_removed: i64 = 0; - for change in &changes { - // changeNameHash: Delete → From.Name/Hash, otherwise To. - let (name, change_hash) = if matches!(change.action, ChangeAction::Delete) { - (&change.from.name, change.from.hash) - } else { - (&change.to.name, change.to.hash) - }; - // filterChanges: path policy, then the --languages check. - if !filter.includes_change(&repo, name, change_hash) { - continue; - } - files_changed += 1; - - // accumulateLanguagesAndAuthors: count each non-empty detected language. - // The reference implementation's Languages plumbing analyzer detects from BLOB CONTENT (not just the - // extension), so a changed file whose extension is unknown but whose - // content is recognized still contributes to language_diversity (e.g. - // hercules's merge HEAD). Mirror the full revwalk path's `devs_detect_language`. - let blob_hash = if matches!(change.action, ChangeAction::Delete) { - change.from.hash - } else { - change.to.hash - }; - let data = CachedBlob::from_repo(&repo, blob_hash) - .map(|b| b.data) - .unwrap_or_default(); - let lang = devs_detect_language(name, &data); - if !lang.is_empty() { - *languages.entry(lang).or_insert(0) += 1; - } - - // accumulateLineStats (skipped for merge commits, mirroring the LineStats - // plumbing analyzer): Insert ⇒ +lines of the new blob; Delete ⇒ +lines of - // the old blob removed; Modify ⇒ diff-match-patch line stats, skipping - // binary / identical-content blobs. - if is_merge { - continue; - } - match change.action { - ChangeAction::Insert => { - if let Ok(blob) = CachedBlob::from_repo(&repo, change.to.hash) { - if let Ok(n) = blob.count_lines() { - lines_added += n as i64; - } - } - } - ChangeAction::Delete => { - if let Ok(blob) = CachedBlob::from_repo(&repo, change.from.hash) { - if let Ok(n) = blob.count_lines() { - lines_removed += n as i64; - } - } - } - ChangeAction::Modify => { - let (Ok(blob_from), Ok(blob_to)) = ( - CachedBlob::from_repo(&repo, change.from.hash), - CachedBlob::from_repo(&repo, change.to.hash), - ) else { - continue; - }; - if change.from.hash == change.to.hash - || blob_from.is_binary() - || blob_to.is_binary() - { - continue; - } - let old_lines = blob_from.count_lines().map_or(0, |n| n as i64); - let (a, r, _changed) = - compute_diff_line_stats(&repo, change.from.hash, change.to.hash, old_lines); - lines_added += a; - lines_removed += r; - } - } - } - - // Per-commit anomaly data: single HEAD commit ⇒ author id 0. - let mut commit_metrics: BTreeMap<String, CommitAnomalyData> = BTreeMap::new(); - commit_metrics.insert( - commit_hash.clone(), - CommitAnomalyData { - files_changed, - lines_added, - lines_removed, - net_churn: lines_added - lines_removed, - files: Vec::new(), - languages, - author_id: 0, - }, - ); - - // Single HEAD commit → tick 0. - let mut commits_by_tick: BTreeMap<i64, Vec<String>> = BTreeMap::new(); - commits_by_tick.insert(0, vec![commit_hash]); - - // tick_bounds[0] = { start: end: committer time } formatted RFC3339 UTC. - let when_rfc3339 = cf_analyze::metadata::format_rfc3339_utc(committer_when); - let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); - tick_bounds.insert( - 0, - TickBounds { - start_time: when_rfc3339.clone(), - end_time: when_rfc3339, - }, - ); - - // Default config: Threshold 2.0, WindowSize 20 (DefaultAnomalyThreshold / - // DefaultAnomalyWindowSize); no --anomaly-threshold/--anomaly-window flags. - let input = build_report_data(&commit_metrics, &commits_by_tick, tick_bounds, 2.0, 20); - Some(cf_anomaly::metrics::compute_all_metrics(&input)) -} - -/// Runs the real `history/anomaly` pipeline over the newest `--limit` commits and -/// returns the aggregated [`cf_anomaly::model::ComputedMetrics`], or `None` if the -/// repository cannot be opened/walked. This is the single report value every -/// machine format (json/yaml/bin) is an encoding of — the non-`--head` analogue of -/// [`anomaly_head_report`]'s closed form, driven through the general -/// revwalk → per-commit → aggregate → `ComputeAllMetrics` pipeline. -/// -/// Faithful port of the reference streaming anomaly path -/// (the reference `initHistoryPipeline` → `framework.RunStreaming` → -/// `plumbing.{TreeDiff,BlobCache,FileDiff,LineStats,Languages,Identity}` → -/// `anomaly.Analyzer.Consume` → `extractTC`/`buildTick` → `ticksToReport` → -/// `AggregateCommitsToTicks` → `ComputeAllMetrics`): -/// - **commit set / order**: `handlers::load_history_commit_hashes` — the `--limit` NEWEST -/// commits, delivered oldest-first. `--first-parent` adds first-parent -/// simplification. -/// - **tick assignment** (`plumbing.TicksSinceStart`, 24 h default): `tick0 = -/// FloorTime(when0, 24h)`; `tick = max(floor((when-tick0)/24h), previousTick)` -/// over the committer time. `commits_by_tick` records each tick's commit -/// hashes; tick bounds = min/max committer time of the tick's commits, the reference implementation -/// `time.RFC3339`-formatted in UTC. -/// - **per-commit changes** (`TreeDiffAnalyzer`): tree diff against the commit's -/// **first git parent** (root → full initial tree), already filtered by the -/// shared vendor/generated path policy (`pathpolicy.Exclude(name, nil)`). -/// `files_changed = len(changes)`; `files` = each change's `To.Name` -/// (`anomaly.Consume`), unconditionally — exactly as the reference implementation appends. -/// - **line stats** (`LinesStatsCalculator`, **skipped for merge commits** — -/// `ac.IsMerge`): Insert ⇒ `CountLines(To)` added; Delete ⇒ `CountLines(From)` -/// removed; Modify ⇒ `computeDiffLineStats` (diff-match-patch line diff), -/// skipping binary / identical-content blobs. `net_churn = added - removed`. -/// - **languages** (`LanguagesDetectionAnalyzer.Languages`, keyed by blob hash: -/// Insert/Delete one side, Modify both sides; binary ⇒ `""`, extension fast -/// path, enry content fallback): `accumulateLanguagesAndAuthors` counts each -/// non-empty detected language into the commit's `languages` map, so -/// `language_diversity` (per tick) is the distinct-language count. -/// - **identity** (`plumbing.IdentityDetector`, loose mode): the author id; -/// `author_count` per tick is the distinct-author-id count. -/// -/// `cf_anomaly::metrics::build_report_data` then aggregates commits to ticks and -/// runs Z-score detection (default threshold 2.0, window 20 — `run` passes no -/// `--anomaly-threshold`/`--anomaly-window`), and `compute_all_metrics` derives -/// the `anomalies`/`time_series`/`aggregate` report value. -pub fn anomaly_run_metrics(sub: &clap::ArgMatches) -> Option<cf_anomaly::model::ComputedMetrics> { - use cf_anomaly::metrics::build_report_data; - let walk = anomaly_walk(sub)?; - // Default config: Threshold 2.0, WindowSize 20. - let input = build_report_data( - &walk.commit_metrics, - &walk.commits_by_tick, - walk.tick_bounds, - 2.0, - 20, - ); - Some(cf_anomaly::metrics::compute_all_metrics(&input)) -} - -/// The raw products of one `history/anomaly` revwalk, shared by the aggregated -/// metrics path and the per-commit ndjson/timeseries paths so every format -/// encodes the SAME walk (reference: runner: one pipeline, per-commit TCs). -pub(crate) struct AnomalyWalk { - /// hex hash → per-commit anomaly data. - pub commit_metrics: std::collections::BTreeMap<String, cf_anomaly::model::CommitAnomalyData>, - /// tick → hashes in walk order. - pub commits_by_tick: std::collections::BTreeMap<i64, Vec<String>>, - /// tick → RFC3339-UTC committer-time bounds. - pub tick_bounds: std::collections::BTreeMap<i64, cf_anomaly::metrics::TickBounds>, - /// hex hash → tick. - pub tick_by_hash: std::collections::BTreeMap<String, i64>, - /// hex hash → (committer seconds, committer UTC-offset minutes). - pub when_by_hash: std::collections::BTreeMap<String, (i64, i32)>, -} - -pub(crate) fn anomaly_walk(sub: &clap::ArgMatches) -> Option<AnomalyWalk> { - use std::collections::BTreeMap; - - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - use cf_anomaly::metrics::TickBounds; - use cf_anomaly::model::CommitAnomalyData; - use cf_gitlib::blob::CachedBlob; - use cf_gitlib::changes::{initial_tree_changes, tree_diff, ChangeAction}; - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - let first_parent = crate::handlers::effective_first_parent(sub); - - // Window: `--head` loads EXACTLY the single HEAD commit (ignoring `--limit` - // and `--since`); otherwise the `--limit` NEWEST commits, oldest-first for - // the analyzers, capped by `--since`. See `handlers::load_history_commit_hashes`. - let hashes = if sub.get_flag("head") { - vec![repo.head().ok()?] - } else { - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - let change_filter = super::history_filter(sub).unwrap_or_default(); - let mut identity = IdentityDetector::new(); - - let mut commit_metrics: BTreeMap<String, CommitAnomalyData> = BTreeMap::new(); - let mut commits_by_tick: BTreeMap<i64, Vec<String>> = BTreeMap::new(); - let mut tick_when: BTreeMap<i64, (i64, i64)> = BTreeMap::new(); // (min, max) secs. - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - // ---- parallel pure-compute stage ----------------------------------------- - // The expensive per-commit work — tree diff + per-change line stats (libgit2) - // + language detection — is a PURE function of (repo, commit) and independent - // across commits, so run it across all cores. The order-dependent reduce below - // (identity ids, ticks, commits_by_tick, tick bounds) then runs sequentially - // over the results in oldest-first order, byte-identically. `author_id` is the - // ONLY order-dependent field, so it is left off here and stamped in the reduce. - /// The expensive, per-commit-independent products of one commit's diff: the - /// fully-built [`CommitAnomalyData`] minus its order-assigned `author_id`. - struct AnomalyPrepared { - data: CommitAnomalyData, - } - let workers = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let filter_ref = &change_filter; - let prepared = parallel_prepare(&path, &hashes, workers, move |repo, hash| { - let commit = repo.lookup_commit(hash).ok()?; - let num_parents = commit.num_parents(); - // The reference `Commit.NumParents()` is reported as 1 for a merge under - // --first-parent (implied when the selection includes history/burndown): - // the simplified walk visits a merge as an ordinary single-parent commit, - // so its first-parent diff line stats ARE accumulated. Only a merge seen - // by the full walk skips accumulateLineStats. - let is_merge = num_parents > 1 && !first_parent; - - // Tree diff against the first parent (root → full initial tree), then the - // shared vendor/generated/languages filter (TreeDiffAnalyzer.filterChanges). - let new_tree = commit.tree().ok()?; - let mut changes = if num_parents > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(repo, Some(&new_tree)).ok()? - }; - filter_ref.retain_changes(repo, &mut changes); - - // anomaly.Consume: FilesChanged = len(changes); Files = each change's - // To.Name (unconditionally, like the reference implementation's append). - let mut cm = CommitAnomalyData { - files_changed: changes.len() as i64, - ..Default::default() - }; - for change in &changes { - cm.files.push(change.to.name.clone()); - } - - // accumulateLineStats (skipped for merge commits): per-change line stats. - if !is_merge { - let mut added = 0i64; - let mut removed = 0i64; - for change in &changes { - match change.action { - ChangeAction::Insert => { - if let Ok(blob) = CachedBlob::from_repo(repo, change.to.hash) { - if let Ok(n) = blob.count_lines() { - added += n as i64; - } - } - } - ChangeAction::Delete => { - if let Ok(blob) = CachedBlob::from_repo(repo, change.from.hash) { - if let Ok(n) = blob.count_lines() { - removed += n as i64; - } - } - } - ChangeAction::Modify => { - // computeModifyStats: needs both blobs, skips binary and - // identical content. - let (Ok(blob_from), Ok(blob_to)) = ( - CachedBlob::from_repo(repo, change.from.hash), - CachedBlob::from_repo(repo, change.to.hash), - ) else { - continue; - }; - if change.from.hash == change.to.hash { - continue; - } - if blob_from.is_binary() || blob_to.is_binary() { - continue; - } - let old_lines = blob_from.count_lines().map_or(0, |n| n as i64); - let (a, r, _changed) = compute_diff_line_stats( - repo, - change.from.hash, - change.to.hash, - old_lines, - ); - added += a; - removed += r; - } - } - } - cm.lines_added = added; - cm.lines_removed = removed; - } - - // accumulateLanguagesAndAuthors: build the blob-hash → language map exactly - // as Languages() does (Insert/Delete one side, Modify both sides), then - // count each non-empty value into cm.languages. - let mut by_blob: BTreeMap<cf_gitlib::hash::Hash, String> = BTreeMap::new(); - let mut detect = |entry: &cf_gitlib::changes::ChangeEntry| { - let data = CachedBlob::from_repo(repo, entry.hash) - .map(|b| b.data) - .unwrap_or_default(); - by_blob.insert(entry.hash, devs_detect_language(&entry.name, &data)); - }; - for change in &changes { - match change.action { - ChangeAction::Insert => detect(&change.to), - ChangeAction::Delete => detect(&change.from), - ChangeAction::Modify => { - detect(&change.to); - detect(&change.from); - } - } - } - for lang in by_blob.values() { - if !lang.is_empty() { - *cm.languages.entry(lang.clone()).or_insert(0) += 1; - } - } - - cm.net_churn = cm.lines_added - cm.lines_removed; - Some(AnomalyPrepared { data: cm }) - })?; - - // ---- sequential ordered-reduce stage ------------------------------------- - let mut tick_by_hash: BTreeMap<String, i64> = BTreeMap::new(); - let mut when_by_hash: BTreeMap<String, (i64, i32)> = BTreeMap::new(); - for (i, hash) in hashes.iter().enumerate() { - let commit = repo.lookup_commit(*hash).ok()?; - let hash_str = hash.to_hex(); - - // Identity: resolve this commit's author id (loose signature), oldest-first. - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - - // Tick assignment from the committer time (24 h default). - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - // Track committer-time bounds for the tick (extractTC updateTimeRange). - tick_when - .entry(tick) - .and_modify(|(lo, hi)| { - if when < *lo { - *lo = when; - } - if when > *hi { - *hi = when; - } - }) - .or_insert((when, when)); - commits_by_tick - .entry(tick) - .or_default() - .push(hash_str.clone()); - tick_by_hash.entry(hash_str.clone()).or_insert(tick); - when_by_hash - .entry(hash_str.clone()) - .or_insert((when, committer_when.offset_minutes())); - - // Consume the precomputed per-commit data (tree diff / line stats / - // languages); only the order-assigned author id is stamped here. - let mut cm = prepared[i].data.clone(); - cm.author_id = author_id; - commit_metrics.insert(hash_str, cm); - } - - // tick_bounds[tick] = { start: end: committer min/max } RFC3339 UTC. - let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); - for (tick, (lo, hi)) in &tick_when { - tick_bounds.insert( - *tick, - TickBounds { - start_time: cf_analyze::metadata::format_rfc3339_utc(*lo), - end_time: cf_analyze::metadata::format_rfc3339_utc(*hi), - }, - ); - } - - Some(AnomalyWalk { - commit_metrics, - commits_by_tick, - tick_bounds, - tick_by_hash, - when_by_hash, - }) -} - -/// Serializes one [`cf_anomaly::model::CommitAnomalyData`] as the reference struct -/// (declaration field order; `files`/`languages` are `omitempty`). -fn anomaly_data_value(cm: &cf_anomaly::model::CommitAnomalyData) -> cf_gojson::GoValue { - use cf_gojson::{GoMap, GoValue}; - let mut data = GoMap::new_struct(); - data.insert("files_changed".to_string(), GoValue::Int(cm.files_changed)); - data.insert("lines_added".to_string(), GoValue::Int(cm.lines_added)); - data.insert("lines_removed".to_string(), GoValue::Int(cm.lines_removed)); - data.insert("net_churn".to_string(), GoValue::Int(cm.net_churn)); - if !cm.files.is_empty() { - data.insert( - "files".to_string(), - GoValue::Array(cm.files.iter().map(|f| GoValue::Str(f.clone())).collect()), - ); - } - if !cm.languages.is_empty() { - let mut langs = GoMap::new_map(); - for (lang, n) in &cm.languages { - langs.insert(lang.clone(), GoValue::Int(*n)); - } - data.insert("languages".to_string(), GoValue::Object(langs)); - } - data.insert("author_id".to_string(), GoValue::Int(cm.author_id)); - cf_gojson::GoValue::Object(data) -} - -/// Per-commit anomaly NDJSON records: every walked commit emits a line (reference: -/// `anomaly.Consume` never returns nil Data for a real commit); `data` is the -/// `*CommitAnomalyData` struct. -pub fn anomaly_ndjson_records( - sub: &clap::ArgMatches, -) -> Option<Vec<super::history_formats::NdjsonRecord>> { - let walk = anomaly_walk(sub)?; - let mut records = Vec::new(); - let mut pos = 0usize; - for hashes in walk.commits_by_tick.values() { - for hex in hashes { - // Every walked commit occupies a consume position (anomaly emits a - // TC for each, so positions and records are 1:1 here). - let p = pos; - pos += 1; - let Some(cm) = walk.commit_metrics.get(hex) else { - continue; - }; - let (secs, off) = walk.when_by_hash.get(hex).copied().unwrap_or((0, 0)); - records.push(super::history_formats::NdjsonRecord { - pos: p, - hash: hex.clone(), - tick: walk.tick_by_hash.get(hex).copied().unwrap_or(0), - author_id: cm.author_id, - time_secs: secs, - tz_offset_min: off, - data: anomaly_data_value(cm), - }); - } - } - Some(records) -} - -/// The anomaly contribution to the merged `--format timeseries` document (reference: -/// `anomaly.ExtractCommitTimeSeries` over `report["commit_metrics"]`); the -/// anomaly report DOES carry `commits_by_tick`, so commits are ordered. -pub fn anomaly_timeseries_contribution( - sub: &clap::ArgMatches, -) -> Option<super::history_formats::TimeSeriesContribution> { - let walk = anomaly_walk(sub)?; - let mut per_commit = Vec::new(); - for (hex, cm) in &walk.commit_metrics { - per_commit.push((hex.clone(), anomaly_data_value(cm))); - } - let mut commit_meta = Vec::new(); - for hashes in walk.commits_by_tick.values() { - for hex in hashes { - let tick = walk.tick_by_hash.get(hex).copied().unwrap_or(0); - let (secs, off) = walk.when_by_hash.get(hex).copied().unwrap_or((0, 0)); - // CommitMeta.Author is "" — ReversedPeopleDict is not finalized when - // recordCommitMeta runs, so the reference implementation's authorName always misses. - commit_meta.push(( - hex.clone(), - tick, - crate::handlers::format_rfc3339_offset(secs, off), - String::new(), - )); - } - } - Some(super::history_formats::TimeSeriesContribution { - flag: "anomaly", - per_commit, - commit_meta, - }) -} - -/// Builds the `run --analyzers history/quality --format json` bytes for the -/// newest `--limit` commits, or `None` if the repository cannot be opened/walked. -/// -/// Reproduces the reference streaming quality pipeline as a closed form: -/// - **commit set / order**: `handlers::load_history_commit_hashes` — the `--limit` NEWEST -/// commits, delivered oldest-first -/// (reference initHistoryPipeline: `commitCount` capped at `opts.Limit`). -/// - **tick assignment** (`plumbing.TicksSinceStart`): `tick0 = FloorTime(when0, -/// 24h)`; `tick = max(floor((when-tick0)/24h), previousTick)` over the -/// committer time; the tick size is the 24 h default (`run` passes no -/// `--tick-size`). Tick bounds = min/max committer time of the commits in the -/// tick, `RFC3339`-formatted in UTC (`FormatStartTime/EndTime`). -/// - **per-commit changes**: tree diff against the commit's **first git parent** -/// (`TreeDiffAnalyzer.ensurePreviousTree` → `Parent(0)`; the quality analyzer -/// is parallel/forked, so every commit diffs against its own parent), or the -/// full initial tree for a root commit (no parent). -/// - **spill rule** (`UASTPipeline.SpillThreshold = 32`): a commit with **> 32** -/// file changes is spilled to disk; on the streaming run the quality analyzer's -/// `TreeDiff.Changes` is empty when it streams a spill, so every spilled -/// record's `ChangeIndex` is out of range and **all** its UAST changes are -/// dropped — such commits contribute **zero** analyzed files. Commits with ≤ 32 -/// changes are parsed in memory. -/// - **per-file filter** (`UASTPipeline.parseBlob` over each Insert/Modify -/// change's *After* version): the shared vendor/generated path policy -/// (`pathpolicy.Exclude(name, nil)`), parser language support (by extension), -/// the 256 KiB blob cap, and content-aware generated detection -/// (`pathpolicy.Exclude(name, content)`); the surviving files are analyzed. -/// -/// For every file the four component analyzers run; in this capture's commit -/// window every surviving file is a function-free document (`.md` / `.sh` with no -/// shell functions), so each analyzer returns its empty result — complexity -/// `0/0/0/0`, Halstead volume `0`, comment score `0`, documentation `0`, and a -/// perfect cohesion score of `1.0` (cohesion of a tree with no methods). The -/// per-tick [`TickQuality`] is fed to `cf_quality::compute_all_metrics` and -/// serialized compact through cf-gojson (`to_json_compact`: the reference `json.Marshal` -/// parity, no trailing newline) — byte-identical to `run/history_quality.json`. -pub fn quality_metrics(sub: &clap::ArgMatches) -> Option<cf_quality::ComputedMetrics> { - use cf_quality::{compute_all_metrics, ReportData, TickBounds, TickQuality}; - use std::collections::BTreeMap; - - let walk = quality_walk(sub)?; - - // Per-tick merged quality + bounds (committer-time min/max), folding each - // commit's per-file samples into its tick in walk order. - let mut tick_quality: BTreeMap<i64, TickQuality> = BTreeMap::new(); - let mut tick_when: BTreeMap<i64, (i64, i64)> = BTreeMap::new(); - for c in &walk { - tick_when - .entry(c.tick) - .and_modify(|(lo, hi)| { - if c.when < *lo { - *lo = c.when; - } - if c.when > *hi { - *hi = c.when; - } - }) - .or_insert((c.when, c.when)); - let tq = tick_quality.entry(c.tick).or_default(); - merge_tick_quality(tq, &c.tq); - } - - // Format tick bounds RFC3339 UTC (FormatStartTime / FormatEndTime). - let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); - for (tick, (lo, hi)) in &tick_when { - tick_bounds.insert( - *tick, - TickBounds { - start_time: cf_analyze::metadata::format_rfc3339_utc(*lo), - end_time: cf_analyze::metadata::format_rfc3339_utc(*hi), - }, - ); - } - - let input = ReportData { - tick_quality, - tick_bounds, - }; - Some(compute_all_metrics(&input)) -} - -/// One walked commit's quality products (the reference `quality.Consume` TC + runner -/// stamps). -#[derive(Clone)] -pub(crate) struct QualityCommit { - /// The commit's position in the ORIGINAL walk window (0-based, counting - /// oversized-dropped commits too): the reference runner assigns consume - /// positions before the oversized drop suppresses a commit's record, and - /// the forked-leaf NDJSON drain order is keyed on this position. - pub pos: usize, - /// Full hex hash. - pub hash: String, - /// This commit's per-file quality samples (reference: per-commit `TickQuality`). - pub tq: cf_quality::TickQuality, - /// TicksSinceStart tick. - pub tick: i64, - /// Loose-identity author id (walk order). - pub author_id: i64, - /// Committer time, Unix seconds. - pub when: i64, - /// Committer UTC-offset minutes. - pub offset_min: i32, -} - -/// The shared `history/quality` revwalk: per-commit `TickQuality` plus the -/// order-assigned identity/tick stamps, in walk order. Every quality format -/// consumes THIS one walk. -pub(crate) fn quality_walk(sub: &clap::ArgMatches) -> Option<Vec<QualityCommit>> { - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - use cf_pathpolicy::Options as PathPolicyOptions; - - // Multi-analyzer runs route through the ONE shared UAST walk (same code, - // one tree diff + one parse per blob per commit across the co-selected - // analyzers); single-analyzer runs keep this direct walk. - if let Some(shared) = super::uast_walk::shared_quality_walk(sub) { - return shared; - } - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - - // Window: `--head` loads EXACTLY the single HEAD commit (ignoring `--limit` - // and `--since`); otherwise the `--limit` NEWEST commits, oldest-first for - // the analyzers, capped by `--since`. See `handlers::load_history_commit_hashes`. - // The HEAD commit on a large repo spills (> 32 changes ⇒ zero UAST files), - // yielding the reference implementation's single all-zero tick-0 report. - let first_parent = crate::handlers::effective_first_parent(sub); - let hashes = if sub.get_flag("head") { - vec![repo.head().ok()?] - } else { - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - // `opts` drives the UAST parse gate only — the reference UASTChanges - // pathfilter gate (always-on vendor/generated detection), NOT the - // flag-controlled policy. The TreeDiff-level filter (policy + --languages) - // is applied to the changes below. - let opts = PathPolicyOptions::default(); - let change_filter = super::history_filter(sub).unwrap_or_default(); - let mut identity = IdentityDetector::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - // Oversized-commit gate: commits whose RAW tree diff exceeds the cap are - // silently dropped from history BEFORE any analyzer (reference framework - // behaviour; flag `--max-changes-per-commit`, 0 = default 10000). - let max_changes = crate::handlers::history::max_changes_per_commit_cap(sub); - - // ---- parallel pure-compute stage ----------------------------------------- - // The expensive per-commit work — tree diff + per-file UAST parse + the four - // component analyzers — is a PURE function of (repo, commit), so run it across - // all cores. The result is a per-commit `TickQuality` carrying that commit's - // per-file samples (empty for a spilled/zero-file commit) plus the RAW - // tree-diff change count for the oversized-commit gate. The order-dependent - // reduce below (monotonic tick assignment, per-tick committer-time bounds, - // merge of each commit's samples into its tick) runs UNCHANGED and - // sequentially over these results. The per-commit body is the SAME - // `quality_commit_product` the shared multi-analyzer walk calls. - let workers = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let opts_ref = &opts; - let filter_ref = &change_filter; - let prepared = parallel_prepare(&path, &hashes, workers, move |repo, hash| { - // Per-thread UAST parser, reused across this thread's commits (tree-sitter - // parsers are not thread-safe, so never shared across threads). - crate::handlers::history::with_uast_parser(|parser| { - let commit = repo.lookup_commit(hash).ok()?; - let raw_changes = commit_tree_changes(repo, &commit)?; - let raw_change_count = raw_changes.len(); - // Oversized commits are dropped before any analyzer — no per-file work. - if raw_change_count > max_changes { - return Some((raw_change_count, cf_quality::TickQuality::default())); - } - // TreeDiffAnalyzer.filterChanges (policy + --languages). - let mut changes = raw_changes; - filter_ref.retain_changes(repo, &mut changes); - let mut cache = super::uast_walk::CommitParseCache::new(repo, parser, opts_ref); - Some(( - raw_change_count, - quality_commit_product(&changes, &mut cache), - )) - }) - })?; - - // ---- sequential ordered identity/tick stamping ---------------------------- - let mut commits = Vec::with_capacity(hashes.len()); - for (i, hash) in hashes.iter().enumerate() { - // Oversized-commit skip: dropped from history before identity/tick - // stamping (the reference framework never shows the commit to any - // analyzer, core or leaf). - if prepared[i].0 > max_changes { - continue; - } - let commit = repo.lookup_commit(*hash).ok()?; - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - - // Identity: resolve this commit's author id (loose signature), oldest-first. - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - commits.push(QualityCommit { - pos: i, - hash: hash.to_string(), - tq: prepared[i].1.clone(), - tick, - author_id, - when, - offset_min: committer_when.offset_minutes(), - }); - } - Some(commits) -} - -/// Computes a commit's tree changes against its first parent (root commit → -/// the full initial tree) — the ONE diff every history UAST walk starts from, -/// shared by the direct walks and the multi-analyzer shared walk. -pub(crate) fn commit_tree_changes( - repo: &cf_gitlib::Repository, - commit: &cf_gitlib::commit::Commit<'_>, -) -> Option<cf_gitlib::changes::Changes> { - use cf_gitlib::changes::{initial_tree_changes, tree_diff}; - let new_tree = commit.tree().ok()?; - if commit.num_parents() > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(repo, Some(&old_tree), Some(&new_tree)).ok() - } else { - initial_tree_changes(repo, Some(&new_tree)).ok() - } -} - -/// The `history/quality` per-commit product (the reference `quality.Consume` -/// body): the per-file quality samples of this commit's surviving After trees. -/// Called by BOTH the direct [`quality_walk`] and the shared multi-analyzer -/// UAST walk, so the two are byte-identical by construction. -/// -/// Gates: the spill rule (> 32 changes ⇒ zero UAST changes), the Delete / -/// zero-hash skip, then the `UASTPipeline.parseBlob` gate chain inside the -/// parse cache. A gates-passed file whose parse fails still counts as one -/// analyzed file (a function-free sample), keeping `files_analyzed` -/// byte-identical (reference: every parsed file has a node there). -pub(crate) fn quality_commit_product( - changes: &[cf_gitlib::changes::Change], - cache: &mut super::uast_walk::CommitParseCache<'_>, -) -> cf_quality::TickQuality { - use super::uast_walk::{ParseOutcome, SPILL_THRESHOLD}; - use cf_gitlib::changes::ChangeAction; - - // This commit's per-file quality samples (the reference implementation appends one sample per - // analyzed file into the tick; here we collect them per commit first). - let mut commit_q = cf_quality::TickQuality::default(); - - // Spill rule: > 32 changes ⇒ the quality analyzer sees zero UAST changes. - if changes.len() > SPILL_THRESHOLD { - return commit_q; - } - - for change in changes { - // Quality analyzes the After version only (Insert / Modify). - if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { - continue; - } - // Parse the file to UAST and run the four component analyzers on the - // `change.After` root, exactly as the reference `quality.(*Analyzer).analyzeNode` - // (complexity -> halstead -> comments -> cohesion), recording the same - // scalar keys. One sample per analyzed file. - match &*cache.parse(&change.to.name, change.to.hash) { - ParseOutcome::Parsed(root) => accumulate_quality_file(root, &mut commit_q), - ParseOutcome::Failed(_) => push_empty_quality_sample(&mut commit_q), - ParseOutcome::Skipped => {} - } - } - commit_q -} - -/// Serializes one per-commit [`cf_quality::TickQuality`] as the reference implementation's `*TickQuality` -/// struct (declaration field order, exported field names, nil slices → null). -fn quality_tq_value(tq: &cf_quality::TickQuality) -> cf_gojson::GoValue { - use cf_gojson::{GoMap, GoValue}; - fn floats(v: &[f64]) -> GoValue { - if v.is_empty() { - GoValue::Null - } else { - GoValue::Array(v.iter().map(|f| GoValue::Float(*f)).collect()) - } - } - fn ints(v: &[i64]) -> GoValue { - if v.is_empty() { - GoValue::Null - } else { - GoValue::Array(v.iter().map(|i| GoValue::Int(*i)).collect()) - } - } - let mut m = GoMap::new_struct(); - m.insert("Complexities".to_string(), floats(&tq.complexities)); - m.insert("Cognitives".to_string(), floats(&tq.cognitives)); - m.insert("MaxComplexities".to_string(), ints(&tq.max_complexities)); - m.insert("Functions".to_string(), ints(&tq.functions)); - m.insert("HalsteadVolumes".to_string(), floats(&tq.halstead_volumes)); - m.insert("HalsteadEfforts".to_string(), floats(&tq.halstead_efforts)); - m.insert("DeliveredBugs".to_string(), floats(&tq.delivered_bugs)); - m.insert("CommentScores".to_string(), floats(&tq.comment_scores)); - m.insert("DocCoverages".to_string(), floats(&tq.doc_coverages)); - m.insert("CohesionScores".to_string(), floats(&tq.cohesion_scores)); - GoValue::Object(m) -} - -/// Per-commit quality NDJSON records (forked leaf): every commit emits a line -/// whose `data` is the reference `*TickQuality` struct (nil slices → null). -pub fn quality_ndjson_records( - sub: &clap::ArgMatches, -) -> Option<Vec<super::history_formats::NdjsonRecord>> { - let walk = quality_walk(sub)?; - Some( - walk.iter() - .map(|c| super::history_formats::NdjsonRecord { - // The consume position counts oversized-dropped commits too - // (the reference runner numbers positions before the drop - // suppresses a record), so the forked-leaf drain order keys on - // the ORIGINAL walk position, not the surviving index. - pos: c.pos, - hash: c.hash.clone(), - tick: c.tick, - author_id: c.author_id, - time_secs: c.when, - tz_offset_min: c.offset_min, - data: quality_tq_value(&c.tq), - }) - .collect(), - ) -} - -/// The quality contribution to the merged `--format timeseries` document (reference: -/// `quality.ExtractCommitTimeSeries` over `report["commit_quality"]`): per -/// commit the 11-key summary map (`cf_quality::commit_summary`). -pub fn quality_timeseries_contribution( - sub: &clap::ArgMatches, -) -> Option<super::history_formats::TimeSeriesContribution> { - let walk = quality_walk(sub)?; - let mut per_commit = Vec::new(); - let mut commit_meta = Vec::new(); - for c in &walk { - let summary = cf_quality::commit_summary(&c.tq); - per_commit.push(( - c.hash.clone(), - cf_quality::serialize::commit_summary_value(&summary), - )); - commit_meta.push(( - c.hash.clone(), - c.tick, - crate::handlers::format_rfc3339_offset(c.when, c.offset_min), - String::new(), - )); - } - Some(super::history_formats::TimeSeriesContribution { - flag: "quality", - per_commit, - commit_meta, - }) -} - -/// Builds the `run --analyzers history/quality --format timeseries --ndjson` -/// bytes: one compact JSON line per walked commit (the reference per-chunk -/// `TimeSeriesChunkFlusher` over `DrainCommitStats`), each line the merged -/// commit object `{author, hash, quality:{...}, tick, timestamp}` with the -/// per-commit summary map (`drainQualityCommitData` keys) under `"quality"`. -/// Same memoized walk as every other quality format. -pub fn quality_timeseries_ndjson(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - use cf_gojson::{GoMap, GoValue, MapOrigin}; - - let contrib = quality_timeseries_contribution(sub)?; - let mut out = Vec::new(); - for (hash, tick, ts, author) in &contrib.commit_meta { - // assembleCommits filters the ordered meta to hashes the analyzer - // contributed data for (nil-Data TCs order but never emit). - let Some((_, v)) = contrib.per_commit.iter().find(|(h, _)| h == hash) else { - continue; - }; - let mut m = GoMap::new(MapOrigin::Map); - m.push("hash", GoValue::Str(hash.clone())); - m.push("timestamp", GoValue::Str(ts.clone())); - m.push("author", GoValue::Str(author.clone())); - m.push("tick", GoValue::Int(*tick)); - m.push("quality", v.clone()); - out.extend_from_slice(&cf_gojson::marshal(&GoValue::Map(m))); - out.push(b'\n'); - } - Some(out) -} - -/// Appends every per-file sample in `src` onto `dst`, preserving order. Used by -/// the parallel quality walk to fold one commit's precomputed [`TickQuality`] -/// (its per-file samples) into its tick's accumulator in walk order — equivalent -/// to having pushed each file's samples directly, but computed off-thread. -fn merge_tick_quality(dst: &mut cf_quality::TickQuality, src: &cf_quality::TickQuality) { - dst.complexities.extend_from_slice(&src.complexities); - dst.cognitives.extend_from_slice(&src.cognitives); - dst.max_complexities - .extend_from_slice(&src.max_complexities); - dst.functions.extend_from_slice(&src.functions); - dst.halstead_volumes - .extend_from_slice(&src.halstead_volumes); - dst.halstead_efforts - .extend_from_slice(&src.halstead_efforts); - dst.delivered_bugs.extend_from_slice(&src.delivered_bugs); - dst.comment_scores.extend_from_slice(&src.comment_scores); - dst.doc_coverages.extend_from_slice(&src.doc_coverages); - dst.cohesion_scores.extend_from_slice(&src.cohesion_scores); -} - -/// Pushes one function-free quality sample (the reference `analyzeNode` over a tree with no -/// functions): complexity 0/0/0/0, Halstead 0/0/0, comments 0/0, cohesion 1.0 -/// (the cohesion analyzer's empty-result `cohesion_score`). -fn push_empty_quality_sample(tq: &mut cf_quality::TickQuality) { - tq.complexities.push(0.0); - tq.cognitives.push(0.0); - tq.max_complexities.push(0); - tq.functions.push(0); - tq.halstead_volumes.push(0.0); - tq.halstead_efforts.push(0.0); - tq.delivered_bugs.push(0.0); - tq.comment_scores.push(0.0); - tq.doc_coverages.push(0.0); - tq.cohesion_scores.push(1.0); -} - -/// Runs the four component analyzers on one file's UAST root and appends their -/// scalars to `tq`, mirroring the reference `quality.(*Analyzer).analyzeNode`: -/// `analyzeComplexity` -> `analyzeHalstead` -> `analyzeComments` -> `analyzeCohesion`. -/// Each component appends exactly one value per file (the reference implementation appends unconditionally -/// on success; the per-file `Analyze` calls here do not error for a parsed root). -fn accumulate_quality_file(root: &cf_uast::Node, tq: &mut cf_quality::TickQuality) { - // The history quality analyzer instantiates its component analyzers with - // the shared traverser's `maxDepth = 10` (the static surfaces run - // uncapped): only function/comment nodes at depth <= 10 below the file - // root are DISCOVERED; per-function analysis over each found subtree is - // unchanged. Halstead is unaffected (measured equal against the live - // reference binary on depth-pathological trees). - const QUALITY_FIND_MAX_DEPTH: usize = 10; - - // --- complexity (cf_complexity::Analyzer::analyze over its node model) --- - let cx_root = uast_to_cx_node(root); - let cx = cf_complexity::Analyzer - .analyze_with_find_depth(Some(&cx_root), Some(QUALITY_FIND_MAX_DEPTH)); - tq.complexities - .push(govalue_int(&cx, "total_complexity") as f64); - tq.cognitives - .push(govalue_int(&cx, "cognitive_complexity") as f64); - tq.max_complexities.push(govalue_int(&cx, "max_complexity")); - tq.functions.push(govalue_int(&cx, "total_functions")); - - // --- halstead (standalone findFunctions/Analyze file-level measures) --- - let h = cf_halstead::analyze(root); - tq.halstead_volumes.push(h.volume); - tq.halstead_efforts.push(h.effort); - tq.delivered_bugs.push(h.delivered_bugs); - - // --- comments (cf_comments::Analyzer::analyze) --- - match cf_comments::Analyzer::new() - .analyze_with_find_depth(Some(root), Some(QUALITY_FIND_MAX_DEPTH)) - { - Ok(c) => { - tq.comment_scores.push(govalue_float(&c, "overall_score")); - tq.doc_coverages - .push(govalue_float(&c, "documentation_coverage")); - } - Err(_) => { - tq.comment_scores.push(0.0); - tq.doc_coverages.push(0.0); - } - } - - // --- cohesion (cf_cohesion::Analyzer::analyze, the findFunctions path) --- - if let Ok(r) = - cf_cohesion::Analyzer::new().analyze_with_find_depth(root, Some(QUALITY_FIND_MAX_DEPTH)) - { - tq.cohesion_scores.push( - r.get("cohesion_score") - .and_then(cf_cohesion::report_value::ReportValue::as_float) - .unwrap_or(0.0), - ); - } -} - -/// `reportutil.GetInt`: integer value at `key` in a `cf-gojson` report map, -/// truncating floats toward zero; `0` when absent or non-numeric. -fn govalue_int(v: &cf_gojson::GoValue, key: &str) -> i64 { - match v { - cf_gojson::GoValue::Map(m) => match m.get(key) { - Some(cf_gojson::GoValue::Int(n)) => *n, - Some(cf_gojson::GoValue::Float(f)) => *f as i64, - _ => 0, - }, - _ => 0, - } -} - -/// `reportutil.GetFloat64`: float value at `key`; `0.0` when absent/non-numeric. -fn govalue_float(v: &cf_gojson::GoValue, key: &str) -> f64 { - match v { - cf_gojson::GoValue::Map(m) => match m.get(key) { - Some(cf_gojson::GoValue::Float(f)) => *f, - Some(cf_gojson::GoValue::Int(n)) => *n as f64, - _ => 0.0, - }, - _ => 0.0, - } -} - -/// Converts a `cf_uast::Node` into the `cf_complexity::node::Node` subset the -/// complexity analyzer reads (type, token, roles, props, children, positions), -/// matching the static complexity handler's bridge. -fn uast_to_cx_node(n: &cf_uast::Node) -> cf_complexity::node::Node { - let mut out = cf_complexity::node::Node::new(n.node_type.clone()); - out.token = n.token.clone(); - out.roles = n.roles.clone(); - out.props = n - .props - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - out.pos = n.pos.as_ref().map(|p| cf_complexity::node::Positions { - start_line: p.start_line as u32, - start_col: p.start_col as u32, - start_offset: p.start_offset as u32, - end_line: p.end_line as u32, - end_col: p.end_col as u32, - end_offset: p.end_offset as u32, - }); - out.children = n.children.iter().map(uast_to_cx_node).collect(); - out -} - -/// Builds the `run --analyzers history/sentiment --format json` bytes for the -/// newest `--limit` commits, or `None` if the repository cannot be opened/walked. -/// -/// Reproduces the reference streaming sentiment pipeline as a closed form: -/// - **commit set / order**: `handlers::load_history_commit_hashes` — the `--limit` NEWEST -/// commits, delivered oldest-first (reference initHistoryPipeline). -/// - **tick assignment** (`plumbing.TicksSinceStart`): `tick0 = FloorTime(when0, -/// 24h)`; `tick = max(floor((when-tick0)/24h), previousTick)` over the -/// committer time. `commits_by_tick` records each tick's commit hashes (drives -/// `commit_count`); tick bounds = min/max committer time of the tick's -/// commits, `RFC3339`-formatted in UTC (`FormatStartTime/EndTime`). -/// - **per-commit changes**: tree diff against the commit's **first git parent** -/// (`TreeDiffAnalyzer` / forked parallel analyzer diffs against its own -/// parent), or the full initial tree for a root commit. -/// - **spill rule** (`UASTPipeline.SpillThreshold = 32`): a commit with **> 32** -/// file changes contributes zero analyzed files (its streamed UAST changes are -/// dropped), matching the quality path. -/// - **per-file filter** (`UASTPipeline.parseBlob` over each Insert/Modify -/// change's *After* version): vendor/generated path policy -/// (`pathpolicy.Exclude(name, nil)`), parser language support (by extension), -/// the 256 KiB blob cap, and content-aware generated detection -/// (`pathpolicy.Exclude(name, content)`). -/// - **comment extraction** (`Analyzer.Consume`): for each surviving After tree, -/// recursively collect `Comment` nodes, then `mergeComments` (group by start -/// line, merge adjacent within `maxEnd+1`, strip delimiters, filter by the -/// default `MinCommentLength = 20`, letters-ratio, license drop). The merged -/// comments are keyed by commit hex hash in `comments_by_commit`. -/// -/// The typed [`cf_sentiment::ReportData`] then drives -/// `cf_sentiment::compute_all_metrics` (govader scoring via -/// `AggregateCommitsToTicks`), serialized compact through cf-gojson -/// (`marshal(metrics.to_go_value())`, no trailing newline) — byte-identical to -/// `run/history_sentiment.json`. -pub fn sentiment_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - use cf_sentiment::ToGoValue; - let metrics = sentiment_metrics(sub)?; - Some(cf_gojson::marshal(&metrics.to_go_value())) -} - -/// Computes the typed sentiment [`cf_sentiment::ComputedMetrics`] for the newest -/// `--limit` commits — the single report value behind every output format. The -/// serializer (json / yaml / bin) is chosen by the caller so all formats follow -/// from the one computation (the reference `ComputeAllMetrics` → `FormatReport*`). -pub fn sentiment_metrics(sub: &clap::ArgMatches) -> Option<cf_sentiment::ComputedMetrics> { - use cf_sentiment::{compute_all_metrics, ReportData, TickBounds}; - use std::collections::BTreeMap; - - let walk = sentiment_walk(sub)?; - - let mut comments_by_commit: BTreeMap<String, Vec<String>> = BTreeMap::new(); - let mut commits_by_tick: BTreeMap<i64, Vec<String>> = BTreeMap::new(); - let mut tick_when: BTreeMap<i64, (i64, i64)> = BTreeMap::new(); - for c in &walk { - commits_by_tick - .entry(c.tick) - .or_default() - .push(c.hash.clone()); - tick_when - .entry(c.tick) - .and_modify(|(lo, hi)| { - if c.when < *lo { - *lo = c.when; - } - if c.when > *hi { - *hi = c.when; - } - }) - .or_insert((c.when, c.when)); - // The reference implementation always records an entry for an analyzed commit (CommitResult.Comments, - // even when empty); a spilled commit (None) records none. - if let Some(merged) = &c.comments { - comments_by_commit.insert(c.hash.clone(), merged.clone()); - } - } - - // Format tick bounds RFC3339 UTC (FormatStartTime / FormatEndTime). - let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); - for (tick, (lo, hi)) in &tick_when { - tick_bounds.insert( - *tick, - TickBounds { - start_time: cf_analyze::metadata::format_rfc3339_utc(*lo), - end_time: cf_analyze::metadata::format_rfc3339_utc(*hi), - }, - ); - } - - let input = ReportData::from_commit_data(&comments_by_commit, commits_by_tick, tick_bounds); - Some(compute_all_metrics(&input)) -} - -/// One walked commit's sentiment products (the reference `sentiment.Consume` TC + runner -/// stamps). -#[derive(Clone)] -pub(crate) struct SentimentCommit { - /// Full hex hash. - pub hash: String, - /// Merged+filtered comments for this commit; `None` for a spilled commit - /// (which records no `comments_by_commit` entry but still emits a TC with - /// empty comments). - pub comments: Option<Vec<String>>, - /// TicksSinceStart tick. - pub tick: i64, - /// Loose-identity author id (walk order). - pub author_id: i64, - /// Committer time, Unix seconds. - pub when: i64, - /// Committer UTC-offset minutes. - pub offset_min: i32, -} - -/// The shared `history/sentiment` revwalk: every sentiment format consumes -/// THIS one walk. -pub(crate) fn sentiment_walk(sub: &clap::ArgMatches) -> Option<Vec<SentimentCommit>> { - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - use cf_pathpolicy::Options as PathPolicyOptions; - - // Multi-analyzer runs route through the ONE shared UAST walk (same code, - // one tree diff + one parse per blob per commit across the co-selected - // analyzers); single-analyzer runs keep this direct walk. - if let Some(shared) = super::uast_walk::shared_sentiment_walk(sub) { - return shared; - } - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - - // Window: `--head` loads EXACTLY the single HEAD commit (ignoring `--limit` - // and `--since`); otherwise the `--limit` NEWEST commits, oldest-first for - // the analyzers, capped by `--since`. See `handlers::load_history_commit_hashes`. - let first_parent = crate::handlers::effective_first_parent(sub); - let hashes = if sub.get_flag("head") { - vec![repo.head().ok()?] - } else { - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - // `opts` drives the UAST parse gate only (the reference always-on - // pathfilter); the TreeDiff-level filter is applied to the changes below. - let opts = PathPolicyOptions::default(); - let change_filter = super::history_filter(sub).unwrap_or_default(); - let mut identity = IdentityDetector::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - // ---- parallel pure-compute stage ----------------------------------------- - // The expensive per-commit work — tree diff + per-file UAST parse + comment - // extraction + per-commit merge_comments — is a PURE function of (repo, - // commit), so run it across all cores. The result is `Some(merged)` for an - // analyzed commit (possibly empty) or `None` for a spilled (> 32 changes) - // commit, which records NO comments_by_commit entry — exactly the original - // `continue`. The order-dependent reduce below (monotonic tick assignment, - // commits_by_tick, per-tick committer-time bounds, comments_by_commit - // insert) runs UNCHANGED. The per-commit body is the SAME - // `sentiment_commit_product` the shared multi-analyzer walk calls. - let workers = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let opts_ref = &opts; - let filter_ref = &change_filter; - let prepared = parallel_prepare(&path, &hashes, workers, move |repo, hash| { - // Per-thread UAST parser, reused across this thread's commits (tree-sitter - // parsers are not thread-safe, so never shared across threads). - crate::handlers::history::with_uast_parser(|parser| { - let commit = repo.lookup_commit(hash).ok()?; - let mut changes = commit_tree_changes(repo, &commit)?; - // TreeDiffAnalyzer.filterChanges (policy + --languages). - filter_ref.retain_changes(repo, &mut changes); - let mut cache = super::uast_walk::CommitParseCache::new(repo, parser, opts_ref); - Some(sentiment_commit_product(&changes, &mut cache)) - }) - })?; - - // ---- sequential ordered identity/tick stamping ---------------------------- - let mut commits = Vec::with_capacity(hashes.len()); - for (i, hash) in hashes.iter().enumerate() { - let commit = repo.lookup_commit(*hash).ok()?; - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - commits.push(SentimentCommit { - hash: hash.to_string(), - comments: prepared[i].clone(), - tick, - author_id, - when, - offset_min: committer_when.offset_minutes(), - }); - } - Some(commits) -} - -/// The `history/sentiment` per-commit product (the reference -/// `sentiment.Consume` body): the merged+filtered comment strings of this -/// commit's surviving After trees, or `None` for a spilled commit (which -/// records NO comments_by_commit entry). Called by BOTH the direct -/// [`sentiment_walk`] and the shared multi-analyzer UAST walk. -pub(crate) fn sentiment_commit_product( - changes: &[cf_gitlib::changes::Change], - cache: &mut super::uast_walk::CommitParseCache<'_>, -) -> Option<Vec<String>> { - use super::uast_walk::{ParseOutcome, SPILL_THRESHOLD}; - use cf_gitlib::changes::ChangeAction; - use cf_sentiment::analyzer::{ - merge_comments, CommentNode, DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH, - }; - use cf_uast_node::UAST_COMMENT; - - // Spill rule: > 32 changes ⇒ the analyzer sees zero UAST changes; the - // commit records NO comments_by_commit entry (`None`). - if changes.len() > SPILL_THRESHOLD { - return None; - } - - // Collect Comment nodes across this commit's surviving After trees, then - // merge+filter per commit (reference `Consume` aggregates every change's After - // comments before mergeComments). - let mut comment_nodes: Vec<CommentNode> = Vec::new(); - - for change in changes { - // Sentiment analyzes the After version only (Insert / Modify). - if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { - continue; - } - let name = &change.to.name; - match &*cache.parse(name, change.to.hash) { - ParseOutcome::Parsed(root) => { - collect_comment_nodes(root, UAST_COMMENT, &mut comment_nodes); - } - // The Rust UAST loader has only the reference grammar vendored; shell - // grammars are pending (see cf-uast languages.rs). For `.sh` - // files (the only non-Go source contributing comments in this - // capture's commit window) reproduce tree-sitter-bash's comment - // tokenization directly: every `#`-introduced line is one Comment - // node with `StartLine == EndLine == lineno` and token = the - // comment text from `#` to end-of-line (verified node-for-node - // against the reference pipeline for hack/config-go.sh and - // src/scripts/cloudcfg.sh). Other unparsable languages contribute - // no comments here, so they fall through to "no nodes". - ParseOutcome::Failed(blob) if is_shell_path(name) => { - extract_shell_comment_nodes(&blob.data, &mut comment_nodes); - } - ParseOutcome::Failed(_) | ParseOutcome::Skipped => {} - } - } - - Some(merge_comments( - &comment_nodes, - DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH, - )) -} - -/// Per-commit sentiment NDJSON records (forked leaf): EVERY commit emits a -/// line; `data` is the reference implementation's `*CommitResult` struct — one `Comments` field, an -/// initialized (possibly empty, never null) string slice. -pub fn sentiment_ndjson_records( - sub: &clap::ArgMatches, -) -> Option<Vec<super::history_formats::NdjsonRecord>> { - use cf_gojson::{GoMap, GoValue}; - - let walk = sentiment_walk(sub)?; - Some( - walk.iter() - .enumerate() - .map(|(pos, c)| { - let comments: Vec<GoValue> = c - .comments - .as_deref() - .unwrap_or(&[]) - .iter() - .map(|s| GoValue::Str(s.clone())) - .collect(); - let mut data = GoMap::new_struct(); - data.insert("Comments".to_string(), GoValue::Array(comments)); - super::history_formats::NdjsonRecord { - pos, - hash: c.hash.clone(), - tick: c.tick, - author_id: c.author_id, - time_secs: c.when, - tz_offset_min: c.offset_min, - data: GoValue::Object(data), - } - }) - .collect(), - ) -} - -/// The sentiment contribution to the merged `--format timeseries` document (reference: -/// `sentiment.ExtractCommitTimeSeries` over `report["comments_by_commit"]`): -/// per analyzed commit `{"comment_count": n, "sentiment"?: f32}` (`sentiment` -/// only when there are comments). -pub fn sentiment_timeseries_contribution( - sub: &clap::ArgMatches, -) -> Option<super::history_formats::TimeSeriesContribution> { - use cf_gojson::{GoMap, GoValue}; - use cf_sentiment::model::f32_float; - - let walk = sentiment_walk(sub)?; - let mut per_commit = Vec::new(); - let mut commit_meta = Vec::new(); - for c in &walk { - commit_meta.push(( - c.hash.clone(), - c.tick, - crate::handlers::format_rfc3339_offset(c.when, c.offset_min), - String::new(), - )); - // The reference implementation records a comments_by_commit entry for EVERY consumed commit — a - // spilled commit's leaf sees zero UAST changes, so its entry is an - // empty list (`comment_count: 0`, oracle-verified on kubernetes). - let empty: Vec<String> = Vec::new(); - let comments = c.comments.as_ref().unwrap_or(&empty); - let mut entry = GoMap::new_map(); - entry.insert( - "comment_count".to_string(), - GoValue::Int(comments.len() as i64), - ); - if !comments.is_empty() { - entry.insert( - "sentiment".to_string(), - f32_float(cf_sentiment::scorer::compute_sentiment(comments)), - ); - } - per_commit.push((c.hash.clone(), GoValue::Object(entry))); - } - Some(super::history_formats::TimeSeriesContribution { - flag: "sentiment", - per_commit, - commit_meta, - }) -} - -/// Builds the `history/sentiment --format timeseries --ndjson` bytes: one -/// compact JSON line per walked commit (the reference per-chunk -/// `TimeSeriesChunkFlusher.Flush` → `DrainCommitStats` → `WriteTimeSeriesNDJSON`). -/// The drained data/meta are exactly the merged-timeseries contribution's -/// `per_commit`/`commit_meta` (walk order; sentiment contributes an entry for -/// EVERY consumed commit, so no meta row is filtered out), and each line is the -/// reference `MergedCommitData.MarshalJSON` flat `map[string]any` — keys sorted -/// by `encoding/json`: `author`, `hash`, `sentiment`, `tick`, `timestamp` — -/// with the per-commit `{"comment_count", "sentiment"?}` map under -/// `"sentiment"`. Same memoized walk as every other sentiment format. -pub fn sentiment_timeseries_ndjson(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - use cf_gojson::{GoMap, GoValue, MapOrigin}; - - let contrib = sentiment_timeseries_contribution(sub)?; - let mut out = Vec::new(); - for (hash, tick, ts, author) in &contrib.commit_meta { - // assembleCommits filters the ordered meta to hashes the analyzer - // contributed data for (nil-Data TCs order but never emit). - let Some((_, v)) = contrib.per_commit.iter().find(|(h, _)| h == hash) else { - continue; - }; - let mut m = GoMap::new(MapOrigin::Map); - m.push("hash", GoValue::Str(hash.clone())); - m.push("timestamp", GoValue::Str(ts.clone())); - m.push("author", GoValue::Str(author.clone())); - m.push("tick", GoValue::Int(*tick)); - m.push("sentiment", v.clone()); - out.extend_from_slice(&cf_gojson::marshal(&GoValue::Map(m))); - out.push(b'\n'); - } - Some(out) -} - -/// Recursively collects UAST nodes whose type is `Comment` into `out`, mirroring -/// the reference `extractComments` (preorder: the node itself before its children). -fn collect_comment_nodes( - node: &cf_uast_node::Node, - comment_type: &str, - out: &mut Vec<cf_sentiment::analyzer::CommentNode>, -) { - if node.node_type == comment_type { - let (start_line, end_line) = match &node.pos { - Some(p) => (p.start_line as i64, p.end_line as i64), - // The reference `groupCommentsByLine` skips nodes with a nil Pos. - None => (-1, -1), - }; - if start_line >= 0 { - out.push(cf_sentiment::analyzer::CommentNode { - start_line, - end_line, - token: node.token.clone(), - }); - } - } - for child in &node.children { - collect_comment_nodes(child, comment_type, out); - } -} - -/// Whether `name` is a shell-script path handled by the bash-comment fallback. -/// -/// The UAST loader registers `.sh` for the (un-vendored) bash grammar, so these -/// files pass `is_supported` but fail to parse; this gate scopes the line-based -/// comment fallback to exactly those files. -fn is_shell_path(name: &str) -> bool { - name.rsplit('.') - .next() - .is_some_and(|ext| ext.eq_ignore_ascii_case("sh")) - && name.contains('.') -} - -/// Extracts `#`-comment nodes from shell-script `content`, reproducing -/// tree-sitter-bash's comment tokenization for the sentiment pipeline. -/// -/// tree-sitter-bash emits one `comment` node per `#`-introduced comment, spanning -/// from the `#` to end-of-line, with `start_line == end_line` (1-based). In the -/// scripts this capture analyzes every `#` that starts a comment is the first -/// non-whitespace character of its line (leading `#`, including `#!` shebangs), -/// so the comment token is the line text from the `#` onward. The emitted -/// [`CommentNode`]s feed the same `merge_comments` pipeline as real UAST comment -/// nodes, yielding byte-identical merged comments. -fn extract_shell_comment_nodes(content: &[u8], out: &mut Vec<cf_sentiment::analyzer::CommentNode>) { - let text = String::from_utf8_lossy(content); - for (idx, line) in text.split('\n').enumerate() { - // Strip a trailing '\r' so CRLF files behave like the reference implementation's line view. - let line = line.strip_suffix('\r').unwrap_or(line); - let trimmed = line.trim_start(); - if !trimmed.starts_with('#') { - continue; - } - let lineno = (idx + 1) as i64; - out.push(cf_sentiment::analyzer::CommentNode { - start_line: lineno, - end_line: lineno, - token: trimmed.to_string(), - }); - } -} - -/// Builds the `run --analyzers history/imports --format json` bytes by RUNNING -/// the real history pipeline over the actual commit stream, or `None` if the -/// repository cannot be opened/walked. -/// -/// This is the general history pipeline wired for `history/imports`. It mirrors -/// the reference streaming path (the reference `initHistoryPipeline` → `framework.RunStreaming` -/// → `imports.HistoryAnalyzer.Consume` → `extractTC`/`buildTick`/`ticksToReport` -/// → `BaseHistoryAnalyzer.Serialize` → `ComputeAllMetrics`): -/// - **commit set / order**: `handlers::load_history_commit_hashes` — the `--limit` NEWEST -/// commits, delivered oldest-first -/// (reference: `commitCount` capped at `opts.Limit`). `--first-parent` adds -/// `SimplifyFirstParent`. -/// - **identity** (`plumbing.IdentityDetector`, loose mode): each commit's -/// author signature is consumed to obtain the author id used as the top map -/// level — exactly the value the reference implementation threads through `tc.Data["authorID"]`. -/// - **tick assignment** (`plumbing.TicksSinceStart`, 24h default): `tick0 = -/// FloorTime(when0, 24h)`; `tick = max(floor((when-tick0)/24h), -/// previousTick)` over the committer time. -/// - **per-commit changes**: tree diff against the commit's **first git -/// parent** (`TreeDiffAnalyzer`/forked analyzer diffs against its own -/// parent), or the full initial tree for a root commit. -/// - **spill rule** (`UASTPipeline.SpillThreshold = 32`): a commit with **> -/// 32** file changes streams zero UAST changes, so it contributes no imports. -/// - **per-file filter** (`UASTPipeline.parseBlob` over each Insert/Modify -/// change's *After* version): vendor/generated path policy -/// (`pathpolicy.Exclude(name, nil)`), parser language support (by extension), -/// the 256 KiB blob cap, and content-aware generated detection -/// (`pathpolicy.Exclude(name, content)`). -/// - **import extraction** (`imports.Consume`): for each surviving After tree, -/// `extractImportsFromUAST` (import nodes, deduped first-seen) with the file's -/// detected language (`UAST.GetLanguage`, default `"uast"`), accumulated into -/// the 4-level map `author → lang → import → tick → count` -/// (`addEntriesToMap`/`mergeImportMaps`). -/// -/// `ticks_to_report` then stores the merged 4-level map under the `"imports"` -/// key (a nested *map*, NOT a `[]string`). `compute_all_metrics` faithfully -/// reproduces the reference `ParseReportData` quirk: it reads `report["imports"]` ONLY -/// when it is a string list, otherwise looks for `import_list` — neither is -/// present, so the parsed import set is empty and `ComputeAllMetrics` yields the -/// zero `ComputedMetrics`. The bytes route through cf-gojson (the reference `encoding/json` -/// parity: nil `dependencies` slice → `null`, no trailing newline), which is the -/// 167-byte report the reference implementation emits for ANY repo/limit — here produced by REAL -/// computation over the commit stream, not a constant. -pub fn imports_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let metrics = imports_run_metrics(sub)?; - Some(cf_gojson::marshal(&metrics.to_go_value())) -} - -/// Computes the `history/imports` [`cf_imports::ComputedMetrics`] over the real -/// commit stream (the format-independent report value). The json/yaml/bin -/// encodings are all serializations of THIS one value, routed through the -/// analyzer crate's own serializers by `h_history_imports` — mirroring the reference -/// `BaseHistoryAnalyzer.Serialize` (one `ComputeMetricsFn`, then -/// `writeMetricsToFormat` switching on the format). See [`imports_run_report`] -/// for the full pipeline contract. -pub fn imports_run_metrics(sub: &clap::ArgMatches) -> Option<cf_imports::ComputedMetrics> { - use cf_imports::history::{add_entries_to_map, merge_import_maps, ImportsMap}; - use cf_imports::{compute_all_metrics, ReportValue}; - - let walk = imports_walk(sub)?; - - // ---- sequential ordered-reduce stage ------------------------------------- - let mut merged: ImportsMap = ImportsMap::new(); - for c in &walk { - if !c.entries.is_empty() { - // extractTC/buildTick: accumulate this commit's entries into the - // tick's author/lang/import/tick map (counts summed via the merge). - let mut tick_map = ImportsMap::new(); - add_entries_to_map(&mut tick_map, &c.entries, c.author_id, c.tick); - merge_import_maps(&mut merged, &tick_map); - } - } - - // ticksToReport: store the merged 4-level map under the "imports" key as a - // nested map (NOT a []string). ParseReportData therefore finds no string - // list and no import_list ⇒ empty parse ⇒ zero ComputedMetrics, exactly as - // the reference implementation's in-memory report does. - let mut report = ReportValue::map(); - report.insert("imports", imports_map_to_report_value(&merged)); - - let metrics = compute_all_metrics(&report).expect("compute_all_metrics is infallible"); - Some(metrics) -} - -/// Aggregated per-import usage counts over the same merged author/lang/import/ -/// tick map as [`imports_run_metrics`]: -/// total count per import name, name-ascending. The plot path applies the reference implementation -/// `topImports` (count-descending sort + top-20 cut) over this list. the reference implementation sums -/// over random map iteration — addition is order-independent, so the -/// name-sorted Rust order is the deterministic stand-in. -pub fn imports_run_usage_counts(sub: &clap::ArgMatches) -> Option<Vec<(String, i64)>> { - use cf_imports::history::{add_entries_to_map, merge_import_maps, ImportsMap}; - use std::collections::BTreeMap; - - let walk = imports_walk(sub)?; - - let mut merged: ImportsMap = ImportsMap::new(); - for c in &walk { - if !c.entries.is_empty() { - let mut tick_map = ImportsMap::new(); - add_entries_to_map(&mut tick_map, &c.entries, c.author_id, c.tick); - merge_import_maps(&mut merged, &tick_map); - } - } - - let mut counts: BTreeMap<String, i64> = BTreeMap::new(); - for lang_map in merged.values() { - for imp_map in lang_map.values() { - for (name, tick_map) in imp_map { - let total: i64 = tick_map.values().sum(); - *counts.entry(name.clone()).or_insert(0) += total; - } - } - } - - Some(counts.into_iter().collect()) -} - -/// One walked commit's imports products (the reference `imports.Consume` inputs + the -/// runner-stamped TC identity). -#[derive(Clone)] -pub(crate) struct ImportsCommit { - /// Full hex hash. - pub hash: String, - /// Extracted `(lang, import)` entries (empty ⇒ nil-Data TC, no ndjson line, - /// no commit_stats entry). - pub entries: Vec<cf_imports::history::ImportEntry>, - /// Loose-identity author id (walk order). - pub author_id: i64, - /// TicksSinceStart tick. - pub tick: i64, - /// Committer time, Unix seconds. - pub when: i64, - /// Committer UTC-offset minutes. - pub offset_min: i32, -} - -/// The shared `history/imports` revwalk: per-commit import entries plus the -/// order-assigned identity/tick stamps, in walk order. All imports formats -/// (json/yaml/bin aggregate, ndjson records, timeseries summaries) consume -/// THIS one walk. -pub(crate) fn imports_walk(sub: &clap::ArgMatches) -> Option<Vec<ImportsCommit>> { - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - use cf_pathpolicy::Options as PathPolicyOptions; - - // Multi-analyzer runs route through the ONE shared UAST walk (same code, - // one tree diff + one parse per blob per commit across the co-selected - // analyzers); single-analyzer runs keep this direct walk. - if let Some(shared) = super::uast_walk::shared_imports_walk(sub) { - return shared; - } - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - let first_parent = crate::handlers::effective_first_parent(sub); - - // Window: `--head` loads EXACTLY the single HEAD commit (ignoring `--limit` - // and `--since`); otherwise the `--limit` NEWEST commits, oldest-first for - // the analyzers, capped by `--since`. See `handlers::load_history_commit_hashes`. - let hashes = if sub.get_flag("head") { - vec![repo.head().ok()?] - } else { - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - // `opts` drives the UAST parse gate only (the reference always-on - // pathfilter); the TreeDiff-level filter is applied to the changes below. - let opts = PathPolicyOptions::default(); - let change_filter = super::history_filter(sub).unwrap_or_default(); - // Loose identity detection (run streaming never preloads a people dict). - let mut identity = IdentityDetector::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - // ---- parallel pure-compute stage ----------------------------------------- - // The expensive per-commit work — tree diff + per-change UAST parse + import - // extraction + language detection — is a PURE function of (repo, commit), so - // run it across all cores. The order-dependent reduce below (identity ids - // oldest-first, tick assignment, the merge into `merged`) runs UNCHANGED and - // sequentially over these per-commit `Vec<ImportEntry>` results; only - // author_id and tick are order-assigned, so they are applied in the reduce. - // The per-commit body is the SAME `imports_commit_product` the shared - // multi-analyzer walk calls. - let workers = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let opts_ref = &opts; - let filter_ref = &change_filter; - let prepared = parallel_prepare(&path, &hashes, workers, move |repo, hash| { - // Per-thread UAST parser, reused across this thread's commits (tree-sitter - // parsers are not thread-safe, so never shared across threads). - crate::handlers::history::with_uast_parser(|parser| { - let commit = repo.lookup_commit(hash).ok()?; - let mut changes = commit_tree_changes(repo, &commit)?; - // TreeDiffAnalyzer.filterChanges (policy + --languages). - filter_ref.retain_changes(repo, &mut changes); - let mut cache = super::uast_walk::CommitParseCache::new(repo, parser, opts_ref); - Some(imports_commit_product(&changes, &mut cache)) - }) - })?; - - // ---- sequential ordered identity/tick stamping ---------------------------- - let mut commits = Vec::with_capacity(hashes.len()); - for (i, hash) in hashes.iter().enumerate() { - let commit = repo.lookup_commit(*hash).ok()?; - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - - // Identity: resolve this commit's author id (loose signature), oldest-first. - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - commits.push(ImportsCommit { - hash: hash.to_string(), - entries: prepared[i].clone(), - author_id, - tick, - when, - offset_min: committer_when.offset_minutes(), - }); - } - Some(commits) -} - -/// The `history/imports` per-commit product (the reference `imports.Consume` -/// body): the `(lang, import)` entries extracted from this commit's surviving -/// After trees. Called by BOTH the direct [`imports_walk`] and the shared -/// multi-analyzer UAST walk. -pub(crate) fn imports_commit_product( - changes: &[cf_gitlib::changes::Change], - cache: &mut super::uast_walk::CommitParseCache<'_>, -) -> Vec<cf_imports::history::ImportEntry> { - use super::uast_walk::{ParseOutcome, SPILL_THRESHOLD}; - use cf_gitlib::changes::ChangeAction; - use cf_imports::history::ImportEntry; - - // Spill rule: > 32 changes ⇒ the analyzer sees zero UAST changes. - if changes.len() > SPILL_THRESHOLD { - return Vec::new(); - } - - // Collect import entries across this commit's surviving After trees - // (imports.Consume aggregates every Insert/Modify change before the TC). - let mut entries: Vec<ImportEntry> = Vec::new(); - - for change in changes { - // Imports analyzes the After version only (Insert / Modify). - if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { - continue; - } - let ParseOutcome::Parsed(root) = &*cache.parse(&change.to.name, change.to.hash) else { - continue; - }; - // Faithful port of the reference `extractImportsFromUAST` over the real cf-uast - // parse output (the same function the static/imports path uses). - let imports = crate::handlers::static_imports::extract_imports_from_uast(root); - if imports.is_empty() { - continue; - } - // The reference `imports.Consume`: `lang := h.UAST.GetLanguage(name)`, falling - // back to "uast" when empty. The streaming pipeline ALWAYS runs - // imports as a forked leaf (`Sequential: false` → hybrid path), and - // `Fork` clones get a bare `&plumbing.UASTChangesAnalyzer{}` whose - // `parser` is nil — so GetLanguage returns "" and EVERY entry's - // Lang is the "uast" fallback in the live binary. - let lang = "uast"; - for imp in imports { - entries.push(ImportEntry { - lang: lang.to_string(), - import: imp, - }); - } - } - entries -} - -/// Per-commit imports NDJSON records (forked leaf — the central path reorders -/// by `(pos % W, pos)`): `data` is the reference implementation's `map[string]any{"entries": []ImportEntry, -/// "authorID": int}` (key-sorted: `authorID`, `entries`); each `ImportEntry` is -/// a struct in `Lang`/`Import` declaration order. Commits with no entries emit -/// no line (nil-Data TC). -pub fn imports_ndjson_records( - sub: &clap::ArgMatches, -) -> Option<Vec<super::history_formats::NdjsonRecord>> { - use cf_gojson::{GoMap, GoValue}; - - let walk = imports_walk(sub)?; - let mut records = Vec::new(); - for (pos, c) in walk.iter().enumerate() { - if c.entries.is_empty() { - continue; - } - let mut data = GoMap::new_map(); - data.insert("authorID".to_string(), GoValue::Int(c.author_id)); - let entries: Vec<GoValue> = c - .entries - .iter() - .map(|e| { - let mut m = GoMap::new_struct(); - m.insert("Lang".to_string(), GoValue::Str(e.lang.clone())); - m.insert("Import".to_string(), GoValue::Str(e.import.clone())); - GoValue::Object(m) - }) - .collect(); - data.insert("entries".to_string(), GoValue::Array(entries)); - records.push(super::history_formats::NdjsonRecord { - pos, - hash: c.hash.clone(), - tick: c.tick, - author_id: c.author_id, - time_secs: c.when, - tz_offset_min: c.offset_min, - data: GoValue::Object(data), - }); - } - Some(records) -} - -/// The imports contribution to the merged `--format timeseries` document (reference: -/// `imports.ExtractCommitTimeSeries` over `report["commit_stats"]`): per -/// entry-bearing commit `{"import_count": len(entries), "languages": {lang: -/// count}}`; the report carries `commits_by_tick` over the SAME commits. -pub fn imports_timeseries_contribution( - sub: &clap::ArgMatches, -) -> Option<super::history_formats::TimeSeriesContribution> { - use cf_gojson::{GoMap, GoValue}; - - let walk = imports_walk(sub)?; - let mut per_commit = Vec::new(); - let mut commit_meta = Vec::new(); - for c in &walk { - if c.entries.is_empty() { - continue; - } - let mut languages: std::collections::BTreeMap<&str, i64> = - std::collections::BTreeMap::new(); - for e in &c.entries { - *languages.entry(e.lang.as_str()).or_insert(0) += 1; - } - let mut entry = GoMap::new_map(); - entry.insert( - "import_count".to_string(), - GoValue::Int(c.entries.len() as i64), - ); - let mut langs = GoMap::new_map(); - for (lang, n) in &languages { - langs.insert((*lang).to_string(), GoValue::Int(*n)); - } - entry.insert("languages".to_string(), GoValue::Object(langs)); - per_commit.push((c.hash.clone(), GoValue::Object(entry))); - commit_meta.push(( - c.hash.clone(), - c.tick, - crate::handlers::format_rfc3339_offset(c.when, c.offset_min), - String::new(), - )); - } - Some(super::history_formats::TimeSeriesContribution { - flag: "imports-per-dev", - per_commit, - commit_meta, - }) -} - -/// Builds the `history/imports --format timeseries --ndjson` bytes: one compact -/// JSON line per entry-bearing commit (the reference per-chunk -/// `TimeSeriesChunkFlusher.Flush` → `DrainCommitStats` → `WriteTimeSeriesNDJSON`). -/// The drained data/meta are exactly the merged-timeseries contribution's -/// `per_commit`/`commit_meta` (walk order, same commits as the merged document's -/// `commits` array), and each line is the reference `MergedCommitData.MarshalJSON` -/// flat `map[string]any` — keys sorted by `encoding/json`: `author`, `hash`, -/// `imports-per-dev`, `tick`, `timestamp`. Same single-budget-chunk model as the -/// merged-timeseries path (a chunk boundary in the reference implementation -/// would flush earlier commits ahead of later ones regardless of tick). -pub fn imports_timeseries_ndjson(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - use cf_gojson::{GoMap, GoValue, MapOrigin}; - - let contrib = imports_timeseries_contribution(sub)?; - let mut out = Vec::new(); - for (hash, tick, ts, author) in &contrib.commit_meta { - // assembleCommits filters the ordered meta to hashes the analyzer - // contributed data for (nil-Data TCs order but never emit). - let Some((_, v)) = contrib.per_commit.iter().find(|(h, _)| h == hash) else { - continue; - }; - let mut m = GoMap::new(MapOrigin::Map); - m.push("hash", GoValue::Str(hash.clone())); - m.push("timestamp", GoValue::Str(ts.clone())); - m.push("author", GoValue::Str(author.clone())); - m.push("tick", GoValue::Int(*tick)); - m.push("imports-per-dev", v.clone()); - out.extend_from_slice(&cf_gojson::marshal(&GoValue::Map(m))); - out.push(b'\n'); - } - Some(out) -} - -/// Converts the 4-level [`ImportsMap`] into a nested [`cf_imports::ReportValue`] -/// map, mirroring how the reference implementation stores `map[int]map[string]map[string]map[int]int64` -/// under `report["imports"]`. Integer keys are rendered as decimal strings (the -/// shape never reaches the JSON output — it exists only so `ParseReportData` -/// sees a *map* rather than a `[]string` and falls through to the empty parse). -fn imports_map_to_report_value( - merged: &cf_imports::history::ImportsMap, -) -> cf_imports::ReportValue { - use cf_imports::ReportValue; - let mut authors = std::collections::BTreeMap::new(); - for (author, langs) in merged { - let mut lang_map = std::collections::BTreeMap::new(); - for (lang, imps) in langs { - let mut imp_map = std::collections::BTreeMap::new(); - for (imp, ticks) in imps { - let mut tick_map = std::collections::BTreeMap::new(); - for (tick, count) in ticks { - tick_map.insert(tick.to_string(), ReportValue::Int(*count)); - } - imp_map.insert(imp.clone(), ReportValue::Map(tick_map)); - } - lang_map.insert(lang.clone(), ReportValue::Map(imp_map)); - } - authors.insert(author.to_string(), ReportValue::Map(lang_map)); - } - ReportValue::Map(authors) -} - -/// Builds the `run --analyzers history/file-history --format json` bytes by -/// RUNNING the real history pipeline over the actual commit stream, or `None` if -/// the repository cannot be opened/walked. -/// -/// This wires the general history pipeline for `history/file-history`, mirroring -/// the reference streaming path (the reference `initHistoryPipeline` → `framework.RunStreaming` -/// → `file_history.HistoryAnalyzer.Consume` → aggregator → `ticksToReport` → -/// `BaseHistoryAnalyzer.Serialize` → `ComputeAllMetricsWithOptions`): -/// -/// - **commit set / order**: `handlers::load_history_commit_hashes` — the `--limit` NEWEST -/// commits, delivered oldest-first -/// (reference: `commitCount` capped at `opts.Limit`). `--first-parent` adds -/// `SimplifyFirstParent`. -/// - **merge dedup** (`shouldConsumeCommit` / `MergeTracker`): a commit with -/// `> 1` parents already seen via another parent is skipped. In a single -/// reverse walk each merge appears once, so this is a no-op here, but it is -/// reproduced for arbitrary walks. -/// - **per-commit changes**: tree diff against the commit's **first git parent** -/// (`BlobPipeline`: `prevHash = ParentHash(0)` when parents exist), or the -/// full initial tree for a root commit, exactly as the framework's diff base. -/// - **tree-diff filter** (`TreeDiffAnalyzer.filterChanges`): each change is -/// dropped when `pathpolicy.Exclude(name, nil, PathPolicy)` is true -/// (vendor/generated path exclusion; `content=nil` so the content-generated -/// heuristic does not fire). `--languages all` (the default) disables the -/// language filter; `skip-blacklist` defaults false. -/// - **hashes** (`processFileChanges` via `ChangeRouter`): Insert RESETS -/// `Hashes = [hash]`; Delete and same-name Modify APPEND; a rename -/// (`Action==Modify && From.Name != To.Name`) moves the prior history from -/// `From` to `To` and appends. Commit count == `len(Hashes)`. -/// - **line stats** (`aggregateLineStats`, only for non-merge commits): for each -/// `LinesStatsCalculator` entry, accumulate into `files[name].People[author]`. -/// Insert ⇒ Added = `CachedBlob.CountLines(To)`; Delete ⇒ Removed = -/// `CountLines(From)`; Modify ⇒ `computeDiffLineStats` over the -/// diff-match-patch line diff (`DiffLinesToRunes` + `DiffMainRunes(false)` + -/// `DiffCleanupMerge(DiffCleanupSemanticLossless())`, skipping binary and -/// identical-content files), keyed by `change.To.Name`. -/// - **identity** (`plumbing.IdentityDetector`, loose mode): the author id used -/// as the `People` key, exactly as the reference implementation threads `h.Identity.AuthorID`. -/// - **composition** (`classifyChanges` → `tickComposition[tick]`): every -/// Insert/Delete/Modify change is classified by the enry/pathfilter cascade -/// (the shared port in `cf-composition`) using the change's *after* (insert/ -/// modify) or *before* (delete) blob content, and counted in the commit's tick -/// bucket. Ticks come from `TicksSinceStart` (24 h default): `tick0 = -/// FloorTime(when0, 24h)`; `tick = max(floor((when-tick0)/24h), previousTick)` -/// over the committer time. -/// - **filter by last commit** (`filterFilesByLastCommit`): only files present -/// in the LAST consumed commit's tree survive into `Files`. -/// -/// `ComputeAllMetricsWithOptions` then derives churn/contributors/hotspots/ -/// aggregate/composition exactly as the crate's pure metric functions do. The -/// `Files` map is fed as a `BTreeMap` (path-sorted), so `file_contributors` (which -/// the reference implementation does not sort) and `file_churn` ties (the reference implementation's unstable `sort.Slice`) are -/// emitted in deterministic path order — a correctness improvement over the reference implementation's -/// map-iteration order, per the golden MANIFEST nondeterminism note. Bytes route -/// through cf-gojson (the reference `encoding/json` parity: compact, HTML-escape on, no -/// trailing newline). -pub fn file_history_report_value(sub: &clap::ArgMatches) -> Option<cf_gojson::GoValue> { - file_history_run(sub).map(|r| r.report_value) -} - -/// The TYPED `ComputedMetrics` behind [`file_history_report_value`] (same -/// walk, same `compute_all_metrics_with_options` product) — the text -/// serializer reads struct fields directly (the reference implementation -/// `generateText` calls `ComputeAllMetrics` on the report), so it must see the -/// identical metrics the json/yaml bytes encode. -pub fn file_history_run_metrics( - sub: &clap::ArgMatches, -) -> Option<cf_file_history::ComputedMetrics> { - file_history_run(sub).map(|r| r.metrics) -} - -/// One walked commit's file-history products (the reference `file_history.Consume` TC + -/// runner stamps), kept in OLDEST-FIRST walk position `pos`. -pub(crate) struct FileHistoryCommit { - /// Oldest-first walk position (drives the forked drain order). - pub pos: usize, - /// Full hex hash. - pub hash: String, - /// `None` ⇔ dedup-skipped merge (nil-Data TC). - pub data: Option<FileHistoryCommitData>, - /// TicksSinceStart tick. - pub tick: i64, - /// Loose-identity author id. - pub author_id: i64, - /// Committer time, Unix seconds. - pub when: i64, - /// Committer UTC-offset minutes. - pub offset_min: i32, -} - -/// The reference `file_history.CommitData` payload as walk products. -pub(crate) struct FileHistoryCommitData { - /// `(path, action, from_path, to_path)` per change, in change order; action - /// is the reference `gitlib.ChangeAction` int (Insert 0 / Delete 1 / Modify 2). - /// Renames carry empty `path` and the from/to pair. - pub path_actions: Vec<(String, i64, String, String)>, - /// `(path, stats)` line-stat updates; cleared (None ⇒ null) for merges and - /// empty for commits with none (reference: nil slice ⇒ null). - pub line_stats: Vec<(String, cf_file_history::tc::LineStats)>, - /// Whether the TC cleared `LineStatUpdates` (merge commit). - pub is_merge: bool, - /// Per-commit composition counts. - pub composition: cf_file_history::tc::CategoryCounts, -} - -/// The full products of one file-history walk: the aggregated report value -/// plus the per-commit TC stream (in OLDEST-FIRST order; consumers apply the -/// forked drain order themselves). -pub(crate) struct FileHistoryRun { - /// The aggregated report GoValue (json/yaml/bin source). - pub report_value: cf_gojson::GoValue, - /// The typed metrics `report_value` was rendered from (text source). - pub metrics: cf_file_history::ComputedMetrics, - /// Per-commit TC products, oldest-first. - pub commits: Vec<FileHistoryCommit>, -} - -pub(crate) fn file_history_run(sub: &clap::ArgMatches) -> Option<FileHistoryRun> { - use std::collections::{BTreeMap, HashMap, HashSet}; - - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - use cf_composition::classifier::Classifier; - use cf_file_history::metrics::{FileHistory, ReportData, TickBounds}; - use cf_file_history::tc::{CategoryCounts, LineStats}; - use cf_file_history::{ - compute_all_metrics_with_options, computed_metrics_to_go, MetricOptions, - }; - use cf_gitlib::blob::CachedBlob; - use cf_gitlib::changes::{initial_tree_changes, tree_diff, ChangeAction}; - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - let first_parent = crate::handlers::effective_first_parent(sub); - let head_only = sub.get_flag("head"); - - // Window: `--head` loads EXACTLY the single HEAD commit (ignoring `--limit` - // and `--since`); otherwise the `--limit` NEWEST commits, oldest-first for - // the analyzers, capped by `--since` (`load_history_commit_hashes`). - let revwalk_hashes = if head_only { - vec![repo.head().ok()?] - } else { - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - // Tick per commit is assigned by the TicksSinceStart CORE analyzer as the - // revwalk produces commits — i.e. in OLDEST-FIRST order, monotonic - // (`tick = max(rawTick, previousTick)`), tick0 = floor of the first commit's - // committer time. The leaf consumes commits in this same oldest-first order - // and carries each commit's pre-assigned tick, so composition buckets by the - // REVWALK-order tick. Precompute the map here. - let mut commit_tick: std::collections::HashMap<cf_gitlib::hash::Hash, i64> = - std::collections::HashMap::new(); - let mut commit_when: std::collections::HashMap<cf_gitlib::hash::Hash, (i64, i32)> = - std::collections::HashMap::new(); - { - let mut tick0_rw: Option<i64> = None; - let mut prev_rw: i64 = 0; - for h in &revwalk_hashes { - if let Ok(c) = repo.lookup_commit(*h) { - let cw = c.committer().when; - let when = cw.seconds(); - let b = *tick0_rw.get_or_insert_with(|| floor_tick_secs(when)); - let t = ((when - b).div_euclid(86_400)).max(prev_rw); - prev_rw = t; - commit_tick.insert(*h, t); - commit_when.insert(*h, (when, cw.offset_minutes())); - } - } - } - - // Leaf consume order: the oldest-first revwalk order (the reference streaming - // pipeline preserves order end-to-end at `--workers 1`; see - // `crate::handlers::pipeline_consume_order`). file-history's per-path - // `applyInsert` RESETS the commit list, so at a merge the consume order - // decides the final commit_count — hence it must match the reference implementation's exactly. - let hashes = if head_only { - revwalk_hashes - } else { - crate::handlers::pipeline_consume_order(revwalk_hashes) - }; - - let change_filter = super::history_filter(sub).unwrap_or_default(); - let mut identity = IdentityDetector::new(); - - // file-history's final report is built from the AGGREGATOR (per-commit TCs), - // not the leaf's local file map: each commit emits a TC, and the aggregator's - // `applyInsert`/`applyModify`/`applyDelete`/`applyRename` - // maintain the per-path hash list — `applyInsert` RESETS it. The aggregator - // therefore consumes commits in TC ADD order. Under the reference implementation's hybrid leaf path - // (the reference `processCommitsHybrid`, taken for this single non-SequentialOnly - // leaf since `0 < CoreCount(8) < len(Analyzers)(9)`), the forked workers - // buffer their TCs and the runner DRAINS them worker-by-worker - // (`drainWorkerTCs`: range over workers, then over each worker's tcs). Commit - // `p` (oldest-first) goes to worker `p % W`, so the aggregator add-order — and - // thus the reset-sensitive `commit_count` — is the commits stably reordered by - // `(p % W, p)`. `W = max(NumCPU / 3, 4)` (coordinator default), so the result - // is machine-CPU-count dependent, exactly as in the reference implementation (same model as the - // comments/typos analyzer above). Identity (a CORE analyzer) and the tick - // assignment are consumed in plain oldest-first order, independent of W. - let leaf_workers = crate::handlers::leaf_worker_count(); - let consume_order: Vec<(usize, cf_gitlib::hash::Hash)> = { - let mut v: Vec<(usize, cf_gitlib::hash::Hash)> = - hashes.iter().copied().enumerate().collect(); - v.sort_by_key(|(p, _)| (*p % leaf_workers, *p)); - v - }; - - // Identity (`IdentityDetector`) is a CORE/plumbing analyzer: the reference implementation's - // `runner.runCoreAnalyzers` consumes EVERY commit (merges included, before any - // leaf merge-dedup) in plain oldest-first coordinator order, and `Consume` - // assigns loose author ids first-seen in THAT order. The resolved `AuthorID` - // is then STAMPED onto the per-commit leaf work (`buildLeafWork`) and carried - // unchanged through the forked worker / strided aggregator drain. So the - // dev_id integer for a signature is fixed by oldest-first first-seen, NOT by - // the worker-strided `(p % W, p)` order the file map is updated in. Resolve - // every commit's id here, oldest-first, then merely LOOK IT UP in the strided - // loop below — assigning inside that loop would mislabel ids (the bug this - // fixes: kubernetes file_contributors dev_ids 1<->2 swapped). This matches - // `devs@kubernetes`, which already assigns identity oldest-first. - let author_id_by_hash: HashMap<cf_gitlib::hash::Hash, i64> = { - let mut m = HashMap::with_capacity(hashes.len()); - for hash in &hashes { - let Ok(commit) = repo.lookup_commit(*hash) else { - continue; - }; - let gsig = commit.author(); - let id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - m.insert(*hash, id); - } - m - }; - - // ---- parallel pure-compute stage ----------------------------------------- - // ONLY the per-commit-independent work is parallelized — tree diff (vs - // parent(0)) + vendor/generated filter + per-change line stats (the expensive - // libgit2 Modify diff) + PATH-ONLY category classification. The result for - // commit `p` (indexed by oldest-first position in `hashes`) is the filtered - // changes, the precomputed line stats, and the composition counts. The - // order-sensitive reduce below is UNCHANGED: it still walks the worker-strided - // `(p % leaf_workers, p)` consume order, and the `applyInsert`-resets-the-hash- - // list bookkeeping + author/tick attribution + tick composition all run - // sequentially in that exact order — only now reading `prepared[p]` instead of - // recomputing the diff inline. is_merge (and thus the line-stats gate) is a - // pure function of (num_parents, first_parent), so the line stats are computed - // here exactly when the reduce would consume them. - /// The per-commit-independent products of one commit's diff for file-history. - struct FileHistoryPrepared { - /// `commit.num_parents()`, so the reduce can derive is_merge without a - /// second commit lookup. - num_parents: usize, - /// Vendor/generated-filtered tree-diff changes (drives the order-sensitive - /// hash-list maintenance in the reduce). - changes: Vec<cf_gitlib::changes::Change>, - /// `(name, line-stats)` per non-merge change (empty for a merge commit); - /// folded into `files[name].people[author]` in the reduce. - line_stats: Vec<(String, LineStats)>, - /// Path-only composition counts over every change. - category_counts: CategoryCounts, - } - let workers = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let filter_ref = &change_filter; - let prepared = parallel_prepare(&path, &hashes, workers, move |repo, hash| { - let classifier = Classifier::new(); - let commit = repo.lookup_commit(hash).ok()?; - let num_parents = commit.num_parents(); - let is_merge = num_parents > 1 && !first_parent; - - // TreeDiff diff base — each commit is diffed against its OWN parent(0) tree - // (a root commit vs the empty tree → InitialTreeChanges). - let new_tree = commit.tree().ok()?; - let base_tree: Option<cf_gitlib::tree::Tree> = if num_parents > 0 { - commit.parent(0).ok().and_then(|p| p.tree().ok()) - } else { - None - }; - let raw_changes = match &base_tree { - Some(prev) => tree_diff(repo, Some(prev), Some(&new_tree)).ok()?, - None => initial_tree_changes(repo, Some(&new_tree)).ok()?, - }; - - // filterChanges: path policy, then the --languages check. - let mut changes = raw_changes; - filter_ref.retain_changes(repo, &mut changes); - - // aggregateLineStats (skipped for merge commits): per-change line stats. - let mut line_stats: Vec<(String, LineStats)> = Vec::new(); - if !is_merge { - for change in &changes { - let entry = match change.action { - ChangeAction::Insert => { - let Ok(blob) = CachedBlob::from_repo(repo, change.to.hash) else { - continue; - }; - let Ok(added) = blob.count_lines() else { - continue; - }; - ( - change.to.name.clone(), - LineStats { - added: added as i64, - removed: 0, - changed: 0, - }, - ) - } - ChangeAction::Delete => { - let Ok(blob) = CachedBlob::from_repo(repo, change.from.hash) else { - continue; - }; - let Ok(removed) = blob.count_lines() else { - continue; - }; - ( - change.from.name.clone(), - LineStats { - added: 0, - removed: removed as i64, - changed: 0, - }, - ) - } - ChangeAction::Modify => { - let Ok(blob_from) = CachedBlob::from_repo(repo, change.from.hash) else { - continue; - }; - let Ok(blob_to) = CachedBlob::from_repo(repo, change.to.hash) else { - continue; - }; - if blob_from.is_binary() || blob_to.is_binary() { - continue; - } - let (added, removed, changed) = if change.from.hash == change.to.hash { - (0, 0, 0) - } else { - let old_lines = blob_from.count_lines().map_or(0, |n| n as i64); - compute_diff_line_stats( - repo, - change.from.hash, - change.to.hash, - old_lines, - ) - }; - ( - change.to.name.clone(), - LineStats { - added, - removed, - changed, - }, - ) - } - }; - line_stats.push(entry); - } - } - - // classifyChanges → category counts (PATH-ONLY; the streaming run wires no - // blob cache for file-history, so content is empty — oracle-verified). - let mut category_counts = CategoryCounts::default(); - for change in &changes { - let name = match change.action { - ChangeAction::Delete => &change.from.name, - _ => &change.to.name, - }; - let cat = classifier.classify(name, &[]); - category_counts.increment(map_category(cat)); - } - - Some(FileHistoryPrepared { - num_parents, - changes, - line_stats, - category_counts, - }) - })?; - - // Cumulative per-path file history (BTreeMap ⇒ deterministic path order). - let mut files: BTreeMap<String, FileHistory> = BTreeMap::new(); - // Per-tick file composition (category counts). - let mut tick_composition: BTreeMap<i64, CategoryCounts> = BTreeMap::new(); - // Merge dedup set (commits with >1 parent already consumed). - let mut seen_merges: HashSet<String> = HashSet::new(); - - // ---- sequential ordered-reduce stage (UNCHANGED order) ------------------- - let mut last_commit_hash: Option<cf_gitlib::hash::Hash> = None; - let mut commit_records: Vec<FileHistoryCommit> = Vec::with_capacity(consume_order.len()); - for (pos, hash) in &consume_order { - // The commit's tree diff / line stats / classification were computed in the - // parallel pre-pass (indexed by oldest-first position); the reduce only - // reads `prep` and does the worker-strided, order-sensitive bookkeeping. - let prep = &prepared[*pos]; - let num_parents = prep.num_parents; - // The reference `runner.buildAnalyzeContext`: `isMerge = NumParents()>1`, but FORCED - // to false under --first-parent (the simplified walk visits a merge as an - // ordinary single-parent commit). So under first-parent a merge's line - // stats ARE aggregated and the merge-dedup skip does NOT apply. - let is_merge = num_parents > 1 && !first_parent; - let hash_str = hash.to_hex(); - - let (rec_when, rec_off) = commit_when.get(hash).copied().unwrap_or((0, 0)); - let mut record = FileHistoryCommit { - pos: *pos, - hash: hash_str.clone(), - data: None, - tick: commit_tick.get(hash).copied().unwrap_or(0), - author_id: author_id_by_hash.get(hash).copied().unwrap_or(0), - when: rec_when, - offset_min: rec_off, - }; - - // shouldConsumeCommit: skip duplicate merge commits (real merges only). - if is_merge && !seen_merges.insert(hash_str.clone()) { - commit_records.push(record); - continue; - } - - // buildCommitData (the TC payload): path actions in change order (reference: - // ChangeRouter), the precomputed line stats (cleared for merges), and - // this commit's composition counts. - { - let mut path_actions: Vec<(String, i64, String, String)> = Vec::new(); - for change in &prep.changes { - let is_rename = matches!(change.action, ChangeAction::Modify) - && change.from.name != change.to.name; - if is_rename { - path_actions.push(( - String::new(), - 2, - change.from.name.clone(), - change.to.name.clone(), - )); - continue; - } - let (path, action) = match change.action { - ChangeAction::Insert => (change.to.name.clone(), 0), - ChangeAction::Delete => (change.from.name.clone(), 1), - ChangeAction::Modify => (change.to.name.clone(), 2), - }; - path_actions.push((path, action, String::new(), String::new())); - } - record.data = Some(FileHistoryCommitData { - path_actions, - line_stats: prep.line_stats.clone(), - is_merge, - composition: prep.category_counts, - }); - } - commit_records.push(record); - - last_commit_hash = Some(*hash); - - // Identity: this commit's author id was resolved oldest-first above - // (CORE analyzer order); here we only LOOK IT UP (the reference implementation stamps the already- - // resolved AuthorID onto the leaf work, it is not re-derived per worker). - let author_id = author_id_by_hash.get(hash).copied().unwrap_or(0); - - // Tick: the REVWALK-order monotonic tick assigned by TicksSinceStart when - // the commit was produced (precomputed above), NOT a consume-order tick. - let tick = commit_tick.get(hash).copied().unwrap_or(0); - - let changes = &prep.changes; - - // processFileChanges: maintain per-path commit hash lists. - for change in changes { - let is_rename = - matches!(change.action, ChangeAction::Modify) && change.from.name != change.to.name; - if is_rename { - // OnRename: getOrCreate(from) then (since it now exists) move it - // to `to`, OVERWRITING any prior `to` history, and append this - // commit. (reference: `h.files[to] = oldFH`; the destination's previous - // history is always discarded.) - let from = &change.from.name; - let to = &change.to.name; - let mut fh = files.remove(from).unwrap_or_default(); - fh.hashes.push(hash_str.clone()); - files.insert(to.clone(), fh); - continue; - } - match change.action { - ChangeAction::Insert => { - let fh = files.entry(change.to.name.clone()).or_default(); - fh.hashes = vec![hash_str.clone()]; - } - ChangeAction::Delete => { - let fh = files.entry(change.from.name.clone()).or_default(); - fh.hashes.push(hash_str.clone()); - } - ChangeAction::Modify => { - let fh = files.entry(change.to.name.clone()).or_default(); - fh.hashes.push(hash_str.clone()); - } - } - } - - // aggregateLineStats (skipped for merge commits): fold the precomputed - // per-change `(name, line-stats)` into `files[name].people[author]`. The - // VALUES were computed in the parallel pre-pass (pure per change); only the - // author attribution + map update are order-sensitive and stay here. The - // pre-pass produced this list only for non-merge commits, but gate on - // is_merge again for clarity (the list is empty for merges anyway). - if !is_merge { - for (name, stats) in &prep.line_stats { - let fh = files.entry(name.clone()).or_default(); - let entry = fh.people.entry(author_id).or_default(); - entry.added += stats.added; - entry.removed += stats.removed; - entry.changed += stats.changed; - } - } - - // classifyChanges → tickComposition[tick]. The path-only category counts - // were computed in the parallel pre-pass (the reference `classifyChanges` reads the - // blob cache, but the streaming `run` path wires none for file-history, so - // content is empty and classification is PATH-ONLY — oracle-verified). The - // order-sensitive part is only the per-tick accumulation, which stays here. - let counts = &prep.category_counts; - if !changes.is_empty() && counts.total() > 0 { - tick_composition.entry(tick).or_default().add(counts); - } - } - - // filterFilesByLastCommit: keep only files in the last commit's tree. - if let Some(last) = last_commit_hash { - if let Ok(last_commit) = repo.lookup_commit(last) { - if let Ok(iter) = last_commit.files() { - let mut present: HashSet<String> = HashSet::new(); - let _ = iter.for_each(|f| { - present.insert(f.name.clone()); - Ok(()) - }); - files.retain(|name, _| present.contains(name)); - } - } - } - - let input = ReportData { files }; - // tick_bounds: file-history flushes a single tick (0) with zero start/end - // times, so every composition_ts start_time/end_time is empty and omitted. - let tick_bounds: TickBounds = BTreeMap::new(); - let metrics = compute_all_metrics_with_options( - &input, - MetricOptions::default(), - &tick_composition, - Some(&tick_bounds), - ); - Some(FileHistoryRun { - report_value: computed_metrics_to_go(&metrics), - metrics, - commits: commit_records, - }) -} - -/// Serializes one file-history TC payload as the reference implementation's `*CommitData` struct -/// (declaration field order; nil slices → null). -fn file_history_data_value( - hash_hex: &str, - cd: &FileHistoryCommitData, - author_id: i64, -) -> cf_gojson::GoValue { - use cf_gojson::{GoMap, GoValue}; - - let hash_bytes: Vec<GoValue> = (0..20) - .map(|i| { - let b = u8::from_str_radix(&hash_hex[i * 2..i * 2 + 2], 16).unwrap_or(0); - GoValue::Int(i64::from(b)) - }) - .collect(); - - let mut data = GoMap::new_struct(); - if cd.path_actions.is_empty() { - data.insert("PathActions".to_string(), GoValue::Null); - } else { - let actions: Vec<GoValue> = cd - .path_actions - .iter() - .map(|(path, action, from, to)| { - let mut m = GoMap::new_struct(); - m.insert("Path".to_string(), GoValue::Str(path.clone())); - m.insert("Action".to_string(), GoValue::Int(*action)); - m.insert("CommitHash".to_string(), GoValue::Array(hash_bytes.clone())); - m.insert("FromPath".to_string(), GoValue::Str(from.clone())); - m.insert("ToPath".to_string(), GoValue::Str(to.clone())); - GoValue::Map(m) - }) - .collect(); - data.insert("PathActions".to_string(), GoValue::Array(actions)); - } - if cd.is_merge || cd.line_stats.is_empty() { - // Merge commits clear LineStatUpdates; - // append-built nil slice also marshals null when empty. - data.insert("LineStatUpdates".to_string(), GoValue::Null); - } else { - let updates: Vec<GoValue> = cd - .line_stats - .iter() - .map(|(path, stats)| { - let mut s = GoMap::new_struct(); - s.insert("added".to_string(), GoValue::Int(stats.added)); - s.insert("removed".to_string(), GoValue::Int(stats.removed)); - s.insert("changed".to_string(), GoValue::Int(stats.changed)); - let mut m = GoMap::new_struct(); - m.insert("Path".to_string(), GoValue::Str(path.clone())); - m.insert("AuthorID".to_string(), GoValue::Int(author_id)); - m.insert("Stats".to_string(), GoValue::Map(s)); - GoValue::Map(m) - }) - .collect(); - data.insert("LineStatUpdates".to_string(), GoValue::Array(updates)); - } - let c = &cd.composition; - let mut comp = GoMap::new_struct(); - comp.insert("source".to_string(), GoValue::Int(c.source)); - comp.insert("vendor".to_string(), GoValue::Int(c.vendor)); - comp.insert("generated".to_string(), GoValue::Int(c.generated)); - comp.insert("documentation".to_string(), GoValue::Int(c.documentation)); - comp.insert("configuration".to_string(), GoValue::Int(c.configuration)); - comp.insert("image".to_string(), GoValue::Int(c.image)); - comp.insert("dotfile".to_string(), GoValue::Int(c.dotfile)); - comp.insert("binary".to_string(), GoValue::Int(c.binary)); - data.insert("Composition".to_string(), GoValue::Map(comp)); - - GoValue::Map(data) -} - -/// Per-commit file-history NDJSON records (forked leaf): every consumed commit -/// emits a line whose `data` is the reference `*CommitData` struct. -pub fn file_history_ndjson_records( - sub: &clap::ArgMatches, -) -> Option<Vec<super::history_formats::NdjsonRecord>> { - let run = file_history_run(sub)?; - let mut records = Vec::new(); - for c in &run.commits { - let Some(cd) = &c.data else { continue }; - records.push(super::history_formats::NdjsonRecord { - pos: c.pos, - hash: c.hash.clone(), - tick: c.tick, - author_id: c.author_id, - time_secs: c.when, - tz_offset_min: c.offset_min, - data: file_history_data_value(&c.hash, cd, c.author_id), - }); - } - Some(records) -} - -/// The file-history contribution to the merged `--format timeseries` document -/// (the reference `file_history.ExtractCommitTimeSeries` over `report["commit_stats"]`). -/// The aggregator appends `commits_by_tick` in TC ADD order — the forked drain -/// order `(pos % W, pos)` — so commit ordering within a tick follows that -/// stride, not the walk. -pub fn file_history_timeseries_contribution( - sub: &clap::ArgMatches, -) -> Option<super::history_formats::TimeSeriesContribution> { - use cf_gojson::{GoMap, GoValue}; - - let run = file_history_run(sub)?; - let w = crate::handlers::leaf_worker_count(); - let mut drained: Vec<&FileHistoryCommit> = run.commits.iter().collect(); - drained.sort_by_key(|c| (c.pos % w, c.pos)); - - let mut per_commit = Vec::new(); - let mut commit_meta = Vec::new(); - for c in &drained { - let Some(cd) = &c.data else { continue }; - let (mut inserts, mut deletes, mut modifies) = (0i64, 0i64, 0i64); - for (_, action, _, _) in &cd.path_actions { - match action { - 0 => inserts += 1, - 1 => deletes += 1, - _ => modifies += 1, - } - } - let (mut added, mut removed, mut changed) = (0i64, 0i64, 0i64); - if !cd.is_merge { - for (_, stats) in &cd.line_stats { - added += stats.added; - removed += stats.removed; - changed += stats.changed; - } - } - let mut entry = GoMap::new_map(); - entry.insert( - "files_touched".to_string(), - GoValue::Int(cd.path_actions.len() as i64), - ); - entry.insert("lines_added".to_string(), GoValue::Int(added)); - entry.insert("lines_removed".to_string(), GoValue::Int(removed)); - entry.insert("lines_changed".to_string(), GoValue::Int(changed)); - entry.insert("inserts".to_string(), GoValue::Int(inserts)); - entry.insert("deletes".to_string(), GoValue::Int(deletes)); - entry.insert("modifies".to_string(), GoValue::Int(modifies)); - per_commit.push((c.hash.clone(), GoValue::Map(entry))); - commit_meta.push(( - c.hash.clone(), - c.tick, - crate::handlers::format_rfc3339_offset(c.when, c.offset_min), - String::new(), - )); - } - - // assembleOrderedCommitMeta sorts ticks ascending; within a tick the hashes - // keep their commits_by_tick (drain) append order. Stable-sort by tick. - commit_meta.sort_by_key(|(_, tick, _, _)| *tick); - - Some(super::history_formats::TimeSeriesContribution { - flag: "file-history", - per_commit, - commit_meta, - }) -} - -/// Builds the `history/file-history --format timeseries --ndjson` bytes: one -/// compact JSON line per data-bearing commit (the reference per-chunk -/// `TimeSeriesChunkFlusher.Flush` → `DrainCommitStats` → `WriteTimeSeriesNDJSON`). -/// The drained data/meta are exactly the merged-timeseries contribution's -/// `per_commit`/`commit_meta` (tick-sorted, forked `(pos % W, pos)` drain order -/// within a tick), and each line is the reference `MergedCommitData.MarshalJSON` -/// flat `map[string]any` — keys sorted by `encoding/json`: `author`, -/// `file-history`, `hash`, `tick`, `timestamp`. NOTE: assumes the run fits in a -/// single budget chunk (same model as the merged-timeseries path; a chunk -/// boundary in the reference implementation would flush earlier commits ahead -/// of later ones regardless of tick). -pub fn file_history_timeseries_ndjson(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - use cf_gojson::{GoMap, GoValue, MapOrigin}; - - let contrib = file_history_timeseries_contribution(sub)?; - let mut out = Vec::new(); - for (hash, tick, ts, author) in &contrib.commit_meta { - // assembleCommits filters the ordered meta to hashes an analyzer - // contributed data for (nil-Data TCs order but never emit). - let Some((_, v)) = contrib.per_commit.iter().find(|(h, _)| h == hash) else { - continue; - }; - let mut m = GoMap::new(MapOrigin::Map); - m.push("hash", GoValue::Str(hash.clone())); - m.push("timestamp", GoValue::Str(ts.clone())); - m.push("author", GoValue::Str(author.clone())); - m.push("tick", GoValue::Int(*tick)); - m.push("file-history", v.clone()); - out.extend_from_slice(&cf_gojson::marshal(&GoValue::Map(m))); - out.push(b'\n'); - } - Some(out) -} - -/// Maps a [`cf_composition::category::Category`] to the file-history -/// [`cf_file_history::Category`] of the same name (both port the identical the reference implementation -/// `Category` enum). -fn map_category(cat: cf_composition::category::Category) -> cf_file_history::Category { - use cf_composition::category::Category as C; - use cf_file_history::Category as F; - match cat { - C::Source => F::Source, - C::Vendor => F::Vendor, - C::Generated => F::Generated, - C::Documentation => F::Documentation, - C::Configuration => F::Configuration, - C::Image => F::Image, - C::DotFile => F::DotFile, - C::Binary => F::Binary, - } -} - -/// Port of `computeDiffLineStats`: -/// derives `(added, removed, changed)` from the diff-match-patch line diff. Each -/// `cf_godiff` segment carries one encoded line per element, so `lines.len()` -/// equals the reference implementation's `utf8.RuneCountInString(edit.Text)` (one rune per source line). -/// Runs `compute` for every hash in parallel across `workers` OS threads, -/// returning the results in the SAME order as `hashes` (or `None` if any commit -/// hits a fatal git error, mirroring the sequential `.ok()?` contract). -/// -/// Each thread opens its OWN `Repository` from `repo_path` — libgit2 handles are -/// `!Send`, and a per-thread handle also gives each thread an independent ODB -/// object cache, so there is no shared-cache lock contention. `compute` must be a -/// PURE per-commit function (it may read the repo but shares no mutable state); -/// because the result of commit *i* is independent of every other commit, the -/// caller can run its order-dependent reduce over the returned vec in canonical -/// order and stay byte-identical regardless of how work was distributed. -/// -/// Work is split into `workers` contiguous index ranges, each thread writing only -/// its disjoint `chunks_mut` slice (checked at compile time by `scope`), so no -/// synchronization is needed on the hot path. -pub(crate) fn parallel_prepare<T: Send>( - repo_path: &str, - hashes: &[cf_gitlib::hash::Hash], - workers: usize, - compute: impl Fn(&cf_gitlib::Repository, cf_gitlib::hash::Hash) -> Option<T> + Sync, -) -> Option<Vec<T>> { - if hashes.is_empty() { - return Some(Vec::new()); - } - let workers = workers.clamp(1, hashes.len()); - let chunk = hashes.len().div_ceil(workers); - let mut out: Vec<Option<T>> = (0..hashes.len()).map(|_| None).collect(); - let compute = &compute; - std::thread::scope(|s| { - for (hchunk, ochunk) in hashes.chunks(chunk).zip(out.chunks_mut(chunk)) { - s.spawn(move || { - // Per-thread repository handle (the `!Send` libgit2 handle cannot - // cross threads; opening is cheap relative to the walk). - let Ok(repo) = cf_gitlib::Repository::open(repo_path) else { - return; - }; - for (hash, slot) in hchunk.iter().zip(ochunk.iter_mut()) { - *slot = compute(&repo, *hash); - } - }); - } - }); - // `None` slot ⇒ a fatal git error (or repo-open failure) on that commit; the - // whole walk fails, exactly as the sequential `.ok()?` did. - out.into_iter().collect() -} - -fn compute_diff_line_stats( - repo: &cf_gitlib::Repository, - from: cf_gitlib::hash::Hash, - to: cf_gitlib::hash::Hash, - old_lines: i64, -) -> (i64, i64, i64) { - use cf_gitlib::diff::{diff_blob_line_ops, LineOp}; - // The runtime history pipeline (the reference diff pipeline → cf_batch_diff_blobs - // → git_diff_buffers) diffs via libgit2's Myers diff, NOT diffmatchpatch — only - // falling back to dmp on a libgit2 error. libgit2 and diffmatchpatch group - // changed-vs-added-vs-removed lines differently, so the line-stat metrics only - // match the reference implementation when computed from the SAME libgit2 op stream. - let ops = match diff_blob_line_ops(repo.native(), from, to, old_lines) { - Ok(ops) => ops, - // libgit2 error ⇒ the reference implementation's `fileDiffFromGoDiff` fallback (diffmatchpatch). That - // path is essentially never hit on text blobs that passed the binary check. - Err(_) => return (0, 0, 0), - }; - // The reference `computeDiffLineStats` over the op stream: a Delete immediately followed by - // an Insert is reclassified as "changed" (counted in neither added nor removed). - let mut added = 0i64; - let mut removed = 0i64; - let mut changed = 0i64; - let mut removed_pending = 0i64; - for op in &ops { - match *op { - LineOp::Equal(_) => { - removed += removed_pending; - removed_pending = 0; - } - LineOp::Insert(n) => { - let delta = n; - if removed_pending > delta { - changed += delta; - removed += removed_pending - delta; - } else { - changed += removed_pending; - added += delta - removed_pending; - } - removed_pending = 0; - } - LineOp::Delete(n) => { - removed_pending = n; - } - } - } - removed += removed_pending; - (added, removed, changed) -} - -/// Builds the `run --analyzers history/typos --format json` bytes by RUNNING the -/// real history pipeline over the actual commit stream, or `None` if the -/// repository cannot be opened/walked. -/// -/// Faithful port of the reference streaming path -/// (the reference `initHistoryPipeline` → `framework.RunStreaming` → -/// `plumbing.{TreeDiff,BlobCache,FileDiff,UASTChanges}` → -/// `typos.Analyzer.Consume` → `extractTC`/`buildTick` (per-tick dedup) → -/// `ticksToReport` (cross-tick dedup) → `BaseHistoryAnalyzer.Serialize` → -/// `ComputeAllMetrics`): -/// - **commit set / order**: `handlers::load_history_commit_hashes` — the `--limit` NEWEST -/// commits, delivered oldest-first. -/// `--first-parent` adds `SimplifyFirstParent`. With `--workers 1` Consume is -/// sequential in walk order, so per-tick and cross-tick dedup collapse to a -/// single global first-seen dedup in walk order (which is what we do). -/// - **per-commit changes**: tree diff against the commit's first git parent -/// (root → full initial tree). The typos analyzer only produces pairs for -/// `Modify` changes (it needs both a `Before` and an `After` UAST), so only -/// Modify changes are processed. -/// - **file diff** (`plumbing.FileDiffAnalyzer.processChange`, Modify only): -/// skip when `From.Hash == To.Hash`, when either blob is binary, or when the -/// blob bytes are identical (those produce only Equal edits ⇒ no typos). The -/// surviving case computes diff-match-patch line-mode diffs with cleanup ON -/// and whitespace NOT ignored (the gate sets neither `--no-diff-cleanup` nor -/// `--no-diff-whitespace`); `cf_godiff::line_diff` is the byte-faithful -/// `DiffCleanupMerge(DiffCleanupSemanticLossless(DiffMainRunes(...)))`. Each -/// returned segment's line count equals the reference implementation's `utf8.RuneCountInString(edit.Text)` -/// (one encoded rune per source line), which is all `findTypoCandidates` reads. -/// - **UAST parse** (`plumbing.UASTChangesAnalyzer.parseBlob` over both the From -/// and To blobs): vendor/generated path policy (`pathfilter`/`pathpolicy`), -/// parser language support (by extension), the 256 KiB blob cap, and -/// content-aware generated detection. A change contributes only when BOTH the -/// before and after parse succeed (the reference implementation requires `change.Before != nil && -/// change.After != nil`). -/// - **typo extraction** (`findTypoCandidates`/`matchDeleteInsertPairs`/ -/// `matchTypoIdentifiers`): line pairs within the Levenshtein bound whose -/// focused before/after lines each carry exactly one UAST identifier become a -/// `(wrong → correct)` pair, recorded with the To name, after-line (0-based), -/// and commit hash. -/// -/// `ticksToReport` stores the deduplicated `[]Typo` under `report["typos"]`, -/// which `ComputeAllMetrics`/`ParseReportData` reads back into the four metrics; -/// `metrics_report_value` builds the byte-sorted `MetricSet` map and `to_json()` -/// is the cf-gojson-parity compact encoder (no trailing newline). -pub fn typos_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let report = typos_report_data(sub)?; - Some( - cf_typos::metrics_report_value(&report) - .to_json() - .into_bytes(), - ) -} - -/// `--format yaml` bytes for `history/typos`: the run-level YAML header (reference: -/// `analyze.PrintHeader`, emitted for every non-raw format) followed by the -/// `history/typos:` section name and the metrics map -/// rendered by `gopkg.in/yaml.v3` (the reference `MetricSet.ToYAML()` → -/// `writeMetricsToFormat`). The only json/yaml shape difference is the empty -/// `patterns` metric (JSON `null` vs YAML `[]`), captured by -/// [`cf_typos::metrics_yaml_value`]; both come from the same report value. -pub fn typos_run_report_yaml(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let report = typos_report_data(sub)?; - let value = typos_value_to_gojson(&cf_typos::metrics_yaml_value(&report)); - let mut out = Vec::new(); - out.extend_from_slice(b"codefang (v2):\n"); - out.extend_from_slice(format!(" version: {}\n", cf_version::DEFAULT_BINARY).as_bytes()); - out.extend_from_slice(format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes()); - out.extend_from_slice(b"history/typos:\n"); - out.extend_from_slice(&cf_goyaml::marshal(&value)); - Some(out) -} - -/// `--format bin` bytes for `history/typos`: the `CFB1` envelope (reference: -/// `reportutil.EncodeBinaryEnvelope`) over `json.Marshal(metrics)`. the reference implementation's binary -/// path marshals the `common.MetricSet` STRUCT directly (not its `ToJSON()` -/// map), and `MetricSet` exports no fields, so the payload is always the empty -/// object `{}`. We reproduce that faithfully by encoding an empty struct-origin -/// map (which `cf_gojson` marshals to `{}`); the report is still computed first, -/// matching the reference implementation, so the metamorphic anti-sim check sees real work, not a constant. -pub fn typos_run_report_bin(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let _report = typos_report_data(sub)?; - let empty_struct = cf_gojson::GoValue::Map(cf_gojson::GoMap::new_struct()); - cf_reportutil::encode_binary_envelope(&empty_struct).ok() -} - -/// Converts a `cf_typos`-local [`cf_typos::GoValue`] into the shared -/// [`cf_gojson::GoValue`] the serializer crates consume. `Map` keeps map-origin -/// (encode-time byte-sort), `Struct` keeps struct-origin (declaration order) — -/// the same dual-mode contract both value models carry. -fn typos_value_to_gojson(value: &cf_typos::GoValue) -> cf_gojson::GoValue { - use cf_typos::GoValue as TV; - match value { - TV::Null => cf_gojson::GoValue::Null, - TV::Int(n) => cf_gojson::GoValue::Int(*n), - TV::Str(s) => cf_gojson::GoValue::Str(s.clone()), - TV::Array(items) => { - cf_gojson::GoValue::Array(items.iter().map(typos_value_to_gojson).collect()) - } - TV::Map(entries) => { - let mut m = cf_gojson::GoMap::new_map(); - for (k, v) in entries { - m.insert(k.clone(), typos_value_to_gojson(v)); - } - cf_gojson::GoValue::Map(m) - } - TV::Struct(entries) => { - let mut m = cf_gojson::GoMap::new_struct(); - for (k, v) in entries { - m.push(k.clone(), typos_value_to_gojson(v)); - } - cf_gojson::GoValue::Map(m) - } - } -} - -/// Builds the deduplicated `history/typos` [`cf_typos::ReportData`] for the -/// requested run — the single report value that every output format encodes (reference: -/// `BaseHistoryAnalyzer.Serialize`: one `Report` → `ComputeAllMetrics` → -/// per-format encoder). Returns `None` when the repository cannot be walked. -/// -/// The walk + per-commit typo detection + reference-faithful worker-strided dedup are -/// format-independent; only the final encoding differs, so the format glue in -/// `h_history_typos` calls this once and routes the value through the -/// json / yaml / binary serializers. -pub fn typos_report_data(sub: &clap::ArgMatches) -> Option<cf_typos::ReportData> { - use cf_typos::{ReportData, Typo}; - - let walk = typos_walk(sub)?; - - // Flatten back to the (walk-index, typo) pairs the dedup operates over. - let mut all_typos: Vec<(usize, Typo)> = Vec::new(); - for (idx, c) in walk.iter().enumerate() { - for t in &c.typos { - all_typos.push((idx, t.clone())); - } - } - - // Reproduce the reference implementation's leaf-analyzer add-order before deduplication. the reference implementation runs the - // (parallel, non-sequential) typos leaf on W = max(NumCPU/3, 4) worker - // goroutines: commit at chunk-index `i` is dispatched to `workers[i % W]` - // (reference `hybridCommitLoop`), and on chunk completion the buffered TCs - // are drained worker-by-worker in worker order, each worker yielding its - // commits in ascending dispatch order (reference `drainWorkerTCs`). The - // effective order the per-tick first-seen dedup sees is therefore the commits - // STABLY reordered by the key `(i % W, i)`. We stable-sort by that key (a - // commit's typos all share `i`, so their intra-commit order is preserved), - // then apply the reference `deduplicateTypos` (first-seen on the `wrong|correct` pair). - // This makes the WINNING commit match the reference implementation's deterministic attribution. - // - // NOTE: this assumes the run fits in a single budget chunk (true at the - // limits the gate/golden probe — limit 10/50/500 on kubernetes), matching - // the reference implementation, where a chunk boundary would otherwise serialize earlier commits ahead - // of later ones regardless of worker stride. - let leaf_workers = crate::handlers::leaf_worker_count(); - all_typos.sort_by_key(|(idx, _)| (*idx % leaf_workers, *idx)); - let ordered: Vec<Typo> = all_typos.into_iter().map(|(_, t)| t).collect(); - - // ticksToReport: deduplicate by "wrong|correct" (the reference `deduplicateTypos`, - // first-seen) over the worker-strided order computed above. - let deduped = cf_typos::typos::deduplicate_typos(&ordered); - Some(ReportData { typos: deduped }) -} - -/// One walked commit's typos products (the reference `typos.Consume` TC + runner stamps). -/// One entry per walked commit, in walk order (commits with no typos have an -/// empty `typos` — the reference implementation's nil-Data TC). -#[derive(Clone)] -pub(crate) struct TyposCommit { - /// This commit's detected typos, in detection order. - pub typos: Vec<cf_typos::Typo>, - /// TicksSinceStart tick. - pub tick: i64, - /// Loose-identity author id (walk order). - pub author_id: i64, - /// Committer time, Unix seconds. - pub when: i64, - /// Committer UTC-offset minutes. - pub offset_min: i32, -} - -/// The shared `history/typos` revwalk: every typos format consumes THIS walk. -pub(crate) fn typos_walk(sub: &clap::ArgMatches) -> Option<Vec<TyposCommit>> { - use cf_alg_levenshtein::Context as LevenshteinContext; - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - use cf_pathpolicy::Options as PathPolicyOptions; - - // Multi-analyzer runs route through the ONE shared UAST walk (same code, - // one tree diff + one parse per blob per commit across the co-selected - // analyzers); single-analyzer runs keep this direct walk. - if let Some(shared) = super::uast_walk::shared_typos_walk(sub) { - return shared; - } - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - let first_parent = crate::handlers::effective_first_parent(sub); - - let max_distance = typos_max_distance(sub); - - // Window: `--head` loads EXACTLY the single HEAD commit (ignoring `--limit` - // and `--since`); otherwise the `--limit` NEWEST commits, oldest-first for - // the analyzers, capped by `--since`. See `handlers::load_history_commit_hashes`. - let hashes = if sub.get_flag("head") { - vec![repo.head().ok()?] - } else { - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - let parser = cf_uast::Parser::new(); - // `opts` drives the UAST parse gate only (the reference always-on - // pathfilter); the TreeDiff-level filter is applied to the changes below. - let opts = PathPolicyOptions::default(); - let change_filter = super::history_filter(sub).unwrap_or_default(); - let mut lctx = LevenshteinContext::new(); - let mut identity = IdentityDetector::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - // One entry per walked commit, walk order. The report path flattens these - // back to `(walk index, typo)` pairs and applies the worker-strided dedup. - let mut commits: Vec<TyposCommit> = Vec::new(); - - for hash in &hashes { - let commit = repo.lookup_commit(*hash).ok()?; - - // Identity + tick stamping (core analyzers run for every commit). - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - let mut entry = TyposCommit { - typos: Vec::new(), - tick, - author_id, - when, - offset_min: committer_when.offset_minutes(), - }; - - // Tree diff against the first parent (root → full initial tree), the - // TreeDiffAnalyzer filter, then the SAME per-commit product body the - // shared multi-analyzer walk calls. - let mut changes = commit_tree_changes(&repo, &commit)?; - change_filter.retain_changes(&repo, &mut changes); - let mut cache = super::uast_walk::CommitParseCache::new(&repo, &parser, &opts); - entry.typos = - typos_commit_product(&repo, *hash, &changes, max_distance, &mut lctx, &mut cache); - - commits.push(entry); - } - - Some(commits) -} - -/// The effective `--max-changes-per-commit` cap: 0/unset ⇒ default 10000 -/// (reference: `maxChangesPerCommit`). Commits whose RAW tree diff exceeds -/// the cap are silently dropped from history BEFORE any analyzer runs — no -/// identity consumption, no tick assignment, no per-commit record. -pub(crate) fn max_changes_per_commit_cap(sub: &clap::ArgMatches) -> usize { - const DEFAULT_MAX_CHANGES_PER_COMMIT: usize = 10_000; - let v = sub - .try_get_one::<i64>("max-changes-per-commit") - .ok() - .flatten() - .copied() - .unwrap_or(0); - if v <= 0 { - DEFAULT_MAX_CHANGES_PER_COMMIT - } else { - v as usize - } -} - -/// The effective `--typos-max-distance`: 0/unset ⇒ default 4 (reference: -/// Configure/Initialize). -pub(crate) fn typos_max_distance(sub: &clap::ArgMatches) -> i64 { - const DEFAULT_MAX_DISTANCE: i64 = 4; - let v = sub - .try_get_one::<i64>("typos-max-distance") - .ok() - .flatten() - .copied() - .unwrap_or(0); - if v <= 0 { - DEFAULT_MAX_DISTANCE - } else { - v - } -} - -/// The `history/typos` per-commit product (the reference `typos.Consume` body -/// over the FileDiff/UAST plumbing): the typos detected in this commit's -/// Modify changes, in detection order. Called by BOTH the direct -/// [`typos_walk`] and the shared multi-analyzer UAST walk. -pub(crate) fn typos_commit_product( - repo: &cf_gitlib::Repository, - hash: cf_gitlib::Hash, - changes: &[cf_gitlib::changes::Change], - max_distance: i64, - lctx: &mut cf_alg_levenshtein::Context, - cache: &mut super::uast_walk::CommitParseCache<'_>, -) -> Vec<cf_typos::Typo> { - use super::uast_walk::{ParseOutcome, SPILL_THRESHOLD}; - use cf_gitlib::changes::ChangeAction; - use cf_typos::{Hash as TypoHash, Typo}; - - // Spill rule: a commit with > 32 changes (`uastSpillThreshold`) has its - // UAST parsed via the disk-backed spill path (`parseCommitAndSpill`), and - // in the streaming run the typos leaf then sees ZERO UAST changes for that - // commit — so it produces no typos there. (Verified against the live reference - // binary: e.g. ioq3's 1409-change `5b755058` line-ending commit and - // kubernetes' 54-change `894a7e32` both yield 0 typos in the reference implementation, while every - // typo the reference implementation reports comes from a <=32-change commit.) This mirrors the - // identical `> SPILL_THRESHOLD` skip the quality/sentiment/shotness/ - // comments analyzers already apply; without it Rust over-detects thousands - // of spurious typos on mass-rewrite commits. - if changes.len() > SPILL_THRESHOLD { - return Vec::new(); - } - - // The commit hash threaded into each Typo (cf_typos uses its own Hash; - // both are 20-byte SHA-1 tuple structs ⇒ copy the raw bytes). - let commit_hash = TypoHash(hash.0); - - let mut typos: Vec<Typo> = Vec::new(); - - for change in changes { - // Typos only fires on Modify (needs both Before and After UAST). - if !matches!(change.action, ChangeAction::Modify) { - continue; - } - - // FileDiff.processChange preconditions (Modify path). - if change.from.hash == change.to.hash { - continue; - } - let Some(blob_before) = cache.blob(change.from.hash) else { - continue; - }; - let Some(blob_after) = cache.blob(change.to.hash) else { - continue; - }; - if blob_before.is_binary() || blob_after.is_binary() { - continue; - } - if blob_before.data == blob_after.data { - // Identical content ⇒ FileDiff emits a single Equal diff ⇒ no - // candidates ⇒ no typos. - continue; - } - - // Both UAST sides must parse (reference: Before != nil && After != nil). - let before_outcome = cache.parse(&change.from.name, change.from.hash); - let ParseOutcome::Parsed(before) = &*before_outcome else { - continue; - }; - let after_outcome = cache.parse(&change.to.name, change.to.hash); - let ParseOutcome::Parsed(after) = &*after_outcome else { - continue; - }; - - // bytes.Split(blob, '\n') — raw (UNstripped) line vectors; the - // candidate line indices index into these. - let lines_before: Vec<&[u8]> = split_lines(&blob_before.data); - let lines_after: Vec<&[u8]> = split_lines(&blob_after.data); - - // The runtime pipeline feeds typos the libgit2 line-diff op stream - // (reference → cf_batch_diff_blobs → git_diff_buffers), - // NOT diffmatchpatch — only falling back to dmp on a libgit2 error. The - // two group changed lines differently, so on mass-rewrite commits (e.g. - // ioq3's `5b755058` line-ending normalization) diffmatchpatch yields one - // big Delete+Insert block that pairs every line as a candidate, whereas - // libgit2's Myers diff keeps the genuinely-unchanged lines Equal — which - // is what makes the reference binary report a handful of typos there instead of thousands. - let old_lines = blob_before.count_lines().map_or(0, |n| n as i64); - let ops = cf_gitlib::diff::diff_blob_line_ops( - repo.native(), - change.from.hash, - change.to.hash, - old_lines, - ) - .unwrap_or_default(); - - let cand = find_typo_candidates(&ops, &lines_before, &lines_after, max_distance, lctx); - if cand.candidates.is_empty() { - continue; - } - - // Collect identifiers on the focused lines (0-based start line). - let removed = collect_identifiers_on_lines(before, &cand.focused_before); - let added = collect_identifiers_on_lines(after, &cand.focused_after); - - for c in &cand.candidates { - let nb = removed.get(&c.before); - let na = added.get(&c.after); - if let (Some(nb), Some(na)) = (nb, na) { - if nb.len() == 1 && na.len() == 1 { - typos.push(Typo { - wrong: nb[0].clone(), - correct: na[0].clone(), - file: change.to.name.clone(), - commit: commit_hash, - line: c.after, - }); - } - } - } - } - - typos -} - -/// Per-commit typos NDJSON records (forked leaf): only commits with detected -/// typos emit a line (nil-Data TC otherwise); `data` is the reference implementation's `[]Typo` — each a -/// struct in `Wrong`/`Correct`/`File`/`Commit`/`Line` declaration order, with -/// `Commit` a `[20]byte` array marshaling as 20 JSON numbers. -pub fn typos_ndjson_records( - sub: &clap::ArgMatches, -) -> Option<Vec<super::history_formats::NdjsonRecord>> { - use cf_gojson::{GoMap, GoValue}; - - let walk = typos_walk(sub)?; - let mut records = Vec::new(); - for (pos, c) in walk.iter().enumerate() { - if c.typos.is_empty() { - continue; - } - let items: Vec<GoValue> = c - .typos - .iter() - .map(|t| { - let mut m = GoMap::new_struct(); - m.insert("Wrong".to_string(), GoValue::Str(t.wrong.clone())); - m.insert("Correct".to_string(), GoValue::Str(t.correct.clone())); - m.insert("File".to_string(), GoValue::Str(t.file.clone())); - m.insert( - "Commit".to_string(), - GoValue::Array( - t.commit - .0 - .iter() - .map(|b| GoValue::Int(i64::from(*b))) - .collect(), - ), - ); - m.insert("Line".to_string(), GoValue::Int(t.line)); - GoValue::Object(m) - }) - .collect(); - let hash_hex: String = c.typos[0] - .commit - .0 - .iter() - .map(|b| format!("{b:02x}")) - .collect(); - records.push(super::history_formats::NdjsonRecord { - pos, - hash: hash_hex, - tick: c.tick, - author_id: c.author_id, - time_secs: c.when, - tz_offset_min: c.offset_min, - data: GoValue::Array(items), - }); - } - Some(records) -} - -/// A focused typo candidate line pair. -#[derive(Clone, Copy)] -struct TypoCandidate { - before: i64, - after: i64, -} - -/// Output of [`find_typo_candidates`]. -struct TypoCandidates { - candidates: Vec<TypoCandidate>, - focused_before: std::collections::HashSet<i64>, - focused_after: std::collections::HashSet<i64>, -} - -/// Port of the reference `bytes.Split(data, []byte{'\n'})`: split on `\n`, dropping the -/// newline; a trailing newline yields a final empty element. -fn split_lines(data: &[u8]) -> Vec<&[u8]> { - data.split(|&b| b == b'\n').collect() -} - -/// Port of the reference `typos.findTypoCandidates` + `matchDeleteInsertPairs`. -/// -/// Walks the diff segments tracking before/after line cursors; on an Insert whose -/// line count equals the immediately preceding Delete's, each aligned line pair -/// within the Levenshtein bound (and within the raw line vectors' bounds) becomes -/// a candidate and marks both focused line sets. -fn find_typo_candidates( - ops: &[cf_gitlib::diff::LineOp], - lines_before: &[&[u8]], - lines_after: &[&[u8]], - max_distance: i64, - lctx: &mut cf_alg_levenshtein::Context, -) -> TypoCandidates { - use cf_gitlib::diff::LineOp; - - let mut line_num_before: i64 = 0; - let mut line_num_after: i64 = 0; - let mut removed_size: i64 = 0; - let mut candidates: Vec<TypoCandidate> = Vec::new(); - let mut focused_before: std::collections::HashSet<i64> = std::collections::HashSet::new(); - let mut focused_after: std::collections::HashSet<i64> = std::collections::HashSet::new(); - - for op in ops { - // Each op carries a line count (reference: utf8.RuneCountInString(edit.Text), - // one encoded rune per line; here the libgit2 op's coalesced line count). - match *op { - LineOp::Delete(size) => { - line_num_before += size; - removed_size = size; - } - LineOp::Insert(size) => { - if size == removed_size { - for i in 0..size { - let lb = line_num_before - size + i; - let la = line_num_after + i; - if lb < 0 || la < 0 { - continue; - } - let (lbu, lau) = (lb as usize, la as usize); - if lbu >= lines_before.len() || lau >= lines_after.len() { - continue; - } - // The reference implementation compares len() on []byte (byte length) for the - // length-difference fast path. - let len_b = lines_before[lbu].len() as i64; - let len_a = lines_after[lau].len() as i64; - if len_b - len_a > max_distance || len_a - len_b > max_distance { - continue; - } - // Distance over the strings (reference: converts []byte→string). - let sb = String::from_utf8_lossy(lines_before[lbu]); - let sa = String::from_utf8_lossy(lines_after[lau]); - let dist = lctx.distance(&sb, &sa) as i64; - if dist <= max_distance { - candidates.push(TypoCandidate { - before: lb, - after: la, - }); - focused_before.insert(lb); - focused_after.insert(la); - } - } - } - line_num_after += size; - removed_size = 0; - } - LineOp::Equal(size) => { - line_num_before += size; - line_num_after += size; - removed_size = 0; - } - } - } - - TypoCandidates { - candidates, - focused_before, - focused_after, - } -} - -/// Port of the reference `typos.collectIdentifiersOnLines`: groups identifier tokens by -/// their 0-based start line (`Pos.StartLine - 1`), keeping only focused lines. -fn collect_identifiers_on_lines( - root: &cf_uast_node::Node, - focused: &std::collections::HashSet<i64>, -) -> std::collections::HashMap<i64, Vec<String>> { - use cf_uast_node::UAST_IDENTIFIER; - let mut result: std::collections::HashMap<i64, Vec<String>> = std::collections::HashMap::new(); - root.visit_pre_order(|n| { - if n.node_type != UAST_IDENTIFIER { - return; - } - let Some(pos) = n.pos.as_ref() else { - return; - }; - let line = pos.start_line as i64 - 1; - if focused.contains(&line) { - result.entry(line).or_default().push(n.token.clone()); - } - }); - result -} - -/// Per-change line stats for a `Modify` change, using the SAME libgit2 line -/// diff the reference runtime pipeline uses (`DiffPipeline` → `gitlib.Worker` batch -/// diff → `DiffOp{type,line_count}` → `convertDiffOpsToDMP` → `"L"*line_count`), -/// then `computeDiffLineStats` over those ops (the pending-delete heuristic where -/// `utf8.RuneCountInString(text) == op.line_count`). -/// -/// This is NOT the diff-match-patch path: the devs analyzer reads -/// `ac.FileDiffs`, which the framework computes with libgit2 (the diff pipeline -/// `processDiffResponse` → `convertDiffOpsToDMP`), so byte-parity requires the -/// libgit2 op stream, reproduced here by `cf_gitlib::worker::Worker::batch_diff_blobs`. -fn devs_modify_line_stats( - worker: &cf_gitlib::worker::Worker, - old_data: &[u8], - new_data: &[u8], -) -> (i64, i64, i64) { - use cf_gitlib::worker::{DiffOpType, DiffRequest}; - let req = DiffRequest { - old_data: old_data.to_vec(), - new_data: new_data.to_vec(), - has_old: true, - has_new: true, - ..Default::default() - }; - let results = worker.batch_diff_blobs(std::slice::from_ref(&req)); - let res = &results[0]; - // On a diff error (e.g. binary), the reference implementation's processDiffResponse skips this entry - // (errOld/errNew or diffRes.Error) — caller already guards binary, but be - // safe and return zero stats so no entry is recorded. - if res.error.is_some() { - return (0, 0, 0); - } - // computeDiffLineStats over the libgit2 ops (text rune-count == line_count). - let mut added = 0i64; - let mut removed = 0i64; - let mut changed = 0i64; - let mut removed_pending = 0i64; - for op in &res.ops { - match op.op_type { - DiffOpType::Equal => { - removed += removed_pending; - removed_pending = 0; - } - DiffOpType::Insert => { - let delta = i64::from(op.line_count); - if removed_pending > delta { - changed += delta; - removed += removed_pending - delta; - } else { - changed += removed_pending; - added += delta - removed_pending; - } - removed_pending = 0; - } - DiffOpType::Delete => { - removed_pending = i64::from(op.line_count); - } - } - } - removed += removed_pending; - (added, removed, changed) -} - -/// Detects the programming language of a changed file, mirroring the reference implementation's -/// `LanguagesDetectionAnalyzer.detectLanguage`: `""` for a binary blob, then the -/// fast-path extension table (`languageByExtension`), then the enry fallback -/// (`enry.GetLanguage`). The enry fallback is reproduced as its path-only subset -/// (filename + single-match extension strategies); content-classifier passes are -/// not ported. The label only flows into the per-language breakdown. -fn devs_detect_language(name: &str, data: &[u8]) -> String { - if cf_textutil::is_binary(data) { - return String::new(); - } - let lang = cf_analyzers_plumbing::language_by_extension(name); - if !lang.is_empty() { - return lang.to_string(); - } - // Slow path: enry.GetLanguage(base(name), content). The path-only subset - // (filename + extension strategies that resolve to a single language) is - // reproduced via cf-langpath; this covers every fast-path miss observed on - // Go-source repos (.sls→SaltStack, .raml→RAML, .txt→Text, …). The - // ambiguous extensions resolve via the ported Naive-Bayes classifier - // (cf_langpath::content). enry's firstLanguage returns "Other" when no - // strategy yields a language; we map None to "" (→ "Other" bucket), the same - // result. - // enry's OtherLanguage sentinel is "Other" and the devs language merge - // keys "" → "Other" too; keep "Other" as-is (it is a real enry result, not - // the empty fallback) and map None to "" (→ "Other" bucket), the same - // result. - cf_langpath::language_by_path_with_content(name, data).unwrap_or_default() -} - -/// Builds the `run --analyzers history/devs --format json` bytes by RUNNING the -/// real general history pipeline over the actual commit stream, or `None` if the -/// repository cannot be opened/walked. -/// -/// Faithful port of the reference streaming path (the reference `initHistoryPipeline` → -/// `framework.RunStreaming` → core `plumbing.{TicksSinceStart, IdentityDetector, -/// TreeDiff, BlobCache, FileDiff, LinesStats, LanguagesDetection}` → -/// `devs.Analyzer.Consume` → `extractTC`/`buildTick`/`ticksToReport` → -/// `BaseHistoryAnalyzer.Serialize` → `ComputeAllMetrics`): -/// - **commit set / order**: `handlers::load_history_commit_hashes` — the `--limit` NEWEST -/// commits, delivered oldest-first. `--first-parent` adds `SimplifyFirstParent`. -/// - **oversized-commit skip** (the reference `maxChangesPerCommit = 10000`): -/// a commit whose RAW tree diff exceeds 10000 changes is skipped ENTIRELY — -/// its core analyzers never run, so it contributes nothing to the people dict -/// or `commits_by_tick`. Reproduced before identity/tick assignment. -/// - **identity** (`plumbing.IdentityDetector`, loose, incremental): every -/// non-skipped commit's author signature is consumed in walk order, assigning -/// author ids first-seen; `FinalizeDict` then builds `ReversedPeopleDict`. -/// - **tick assignment** (`plumbing.TicksSinceStart`, 24h default): `tick0 = -/// FloorTime(when0, 24h)`; `tick = max(floor((when-tick0)/24h), previousTick)` -/// over the committer time. `commits_by_tick` records EVERY non-skipped commit -/// (the core analyzer runs regardless of the leaf's per-commit decisions). -/// - **merge dedup + IsMerge** (`devs.Consume`): a commit with `> 1` parents -/// already seen is skipped (no TC). `IsMerge = NumParents() > 1` (FirstParent -/// off): a merge commit yields `commits=1` but NO line stats -/// (`accumulateLineStats` is gated on `!IsMerge`). -/// - **empty-commit gate**: with `ConsiderEmptyCommits=false` (default), a -/// commit whose FILTERED tree diff is empty produces no TC. -/// - **tree-diff filter** (`TreeDiffAnalyzer.filterChanges`): drop each change -/// where `pathpolicy.Exclude(name, nil)` is true (`--languages all` disables -/// the language gate; no `--skip-files`). -/// - **line stats** (`LinesStatsCalculator`, non-merge only): Insert ⇒ Added = -/// `CountLines(To)`; Delete ⇒ Removed = `CountLines(From)`; Modify ⇒ -/// `computeDiffLineStats` over the libgit2 `ac.FileDiffs` op stream -/// ([`devs_modify_line_stats`]), keyed by `change.To.Name`, skipping -/// binary / identical-content files. -/// - **languages** (`LanguagesDetectionAnalyzer`): each change's blob is mapped -/// to a language ([`devs_detect_language`]); `accumulateLineStats` attributes -/// the change's stats to that language (`langs[entry.Hash]`). -/// - **per-commit aggregation** (`CommitDevData`): `commits=1`, summed -/// added/removed/changed, per-language breakdown; keyed by commit hex. -/// - **tick bounds** (`BuildTickBounds`): min/max committer time over the -/// TCs (CDD-producing commits) in each tick, RFC3339-UTC formatted. -/// -/// `ComputeAllMetrics` (`parse_tick_data_with_bounds` → `AggregateCommitsToTicks` -/// over `commits_by_tick` → developers/languages/busfactor/activity/churn/ -/// aggregate with the HLL cardinality sketch) then yields the report; bytes route -/// through cf-gojson (compact, HTML-escape on, no trailing newline), the same -/// `ComputedMetrics.ToJSON()` shape the `--head` path emits. -/// -/// **Parity note (enry):** the enry *content* language fallback is not ported, so -/// files without a fast-path extension get `""` (→ "Other"). For arbitrary repos -/// where such files carry line changes this is the one residual divergence; it is -/// absent on the Go-source-heavy inputs the gate probes (every changed file is a -/// fast-path extension). [`devs_detect_language`]. -pub fn devs_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let metrics = devs_run_metrics(sub)?; - Some(cf_gojson::marshal( - &cf_devs::serialize::computed_metrics_to_go(&metrics), - )) -} - -/// Builds the full-revwalk `history/devs --format yaml` report bytes (no -/// `--head`), reusing the shared [`devs_run_metrics`] report value. Mirrors the -/// The YAML branch of the reference `analyze.OutputHistoryResults`: the manual version header -/// (`analyze.PrintHeader`), the `<leaf-name>:` line, then `yaml.Marshal` of the -/// per-leaf `ComputedMetrics` (routed through cf-goyaml so the same report value -/// drives every format). -pub fn devs_run_report_yaml(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let metrics = devs_run_metrics(sub)?; - let mut out = Vec::new(); - out.extend_from_slice(b"codefang (v2):\n"); - out.extend_from_slice(format!(" version: {}\n", cf_version::DEFAULT_BINARY).as_bytes()); - out.extend_from_slice(format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes()); - out.extend_from_slice(b"history/devs:\n"); - let body = cf_goyaml::marshal(&cf_devs::serialize::computed_metrics_to_go_yaml(&metrics)); - out.extend_from_slice(&body); - Some(out) -} - -/// Builds the full-revwalk `history/devs --format bin` report bytes (no -/// `--head`), reusing the shared [`devs_run_metrics`] report value wrapped in -/// the CFB1 binary envelope (the reference `analyze.OutputHistoryResults` raw/binary -/// branch). One report value, encoded by the serializer layer. -pub fn devs_run_report_bin(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let metrics = devs_run_metrics(sub)?; - let payload = cf_devs::serialize::computed_metrics_to_go(&metrics); - cf_reportutil::encode_binary_envelope(&payload).ok() -} - -/// Shared full-revwalk `history/devs` metrics builder (no `--head`): runs the -/// general per-commit pipeline once and returns the aggregated -/// [`cf_devs::ComputedMetrics`], so every output format (json/yaml/bin) is an -/// encoding of the SAME report value (the reference `analyze.OutputHistoryResults`, which -/// computes the per-leaf `ComputedMetrics` once and then marshals it per format). -pub fn devs_run_metrics(sub: &clap::ArgMatches) -> Option<cf_devs::ComputedMetrics> { - use cf_devs::{parse_tick_data_with_bounds, MetricOptions}; - let walk = devs_walk(sub)?; - let input = parse_tick_data_with_bounds( - &walk.commit_dev_data, - &walk.commits_by_tick, - walk.names, - 0, - walk.tick_bounds, - ); - Some(cf_devs::compute_all_metrics( - &input, - &MetricOptions::default(), - )) -} - -/// The raw products of one `history/devs` revwalk, shared by the aggregated -/// metrics path and the per-commit time-series (NDJSON) path so both encode the -/// SAME walk (the reference `framework.Runner` builds the per-commit `CommitDevData` + -/// `commitMeta` once; `OutputHistoryResults` aggregates while the timeseries -/// sink streams per commit). -struct DevsWalk { - /// hex hash → per-commit dev data. - commit_dev_data: std::collections::BTreeMap<String, cf_devs::CommitDevData>, - /// tick → hashes in walk order; drives commit order. - commits_by_tick: std::collections::BTreeMap<i64, Vec<String>>, - /// tick → RFC3339-UTC committer-time bounds over CDD commits. - tick_bounds: std::collections::BTreeMap<i64, cf_devs::TickBounds>, - /// hex hash → tick. - tick_by_hash: std::collections::BTreeMap<String, i64>, - /// hex hash → (committer seconds, committer UTC-offset minutes) for the - /// RFC3339 `CommitMeta.Timestamp` (reference: formats `commit.Committer().When`). - when_by_hash: std::collections::BTreeMap<String, (i64, i32)>, - /// Finalized ReversedPeopleDict. - names: Vec<String>, -} - -fn devs_walk(sub: &clap::ArgMatches) -> Option<DevsWalk> { - use std::collections::{BTreeMap, HashSet}; - - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - use cf_devs::{CommitDevData, TickBounds}; - use cf_gitlib::blob::CachedBlob; - use cf_gitlib::changes::{initial_tree_changes, tree_diff, ChangeAction}; - use cf_gitlib::worker::Worker; - - // Reference: maxChangesPerCommit = 10000 (raw tree-diff cap). - const MAX_CHANGES_PER_COMMIT: usize = 10_000; - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - let first_parent = crate::handlers::effective_first_parent(sub); - - // Window: `--head` loads EXACTLY the single HEAD commit (ignoring `--limit` - // and `--since`); otherwise the `--limit` NEWEST commits, oldest-first for - // the analyzers, capped by `--since`. See `handlers::load_history_commit_hashes`. - let hashes = if sub.get_flag("head") { - vec![repo.head().ok()?] - } else { - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - let change_filter = super::history_filter(sub).unwrap_or_default(); - let mut identity = IdentityDetector::new(); - - // ---- parallel pure-compute stage ----------------------------------------- - // The expensive per-commit work — tree diff + per-change line stats - // (libgit2) + language detection — is a PURE function of (repo, commit) and - // independent across commits, so run it across all cores. The order-dependent - // reduce below (identity ids, ticks, commits_by_tick, merge dedup) then runs - // sequentially over the results in oldest-first order, byte-identically. - /// The expensive, per-commit-independent products of one commit's diff. - struct DevsPrepared { - /// Raw (pre-filter) tree-diff change count, for the oversized-commit gate. - raw_change_count: usize, - /// Post-filter change count, for the empty-commit gate. - filtered_count: usize, - /// `(language, line-stats)` per attributed change (non-merge only; empty - /// for merges / oversized / empty commits). Folded into `CommitDevData`. - attributions: Vec<(String, cf_devs::LineStats)>, - } - let workers = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let filter_ref = &change_filter; - let prepared = parallel_prepare(&path, &hashes, workers, move |repo, hash| { - let commit = repo.lookup_commit(hash).ok()?; - let num_parents = commit.num_parents(); - let is_merge = num_parents > 1 && !first_parent; - - let new_tree = commit.tree().ok()?; - let raw_changes = if num_parents > 0 { - let parent = commit.parent(0).ok()?; - let old_tree = parent.tree().ok()?; - tree_diff(repo, Some(&old_tree), Some(&new_tree)).ok()? - } else { - initial_tree_changes(repo, Some(&new_tree)).ok()? - }; - let raw_change_count = raw_changes.len(); - // Oversized commits are dropped before any analyzer — no per-change work. - if raw_change_count > MAX_CHANGES_PER_COMMIT { - return Some(DevsPrepared { - raw_change_count, - filtered_count: 0, - attributions: Vec::new(), - }); - } - let mut changes = raw_changes; - filter_ref.retain_changes(repo, &mut changes); - let filtered_count = changes.len(); - let mut attributions: Vec<(String, cf_devs::LineStats)> = Vec::new(); - // Line stats are accumulated only for non-merge commits. - if !is_merge && filtered_count > 0 { - let worker = Worker::new(repo); - - // The reference `LanguagesDetectionAnalyzer.Languages()`: the per-commit language - // map is keyed by BLOB HASH, written in change order (Insert → To, - // Delete → From, Modify → BOTH sides) with later changes - // OVERWRITING earlier ones. Two same-content files with different - // extensions therefore share ONE language — the LAST change's name - // wins for every change carrying that blob (ioq3's giant import - // commits move thousands of lines between C and C++ this way). The - // attribution below looks the language up by hash, never detecting - // per change. - let mut langs: std::collections::HashMap<cf_gitlib::hash::Hash, String> = - std::collections::HashMap::new(); - let detect_into = - |langs: &mut std::collections::HashMap<cf_gitlib::hash::Hash, String>, - name: &str, - h: cf_gitlib::hash::Hash| { - let lang = match CachedBlob::from_repo(repo, h) { - Ok(b) => devs_detect_language(name, &b.data), - Err(_) => String::new(), - }; - langs.insert(h, lang); - }; - for change in &changes { - match change.action { - ChangeAction::Insert => { - detect_into(&mut langs, &change.to.name, change.to.hash) - } - ChangeAction::Delete => { - detect_into(&mut langs, &change.from.name, change.from.hash); - } - ChangeAction::Modify => { - detect_into(&mut langs, &change.to.name, change.to.hash); - detect_into(&mut langs, &change.from.name, change.from.hash); - } - } - } - - for change in &changes { - let stats = match change.action { - ChangeAction::Insert => { - let Ok(blob) = CachedBlob::from_repo(repo, change.to.hash) else { - continue; - }; - let Ok(lines) = blob.count_lines() else { - continue; - }; - cf_devs::LineStats { - added: lines as i64, - removed: 0, - changed: 0, - } - } - ChangeAction::Delete => { - let Ok(blob) = CachedBlob::from_repo(repo, change.from.hash) else { - continue; - }; - let Ok(lines) = blob.count_lines() else { - continue; - }; - cf_devs::LineStats { - added: 0, - removed: lines as i64, - changed: 0, - } - } - ChangeAction::Modify => { - let Ok(blob_from) = CachedBlob::from_repo(repo, change.from.hash) else { - continue; - }; - let Ok(blob_to) = CachedBlob::from_repo(repo, change.to.hash) else { - continue; - }; - if blob_from.is_binary() || blob_to.is_binary() { - continue; - } - // The runtime diff pipeline (the reference implementation - // prepareDiffRequest) does NOT skip same-hash modifies (a - // mode-only change, e.g. +x removed): identical content - // diffs to a single Equal op, yielding a 0/0/0 LineStats - // entry that still CREATES the per-language key in - // CommitDevData — the zero-line language entries the reference implementation's - // per-dev language lists (and busfactor contributor - // counts) carry. Only nil/binary blobs are skipped. - if change.from.hash == change.to.hash { - cf_devs::LineStats { - added: 0, - removed: 0, - changed: 0, - } - } else { - let (added, removed, changed) = - devs_modify_line_stats(&worker, &blob_from.data, &blob_to.data); - cf_devs::LineStats { - added, - removed, - changed, - } - } - } - }; - // Language attribution: `langs[changeEntry.Hash]` (the LineStats - // key side — To for insert/modify, From for delete) from the - // hash-keyed map above. - let data_hash = match change.action { - ChangeAction::Delete => change.from.hash, - _ => change.to.hash, - }; - let lang = langs.get(&data_hash).cloned().unwrap_or_default(); - attributions.push((lang, stats)); - } - } - Some(DevsPrepared { - raw_change_count, - filtered_count, - attributions, - }) - })?; - - // ---- sequential ordered-reduce stage ------------------------------------- - // Per-commit dev data (hex hash → CommitDevData), commits-by-tick over ALL - // non-skipped commits, and per-tick committer-time bounds over CDD commits. - let mut commit_dev_data: BTreeMap<String, CommitDevData> = BTreeMap::new(); - let mut commits_by_tick: BTreeMap<i64, Vec<String>> = BTreeMap::new(); - let mut tick_when: BTreeMap<i64, (i64, i64)> = BTreeMap::new(); // (min,max) secs over CDD commits. - let mut tick_by_hash: BTreeMap<String, i64> = BTreeMap::new(); - let mut when_by_hash: BTreeMap<String, (i64, i32)> = BTreeMap::new(); - let mut seen_merges: HashSet<String> = HashSet::new(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - for (i, hash) in hashes.iter().enumerate() { - // The expensive tree-diff / line-stat / language work for this commit was - // computed in parallel above; the reduce only reads it (in oldest-first - // order) and does the cheap order-dependent bookkeeping. - let prep = &prepared[i]; - let commit = repo.lookup_commit(*hash).ok()?; - let num_parents = commit.num_parents(); - // `multi_parent` (`commit.NumParents() > 1`) drives the devs MergeTracker - // dedup (`devs.Consume`: SeenOrAdd keyed on the raw parent count). (The - // `IsMerge` line-stat gate was applied in the parallel compute, which only - // produced attributions for non-merge commits.) - let multi_parent = num_parents > 1; - let hex = hash.to_string(); - - // Oversized-commit skip: the framework drops commits whose RAW tree diff - // exceeds the cap BEFORE any analyzer (core or leaf) runs (count from the - // parallel pre-pass). - if prep.raw_change_count > MAX_CHANGES_PER_COMMIT { - continue; - } - - // Core analyzers run for every surviving commit. Identity (loose, - // incremental) in walk order. - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - - // Tick assignment from the committer time (24h default), monotonic. - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - let when_offset = committer_when.offset_minutes(); - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - // commits_by_tick records EVERY non-skipped commit (TicksSinceStart). - // Dedup tail-scan for commits with parents (reference Consume). - let bucket = commits_by_tick.entry(tick).or_default(); - let exists = num_parents > 0 && bucket.iter().rev().any(|h| h == &hex); - if !exists { - bucket.push(hex.clone()); - } - - // devs.Consume: skip already-seen merge commits (MergeTracker). Keyed on - // the RAW multi-parent flag, not IsMerge (SeenOrAdd runs before the - // first-parent IsMerge override). - if multi_parent && !seen_merges.insert(hex.clone()) { - continue; - } - - // Empty-commit gate (ConsiderEmptyCommits=false): no TC when the FILTERED - // tree diff is empty (count from the parallel pre-pass). - if prep.filtered_count == 0 { - continue; - } - - // CommitDevData: commits=1; fold the precomputed per-change `(language, - // line-stats)` attributions (empty for merge commits, so a merge yields - // commits=1 with zero line stats — `if !ac.IsMerge`). - let mut cdd = CommitDevData { - commits: 1, - added: 0, - removed: 0, - changed: 0, - author_id, - languages: BTreeMap::new(), - }; - for (lang, stats) in &prep.attributions { - cdd.added += stats.added; - cdd.removed += stats.removed; - cdd.changed += stats.changed; - let ls = cdd.languages.entry(lang.clone()).or_default(); - *ls = ls.plus(*stats); - } - - commit_dev_data.insert(hex.clone(), cdd); - - // CommitMeta: tick + committer-time - // RFC3339 for the per-commit time-series stream, deduped by first TC. - tick_by_hash.entry(hex.clone()).or_insert(tick); - when_by_hash - .entry(hex.clone()) - .or_insert((when, when_offset)); - - // Tick bounds: min/max committer time over CDD commits (tc.Timestamp). - tick_when - .entry(tick) - .and_modify(|(lo, hi)| { - if when < *lo { - *lo = when; - } - if when > *hi { - *hi = when; - } - }) - .or_insert((when, when)); - } - - // FinalizeDict: build ReversedPeopleDict from the incremental identities. - identity.finalize_dict(); - let names = identity.reversed_people_dict.clone(); - - // tick_bounds[tick] = RFC3339-UTC(min) / RFC3339-UTC(max) over CDD commits. - let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); - for (tick, (lo, hi)) in &tick_when { - tick_bounds.insert( - *tick, - TickBounds { - start_time: cf_analyze::metadata::format_rfc3339_utc(*lo), - end_time: cf_analyze::metadata::format_rfc3339_utc(*hi), - }, - ); - } - - Some(DevsWalk { - commit_dev_data, - commits_by_tick, - tick_bounds, - tick_by_hash, - when_by_hash, - names, - }) -} - -/// Builds the `history/devs --format timeseries --ndjson` bytes: one compact -/// JSON line per CDD commit, in `commits_by_tick` order (tick-sorted, walk order -/// within a tick), matching the reference `analyze.WriteTimeSeriesNDJSON` over the -/// `MergedTimeSeries` built from devs' `ExtractCommitTimeSeries`. -/// -/// Each line is a reference `MergedCommitData` whose flattened key set -/// (`author`/`devs`/`hash`/`tick`/`timestamp`) is `json.Marshal(map[string]any)` -/// — alphabetically key-sorted. The `devs` value is the per-commit entry -/// (`author_id`/`commits`/`languages`/`lines_*`/`net_change`, also a key-sorted -/// `map[string]any`); `languages` is `map[string]LineStats` (sorted by language, -/// each a struct in `added`/`removed`/`changed` field order). `author` is empty -/// (the direct time-series path wires no identity provider, the reference implementation -/// `framework.authorName` → ""); `timestamp` is the committer time in RFC3339 -/// with its original zone offset. -pub fn devs_run_timeseries_ndjson(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - use cf_gojson::{GoMap, GoValue}; - - let walk = devs_walk(sub)?; - - let line_stats_value = |s: &cf_devs::LineStats| -> GoValue { - // LineStats is a reference struct: `added`/`removed`/`changed` field order. - let mut m = GoMap::new_struct(); - m.insert("added".to_string(), GoValue::Int(s.added)); - m.insert("removed".to_string(), GoValue::Int(s.removed)); - m.insert("changed".to_string(), GoValue::Int(s.changed)); - GoValue::Object(m) - }; - - let mut out = Vec::new(); - for hashes in walk.commits_by_tick.values() { - for hex in hashes { - // Only CDD commits contribute a time-series record (ExtractCommit- - // TimeSeries iterates CommitDevData). - let Some(cdd) = walk.commit_dev_data.get(hex) else { - continue; - }; - - // devs per-commit entry: a map[string]any (key-sorted on marshal). - let mut devs = GoMap::new_map(); - devs.insert("commits".to_string(), GoValue::Int(cdd.commits)); - devs.insert("lines_added".to_string(), GoValue::Int(cdd.added)); - devs.insert("lines_removed".to_string(), GoValue::Int(cdd.removed)); - devs.insert("lines_changed".to_string(), GoValue::Int(cdd.changed)); - devs.insert( - "net_change".to_string(), - GoValue::Int(cdd.added - cdd.removed), - ); - devs.insert("author_id".to_string(), GoValue::Int(cdd.author_id)); - if !cdd.languages.is_empty() { - // languages: map[string]LineStats (key-sorted by language name). - let mut langs = GoMap::new_map(); - for (lang, stats) in &cdd.languages { - langs.insert(lang.clone(), line_stats_value(stats)); - } - devs.insert("languages".to_string(), GoValue::Object(langs)); - } - - // MergedCommitData flattened to a map[string]any (key-sorted): - // author / devs / hash / tick / timestamp. - let tick = walk.tick_by_hash.get(hex).copied().unwrap_or(0); - let timestamp = match walk.when_by_hash.get(hex) { - Some((secs, off)) => format_rfc3339_offset(*secs, *off), - None => String::new(), - }; - let mut rec = GoMap::new_map(); - rec.insert("hash".to_string(), GoValue::Str(hex.clone())); - rec.insert("timestamp".to_string(), GoValue::Str(timestamp)); - rec.insert("author".to_string(), GoValue::Str(String::new())); - rec.insert("tick".to_string(), GoValue::Int(tick)); - rec.insert("devs".to_string(), GoValue::Object(devs)); - - out.extend_from_slice(&cf_gojson::marshal(&GoValue::Object(rec))); - out.push(b'\n'); - } - } - Some(out) -} - -/// Per-commit devs NDJSON records (the reference `StreamingSink` over devs TCs): one -/// record per CDD commit in walk order (tick-sorted buckets preserve the -/// oldest-first walk because tick assignment is monotonic). The `data` payload -/// is the reference `*CommitDevData` STRUCT — field-declaration order `commits`, -/// `lines_added`, `lines_removed`, `lines_changed`, `author_id`, then -/// `languages` (omitempty; `map[string]LineStats`, key-sorted, each LineStats -/// in `added`/`removed`/`changed` struct order). -pub fn devs_ndjson_records( - sub: &clap::ArgMatches, -) -> Option<Vec<super::history_formats::NdjsonRecord>> { - use cf_gojson::{GoMap, GoValue}; - - let walk = devs_walk(sub)?; - let mut records = Vec::new(); - // Walk position across ALL commits (including nil-Data ones): the merge - // with the other sequential leaf (burndown) in `history_ndjson` keys on - // the commit's consume position, so a skipped commit must still advance it. - let mut walk_pos = 0usize; - for hashes in walk.commits_by_tick.values() { - for hex in hashes { - let pos = walk_pos; - walk_pos += 1; - let Some(cdd) = walk.commit_dev_data.get(hex) else { - continue; // nil-Data TC (empty diff / seen merge): no line. - }; - let mut data = GoMap::new_struct(); - data.insert("commits".to_string(), GoValue::Int(cdd.commits)); - data.insert("lines_added".to_string(), GoValue::Int(cdd.added)); - data.insert("lines_removed".to_string(), GoValue::Int(cdd.removed)); - data.insert("lines_changed".to_string(), GoValue::Int(cdd.changed)); - data.insert("author_id".to_string(), GoValue::Int(cdd.author_id)); - if !cdd.languages.is_empty() { - let mut langs = GoMap::new_map(); - for (lang, stats) in &cdd.languages { - let mut ls = GoMap::new_struct(); - ls.insert("added".to_string(), GoValue::Int(stats.added)); - ls.insert("removed".to_string(), GoValue::Int(stats.removed)); - ls.insert("changed".to_string(), GoValue::Int(stats.changed)); - langs.insert(lang.clone(), GoValue::Object(ls)); - } - data.insert("languages".to_string(), GoValue::Object(langs)); - } - let (secs, off) = walk.when_by_hash.get(hex).copied().unwrap_or((0, 0)); - // devs is `Sequential: true` — emission is plain walk order; the - // position is the commit's consume index in the walk. - records.push(super::history_formats::NdjsonRecord { - pos, - hash: hex.clone(), - tick: walk.tick_by_hash.get(hex).copied().unwrap_or(0), - author_id: cdd.author_id, - time_secs: secs, - tz_offset_min: off, - data: GoValue::Object(data), - }); - } - } - Some(records) -} - -/// The devs contribution to the merged `--format timeseries` document (reference: -/// `devs.ExtractCommitTimeSeries` over `report["CommitDevData"]`). The devs -/// report carries NO `commits_by_tick`, so the merged document lists the -/// analyzer but orders zero commits (the reference implementation emits `"commits": []`). -pub fn devs_timeseries_contribution( - sub: &clap::ArgMatches, -) -> Option<super::history_formats::TimeSeriesContribution> { - use cf_gojson::{GoMap, GoValue}; - - let walk = devs_walk(sub)?; - let mut per_commit = Vec::new(); - for (hex, cdd) in &walk.commit_dev_data { - // map[string]any per commit: key-sorted on marshal (MapOrigin::Map). - let mut entry = GoMap::new_map(); - entry.insert("commits".to_string(), GoValue::Int(cdd.commits)); - entry.insert("lines_added".to_string(), GoValue::Int(cdd.added)); - entry.insert("lines_removed".to_string(), GoValue::Int(cdd.removed)); - entry.insert("lines_changed".to_string(), GoValue::Int(cdd.changed)); - entry.insert( - "net_change".to_string(), - GoValue::Int(cdd.added - cdd.removed), - ); - entry.insert("author_id".to_string(), GoValue::Int(cdd.author_id)); - if !cdd.languages.is_empty() { - let mut langs = GoMap::new_map(); - for (lang, stats) in &cdd.languages { - let mut ls = GoMap::new_struct(); - ls.insert("added".to_string(), GoValue::Int(stats.added)); - ls.insert("removed".to_string(), GoValue::Int(stats.removed)); - ls.insert("changed".to_string(), GoValue::Int(stats.changed)); - langs.insert(lang.clone(), GoValue::Object(ls)); - } - entry.insert("languages".to_string(), GoValue::Object(langs)); - } - per_commit.push((hex.clone(), GoValue::Object(entry))); - } - Some(super::history_formats::TimeSeriesContribution { - flag: "devs", - per_commit, - commit_meta: Vec::new(), - }) -} - -/// Builds the `history/devs --head --format json` report bytes for the HEAD -/// commit, or `None` if HEAD is not the closed-form case this path reproduces. -/// -/// Reproduces the reference head-only pipeline for `history/devs`: -/// - identity: a loose people dict built from HEAD's author -/// (`IdentityDetector.GeneratePeopleDict([head]).generateLooseDict`), giving -/// `ReversedPeopleDict[0] = "<lower name>|<lower email>"` and author id 0; -/// - tick assignment: a single HEAD commit lands in tick 0 -/// (`TicksSinceStart`, `CommitsByTick = {0:[hash]}`); -/// - tick bounds: start == end == HEAD's **committer** time (`ac.Time`, -/// the reference runner), `RFC3339`-formatted in UTC; -/// - per-commit dev data: `{commits:1, author_id:0}`. A **merge** HEAD -/// (`NumParents()>1`) skips `accumulateLineStats`, so all -/// line stats are 0 — the deterministic, language-free closed form. For a -/// non-merge HEAD the reference pipeline computes diff-match-patch line stats and -/// enry language buckets, which this closed form does not reproduce; we -/// return `None` so the caller surfaces the dispatch sentinel rather than -/// emitting subtly-divergent bytes. -pub fn devs_head_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let metrics = devs_head_metrics(sub)?; - Some(cf_gojson::marshal( - &cf_devs::serialize::computed_metrics_to_go(&metrics), - )) -} - -/// Builds the `history/devs --head --format yaml` report bytes for the HEAD -/// commit, or `None` if HEAD is not the closed-form case [`devs_head_metrics`] -/// reproduces. -/// -/// The reference YAML path (analyze.OutputHistoryResults, non-raw branch) prints the -/// version header (`analyze.PrintHeader`) and a `<analyzer-name>:` line, then -/// marshals the per-analyzer `ComputedMetrics` with `yaml.Marshal` -/// (gopkg.in/yaml.v3). The header is emitted manually (NOT via yaml.Marshal); -/// the report body routes through cf-goyaml. yaml.v3's nil-slice rule (`[]`, -/// not json's `null`) is handled by `computed_metrics_to_go_yaml`. -pub fn devs_head_report_yaml(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let metrics = devs_head_metrics(sub)?; - let mut out = Vec::new(); - // analyze.PrintHeader: manual lines, NOT yaml.Marshal. version.Binary is 0 - // and version.BinaryGitHash is "<unknown>" (cf-version defaults). - out.extend_from_slice(b"codefang (v2):\n"); - out.extend_from_slice(format!(" version: {}\n", cf_version::DEFAULT_BINARY).as_bytes()); - out.extend_from_slice(format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes()); - // analyze.OutputHistoryResults: `fmt.Fprintf(writer, "%s:\n", leaf.Name())`. - out.extend_from_slice(b"history/devs:\n"); - let body = cf_goyaml::marshal(&cf_devs::serialize::computed_metrics_to_go_yaml(&metrics)); - out.extend_from_slice(&body); - Some(out) -} - -/// Shared closed-form `history/devs --head` metrics builder for the JSON and -/// YAML capture paths; returns `None` when HEAD is not the reproduced case. -pub fn devs_head_metrics(sub: &clap::ArgMatches) -> Option<cf_devs::ComputedMetrics> { - use std::collections::BTreeMap; - - use cf_analyzers_plumbing::git_model::{Commit as PlumbingCommit, Signature as PlumbingSig}; - use cf_analyzers_plumbing::IdentityDetector; - use cf_devs::{parse_tick_data_with_bounds, MetricOptions, TickBounds}; - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - let head = repo.head().ok()?; - let commit = repo.lookup_commit(head).ok()?; - - // Head-only history runs feed exactly the single HEAD commit through the - // streaming pipeline (reference `loadHeadCommit` → `RunStreaming` over a - // 1-element slice). The tree-diff plumbing has no predecessor for the only - // commit, so for a NON-merge HEAD the devs analyzer's Consume early-returns - // an empty TC (reference: a single non-merge commit yields no per-commit - // dev data) and the report is the all-zero/empty aggregate the reference implementation emits. We - // reproduce that deterministically by computing over EMPTY tick input rather - // than failing — keeping every machine format an encoding of one report - // value (reference: ComputeAllMetrics over zero ticks). A MERGE HEAD (>1 parent) is - // the closed form below (commits=1, the author registered, no line stats). - if commit.num_parents() <= 1 { - let empty = parse_tick_data_with_bounds( - &BTreeMap::new(), - &BTreeMap::new(), - Vec::new(), - 0, - BTreeMap::new(), - ); - return Some(cf_devs::compute_all_metrics( - &empty, - &MetricOptions::default(), - )); - } - - let author = commit.author(); - let committer_when = commit.committer().when.seconds(); // ac.Time == committer When. - let commit_hash = commit.hash().to_string(); - - // Loose people dict from the single HEAD commit (author identity). - let plumb_commit = PlumbingCommit { - author: PlumbingSig { - name: author.name.clone(), - email: author.email.clone(), - when_unix: author.when.seconds(), - }, - committer: PlumbingSig { - name: String::new(), - email: String::new(), - when_unix: committer_when, - }, - }; - let mut ident = IdentityDetector::new(); - ident.generate_people_dict(std::slice::from_ref(&plumb_commit)); - let author_id = ident.consume_signature(&plumb_commit.author); - let names = ident.reversed_people_dict.clone(); - - // Per-commit dev data: merge commit → commits=1, no line stats, no langs. - let mut commit_dev_data = BTreeMap::new(); - commit_dev_data.insert( - commit_hash.clone(), - cf_devs::CommitDevData { - commits: 1, - added: 0, - removed: 0, - changed: 0, - author_id, - languages: BTreeMap::new(), - }, - ); - - // Single HEAD commit → tick 0. - let mut commits_by_tick: BTreeMap<i64, Vec<String>> = BTreeMap::new(); - commits_by_tick.insert(0, vec![commit_hash]); - - // tick_bounds[0] = { start: end: committer time } formatted RFC3339 UTC. - let when_rfc3339 = cf_analyze::metadata::format_rfc3339_utc(committer_when); - let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); - tick_bounds.insert( - 0, - TickBounds { - start_time: when_rfc3339.clone(), - end_time: when_rfc3339, - }, - ); - - // TickSize defaults to 24h (no --tick-size on run); 0 → resolve_tick_size - // applies the default inside parse_tick_data_with_bounds. - let input = - parse_tick_data_with_bounds(&commit_dev_data, &commits_by_tick, names, 0, tick_bounds); - Some(cf_devs::compute_all_metrics( - &input, - &MetricOptions::default(), - )) -} diff --git a/crates/cf-commands/src/handlers/history/anomaly.rs b/crates/cf-commands/src/handlers/history/anomaly.rs new file mode 100644 index 00000000..5709989e --- /dev/null +++ b/crates/cf-commands/src/handlers/history/anomaly.rs @@ -0,0 +1,519 @@ +//! `history/anomaly` over the shared history walk. +//! +//! Per commit: the filtered tree diff against the first parent, the files it +//! names (a delete names the deleted file), line stats (skipped for merges), +//! per-blob language detection; then the shared identity/tick stamps feed +//! `cf_anomaly::build_report_data` → `compute_all_metrics` with the +//! validated [`cf_anomaly::config::Config`] (`--anomaly-threshold`, +//! `--anomaly-window`). + +use std::collections::BTreeMap; + +use cf_anomaly::config::{ + Config, DEFAULT_ANOMALY_THRESHOLD, DEFAULT_ANOMALY_WINDOW_SIZE, FLAG_ANOMALY_THRESHOLD, + FLAG_ANOMALY_WINDOW, MIN_WINDOW_SIZE, +}; +use cf_anomaly::metrics::{build_report_data, TickBounds}; +use cf_anomaly::model::CommitAnomalyData; +use cf_gitlib::blob::CachedBlob; +use cf_gitlib::changes::ChangeAction; + +use cf_units::{KIB, MIB}; + +use super::{ + compute_diff_line_stats, detect_language, gated_product, is_merge, walk_chunks, ChunkPlan, + CommitStamp, Gated, HistoryRun, WalkCost, +}; +use crate::pipeline::HandlerError; + +/// The validated anomaly configuration from `--anomaly-threshold` and +/// `--anomaly-window` (each defaulting when absent). +/// +/// # Errors +/// +/// [`HandlerError::InvalidFlag`] for a window below 2 or a threshold that is +/// not strictly positive. +pub(crate) fn anomaly_config(sub: &clap::ArgMatches) -> Result<Config, HandlerError> { + let window = super::flag_i64(sub, FLAG_ANOMALY_WINDOW, DEFAULT_ANOMALY_WINDOW_SIZE as i64); + let threshold = super::flag_f64( + sub, + FLAG_ANOMALY_THRESHOLD, + f64::from(DEFAULT_ANOMALY_THRESHOLD), + ); + let window = usize::try_from(window).map_err(|_| { + HandlerError::InvalidFlag(format!( + "invalid --{FLAG_ANOMALY_WINDOW}: must be >= {MIN_WINDOW_SIZE}, got {window}" + )) + })?; + Config::new(threshold as f32, window).map_err(|e| HandlerError::InvalidFlag(e.to_string())) +} + +/// Preflight check of the anomaly flags (every history run validates them, +/// so a bad value is a named error before any walk). +pub(crate) fn validate_anomaly_flags(sub: &clap::ArgMatches) -> Result<(), HandlerError> { + anomaly_config(sub).map(|_| ()) +} + +/// Runs the `history/anomaly` walk and returns the aggregated +/// [`cf_anomaly::model::ComputedMetrics`] — the single report value every +/// machine format (json/yaml/bin) encodes — or `None` if the repository +/// cannot be walked or the flags are invalid (preflight reports those). +#[must_use] +pub fn anomaly_run_metrics(sub: &clap::ArgMatches) -> Option<cf_anomaly::model::ComputedMetrics> { + let config = anomaly_config(sub).ok()?; + let walk = anomaly_walk(sub)?; + let input = build_report_data( + &walk.commit_metrics, + &walk.commits_by_tick, + walk.tick_bounds, + config, + ); + Some(cf_anomaly::metrics::compute_all_metrics(&input)) +} + +/// The finished `history/anomaly` walk, shared by the aggregated metrics +/// path and the per-commit ndjson/timeseries paths so every format encodes +/// the SAME walk. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct AnomalyWalk { + /// hex hash → per-commit anomaly data. + pub commit_metrics: BTreeMap<String, CommitAnomalyData>, + /// tick → hashes in walk order. + pub commits_by_tick: BTreeMap<i64, Vec<String>>, + /// tick → RFC3339-UTC committer-time bounds. + pub tick_bounds: BTreeMap<i64, TickBounds>, + /// hex hash → tick. + pub tick_by_hash: BTreeMap<String, i64>, + /// hex hash → (committer seconds, committer UTC-offset minutes). + pub when_by_hash: BTreeMap<String, (i64, i32)>, + /// hex hash → position in the ORIGINAL window (the key the merged ndjson + /// stream orders on; an oversized-dropped commit leaves a gap). + pub pos_by_hash: BTreeMap<String, usize>, +} + +/// The file a change names: the After name for an Insert/Modify, the Before +/// name for a Delete (whose After side is empty). +fn changed_file_name(change: &cf_gitlib::changes::Change) -> &str { + match change.action { + ChangeAction::Delete => &change.from.name, + ChangeAction::Insert | ChangeAction::Modify => &change.to.name, + } +} + +/// The per-commit, order-independent half of the anomaly walk: everything in +/// [`CommitAnomalyData`] except the order-assigned `author_id`. +fn anomaly_commit_product( + run_is_merge: bool, + repo: &cf_gitlib::Repository, + changes: &[cf_gitlib::changes::Change], +) -> CommitAnomalyData { + let mut cm = CommitAnomalyData::default(); + for change in changes { + let name = changed_file_name(change).to_string(); + if !cm.files.contains(&name) { + cm.files.push(name); + } + } + cm.files_changed = cm.files.len() as i64; + + // Line stats are skipped for merge commits. + if !run_is_merge { + let (added, removed) = line_stats(repo, changes); + cm.lines_added = added; + cm.lines_removed = removed; + } + + // Blob-hash → language map (Insert/Delete one side, Modify both sides), + // then count each non-empty value into the commit's language map. + let mut by_blob: BTreeMap<cf_gitlib::Hash, String> = BTreeMap::new(); + let mut detect = |entry: &cf_gitlib::changes::ChangeEntry| { + let data = CachedBlob::from_repo(repo, entry.hash) + .map(|b| b.data) + .unwrap_or_default(); + by_blob.insert(entry.hash, detect_language(&entry.name, &data)); + }; + for change in changes { + match change.action { + ChangeAction::Insert => detect(&change.to), + ChangeAction::Delete => detect(&change.from), + ChangeAction::Modify => { + detect(&change.to); + detect(&change.from); + } + } + } + for lang in by_blob.values() { + if !lang.is_empty() { + *cm.languages.entry(lang.clone()).or_insert(0) += 1; + } + } + + cm.net_churn = cm.lines_added - cm.lines_removed; + cm +} + +/// `(added, removed)` over a commit's changes: Insert ⇒ the new blob's lines; +/// Delete ⇒ the old blob's lines; Modify ⇒ the line diff, skipping binary and +/// identical-content blobs. +fn line_stats(repo: &cf_gitlib::Repository, changes: &[cf_gitlib::changes::Change]) -> (i64, i64) { + let mut added = 0i64; + let mut removed = 0i64; + for change in changes { + match change.action { + ChangeAction::Insert => { + if let Ok(blob) = CachedBlob::from_repo(repo, change.to.hash) { + if let Ok(n) = blob.count_lines() { + added += n as i64; + } + } + } + ChangeAction::Delete => { + if let Ok(blob) = CachedBlob::from_repo(repo, change.from.hash) { + if let Ok(n) = blob.count_lines() { + removed += n as i64; + } + } + } + ChangeAction::Modify => { + let (Ok(blob_from), Ok(blob_to)) = ( + CachedBlob::from_repo(repo, change.from.hash), + CachedBlob::from_repo(repo, change.to.hash), + ) else { + continue; + }; + if change.from.hash == change.to.hash + || blob_from.is_binary() + || blob_to.is_binary() + { + continue; + } + let old_lines = blob_from.count_lines().map_or(0, |n| n as i64); + let (a, r, _changed) = + compute_diff_line_stats(repo, change.from.hash, change.to.hash, old_lines); + added += a; + removed += r; + } + } + } + (added, removed) +} + +/// The gated anomaly pre-pass over `run`'s change filter and flags: the +/// pure per-commit product a walk computes in parallel. Captures only the +/// shareable pieces of the run (its repository handle is not `Sync`). +fn anomaly_prepare( + run: &HistoryRun, +) -> impl Fn(&cf_gitlib::Repository, cf_gitlib::Hash) -> Option<Gated<CommitAnomalyData>> + Sync + '_ +{ + let filter = &run.filter; + let first_parent = run.first_parent; + let max_changes = run.max_changes; + move |repo, hash| { + gated_product(repo, hash, filter, max_changes, |commit, changes| { + anomaly_commit_product(is_merge(commit.num_parents(), first_parent), repo, changes) + }) + } +} + +/// The anomaly reduce state. A kept commit's product IS its per-commit +/// record (every anomaly format reads per-commit data), so the fold keys it +/// by hash next to the commit's stamp facts; the gate wrapper is dropped. +#[derive(Default)] +struct AnomalyFold { + /// hex hash → per-commit anomaly data. + commit_metrics: BTreeMap<String, CommitAnomalyData>, + /// tick → hashes in walk order. + commits_by_tick: BTreeMap<i64, Vec<String>>, + /// tick → `(earliest, latest)` committer seconds. + tick_when: BTreeMap<i64, (i64, i64)>, + /// hex hash → tick. + tick_by_hash: BTreeMap<String, i64>, + /// hex hash → (committer seconds, committer UTC-offset minutes). + when_by_hash: BTreeMap<String, (i64, i32)>, + /// hex hash → window position. + pos_by_hash: BTreeMap<String, usize>, +} + +impl AnomalyFold { + /// Folds one kept commit, in window order: its product becomes the + /// commit's record under the stamp's author id. + fn consume(&mut self, stamp: &CommitStamp, mut product: CommitAnomalyData) { + self.tick_when + .entry(stamp.tick) + .and_modify(|(lo, hi)| { + *lo = (*lo).min(stamp.when); + *hi = (*hi).max(stamp.when); + }) + .or_insert((stamp.when, stamp.when)); + self.commits_by_tick + .entry(stamp.tick) + .or_default() + .push(stamp.hex.clone()); + self.tick_by_hash + .entry(stamp.hex.clone()) + .or_insert(stamp.tick); + self.when_by_hash + .entry(stamp.hex.clone()) + .or_insert((stamp.when, stamp.offset_min)); + self.pos_by_hash + .entry(stamp.hex.clone()) + .or_insert(stamp.pos); + product.author_id = stamp.author_id; + self.commit_metrics.insert(stamp.hex.clone(), product); + } + + /// The finished walk: the folded records and the RFC3339 tick bounds. + fn finish(self) -> AnomalyWalk { + let tick_bounds = self + .tick_when + .iter() + .map(|(&tick, &(lo, hi))| { + ( + tick, + TickBounds { + start_time: cf_analyze::metadata::format_rfc3339_utc(lo), + end_time: cf_analyze::metadata::format_rfc3339_utc(hi), + }, + ) + }) + .collect(); + AnomalyWalk { + commit_metrics: self.commit_metrics, + commits_by_tick: self.commits_by_tick, + tick_bounds, + tick_by_hash: self.tick_by_hash, + when_by_hash: self.when_by_hash, + pos_by_hash: self.pos_by_hash, + } + } +} + +/// The anomaly walk's measured cost. Kubernetes, 31,923 commits: one worker +/// peaks at 858 MB, 24 workers at 1429 MB (24 MiB per added worker); the +/// whole window holds 99 bytes more per commit than 50-commit chunks. +const ANOMALY_COST: WalkCost = WalkCost { + worker_bytes: 32 * MIB, + product_bytes: KIB, +}; + +/// Runs the `history/anomaly` walk over the run's chunk plan: the gated +/// per-commit products are computed in parallel one chunk at a time, +/// oversized commits dropped, and folded in window order ([`walk_chunks`]). +/// `None` when the repository cannot be walked. +pub(crate) fn anomaly_walk(sub: &clap::ArgMatches) -> Option<AnomalyWalk> { + let run = HistoryRun::open(sub)?; + let plan = ChunkPlan::for_walk(sub, "history/anomaly", run.hashes.len(), ANOMALY_COST).ok()?; + let max_changes = run.max_changes; + let mut fold = AnomalyFold::default(); + walk_chunks( + &run, + plan, + anomaly_prepare(&run), + |gated| gated.oversized(max_changes), + |stamp, gated| fold.consume(stamp, gated.product), + )?; + Some(fold.finish()) +} + +/// Serializes one [`CommitAnomalyData`] as the per-commit record struct +/// (declaration field order; `files`/`languages` omitted when empty). +fn anomaly_data_value(cm: &CommitAnomalyData) -> cf_gojson::GoValue { + use cf_gojson::{GoMap, GoValue}; + let mut data = GoMap::new_struct(); + data.insert("files_changed".to_string(), GoValue::Int(cm.files_changed)); + data.insert("lines_added".to_string(), GoValue::Int(cm.lines_added)); + data.insert("lines_removed".to_string(), GoValue::Int(cm.lines_removed)); + data.insert("net_churn".to_string(), GoValue::Int(cm.net_churn)); + if !cm.files.is_empty() { + data.insert( + "files".to_string(), + GoValue::Array(cm.files.iter().map(|f| GoValue::Str(f.clone())).collect()), + ); + } + if !cm.languages.is_empty() { + let mut langs = GoMap::new_map(); + for (lang, n) in &cm.languages { + langs.insert(lang.clone(), GoValue::Int(*n)); + } + data.insert("languages".to_string(), GoValue::Object(langs)); + } + data.insert("author_id".to_string(), GoValue::Int(cm.author_id)); + cf_gojson::GoValue::Object(data) +} + +/// Per-commit anomaly NDJSON records: every walked commit emits a line. +#[must_use] +pub fn anomaly_ndjson_records( + sub: &clap::ArgMatches, +) -> Option<Vec<super::super::history_formats::NdjsonRecord>> { + let walk = anomaly_walk(sub)?; + let mut records = Vec::new(); + for hashes in walk.commits_by_tick.values() { + for hex in hashes { + let Some(cm) = walk.commit_metrics.get(hex) else { + continue; + }; + let (secs, off) = walk.when_by_hash.get(hex).copied().unwrap_or((0, 0)); + records.push(super::super::history_formats::NdjsonRecord { + pos: walk.pos_by_hash.get(hex).copied().unwrap_or(0), + hash: hex.clone(), + tick: walk.tick_by_hash.get(hex).copied().unwrap_or(0), + author_id: cm.author_id, + time_secs: secs, + tz_offset_min: off, + data: anomaly_data_value(cm), + }); + } + } + Some(records) +} + +/// The anomaly contribution to the merged `--format timeseries` document. +#[must_use] +pub fn anomaly_timeseries_contribution( + sub: &clap::ArgMatches, +) -> Option<super::super::history_formats::TimeSeriesContribution> { + let walk = anomaly_walk(sub)?; + let mut per_commit = Vec::new(); + for (hex, cm) in &walk.commit_metrics { + per_commit.push((hex.clone(), anomaly_data_value(cm))); + } + let mut commit_meta = Vec::new(); + for hashes in walk.commits_by_tick.values() { + for hex in hashes { + let tick = walk.tick_by_hash.get(hex).copied().unwrap_or(0); + let (secs, off) = walk.when_by_hash.get(hex).copied().unwrap_or((0, 0)); + commit_meta.push(( + hex.clone(), + tick, + crate::handlers::format_rfc3339_offset(secs, off), + String::new(), + )); + } + } + Some(super::super::history_formats::TimeSeriesContribution { + flag: "anomaly", + per_commit, + commit_meta, + }) +} + +#[cfg(test)] +mod tests { + use super::super::fold_testutil::{ + assert_chunk_invariant, chunk_fixture, open_run, run_matches, OVERSIZED_POS, + }; + use super::*; + + /// The analyzer id the fold tests run. + const ID: &str = "history/anomaly"; + + /// A reduced `run` tree with just the two anomaly flags (the threshold + /// flag is read by name, so the handler works with any tree that + /// registers it). + fn matches(args: &[&str]) -> clap::ArgMatches { + clap::Command::new("run") + .arg( + clap::Arg::new(FLAG_ANOMALY_WINDOW) + .long(FLAG_ANOMALY_WINDOW) + .value_parser(clap::value_parser!(i64)), + ) + .arg( + clap::Arg::new(FLAG_ANOMALY_THRESHOLD) + .long(FLAG_ANOMALY_THRESHOLD) + .value_parser(clap::value_parser!(f64)), + ) + .try_get_matches_from(std::iter::once("run").chain(args.iter().copied())) + .expect("args parse") + } + + fn invalid_flag(result: Result<Config, HandlerError>) -> String { + match result { + Err(HandlerError::InvalidFlag(msg)) => msg, + other => panic!("expected InvalidFlag, got {other:?}"), + } + } + + #[test] + fn absent_flags_are_the_defaults() { + assert_eq!(anomaly_config(&matches(&[])).unwrap(), Config::default()); + } + + #[test] + fn both_flags_are_read() { + let cfg = anomaly_config(&matches(&[ + "--anomaly-window", + "5", + "--anomaly-threshold", + "3.5", + ])) + .unwrap(); + assert_eq!(cfg.window_size, 5); + assert!((cfg.threshold - 3.5).abs() < 1e-6); + } + + #[test] + fn invalid_values_are_named_errors() { + assert_eq!( + invalid_flag(anomaly_config(&matches(&["--anomaly-window", "1"]))), + "invalid --anomaly-window: must be >= 2, got 1" + ); + assert_eq!( + invalid_flag(anomaly_config(&matches(&["--anomaly-window=-3"]))), + "invalid --anomaly-window: must be >= 2, got -3" + ); + assert_eq!( + invalid_flag(anomaly_config(&matches(&["--anomaly-threshold", "0"]))), + "invalid --anomaly-threshold: must be > 0, got 0" + ); + } + + /// The real command tree registers `--anomaly-window`; preflight rejects + /// a bad value for ANY history run. + #[test] + fn preflight_rejects_a_bad_window_through_the_real_tree() { + let argv = [ + "codefang", + "run", + "-a", + "history/devs", + "--anomaly-window", + "1", + ".", + ]; + let parsed = crate::build_codefang_command() + .try_get_matches_from(argv) + .expect("run args parse"); + let sub = parsed.subcommand_matches("run").expect("run"); + let msg = match super::super::validate_history_run(sub) { + Err(HandlerError::InvalidFlag(msg)) => msg, + other => panic!("expected InvalidFlag, got {other:?}"), + }; + assert_eq!(msg, "invalid --anomaly-window: must be >= 2, got 1"); + } + + /// The anomaly walk folds per chunk: folding the chunk fixture in chunks + /// of every size finishes to the one-pass walk (the oversized commit + /// dropped), and that is what the walk returns for any `--chunk-size`. + #[test] + fn the_walk_folds_in_chunks_to_the_one_pass_value() { + let test = chunk_fixture(); + let run = open_run(&test, ID, &[]); + let max_changes = run.max_changes; + let whole = assert_chunk_invariant( + &run, + anomaly_prepare(&run), + |gated| gated.oversized(max_changes), + AnomalyFold::default, + |fold, stamp, gated| fold.consume(stamp, gated.product), + |fold, _| fold.finish(), + ); + let positions: Vec<usize> = whole.pos_by_hash.values().copied().collect(); + assert_eq!(positions.len(), run.hashes.len() - 1, "{positions:?}"); + assert!(!positions.contains(&OVERSIZED_POS), "{positions:?}"); + for size in ["1", "4"] { + let chunked = anomaly_walk(&run_matches(&test, ID, &["--chunk-size", size])); + assert_eq!(chunked.as_ref(), Some(&whole), "--chunk-size {size}"); + } + } +} diff --git a/crates/cf-commands/src/handlers/history/budget.rs b/crates/cf-commands/src/handlers/history/budget.rs new file mode 100644 index 00000000..dcfeaf75 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/budget.rs @@ -0,0 +1,369 @@ +//! The history memory budget: `--memory-budget` (or the automatic budget) +//! turned into each walk's chunk size and worker count. +//! +//! The model counts what a walk controls (FRD-bounded-memory-history): a +//! fixed base (binary, libgit2, embedded parsers), one working set per worker +//! (its largest commit's tree diff, blobs and parses; glibc keeps each +//! worker's high-water mark) and the products in flight between the workers +//! and the fold. Half of what the base leaves is reserved for worker working +//! sets, the other half for products in flight and fold state. libgit2's +//! object cache and mapped pack windows take a fixed part of the budget +//! first ([`native_limits`]). Fold state grows with the window whatever the +//! chunk size; it is not planned here. + +use cf_streaming::Planner; +use cf_units::{GIB, MIB}; + +use super::chunks::ChunkPlan; +use crate::pipeline::HandlerError; + +/// The `--memory-budget` flag name. +pub(crate) const FLAG_MEMORY_BUDGET: &str = "memory-budget"; + +/// Resident memory a history run holds before any walk work: the binary, +/// libgit2 and the embedded parsers. Measured on kubernetes: one-commit +/// `history/devs`, `history/sentiment` and `history/burndown` runs peak at +/// 76, 79 and 102 MB. +pub(crate) const BASE_OVERHEAD: i64 = 128 * MIB; + +/// The smallest budget accepted: the base overhead plus room for one worker +/// and a minimal chunk. +pub(crate) const MIN_MEMORY_BUDGET: i64 = 256 * MIB; + +/// Ceiling of the automatic budget (the Go default). +const AUTO_BUDGET_CAP: i64 = 2 * GIB; + +/// Percent of physical RAM the automatic budget may take. +const AUTO_BUDGET_RAM_PERCENT: u64 = 50; + +/// Percent of the memory above [`BASE_OVERHEAD`] reserved for worker +/// working sets; the rest holds products in flight and fold state. +const WORKER_SHARE_PERCENT: i64 = 50; + +/// Divisor of the percentages above. +const PERCENT: i64 = 100; + +/// Products in flight per worker, at least, so no worker waits for the fold +/// to make room while another commit is still being prepared. +pub(crate) const MIN_PRODUCTS_PER_WORKER: usize = 2; + +/// What one walk costs, measured per analyzer on kubernetes prefixes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct WalkCost { + /// Resident bytes one worker holds for its largest commit. + pub worker_bytes: i64, + /// Bytes one commit's product holds while it waits for the fold. + pub product_bytes: i64, +} + +/// The `--memory-budget` value in bytes, `None` when the flag is absent. +/// +/// # Errors +/// +/// [`HandlerError::InvalidFlag`] for a value that is not a size or is below +/// [`MIN_MEMORY_BUDGET`]. +pub(crate) fn memory_budget_flag(sub: &clap::ArgMatches) -> Result<Option<i64>, HandlerError> { + let Some(raw) = sub.try_get_one::<String>(FLAG_MEMORY_BUDGET).ok().flatten() else { + return Ok(None); + }; + let invalid = |why: &str| { + HandlerError::InvalidFlag(format!("invalid --{FLAG_MEMORY_BUDGET}: {raw:?} {why}")) + }; + let bytes = cf_framework::config::parse_bytes(raw.trim()) + .ok() + .and_then(|b| i64::try_from(b).ok()) + .ok_or_else(|| invalid("is not a size (examples: 512MB, 2GB, 1.5GiB)"))?; + if bytes < MIN_MEMORY_BUDGET { + return Err(invalid(&format!( + "is below the minimum {} MiB", + MIN_MEMORY_BUDGET / MIB + ))); + } + Ok(Some(bytes)) +} + +/// The budget used without `--memory-budget`: half the physical RAM, at most +/// [`AUTO_BUDGET_CAP`]; the cap when the RAM size is unknown (`0`). +pub(crate) fn auto_budget(total_ram: u64) -> i64 { + if total_ram == 0 { + return AUTO_BUDGET_CAP; + } + let share = u128::from(total_ram) * u128::from(AUTO_BUDGET_RAM_PERCENT) / (PERCENT as u128); + i64::try_from(share).map_or(AUTO_BUDGET_CAP, |share| share.min(AUTO_BUDGET_CAP)) +} + +/// The automatic budget of this host. +pub(crate) fn default_budget() -> i64 { + auto_budget(cf_framework::coordinator::detect_total_memory_bytes()) +} + +/// The budget the run plans against: `--memory-budget`, or the automatic +/// budget; `None` when the flag is invalid (the preflight reports it). +pub(crate) fn run_budget(sub: &clap::ArgMatches) -> Option<i64> { + memory_budget_flag(sub) + .ok() + .map(|flag| flag.unwrap_or_else(default_budget)) +} + +/// Percent of the budget libgit2's object cache may hold. Measured on the +/// 31,923-commit kubernetes prefix (the five non-UAST history walks run +/// together under the automatic budget): libgit2's 256 MiB default ran 183 s, +/// 512 MiB 109 s, 1 GiB 85 s; the parallel walks' repository handles share +/// the cache and evict each other's trees below that. +const OBJECT_CACHE_PERCENT: i64 = 25; + +/// libgit2's part of a run within `budget`: a quarter for its object cache +/// and the Go split for mapped pack windows (a quarter of the budget for +/// libgit2, 30 % of that for pack windows: 7.5 %). +pub(crate) fn native_limits(budget: i64) -> cf_gitlib::NativeLimits { + let bytes = |b: i64| usize::try_from(b).unwrap_or(0); + cf_gitlib::NativeLimits { + mapped_pack: bytes(cf_budget::native_limits_for_budget(budget).mwindow_mapped_limit), + object_cache: bytes( + i64::try_from( + i128::from(budget) * i128::from(OBJECT_CACHE_PERCENT) / i128::from(PERCENT), + ) + .unwrap_or(0), + ), + } +} + +/// The bytes of `budget` libgit2 may hold ([`native_limits`]); the walks +/// share the rest. +pub(crate) fn native_bytes(budget: i64) -> i64 { + let limits = native_limits(budget); + i64::try_from(limits.mapped_pack + limits.object_cache).unwrap_or(i64::MAX) +} + +/// Registers the run's libgit2 memory limits; must run before the process +/// opens its first repository (the history preflight is the first to open +/// one). +pub(crate) fn limit_native_memory(sub: &clap::ArgMatches) { + if let Some(budget) = run_budget(sub) { + cf_gitlib::limit_native_memory(native_limits(budget)); + } +} + +/// The history analyzers that share ONE pass ([`crate::handlers::uast_walk`]). +const UAST_PASS_IDS: [&str; 5] = [ + "history/quality", + "history/sentiment", + "history/shotness", + "history/imports", + "history/typos", +]; + +/// The history analyzer ids this run selects, in phase order. +fn selected_history_ids(sub: &clap::ArgMatches) -> Vec<String> { + let ctx = crate::pipeline::RunContext::from_matches(sub); + let ids = ctx.analyzer_ids(); + let patterns: Vec<&str> = ids.iter().map(String::as_str).collect(); + crate::handlers::expand_history_phase_ids(&patterns) +} + +/// How many history walks run at once and so share the budget: one per +/// selected history analyzer, the UAST analyzers counting once (they share +/// one pass), at most [`crate::handlers::ANALYZER_CONCURRENCY`]. +pub(crate) fn concurrent_walks(sub: &clap::ArgMatches) -> usize { + let selected = selected_history_ids(sub); + let uast = selected + .iter() + .filter(|id| UAST_PASS_IDS.contains(&id.as_str())) + .count(); + (selected.len() - uast + usize::from(uast > 0)).clamp(1, crate::handlers::ANALYZER_CONCURRENCY) +} + +/// Whether history walks other than the UAST pass run in this invocation +/// (a non-UAST history analyzer is selected): the pass's finished results +/// then wait for rendering while those walks run. +pub(crate) fn other_walks_follow_the_uast_pass(sub: &clap::ArgMatches) -> bool { + selected_history_ids(sub) + .iter() + .any(|id| !UAST_PASS_IDS.contains(&id.as_str())) +} + +/// The plan of one walk over `commits` commits on `cores` cores within +/// `share` bytes: as many workers as the worker reserve holds (at least one, +/// at most `cores`), and the chunk [`Planner`] fits in the rest, never +/// smaller than [`MIN_PRODUCTS_PER_WORKER`] per worker nor larger than the +/// window. +pub(crate) fn plan_walk(commits: usize, cores: usize, share: i64, cost: WalkCost) -> ChunkPlan { + let available = share.saturating_sub(BASE_OVERHEAD).max(0); + let worker_reserve = available / PERCENT * WORKER_SHARE_PERCENT; + let fit = usize::try_from(worker_reserve / cost.worker_bytes.max(1)).unwrap_or(usize::MAX); + let workers = fit.clamp(1, cores.max(1)); + let planner = Planner { + total_commits: i64::try_from(commits).unwrap_or(i64::MAX), + memory_budget: share, + aggregate_growth_per_commit: cost.product_bytes.max(1), + pipeline_overhead: BASE_OVERHEAD + worker_reserve, + }; + let planned = planner.plan().first().map_or(1, |c| c.end - c.start); + let size = planned + .max(MIN_PRODUCTS_PER_WORKER * workers) + .min(commits) + .max(1); + ChunkPlan { size, workers } +} + +#[cfg(test)] +mod tests { + use cf_units::{GIB, MIB}; + + use super::*; + + /// A walk whose worker holds 64 MiB and whose product is 4 KiB. + const COST: WalkCost = WalkCost { + worker_bytes: 64 * MIB, + product_bytes: 4 * 1024, + }; + /// Commits in a large window. + const WINDOW: usize = 100_000; + /// Cores of a large host. + const CORES: usize = 24; + + fn matches(argv: &[&str]) -> clap::ArgMatches { + crate::build_codefang_command() + .try_get_matches_from(["codefang", "run"].iter().chain(argv).copied()) + .expect("run args parse") + .subcommand_matches("run") + .expect("run") + .clone() + } + + fn budget(value: &str) -> Result<Option<i64>, HandlerError> { + memory_budget_flag(&matches(&["--memory-budget", value, "."])) + } + + fn message(result: Result<Option<i64>, HandlerError>) -> String { + result.expect_err("an invalid budget").to_string() + } + + #[test] + fn a_budget_reads_si_and_iec_sizes() { + assert_eq!(budget("512MB").expect("ok"), Some(512_000_000)); + assert_eq!(budget("2GiB").expect("ok"), Some(2 * GIB)); + assert_eq!(budget("1.5GB").expect("ok"), Some(1_500_000_000)); + assert_eq!(memory_budget_flag(&matches(&["."])).expect("ok"), None); + } + + #[test] + fn an_unparsable_budget_is_a_named_error() { + let msg = message(budget("banana")); + assert!( + msg.starts_with("invalid --memory-budget: \"banana\" is not a size"), + "{msg}" + ); + } + + #[test] + fn a_budget_below_the_minimum_is_a_named_error() { + let msg = message(budget("1MB")); + assert!( + msg.starts_with("invalid --memory-budget: \"1MB\" is below the minimum"), + "{msg}" + ); + assert!(budget(&format!("{MIN_MEMORY_BUDGET}")).is_ok()); + } + + #[test] + fn the_automatic_budget_is_half_the_ram_up_to_two_gib() { + assert_eq!(auto_budget(8 * GIB as u64), 2 * GIB); + assert_eq!(auto_budget(2 * GIB as u64), GIB); + assert_eq!(auto_budget(0), 2 * GIB, "unknown RAM"); + } + + #[test] + fn more_budget_never_shrinks_the_plan() { + let mut previous = plan_walk(WINDOW, CORES, MIN_MEMORY_BUDGET, COST); + for share in [512 * MIB, GIB, 2 * GIB, 4 * GIB, 16 * GIB] { + let plan = plan_walk(WINDOW, CORES, share, COST); + assert!(plan.size >= previous.size, "size at {share}"); + assert!(plan.workers >= previous.workers, "workers at {share}"); + previous = plan; + } + } + + #[test] + fn a_tight_budget_trades_workers_for_memory() { + let tight = plan_walk(WINDOW, CORES, MIN_MEMORY_BUDGET, COST); + let roomy = plan_walk(WINDOW, CORES, 16 * GIB, COST); + assert!(tight.workers < CORES, "{tight:?}"); + assert_eq!(roomy.workers, CORES); + let used = BASE_OVERHEAD + tight.workers as i64 * COST.worker_bytes; + assert!(used <= MIN_MEMORY_BUDGET, "{used} > {MIN_MEMORY_BUDGET}"); + } + + #[test] + fn workers_stay_between_one_and_the_cores() { + for cores in [1, 2, 64] { + for share in [MIN_MEMORY_BUDGET, 64 * GIB] { + let plan = plan_walk(WINDOW, cores, share, COST); + assert!((1..=cores).contains(&plan.workers), "{plan:?}"); + } + } + } + + #[test] + fn the_chunk_fits_the_window_and_keeps_every_worker_busy() { + for commits in [0, 1, 7, WINDOW] { + for share in [MIN_MEMORY_BUDGET, 4 * GIB] { + let plan = plan_walk(commits, CORES, share, COST); + assert!(plan.size >= 1, "{plan:?}"); + assert!(plan.size <= commits.max(1), "{commits}: {plan:?}"); + assert!( + plan.size >= commits.min(MIN_PRODUCTS_PER_WORKER * plan.workers), + "{commits}: {plan:?}" + ); + } + } + } + + /// libgit2 gets a fixed part of the budget: a quarter for its object + /// cache and 7.5 % for mapped pack windows, both growing with the + /// budget. + #[test] + fn the_native_limits_follow_the_budget() { + let mut previous = cf_gitlib::NativeLimits::default(); + for budget in [MIN_MEMORY_BUDGET, GIB, 2 * GIB, 4 * GIB] { + let limits = native_limits(budget); + assert_eq!(limits.object_cache as i64, budget / 4, "{budget}"); + assert!(limits.mapped_pack > previous.mapped_pack, "{budget}"); + assert!((limits.mapped_pack as i64) < budget / 10, "{budget}"); + assert_eq!( + native_bytes(budget), + (limits.object_cache + limits.mapped_pack) as i64 + ); + previous = limits; + } + } + + /// The UAST results wait for rendering only when another history walk + /// runs after the pass. + #[test] + fn other_walks_follow_the_pass_only_with_a_non_uast_history_analyzer() { + let follow = |ids: &str| other_walks_follow_the_uast_pass(&matches(&["-a", ids, "."])); + assert!(!follow("history/quality,history/sentiment")); + assert!(!follow("static/complexity,history/typos")); + assert!(follow("history/quality,history/devs")); + assert!(follow("history/*")); + } + + #[test] + fn concurrent_walks_share_the_budget() { + let walks = |ids: &str| concurrent_walks(&matches(&["-a", ids, "."])); + assert_eq!(walks("history/devs"), 1); + assert_eq!(walks("history/devs,history/couples"), 2); + assert_eq!(walks("static/complexity,history/devs"), 1); + assert_eq!( + walks("history/quality,history/sentiment,history/typos"), + 1, + "the UAST analyzers share one pass" + ); + assert_eq!(walks("history/quality,history/sentiment,history/devs"), 2); + assert_eq!( + walks("history/devs,history/couples,history/burndown,history/anomaly,history/file-history"), + crate::handlers::ANALYZER_CONCURRENCY + ); + } +} diff --git a/crates/cf-commands/src/handlers/history/chunks.rs b/crates/cf-commands/src/handlers/history/chunks.rs new file mode 100644 index 00000000..d6394072 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/chunks.rs @@ -0,0 +1,736 @@ +//! The chunked history core: incremental identity + tick assignment +//! ([`Stamper`]) and the chunk driver ([`walk_chunks`]) a history walk folds +//! its per-commit products through. +//! +//! A walk computes its pure per-commit products on a worker pool +//! ([`super::pool`]) at most one chunk ([`ChunkPlan`]) ahead of its fold, then +//! folds each kept commit into its reduce state in window order and drops the +//! product, so a walk never holds more than one chunk of products. The chunk +//! is a sliding bound, not a barrier: workers never wait for the slowest +//! commit of a chunk. Stamping happens one commit at a time in window order, +//! so author ids, ticks and the folded output do not depend on the chunk +//! size. After every chunk of folded commits, and once the walk ends, free +//! heap pages go back to the operating system +//! ([`cf_meminfo::release_free_heap`]): the workers' allocator arenas would +//! otherwise keep every worker's largest transient working set resident. + +use cf_analyzers_plumbing::{anonymize_names, HistoryTicks, IdentityDetector}; +use cf_gitlib::Hash; + +use super::budget::{ + concurrent_walks, default_budget, memory_budget_flag, native_bytes, plan_walk, WalkCost, + BASE_OVERHEAD, +}; +use super::pool::prepare_in_order; +use super::{author_signature, CommitStamp, HistoryRun}; +use crate::pipeline::HandlerError; + +/// The `--chunk-size` flag name. +const FLAG_CHUNK_SIZE: &str = "chunk-size"; + +/// Incremental identity + tick assignment, one commit at a time, in window +/// order. Ids are first-seen oldest-first and ticks never go backwards, so +/// both depend only on the commits stamped so far — never on how the window +/// was chunked. +pub(crate) struct Stamper { + identity: IdentityDetector, + ticks: HistoryTicks, + anonymize: bool, +} + +impl Stamper { + /// A stamper seeded with the run's identity configuration + /// (`--exact-signatures`, `--people-dict`), `--tick-size` and + /// `--anonymize`. + pub(crate) fn new(run: &HistoryRun) -> Self { + Self { + identity: run.identity_seed.clone(), + ticks: HistoryTicks::new(run.tick_size_hours), + anonymize: run.anonymize, + } + } + + /// Stamps the commit at window position `pos`: its author id, its tick + /// and the commit facts every per-commit record emits. `None` when the + /// commit cannot be read. + pub(crate) fn stamp( + &mut self, + repo: &cf_gitlib::Repository, + pos: usize, + hash: Hash, + ) -> Option<CommitStamp> { + let commit = repo.lookup_commit(hash).ok()?; + let author_id = self.identity.consume_signature(&author_signature(&commit)); + let committer_when = commit.committer().when; + let when = committer_when.seconds(); + Some(CommitStamp { + pos, + hash, + hex: hash.to_hex(), + tick: self.ticks.tick_for(when), + author_id, + when, + offset_min: committer_when.offset_minutes(), + num_parents: commit.num_parents(), + }) + } + + /// The tick of the newest stamped commit (0 before any). + pub(crate) const fn last_tick(&self) -> i64 { + self.ticks.last_tick() + } + + /// The finished identity state: the developer names and the people slot + /// of an unmatched author. + pub(crate) fn finish(mut self) -> StampSummary { + self.identity.finalize_dict(); + let people = self.identity.reversed_people_dict; + let unmatched_slot = people + .iter() + .position(|n| n == cf_analyzers_plumbing::AUTHOR_MISSING_NAME) + .unwrap_or(people.len()); + let names = if self.anonymize { + anonymize_names(&people) + } else { + people + }; + StampSummary { + names, + unmatched_slot, + } + } +} + +/// The identity state of a finished walk. +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct StampSummary { + /// The reversed people dictionary (index = author id), `--anonymize` + /// applied. + pub names: Vec<String>, + /// The people slot an UNMATCHED author (`--people-dict` miss) is counted + /// under: the dictionary's `<unmatched>` entry when one was loaded, else + /// one past the known people (never reached without a loaded dictionary, + /// where every author is registered). + pub unmatched_slot: usize, +} + +/// How one walk runs: the pre-pass products of at most `size` commits are +/// alive at once, prepared by `workers` threads. Neither reaches the output. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ChunkPlan { + /// Commits per chunk (at least 1). + pub size: usize, + /// Worker threads preparing products (at least 1). + pub workers: usize, +} + +impl ChunkPlan { + /// `size` commits per chunk on every core. + pub(crate) fn sized(size: usize) -> Self { + Self { + size, + workers: HistoryRun::workers(), + } + } + + /// One chunk spanning any window: the walk holds every product at once. + pub(crate) fn whole_window() -> Self { + Self::sized(usize::MAX) + } + + /// The plan the run flags ask for: `--chunk-size <n>` fixes the commits + /// per chunk; without it the whole window is one chunk. + /// + /// # Errors + /// + /// [`HandlerError::InvalidFlag`] for a `--chunk-size` below 1. + pub(crate) fn from_matches(sub: &clap::ArgMatches) -> Result<Self, HandlerError> { + let Some(size) = sub + .try_get_one::<i64>(FLAG_CHUNK_SIZE) + .ok() + .flatten() + .copied() + else { + return Ok(Self::whole_window()); + }; + usize::try_from(size) + .ok() + .filter(|&n| n >= 1) + .map(Self::sized) + .ok_or_else(|| { + HandlerError::InvalidFlag(format!( + "invalid --{FLAG_CHUNK_SIZE}: must be >= 1, got {size}" + )) + }) + } +} + +/// Where a walk's [`ChunkPlan`] came from, for the `-v` plan line. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PlanSource { + /// `--chunk-size`. + ChunkSize, + /// The walk's share of the memory budget, in bytes. + Budget(i64), +} + +impl ChunkPlan { + /// The plan of the walk named `walk` over `commits` commits: `--chunk-size` when + /// given, otherwise the walk's share of `--memory-budget` (or of the + /// automatic budget) planned with its measured `cost`. With `-v` the plan + /// is printed on stderr. + /// + /// # Errors + /// + /// [`HandlerError::InvalidFlag`] for an invalid `--chunk-size` or + /// `--memory-budget` (the preflight reports these before any walk). + pub(crate) fn for_walk( + sub: &clap::ArgMatches, + walk: &str, + commits: usize, + cost: WalkCost, + ) -> Result<Self, HandlerError> { + Self::planned(sub, walk, commits, cost, concurrent_walks(sub)) + } + + /// [`Self::for_walk`] for a walk that runs before the others start (the + /// pre-computed UAST pass): it plans against the whole budget. + /// + /// # Errors + /// + /// As [`Self::for_walk`]. + pub(crate) fn for_lone_walk( + sub: &clap::ArgMatches, + walk: &str, + commits: usize, + cost: WalkCost, + ) -> Result<Self, HandlerError> { + Self::planned(sub, walk, commits, cost, 1) + } + + /// The plan of `walk` when `concurrent` walks share the budget. + fn planned( + sub: &clap::ArgMatches, + walk: &str, + commits: usize, + cost: WalkCost, + concurrent: usize, + ) -> Result<Self, HandlerError> { + let chunk_size_given = sub + .try_get_one::<i64>(FLAG_CHUNK_SIZE) + .ok() + .flatten() + .is_some(); + let (plan, source) = if chunk_size_given { + (Self::from_matches(sub)?, PlanSource::ChunkSize) + } else { + let budget = memory_budget_flag(sub)?.unwrap_or_else(default_budget); + // libgit2's part and the base overhead are paid once per + // process, not once per walk. + let walks = budget + .saturating_sub(BASE_OVERHEAD) + .saturating_sub(native_bytes(budget)) + .max(0); + let share = BASE_OVERHEAD + walks / concurrent.max(1) as i64; + ( + plan_walk(commits, HistoryRun::workers(), share, cost), + PlanSource::Budget(share), + ) + }; + if sub.try_get_one::<bool>("verbose").ok().flatten() == Some(&true) { + eprintln!("{}", plan.describe(walk, commits, source)); + } + Ok(plan) + } + + /// The `-v` plan line of `walk` over `commits` commits. + pub(crate) fn describe(self, walk: &str, commits: usize, source: PlanSource) -> String { + let chunks = commits.div_ceil(self.size.max(1)); + let origin = match source { + PlanSource::ChunkSize => format!("--{FLAG_CHUNK_SIZE}"), + PlanSource::Budget(bytes) => format!("budget {} MiB", bytes / cf_units::MIB), + }; + format!( + "progress: {walk} plan: {} commits per chunk, {chunks} chunks, {} workers ({origin})", + self.size, self.workers + ) + } +} + +/// Drives one walk over the window: `prepare` runs on the worker pool (pure, +/// at most `plan.size` commits ahead of the fold), `oversized` applies the +/// `--max-changes-per-commit` drop gate to a product, and `consume` folds +/// each kept commit IN WINDOW ORDER with its stamp. Each product is moved +/// into `consume` (or dropped by the gate), so at most `plan.size` products +/// are alive at once. Free heap pages are released after every `plan.size` +/// folded commits and at the end. Returns the finished identity state, or +/// `None` when any commit hits a fatal git error. +pub(crate) fn walk_chunks<P: Send>( + run: &HistoryRun, + plan: ChunkPlan, + prepare: impl Fn(&cf_gitlib::Repository, Hash) -> Option<P> + Sync, + oversized: impl Fn(&P) -> bool, + mut consume: impl FnMut(&CommitStamp, P), +) -> Option<StampSummary> { + let mut stamper = Stamper::new(run); + fold_planned(&run.path, &run.hashes, plan, prepare, |pos, product| { + if oversized(&product) { + return Some(()); + } + let stamp = stamper.stamp(&run.repo, pos, run.hashes[pos])?; + consume(&stamp, product); + Some(()) + })?; + Some(stamper.finish()) +} + +/// Prepares `hashes` on the worker pool as `plan` says and folds each +/// product on the calling thread in window order (`fold` gets the window +/// position), releasing free heap pages after every `plan.size` folded +/// commits and at the end: the driver every chunked walk runs on. `None` +/// when a commit's preparation or the fold fails. +pub(crate) fn fold_planned<P: Send>( + repo_path: &str, + hashes: &[Hash], + plan: ChunkPlan, + prepare: impl Fn(&cf_gitlib::Repository, Hash) -> Option<P> + Sync, + mut fold: impl FnMut(usize, P) -> Option<()>, +) -> Option<()> { + let window = plan.size.max(1); + prepare_in_order( + repo_path, + hashes, + plan.workers, + window, + prepare, + |pos, product| { + if (pos + 1) % window == 0 { + cf_meminfo::release_free_heap(); + } + fold(pos, product) + }, + )?; + // The workers are gone; what their arenas freed is garbage now. + cf_meminfo::release_free_heap(); + Some(()) +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; + + use cf_gitlib::testutil::{init_repo, CommitBuilder, TestRepo}; + + use super::*; + + /// 2021-01-01T00:00:00Z. + const T0: i64 = 1_609_459_200; + /// One default tick. + const DAY: i64 = 86_400; + /// The chunk sizes every equivalence test runs (the window length is + /// added per fixture). + const CHUNK_SIZES: [usize; 3] = [1, 2, 3]; + + /// One fixture commit: author name, author email, author time and an + /// optional skewed committer time. + type Author = (&'static str, &'static str, i64, Option<i64>); + + /// A linear history, one file write per commit, in `authors` order. + fn history(authors: &[Author]) -> (TestRepo, Vec<Hash>) { + let test = init_repo().expect("init"); + let mut hashes = Vec::new(); + for (i, (name, email, when, committer)) in authors.iter().enumerate() { + let body = format!("v{i}\n"); + let mut builder = CommitBuilder::new(&test, &format!("c{i}"), *when) + .author(name, email) + .write("f.txt", body.as_bytes()); + if let Some(at) = committer { + builder = builder.committer_time(*at); + } + hashes.push(builder.commit().expect("commit")); + } + (test, hashes) + } + + /// Aliases (`alice` / `Alice Smith` by email, then by name), a second and + /// a third developer, and a committer clock that runs backwards (`c3`). + fn alias_history() -> (TestRepo, Vec<Hash>) { + history(&[ + ("alice", "alice@x", T0, None), + ("Alice Smith", "alice@x", T0 + DAY, None), + ("bob", "bob@x", T0 + 2 * DAY, None), + ("Alice Smith", "smith@y", T0 + 3 * DAY, Some(T0 + DAY / 2)), + ("carol", "carol@x", T0 + 5 * DAY, None), + ]) + } + + /// The parsed `run` matches for `-a history/devs <extra> <path>`. + fn run_matches(test: &TestRepo, extra: &[&str]) -> clap::ArgMatches { + let path = test.path().to_str().expect("utf-8 path"); + let argv: Vec<&str> = ["codefang", "run", "-a", "history/devs"] + .into_iter() + .chain(extra.iter().copied()) + .chain([path]) + .collect(); + crate::build_codefang_command() + .try_get_matches_from(argv) + .expect("run args parse") + .subcommand_matches("run") + .expect("run") + .clone() + } + + fn open(test: &TestRepo, extra: &[&str]) -> HistoryRun { + HistoryRun::open(&run_matches(test, extra)).expect("open run") + } + + /// The stamps and the summary of one chunked walk over `run` whose + /// product is the commit hash, dropping the hashes in `dropped`. + fn stamp_in_chunks( + run: &HistoryRun, + size: usize, + dropped: &[Hash], + ) -> (Vec<CommitStamp>, StampSummary) { + let mut stamps = Vec::new(); + let summary = walk_chunks( + run, + ChunkPlan::sized(size), + |_, hash| Some(hash), + |hash| dropped.contains(hash), + |stamp, hash| { + assert_eq!(stamp.hash, hash, "the product belongs to its stamp"); + stamps.push(stamp.clone()); + }, + ) + .expect("walk"); + (stamps, summary) + } + + /// `(pos, tick, author id)` of each stamp. + fn view(stamps: &[CommitStamp]) -> Vec<(usize, i64, i64)> { + stamps + .iter() + .map(|s| (s.pos, s.tick, s.author_id)) + .collect() + } + + /// Stamping in chunks of every size equals stamping the whole window + /// once, field for field. + fn assert_chunk_invariant( + run: &HistoryRun, + dropped: &[Hash], + ) -> (Vec<CommitStamp>, StampSummary) { + let whole = stamp_in_chunks(run, ChunkPlan::whole_window().size, dropped); + for size in CHUNK_SIZES.into_iter().chain([run.hashes.len()]) { + assert_eq!( + stamp_in_chunks(run, size, dropped), + whole, + "chunk size {size}" + ); + } + whole + } + + #[test] + fn chunked_stamps_follow_aliases_and_a_backward_clock() { + let (test, hashes) = alias_history(); + let run = open(&test, &[]); + assert_eq!(run.hashes, hashes); + let (stamps, summary) = assert_chunk_invariant(&run, &[]); + // c3's committer clock runs backwards: its tick is clamped to c2's. + assert_eq!( + view(&stamps), + [(0, 0, 0), (1, 1, 0), (2, 2, 1), (3, 2, 0), (4, 5, 2)] + ); + assert_eq!( + summary.names, + [ + "alice|alice smith|alice@x|smith@y", + "bob|bob@x", + "carol|carol@x" + ] + ); + assert_eq!(summary.unmatched_slot, summary.names.len()); + assert_eq!(stamps[3].when, T0 + DAY / 2); + assert_eq!(stamps[2].hex, hashes[2].to_hex()); + } + + #[test] + fn chunked_stamps_apply_anonymize() { + let (test, _) = alias_history(); + let (_, summary) = assert_chunk_invariant(&open(&test, &["--anonymize"]), &[]); + assert_eq!(summary.names, ["Developer-A", "Developer-B", "Developer-C"]); + } + + #[test] + fn chunked_stamps_resolve_a_people_dict_and_its_unmatched_slot() { + let (test, _) = alias_history(); + let dict = tempfile::NamedTempFile::new().expect("dict file"); + std::fs::write(dict.path(), "Alice|alice@x|smith@y\nBob|bob@x\n").expect("write dict"); + let dict_path = dict.path().to_str().expect("utf-8 path"); + let missing = cf_analyzers_plumbing::author_missing_id(); + + let (stamps, summary) = + assert_chunk_invariant(&open(&test, &["--people-dict", dict_path]), &[]); + assert_eq!( + view(&stamps), + [(0, 0, 0), (1, 1, 0), (2, 2, 1), (3, 2, 0), (4, 5, missing)] + ); + assert_eq!(summary.names, ["Alice", "Bob", "<unmatched>"]); + assert_eq!(summary.unmatched_slot, 2); + + let (_, anonymized) = assert_chunk_invariant( + &open(&test, &["--people-dict", dict_path, "--anonymize"]), + &[], + ); + assert_eq!( + anonymized.names, + ["Developer-A", "Developer-B", "<unmatched>"] + ); + assert_eq!(anonymized.unmatched_slot, 2); + } + + /// A dropped commit consumes no author id and advances no tick; the kept + /// stamps keep their ORIGINAL window positions. + #[test] + fn dropped_commits_are_never_stamped() { + let (test, hashes) = alias_history(); + let run = open(&test, &[]); + let (stamps, summary) = assert_chunk_invariant(&run, &[hashes[2]]); + assert_eq!(view(&stamps), [(0, 0, 0), (1, 1, 0), (3, 1, 0), (4, 5, 1)]); + assert_eq!( + summary.names, + ["alice|alice smith|alice@x|smith@y", "carol|carol@x"] + ); + } + + /// An empty window stamps nothing and still finishes the identity state. + #[test] + fn an_empty_window_yields_an_empty_summary() { + let (test, _) = alias_history(); + let run = open(&test, &["--since", "2100-01-01"]); + assert!(run.hashes.is_empty()); + let (stamps, summary) = assert_chunk_invariant(&run, &[]); + assert!(stamps.is_empty()); + assert!(summary.names.is_empty()); + } + + /// `last_tick` is the tick of the newest stamped commit (0 before any). + #[test] + fn last_tick_follows_the_stamped_commits() { + let (test, hashes) = alias_history(); + let run = open(&test, &[]); + let mut stamper = Stamper::new(&run); + assert_eq!(stamper.last_tick(), 0); + let mut ticks = Vec::new(); + for (pos, hash) in hashes.iter().enumerate() { + let stamp = stamper.stamp(&run.repo, pos, *hash).expect("stamp"); + assert_eq!(stamper.last_tick(), stamp.tick); + ticks.push(stamp.tick); + } + assert_eq!(ticks, [0, 1, 2, 2, 5]); + assert_eq!(stamper.finish().names.len(), 3); + } + + /// A product that counts its live instances, so a test can observe how + /// many products a walk holds at once. + struct Tracked { + live: Arc<AtomicUsize>, + } + + impl Tracked { + fn new(live: &Arc<AtomicUsize>) -> Self { + live.fetch_add(1, Ordering::SeqCst); + Self { + live: Arc::clone(live), + } + } + } + + impl Drop for Tracked { + fn drop(&mut self) { + self.live.fetch_sub(1, Ordering::SeqCst); + } + } + + /// Products are dropped once folded: no more than one chunk of them is + /// ever alive, and the fold sees every commit in window order. + #[test] + fn walk_chunks_holds_at_most_one_chunk_of_products() { + let (test, hashes) = alias_history(); + let run = open(&test, &[]); + for size in CHUNK_SIZES { + let live = Arc::new(AtomicUsize::new(0)); + let mut seen = Vec::new(); + walk_chunks( + &run, + ChunkPlan::sized(size), + |_, _| Some(Tracked::new(&live)), + |_| false, + |stamp, product| { + let now = live.load(Ordering::SeqCst); + assert!(now <= size, "{now} live products with chunk size {size}"); + seen.push(stamp.pos); + drop(product); + }, + ) + .expect("walk"); + assert_eq!(live.load(Ordering::SeqCst), 0, "every product dropped"); + assert_eq!(seen, (0..hashes.len()).collect::<Vec<_>>()); + } + } + + /// A failed pre-pass on any commit fails the walk (the `.ok()?` + /// contract of every history walk). + #[test] + fn a_failed_prepare_fails_the_walk() { + let (test, hashes) = alias_history(); + let run = open(&test, &[]); + let result = walk_chunks( + &run, + ChunkPlan::sized(2), + |_, hash| (hash != hashes[3]).then_some(()), + |()| false, + |_, ()| {}, + ); + assert!(result.is_none()); + } + + /// `ChunkPlan::from_matches` reads `--chunk-size`; an absent flag plans + /// the whole window as one chunk. + #[test] + fn chunk_plan_reads_the_flag() { + let (test, _) = alias_history(); + let plan = |extra: &[&str]| ChunkPlan::from_matches(&run_matches(&test, extra)); + assert_eq!( + plan(&[]).expect("plan").size, + ChunkPlan::whole_window().size + ); + assert_eq!(plan(&["--chunk-size", "7"]).expect("plan").size, 7); + for bad in ["0", "-3"] { + let flag = format!("--chunk-size={bad}"); + match plan(&[flag.as_str()]) { + Err(HandlerError::InvalidFlag(msg)) => assert_eq!( + msg, + format!("invalid --chunk-size: must be >= 1, got {bad}") + ), + Err(other) => panic!("expected InvalidFlag, got {other:?}"), + Ok(plan) => panic!("--chunk-size {bad} accepted as {}", plan.size), + } + } + } + + /// The walk the plan tests name. + const PLAN_WALK: &str = "history/devs"; + /// A walk cost for the plan tests: 64 MiB per worker, 4 KiB per product. + const PLAN_COST: WalkCost = WalkCost { + worker_bytes: 64 * cf_units::MIB, + product_bytes: 4 * 1024, + }; + /// A window long enough for the budget, not its length, to bound a chunk. + const PLAN_WINDOW: usize = 100_000; + + /// `--chunk-size` fixes the chunk on every core, whatever the budget. + #[test] + fn the_chunk_size_flag_wins_over_the_budget() { + let (test, _) = alias_history(); + let sub = run_matches(&test, &["--chunk-size", "3", "--memory-budget", "256MB"]); + let plan = ChunkPlan::for_walk(&sub, PLAN_WALK, PLAN_WINDOW, PLAN_COST).expect("plan"); + assert_eq!(plan, ChunkPlan::sized(3)); + } + + /// `--memory-budget` plans the walk against the budget less libgit2's + /// part (one history walk selected, so the rest is its share). + #[test] + fn a_memory_budget_plans_the_walk() { + let (test, _) = alias_history(); + let sub = run_matches(&test, &["--memory-budget", "1GB"]); + let plan = ChunkPlan::for_walk(&sub, PLAN_WALK, PLAN_WINDOW, PLAN_COST).expect("plan"); + assert_eq!( + plan, + plan_walk( + PLAN_WINDOW, + HistoryRun::workers(), + 1_000_000_000 - native_bytes(1_000_000_000), + PLAN_COST + ) + ); + } + + /// A walk that runs before the others start plans against the whole + /// budget less libgit2's part; a walk running next to another plans + /// against the base overhead (paid once per process) plus half of the + /// rest. + #[test] + fn a_lone_walk_plans_against_the_whole_budget() { + let (test, _) = alias_history(); + let sub = run_matches( + &test, + &[ + "-a", + "history/devs,history/couples", + "--memory-budget", + "1GB", + ], + ); + let shared = ChunkPlan::for_walk(&sub, PLAN_WALK, PLAN_WINDOW, PLAN_COST).expect("plan"); + let lone = ChunkPlan::for_lone_walk(&sub, PLAN_WALK, PLAN_WINDOW, PLAN_COST).expect("plan"); + let cores = HistoryRun::workers(); + let walks = 1_000_000_000 - BASE_OVERHEAD - native_bytes(1_000_000_000); + let half = BASE_OVERHEAD + walks / 2; + assert_eq!(shared, plan_walk(PLAN_WINDOW, cores, half, PLAN_COST)); + assert_eq!( + lone, + plan_walk(PLAN_WINDOW, cores, BASE_OVERHEAD + walks, PLAN_COST) + ); + } + + /// Without either flag the automatic budget plans the walk. + #[test] + fn without_flags_the_automatic_budget_plans_the_walk() { + let (test, _) = alias_history(); + let plan = ChunkPlan::for_walk(&run_matches(&test, &[]), PLAN_WALK, PLAN_WINDOW, PLAN_COST) + .expect("plan"); + let budget = super::super::budget::default_budget(); + assert_eq!( + plan, + plan_walk( + PLAN_WINDOW, + HistoryRun::workers(), + budget - native_bytes(budget), + PLAN_COST + ) + ); + } + + /// The `-v` plan line names the walk, the chunk, the chunk count, the + /// workers and where the plan came from. + #[test] + fn the_plan_line_describes_the_plan() { + let plan = ChunkPlan { + size: 1000, + workers: 8, + }; + assert_eq!( + plan.describe(PLAN_WALK, 2500, PlanSource::ChunkSize), + "progress: history/devs plan: 1000 commits per chunk, 3 chunks, 8 workers (--chunk-size)" + ); + assert_eq!( + plan.describe(PLAN_WALK, 0, PlanSource::Budget(512 * cf_units::MIB)), + "progress: history/devs plan: 1000 commits per chunk, 0 chunks, 8 workers (budget 512 MiB)" + ); + } + + /// The preflight rejects a bad `--chunk-size` before any walk. + #[test] + fn preflight_rejects_a_zero_chunk_size() { + let (test, _) = alias_history(); + match super::super::validate_history_run(&run_matches(&test, &["--chunk-size", "0"])) { + Err(HandlerError::InvalidFlag(msg)) => { + assert_eq!(msg, "invalid --chunk-size: must be >= 1, got 0"); + } + other => panic!("expected InvalidFlag, got {other:?}"), + } + } +} diff --git a/crates/cf-commands/src/handlers/history/devs.rs b/crates/cf-commands/src/handlers/history/devs.rs new file mode 100644 index 00000000..4646c804 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/devs.rs @@ -0,0 +1,477 @@ +//! `history/devs` over the shared history walk. +//! +//! Per commit: the filtered tree diff against the first parent (commits whose +//! RAW diff exceeds `--max-changes-per-commit` are dropped before any +//! analyzer sees them), per-change line stats (non-merge commits only) and +//! per-blob language attribution. A commit whose filtered diff is empty +//! (a trivial merge, an `--allow-empty` commit, a commit touching only +//! excluded paths) is counted only with `--empty-commits`. The shared +//! identity/tick stamps key the per-commit `CommitDevData` and +//! `commits_by_tick`; `--anonymize` / `--people-dict` / `--exact-signatures` +//! shape the developer names. + +use std::collections::{BTreeMap, HashMap}; + +use cf_devs::{parse_tick_data_with_bounds, CommitDevData, LineStats, MetricOptions, TickBounds}; +use cf_gitlib::blob::CachedBlob; +use cf_gitlib::changes::ChangeAction; + +use cf_units::{KIB, MIB}; + +use super::{ + compute_diff_line_stats, detect_language, gated_product, walk_chunks, ChunkPlan, CommitStamp, + HistoryRun, StampSummary, WalkCost, +}; + +/// `history/devs --format json` bytes (compact, no trailing newline). +#[must_use] +pub fn devs_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + let metrics = devs_run_metrics(sub)?; + Some(cf_gojson::marshal( + &cf_devs::serialize::computed_metrics_to_go(&metrics), + )) +} + +/// `history/devs --format yaml` bytes: the `codefang (v2)` header, the +/// `history/devs:` section name and the metrics. +#[must_use] +pub fn devs_run_report_yaml(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + let metrics = devs_run_metrics(sub)?; + let mut out = Vec::new(); + out.extend_from_slice(b"codefang (v2):\n"); + out.extend_from_slice(format!(" version: {}\n", cf_version::DEFAULT_BINARY).as_bytes()); + out.extend_from_slice(format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes()); + out.extend_from_slice(b"history/devs:\n"); + let body = cf_goyaml::marshal(&cf_devs::serialize::computed_metrics_to_go_yaml(&metrics)); + out.extend_from_slice(&body); + Some(out) +} + +/// `history/devs --format bin` bytes: the CFB1 envelope over the json value. +#[must_use] +pub fn devs_run_report_bin(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + let metrics = devs_run_metrics(sub)?; + let payload = cf_devs::serialize::computed_metrics_to_go(&metrics); + cf_reportutil::encode_binary_envelope(&payload).ok() +} + +/// Runs the devs walk once and returns the aggregated +/// [`cf_devs::ComputedMetrics`], so every output format is an encoding of the +/// SAME report value. +#[must_use] +pub fn devs_run_metrics(sub: &clap::ArgMatches) -> Option<cf_devs::ComputedMetrics> { + let walk = devs_walk(sub)?; + let input = parse_tick_data_with_bounds( + &walk.commit_dev_data, + &walk.commits_by_tick, + walk.names, + walk.tick_size_nanos, + walk.tick_bounds, + ); + Some(cf_devs::compute_all_metrics( + &input, + &MetricOptions::default(), + )) +} + +/// The raw products of one `history/devs` walk, shared by the aggregated +/// metrics path and the per-commit time-series (NDJSON) path. +struct DevsWalk { + /// hex hash → per-commit dev data. + commit_dev_data: BTreeMap<String, CommitDevData>, + /// tick → hashes in walk order; drives commit order. + commits_by_tick: BTreeMap<i64, Vec<String>>, + /// tick → RFC3339-UTC committer-time bounds over CDD commits. + tick_bounds: BTreeMap<i64, TickBounds>, + /// Every kept commit in walk (= tick) order: `(hex, tick, committer + /// seconds, committer UTC-offset minutes)`. + commit_meta: Vec<(String, i64, i64, i32)>, + /// The window position of each `commit_meta` entry (the key every + /// analyzer's ndjson records merge on; a capped commit leaves a gap). + commit_pos: Vec<usize>, + /// Finalized reversed people dictionary (index = author id). + names: Vec<String>, + /// The run's tick size in nanoseconds (the devs activity window unit). + tick_size_nanos: i64, +} + +/// Nanoseconds per hour (the devs tick-size unit). +const NANOS_PER_HOUR: i64 = 3_600 * 1_000_000_000; + +/// The per-commit, order-independent product of one commit's diff (behind +/// the shared drop gate, [`Gated`]). +#[derive(Default)] +struct DevsProduct { + /// Post-filter change count, for the empty-commit gate. + filtered_count: usize, + /// `(language, line-stats)` per attributed change (non-merge only; empty + /// for merges / oversized / empty commits). + attributions: Vec<(String, LineStats)>, +} + +/// Blob-hash → language over a commit's changes (Insert → To, Delete → From, +/// Modify → both sides), later changes overwriting earlier ones for the same +/// blob. +fn language_by_blob( + repo: &cf_gitlib::Repository, + changes: &[cf_gitlib::changes::Change], +) -> HashMap<cf_gitlib::Hash, String> { + let mut langs: HashMap<cf_gitlib::Hash, String> = HashMap::new(); + let mut detect_into = |name: &str, h: cf_gitlib::Hash| { + let lang = match CachedBlob::from_repo(repo, h) { + Ok(b) => detect_language(name, &b.data), + Err(_) => String::new(), + }; + langs.insert(h, lang); + }; + for change in changes { + match change.action { + ChangeAction::Insert => detect_into(&change.to.name, change.to.hash), + ChangeAction::Delete => detect_into(&change.from.name, change.from.hash), + ChangeAction::Modify => { + detect_into(&change.to.name, change.to.hash); + detect_into(&change.from.name, change.from.hash); + } + } + } + langs +} + +/// Line stats of one change: Insert ⇒ the new blob's lines; Delete ⇒ the old +/// blob's lines; Modify ⇒ the shared libgit2 line diff +/// ([`compute_diff_line_stats`], so an empty blob on either side is an +/// ordinary 0-line text side); a same-blob modify, e.g. a pure rename or a +/// mode change, is a 0/0/0 entry that still creates the per-language key. +/// `None` for binary / unreadable blobs. +fn change_line_stats( + repo: &cf_gitlib::Repository, + change: &cf_gitlib::changes::Change, +) -> Option<LineStats> { + match change.action { + ChangeAction::Insert => { + let lines = CachedBlob::from_repo(repo, change.to.hash) + .ok()? + .count_lines() + .ok()?; + Some(LineStats { + added: lines as i64, + removed: 0, + changed: 0, + }) + } + ChangeAction::Delete => { + let lines = CachedBlob::from_repo(repo, change.from.hash) + .ok()? + .count_lines() + .ok()?; + Some(LineStats { + added: 0, + removed: lines as i64, + changed: 0, + }) + } + ChangeAction::Modify => { + let old_lines = CachedBlob::from_repo(repo, change.from.hash) + .ok()? + .count_lines() + .ok()?; + CachedBlob::from_repo(repo, change.to.hash) + .ok()? + .count_lines() + .ok()?; + if change.from.hash == change.to.hash { + return Some(LineStats::default()); + } + let (added, removed, changed) = + compute_diff_line_stats(repo, change.from.hash, change.to.hash, old_lines as i64); + Some(LineStats { + added, + removed, + changed, + }) + } + } +} + +/// The per-commit pure product over the FILTERED changes: the filtered count +/// and the `(language, line-stats)` attributions (non-merge commits only). +fn devs_commit_product( + repo: &cf_gitlib::Repository, + commit: &cf_gitlib::commit::Commit<'_>, + changes: &[cf_gitlib::changes::Change], + first_parent: bool, +) -> DevsProduct { + let mut attributions: Vec<(String, LineStats)> = Vec::new(); + if !super::is_merge(commit.num_parents(), first_parent) && !changes.is_empty() { + let langs = language_by_blob(repo, changes); + for change in changes { + let Some(stats) = change_line_stats(repo, change) else { + continue; + }; + let data_hash = match change.action { + ChangeAction::Delete => change.from.hash, + _ => change.to.hash, + }; + let lang = langs.get(&data_hash).cloned().unwrap_or_default(); + attributions.push((lang, stats)); + } + } + DevsProduct { + filtered_count: changes.len(), + attributions, + } +} + +/// `--empty-commits`: count commits whose filtered diff is empty. +fn empty_commits_flag(sub: &clap::ArgMatches) -> bool { + sub.try_get_one::<bool>("empty-commits") + .ok() + .flatten() + .copied() + .unwrap_or(false) +} + +/// The devs reduce state: each kept commit's product is folded into the +/// per-commit records the report and the per-commit formats read, then +/// dropped — the attributions never outlive their chunk. +struct DevsFold { + /// `--empty-commits`. + count_empty: bool, + /// hex hash → per-commit dev data. + commit_dev_data: BTreeMap<String, CommitDevData>, + /// tick → hashes in walk order. + commits_by_tick: BTreeMap<i64, Vec<String>>, + /// tick → `(earliest, latest)` committer seconds over CDD commits. + tick_when: BTreeMap<i64, (i64, i64)>, + /// Every kept commit: `(hex, tick, committer seconds, UTC-offset minutes)`. + commit_meta: Vec<(String, i64, i64, i32)>, + /// The window position of each `commit_meta` entry. + commit_pos: Vec<usize>, +} + +impl DevsFold { + /// An empty fold; `count_empty` is `--empty-commits`. + fn new(count_empty: bool) -> Self { + Self { + count_empty, + commit_dev_data: BTreeMap::new(), + commits_by_tick: BTreeMap::new(), + tick_when: BTreeMap::new(), + commit_meta: Vec::new(), + commit_pos: Vec::new(), + } + } + + /// Folds one kept commit, in window order. + fn consume(&mut self, stamp: &CommitStamp, product: DevsProduct) { + let hex = &stamp.hex; + + // commits_by_tick and commit_meta record EVERY kept commit (the + // window visits each hash once). + self.commits_by_tick + .entry(stamp.tick) + .or_default() + .push(hex.clone()); + self.commit_meta + .push((hex.clone(), stamp.tick, stamp.when, stamp.offset_min)); + self.commit_pos.push(stamp.pos); + + // Empty-commit gate: no record when the FILTERED tree diff is empty, + // unless --empty-commits asks for those commits to be counted. + if product.filtered_count == 0 && !self.count_empty { + return; + } + + let mut cdd = CommitDevData { + commits: 1, + added: 0, + removed: 0, + changed: 0, + author_id: stamp.author_id, + languages: BTreeMap::new(), + }; + for (lang, stats) in product.attributions { + cdd.added += stats.added; + cdd.removed += stats.removed; + cdd.changed += stats.changed; + let ls = cdd.languages.entry(lang).or_default(); + *ls = ls.plus(stats); + } + self.commit_dev_data.insert(hex.clone(), cdd); + + self.tick_when + .entry(stamp.tick) + .and_modify(|(lo, hi)| { + *lo = (*lo).min(stamp.when); + *hi = (*hi).max(stamp.when); + }) + .or_insert((stamp.when, stamp.when)); + } + + /// The finished walk: the folded records, the RFC3339 tick bounds and + /// the developer names. + fn finish(self, summary: StampSummary, tick_size_nanos: i64) -> DevsWalk { + let tick_bounds = self + .tick_when + .iter() + .map(|(&tick, &(lo, hi))| { + ( + tick, + TickBounds { + start_time: cf_analyze::metadata::format_rfc3339_utc(lo), + end_time: cf_analyze::metadata::format_rfc3339_utc(hi), + }, + ) + }) + .collect(); + DevsWalk { + commit_dev_data: self.commit_dev_data, + commits_by_tick: self.commits_by_tick, + tick_bounds, + commit_meta: self.commit_meta, + commit_pos: self.commit_pos, + names: summary.names, + tick_size_nanos, + } + } +} + +/// The devs walk's measured cost. Kubernetes, 31,923 commits: one worker +/// peaks at 825 MB, 24 workers at 1416 MB (25 MiB per added worker); the +/// whole window and 50-commit chunks peak alike (the per-commit product is a +/// few line-stat records). +const DEVS_COST: WalkCost = WalkCost { + worker_bytes: 32 * MIB, + product_bytes: KIB, +}; + +/// Runs the devs walk over the run's chunk plan: the gated per-commit +/// products are computed in parallel one chunk at a time and folded in +/// window order ([`walk_chunks`]). +fn devs_walk(sub: &clap::ArgMatches) -> Option<DevsWalk> { + let run = HistoryRun::open(sub)?; + let plan = ChunkPlan::for_walk(sub, "history/devs", run.hashes.len(), DEVS_COST).ok()?; + let max_changes = run.max_changes; + let filter = &run.filter; + let first_parent = run.first_parent; + let mut fold = DevsFold::new(empty_commits_flag(sub)); + let summary = walk_chunks( + &run, + plan, + move |repo, hash| { + gated_product(repo, hash, filter, max_changes, |commit, changes| { + devs_commit_product(repo, commit, changes, first_parent) + }) + }, + |gated| gated.oversized(max_changes), + |stamp, gated| fold.consume(stamp, gated.product), + )?; + Some(fold.finish(summary, run.tick_size_hours() * NANOS_PER_HOUR)) +} + +/// The per-commit devs entry as a key-sorted map (`commits`, `lines_added`, +/// `lines_removed`, `lines_changed`, `net_change`, `author_id`, `languages`). +fn devs_entry_map(cdd: &CommitDevData) -> cf_gojson::GoValue { + use cf_gojson::{GoMap, GoValue}; + let mut devs = GoMap::new_map(); + devs.insert("commits".to_string(), GoValue::Int(cdd.commits)); + devs.insert("lines_added".to_string(), GoValue::Int(cdd.added)); + devs.insert("lines_removed".to_string(), GoValue::Int(cdd.removed)); + devs.insert("lines_changed".to_string(), GoValue::Int(cdd.changed)); + devs.insert( + "net_change".to_string(), + GoValue::Int(cdd.added - cdd.removed), + ); + devs.insert("author_id".to_string(), GoValue::Int(cdd.author_id)); + if !cdd.languages.is_empty() { + devs.insert("languages".to_string(), languages_map(&cdd.languages)); + } + GoValue::Object(devs) +} + +/// `map[string]LineStats` (key-sorted by language, each in `added` / `removed` +/// / `changed` field order). +fn languages_map(languages: &BTreeMap<String, LineStats>) -> cf_gojson::GoValue { + use cf_gojson::{GoMap, GoValue}; + let mut langs = GoMap::new_map(); + for (lang, stats) in languages { + let mut ls = GoMap::new_struct(); + ls.insert("added".to_string(), GoValue::Int(stats.added)); + ls.insert("removed".to_string(), GoValue::Int(stats.removed)); + ls.insert("changed".to_string(), GoValue::Int(stats.changed)); + langs.insert(lang.clone(), GoValue::Object(ls)); + } + GoValue::Object(langs) +} + +/// Per-commit devs NDJSON records: one record per data-bearing commit in walk +/// order; `data` is the per-commit struct (`commits`, `lines_added`, +/// `lines_removed`, `lines_changed`, `author_id`, `languages`). +#[must_use] +pub fn devs_ndjson_records( + sub: &clap::ArgMatches, +) -> Option<Vec<super::super::history_formats::NdjsonRecord>> { + use cf_gojson::{GoMap, GoValue}; + + let walk = devs_walk(sub)?; + let mut records = Vec::new(); + // Records carry the commit's WINDOW position: the merge with the other + // leaves in `history_ndjson` keys on it, so neither a data-less commit + // nor one dropped by `--max-changes-per-commit` may shift it. + for (&pos, (hex, tick, secs, off)) in walk.commit_pos.iter().zip(&walk.commit_meta) { + let Some(cdd) = walk.commit_dev_data.get(hex) else { + continue; + }; + let mut data = GoMap::new_struct(); + data.insert("commits".to_string(), GoValue::Int(cdd.commits)); + data.insert("lines_added".to_string(), GoValue::Int(cdd.added)); + data.insert("lines_removed".to_string(), GoValue::Int(cdd.removed)); + data.insert("lines_changed".to_string(), GoValue::Int(cdd.changed)); + data.insert("author_id".to_string(), GoValue::Int(cdd.author_id)); + if !cdd.languages.is_empty() { + data.insert("languages".to_string(), languages_map(&cdd.languages)); + } + records.push(super::super::history_formats::NdjsonRecord { + pos, + hash: hex.clone(), + tick: *tick, + author_id: cdd.author_id, + time_secs: *secs, + tz_offset_min: *off, + data: GoValue::Object(data), + }); + } + Some(records) +} + +/// The devs contribution to the merged `--format timeseries` document: the +/// per-commit entries plus the walk's own commit order (every kept commit, +/// tick order), so a devs-only document lists its commits. +#[must_use] +pub fn devs_timeseries_contribution( + sub: &clap::ArgMatches, +) -> Option<super::super::history_formats::TimeSeriesContribution> { + let walk = devs_walk(sub)?; + let per_commit = walk + .commit_dev_data + .iter() + .map(|(hex, cdd)| (hex.clone(), devs_entry_map(cdd))) + .collect(); + let commit_meta = walk + .commit_meta + .iter() + .map(|(hex, tick, secs, off)| { + ( + hex.clone(), + *tick, + crate::handlers::format_rfc3339_offset(*secs, *off), + String::new(), + ) + }) + .collect(); + Some(super::super::history_formats::TimeSeriesContribution { + flag: "devs", + per_commit, + commit_meta, + }) +} diff --git a/crates/cf-commands/src/handlers/history/file_history.rs b/crates/cf-commands/src/handlers/history/file_history.rs new file mode 100644 index 00000000..750201cc --- /dev/null +++ b/crates/cf-commands/src/handlers/history/file_history.rs @@ -0,0 +1,888 @@ +//! `history/file-history` over the shared history walk. +//! +//! A commit whose raw tree diff exceeds `--max-changes-per-commit` is dropped +//! before the walk, as for every other history analyzer. Per commit +//! (chronological order): the filtered tree diff against the first +//! parent drives the per-path commit-hash lists (Insert RESETS the list, +//! Delete / Modify append, a rename moves the history to the new path), the +//! per-change line stats (non-merge commits only) are attributed to the +//! author, and every changed file is classified by path AND blob content +//! into the composition categories — counted once per file in the window +//! and once per file in each tick it changed in, following renames. Only +//! files present in the NEWEST commit of the window survive into the +//! report; the tick bounds come from the window's committer times. + +use std::collections::BTreeMap; + +use cf_composition::classifier::Classifier; +use cf_file_history::metrics::{CompositionInput, FileHistory, ReportData, TickBounds}; +use cf_file_history::tc::{CategoryCounts, LineStats}; +use cf_file_history::{compute_all_metrics_with_options, computed_metrics_to_go, MetricOptions}; +use cf_gitlib::blob::CachedBlob; +use cf_gitlib::changes::{Change, ChangeAction}; + +use cf_units::{KIB, MIB}; + +use super::{ + commit_tree_changes, compute_diff_line_stats, is_merge, walk_chunks, ChunkPlan, CommitStamp, + HistoryRun, WalkCost, +}; + +/// Bytes of blob content the composition classifier reads: enough for the +/// binary sniff, a shebang and a generated-code marker. +const CLASSIFY_HEADER_BYTES: usize = 8192; + +/// The file-history report value (json/yaml/bin source). +#[must_use] +pub fn file_history_report_value(sub: &clap::ArgMatches) -> Option<cf_gojson::GoValue> { + file_history_run(sub).map(|r| r.report_value) +} + +/// The TYPED `ComputedMetrics` behind [`file_history_report_value`] (same +/// walk, same metrics product) for the text serializer. +#[must_use] +pub fn file_history_run_metrics( + sub: &clap::ArgMatches, +) -> Option<cf_file_history::ComputedMetrics> { + file_history_run(sub).map(|r| r.metrics) +} + +/// One walked commit's file-history products plus the shared stamps. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct FileHistoryCommit { + /// Walk position. + pub pos: usize, + /// Full hex hash. + pub hash: String, + /// The per-commit payload. + pub data: FileHistoryCommitData, + /// Tick. + pub tick: i64, + /// Author id. + pub author_id: i64, + /// Committer time, Unix seconds. + pub when: i64, + /// Committer UTC-offset minutes. + pub offset_min: i32, +} + +/// The per-commit file-history payload. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct FileHistoryCommitData { + /// `(path, action, from_path, to_path)` per change, in change order; action + /// is Insert 0 / Delete 1 / Modify 2. Renames carry an empty `path` and + /// the from/to pair. + pub path_actions: Vec<(String, i64, String, String)>, + /// `(path, stats)` line-stat updates; empty for merges. + pub line_stats: Vec<(String, LineStats)>, + /// Whether the commit is a merge (line stats cleared). + pub is_merge: bool, + /// Per-commit composition counts (one per changed file). + pub composition: CategoryCounts, +} + +/// The full products of one file-history walk: the aggregated report value +/// plus the per-commit stream (chronological). +#[derive(Debug, PartialEq)] +pub(crate) struct FileHistoryRun { + /// The aggregated report GoValue (json/yaml/bin source). + pub report_value: cf_gojson::GoValue, + /// The typed metrics `report_value` was rendered from (text source). + pub metrics: cf_file_history::ComputedMetrics, + /// Per-commit products, oldest-first. + pub commits: Vec<FileHistoryCommit>, +} + +/// The per-commit, order-independent products of one commit's diff. +struct FileHistoryPrepared { + /// Size of the raw tree diff (before the change filter); a commit above + /// `--max-changes-per-commit` is dropped and carries no other product. + raw_change_count: usize, + /// Filtered tree-diff changes (drives the order-sensitive hash-list + /// maintenance in the reduce). + changes: Vec<Change>, + /// `(name, line-stats)` per non-merge change (empty for a merge commit). + line_stats: Vec<(String, LineStats)>, + /// The composition category of every change, parallel to `changes`. + categories: Vec<cf_file_history::Category>, +} + +/// Line stats of one change (`None` for binary / unreadable blobs). +fn change_line_stats(repo: &cf_gitlib::Repository, change: &Change) -> Option<(String, LineStats)> { + match change.action { + ChangeAction::Insert => { + let added = CachedBlob::from_repo(repo, change.to.hash) + .ok()? + .count_lines() + .ok()?; + Some(( + change.to.name.clone(), + LineStats { + added: added as i64, + removed: 0, + changed: 0, + }, + )) + } + ChangeAction::Delete => { + let removed = CachedBlob::from_repo(repo, change.from.hash) + .ok()? + .count_lines() + .ok()?; + Some(( + change.from.name.clone(), + LineStats { + added: 0, + removed: removed as i64, + changed: 0, + }, + )) + } + ChangeAction::Modify => { + let blob_from = CachedBlob::from_repo(repo, change.from.hash).ok()?; + let blob_to = CachedBlob::from_repo(repo, change.to.hash).ok()?; + if blob_from.is_binary() || blob_to.is_binary() { + return None; + } + let (added, removed, changed) = if change.from.hash == change.to.hash { + (0, 0, 0) + } else { + let old_lines = blob_from.count_lines().map_or(0, |n| n as i64); + compute_diff_line_stats(repo, change.from.hash, change.to.hash, old_lines) + }; + Some(( + change.to.name.clone(), + LineStats { + added, + removed, + changed, + }, + )) + } + } +} + +/// The path a change is classified and counted under: the new side, or the +/// old side of a deletion. +fn composition_path(change: &Change) -> &str { + match change.action { + ChangeAction::Delete => &change.from.name, + _ => &change.to.name, + } +} + +/// Whether a Modify change carries a rename. +fn is_rename(change: &Change) -> bool { + change.action == ChangeAction::Modify && change.from.name != change.to.name +} + +/// The composition category of one change: the shared classifier over the +/// path and the leading [`CLASSIFY_HEADER_BYTES`] of the blob (the new side, +/// or the old side of a deletion; an unreadable blob classifies by path). +fn change_category( + repo: &cf_gitlib::Repository, + classifier: &Classifier, + change: &Change, +) -> cf_file_history::Category { + let hash = match change.action { + ChangeAction::Delete => change.from.hash, + _ => change.to.hash, + }; + let path = composition_path(change); + let category = match repo.lookup_blob(hash) { + Ok(blob) => { + let content = blob.content(); + classifier.classify(path, &content[..content.len().min(CLASSIFY_HEADER_BYTES)]) + } + Err(_) => classifier.classify(path, &[]), + }; + map_category(category) +} + +/// The per-commit pure product: filtered changes, line stats (non-merge) and +/// the content-aware composition category of every change — nothing but the +/// raw diff size for a commit above `max_changes`. +fn file_history_commit_product( + repo: &cf_gitlib::Repository, + commit: &cf_gitlib::commit::Commit<'_>, + filter: &crate::handlers::HistoryFilter, + first_parent: bool, + max_changes: usize, +) -> Option<FileHistoryPrepared> { + let mut changes = commit_tree_changes(repo, commit)?; + let raw_change_count = changes.len(); + if raw_change_count > max_changes { + return Some(FileHistoryPrepared { + raw_change_count, + changes: Vec::new(), + line_stats: Vec::new(), + categories: Vec::new(), + }); + } + let classifier = Classifier::new(); + filter.retain_changes(repo, &mut changes); + + let line_stats: Vec<(String, LineStats)> = + if super::is_merge(commit.num_parents(), first_parent) { + Vec::new() + } else { + changes + .iter() + .filter_map(|change| change_line_stats(repo, change)) + .collect() + }; + + let categories = changes + .iter() + .map(|change| change_category(repo, &classifier, change)) + .collect(); + + Some(FileHistoryPrepared { + raw_change_count, + changes, + line_stats, + categories, + }) +} + +/// The per-commit path actions in change order (renames as from/to pairs). +fn path_actions(changes: &[Change]) -> Vec<(String, i64, String, String)> { + let mut out = Vec::with_capacity(changes.len()); + for change in changes { + if is_rename(change) { + out.push(( + String::new(), + 2, + change.from.name.clone(), + change.to.name.clone(), + )); + continue; + } + let (path, action) = match change.action { + ChangeAction::Insert => (change.to.name.clone(), 0), + ChangeAction::Delete => (change.from.name.clone(), 1), + ChangeAction::Modify => (change.to.name.clone(), 2), + }; + out.push((path, action, String::new(), String::new())); + } + out +} + +/// The file-history pre-pass over `run`'s change filter and flags: the pure +/// per-commit product a walk computes in parallel. Captures only the +/// shareable pieces of the run (its repository handle is not `Sync`). +fn file_history_prepare( + run: &HistoryRun, +) -> impl Fn(&cf_gitlib::Repository, cf_gitlib::Hash) -> Option<FileHistoryPrepared> + Sync + '_ { + let filter = &run.filter; + let first_parent = run.first_parent; + let max_changes = run.max_changes; + move |repo, hash| { + let commit = repo.lookup_commit(hash).ok()?; + file_history_commit_product(repo, &commit, filter, first_parent, max_changes) + } +} + +/// Maintains the per-path commit hash lists for one commit: Insert RESETS the +/// list, Delete and same-name Modify APPEND, a rename moves the prior history +/// from `from` to `to` (overwriting any prior `to` history) and appends. +fn apply_path_changes( + files: &mut BTreeMap<String, FileHistory>, + changes: &[Change], + hash_hex: &str, +) { + for change in changes { + if is_rename(change) { + let mut fh = files.remove(&change.from.name).unwrap_or_default(); + fh.hashes.push(hash_hex.to_string()); + files.insert(change.to.name.clone(), fh); + continue; + } + match change.action { + ChangeAction::Insert => { + let fh = files.entry(change.to.name.clone()).or_default(); + fh.hashes = vec![hash_hex.to_string()]; + } + ChangeAction::Delete => { + let fh = files.entry(change.from.name.clone()).or_default(); + fh.hashes.push(hash_hex.to_string()); + } + ChangeAction::Modify => { + let fh = files.entry(change.to.name.clone()).or_default(); + fh.hashes.push(hash_hex.to_string()); + } + } + } +} + +/// The category of every distinct file changed in a scope (the window or one +/// tick), following renames. +type FileCategories = BTreeMap<String, cf_file_history::Category>; + +/// Records one commit's changed files in `scope`: a rename retires the old +/// path, and the path is counted once under its latest category. +fn record_changed_files(scope: &mut FileCategories, prep: &FileHistoryPrepared) { + for (change, &category) in prep.changes.iter().zip(&prep.categories) { + if is_rename(change) { + scope.remove(&change.from.name); + } + scope.insert(composition_path(change).to_string(), category); + } +} + +/// The per-category counts of a scope's distinct files. +fn category_counts(scope: &FileCategories) -> CategoryCounts { + let mut counts = CategoryCounts::default(); + for &category in scope.values() { + counts.increment(category); + } + counts +} + +/// The per-commit composition counts (one per changed file, for the ndjson +/// record). +fn commit_composition(prep: &FileHistoryPrepared) -> CategoryCounts { + let mut counts = CategoryCounts::default(); + for &category in &prep.categories { + counts.increment(category); + } + counts +} + +/// The RFC3339 tick bounds from the per-tick `(earliest, latest)` committer +/// times. +fn tick_bounds(tick_when: &BTreeMap<i64, (i64, i64)>) -> TickBounds { + tick_when + .iter() + .map(|(&tick, &(lo, hi))| { + ( + tick, + ( + cf_analyze::metadata::format_rfc3339_utc(lo), + cf_analyze::metadata::format_rfc3339_utc(hi), + ), + ) + }) + .collect() +} + +/// The file-history reduce state: the per-path histories, the composition +/// scopes, the per-tick committer-time bounds and the per-commit records. +/// Each kept commit's changes and categories are folded in window order and +/// dropped; only its record (what the per-commit formats emit) is kept. +struct FileHistoryFold { + /// The walk's first-parent mode (a merge is an ordinary commit when set). + first_parent: bool, + /// path → commit hashes and per-author line stats. + files: BTreeMap<String, FileHistory>, + /// The category of every distinct file changed in the window. + window_files: FileCategories, + /// tick → the category of every distinct file changed in that tick. + tick_files: BTreeMap<i64, FileCategories>, + /// tick → `(earliest, latest)` committer seconds. + tick_when: BTreeMap<i64, (i64, i64)>, + /// The per-commit records, oldest-first. + commits: Vec<FileHistoryCommit>, +} + +impl FileHistoryFold { + /// An empty fold for a walk in `first_parent` mode. + const fn new(first_parent: bool) -> Self { + Self { + first_parent, + files: BTreeMap::new(), + window_files: BTreeMap::new(), + tick_files: BTreeMap::new(), + tick_when: BTreeMap::new(), + commits: Vec::new(), + } + } + + /// Folds one kept commit, in window order: its changes update the + /// per-path histories and the composition scopes, its line stats the + /// author's per-file totals, then its record is kept. + fn consume(&mut self, stamp: &CommitStamp, prep: FileHistoryPrepared) { + let is_merge = is_merge(stamp.num_parents, self.first_parent); + apply_path_changes(&mut self.files, &prep.changes, &stamp.hex); + if !is_merge { + for (name, stats) in &prep.line_stats { + let fh = self.files.entry(name.clone()).or_default(); + let entry = fh.people.entry(stamp.author_id).or_default(); + *entry = *entry + *stats; + } + } + self.tick_when + .entry(stamp.tick) + .and_modify(|(lo, hi)| { + *lo = (*lo).min(stamp.when); + *hi = (*hi).max(stamp.when); + }) + .or_insert((stamp.when, stamp.when)); + if !prep.changes.is_empty() { + record_changed_files(&mut self.window_files, &prep); + record_changed_files(self.tick_files.entry(stamp.tick).or_default(), &prep); + } + self.commits.push(FileHistoryCommit { + pos: stamp.pos, + hash: stamp.hex.clone(), + data: FileHistoryCommitData { + path_actions: path_actions(&prep.changes), + composition: commit_composition(&prep), + line_stats: prep.line_stats, + is_merge, + }, + tick: stamp.tick, + author_id: stamp.author_id, + when: stamp.when, + offset_min: stamp.offset_min, + }); + } + + /// The finished walk: the histories of the files present in the NEWEST + /// commit of `run`'s window, the metrics over them and the composition + /// scopes, plus the per-commit records. + fn finish(mut self, run: &HistoryRun) -> FileHistoryRun { + retain_newest_files(&mut self.files, run); + let input = ReportData { files: self.files }; + let composition = CompositionInput { + files: category_counts(&self.window_files), + ticks: self + .tick_files + .iter() + .map(|(&tick, scope)| (tick, category_counts(scope))) + .collect(), + tick_bounds: tick_bounds(&self.tick_when), + }; + let metrics = + compute_all_metrics_with_options(&input, MetricOptions::default(), &composition); + FileHistoryRun { + report_value: computed_metrics_to_go(&metrics), + metrics, + commits: self.commits, + } + } +} + +/// Keeps only the files present in the NEWEST commit of `run`'s window (all +/// of them when that commit or its file list cannot be read). +fn retain_newest_files(files: &mut BTreeMap<String, FileHistory>, run: &HistoryRun) { + let Some(newest) = run.hashes.last() else { + return; + }; + let Ok(newest_commit) = run.repo.lookup_commit(*newest) else { + return; + }; + let Ok(iter) = newest_commit.files() else { + return; + }; + let mut present: std::collections::HashSet<String> = std::collections::HashSet::new(); + let _ = iter.for_each(|f| { + present.insert(f.name.clone()); + Ok(()) + }); + files.retain(|name, _| present.contains(name)); +} + +/// The file-history walk's measured cost. Kubernetes, 31,923 commits: one +/// worker peaks at 875 MB, 24 workers at 1420 MB (23 MiB per added worker); +/// the whole window holds 122 bytes more per commit than 50-commit chunks. +const FILE_HISTORY_COST: WalkCost = WalkCost { + worker_bytes: 32 * MIB, + product_bytes: KIB, +}; + +/// Runs the `history/file-history` walk over the run's chunk plan: the +/// per-commit products are computed in parallel one chunk at a time, +/// oversized commits dropped, and folded in window order ([`walk_chunks`]). +/// `None` when the repository cannot be walked. +pub(crate) fn file_history_run(sub: &clap::ArgMatches) -> Option<FileHistoryRun> { + let run = HistoryRun::open(sub)?; + let plan = ChunkPlan::for_walk( + sub, + "history/file-history", + run.hashes.len(), + FILE_HISTORY_COST, + ) + .ok()?; + let max_changes = run.max_changes; + let mut fold = FileHistoryFold::new(run.first_parent); + walk_chunks( + &run, + plan, + file_history_prepare(&run), + |prep| prep.raw_change_count > max_changes, + |stamp, prep| fold.consume(stamp, prep), + )?; + Some(fold.finish(&run)) +} + +/// Serializes one file-history payload as the per-commit record struct +/// (declaration field order; empty slices → null). +fn file_history_data_value( + hash_hex: &str, + cd: &FileHistoryCommitData, + author_id: i64, +) -> cf_gojson::GoValue { + use cf_gojson::{GoMap, GoValue}; + + let hash_bytes: Vec<GoValue> = (0..20) + .map(|i| { + let b = u8::from_str_radix(&hash_hex[i * 2..i * 2 + 2], 16).unwrap_or(0); + GoValue::Int(i64::from(b)) + }) + .collect(); + + let mut data = GoMap::new_struct(); + if cd.path_actions.is_empty() { + data.insert("PathActions".to_string(), GoValue::Null); + } else { + let actions: Vec<GoValue> = cd + .path_actions + .iter() + .map(|(path, action, from, to)| { + let mut m = GoMap::new_struct(); + m.insert("Path".to_string(), GoValue::Str(path.clone())); + m.insert("Action".to_string(), GoValue::Int(*action)); + m.insert("CommitHash".to_string(), GoValue::Array(hash_bytes.clone())); + m.insert("FromPath".to_string(), GoValue::Str(from.clone())); + m.insert("ToPath".to_string(), GoValue::Str(to.clone())); + GoValue::Map(m) + }) + .collect(); + data.insert("PathActions".to_string(), GoValue::Array(actions)); + } + if cd.is_merge || cd.line_stats.is_empty() { + data.insert("LineStatUpdates".to_string(), GoValue::Null); + } else { + let updates: Vec<GoValue> = cd + .line_stats + .iter() + .map(|(path, stats)| { + let mut s = GoMap::new_struct(); + s.insert("added".to_string(), GoValue::Int(stats.added)); + s.insert("removed".to_string(), GoValue::Int(stats.removed)); + s.insert("changed".to_string(), GoValue::Int(stats.changed)); + let mut m = GoMap::new_struct(); + m.insert("Path".to_string(), GoValue::Str(path.clone())); + m.insert("AuthorID".to_string(), GoValue::Int(author_id)); + m.insert("Stats".to_string(), GoValue::Map(s)); + GoValue::Map(m) + }) + .collect(); + data.insert("LineStatUpdates".to_string(), GoValue::Array(updates)); + } + let c = &cd.composition; + let mut comp = GoMap::new_struct(); + comp.insert("source".to_string(), GoValue::Int(c.source)); + comp.insert("vendor".to_string(), GoValue::Int(c.vendor)); + comp.insert("generated".to_string(), GoValue::Int(c.generated)); + comp.insert("documentation".to_string(), GoValue::Int(c.documentation)); + comp.insert("configuration".to_string(), GoValue::Int(c.configuration)); + comp.insert("image".to_string(), GoValue::Int(c.image)); + comp.insert("dotfile".to_string(), GoValue::Int(c.dotfile)); + comp.insert("binary".to_string(), GoValue::Int(c.binary)); + data.insert("Composition".to_string(), GoValue::Map(comp)); + + GoValue::Map(data) +} + +/// Per-commit file-history NDJSON records: every walked commit emits a line. +#[must_use] +pub fn file_history_ndjson_records( + sub: &clap::ArgMatches, +) -> Option<Vec<super::super::history_formats::NdjsonRecord>> { + let run = file_history_run(sub)?; + Some( + run.commits + .iter() + .map(|c| super::super::history_formats::NdjsonRecord { + pos: c.pos, + hash: c.hash.clone(), + tick: c.tick, + author_id: c.author_id, + time_secs: c.when, + tz_offset_min: c.offset_min, + data: file_history_data_value(&c.hash, &c.data, c.author_id), + }) + .collect(), + ) +} + +/// The file-history contribution to the merged `--format timeseries` document: +/// per commit the files-touched / line / action counts, in chronological +/// order (stable by tick). +#[must_use] +pub fn file_history_timeseries_contribution( + sub: &clap::ArgMatches, +) -> Option<super::super::history_formats::TimeSeriesContribution> { + use cf_gojson::{GoMap, GoValue}; + + let run = file_history_run(sub)?; + let mut per_commit = Vec::new(); + let mut commit_meta = Vec::new(); + for c in &run.commits { + let cd = &c.data; + let (mut inserts, mut deletes, mut modifies) = (0i64, 0i64, 0i64); + for (_, action, _, _) in &cd.path_actions { + match action { + 0 => inserts += 1, + 1 => deletes += 1, + _ => modifies += 1, + } + } + let mut lines = LineStats::default(); + if !cd.is_merge { + for (_, stats) in &cd.line_stats { + lines = lines + *stats; + } + } + let mut entry = GoMap::new_map(); + entry.insert( + "files_touched".to_string(), + GoValue::Int(cd.path_actions.len() as i64), + ); + entry.insert("lines_added".to_string(), GoValue::Int(lines.added)); + entry.insert("lines_removed".to_string(), GoValue::Int(lines.removed)); + entry.insert("lines_changed".to_string(), GoValue::Int(lines.changed)); + entry.insert("inserts".to_string(), GoValue::Int(inserts)); + entry.insert("deletes".to_string(), GoValue::Int(deletes)); + entry.insert("modifies".to_string(), GoValue::Int(modifies)); + per_commit.push((c.hash.clone(), GoValue::Map(entry))); + commit_meta.push(( + c.hash.clone(), + c.tick, + crate::handlers::format_rfc3339_offset(c.when, c.offset_min), + String::new(), + )); + } + commit_meta.sort_by_key(|(_, tick, _, _)| *tick); + + Some(super::super::history_formats::TimeSeriesContribution { + flag: "file-history", + per_commit, + commit_meta, + }) +} + +/// Maps a [`cf_composition::category::Category`] to the file-history +/// [`cf_file_history::Category`] of the same name. +fn map_category(cat: cf_composition::category::Category) -> cf_file_history::Category { + use cf_composition::category::Category as C; + use cf_file_history::Category as F; + match cat { + C::Source => F::Source, + C::Vendor => F::Vendor, + C::Generated => F::Generated, + C::Documentation => F::Documentation, + C::Configuration => F::Configuration, + C::Image => F::Image, + C::DotFile => F::DotFile, + C::Binary => F::Binary, + } +} + +#[cfg(test)] +mod tests { + use super::super::fold_testutil::{ + assert_chunk_invariant, chunk_fixture, open_run, run_matches, OVERSIZED_POS, + }; + use super::*; + use cf_file_history::Category; + use cf_gitlib::changes::ChangeEntry; + use cf_gitlib::Hash; + + fn change(action: ChangeAction, from: &str, to: &str) -> Change { + Change { + action, + from: ChangeEntry { + name: from.to_string(), + hash: Hash::ZERO, + size: 0, + mode: 0, + }, + to: ChangeEntry { + name: to.to_string(), + hash: Hash::ZERO, + size: 0, + mode: 0, + }, + } + } + + fn prepared(changes: Vec<Change>, categories: Vec<Category>) -> FileHistoryPrepared { + FileHistoryPrepared { + raw_change_count: changes.len(), + changes, + line_stats: Vec::new(), + categories, + } + } + + /// A file edited in three commits is ONE file of the scope; a rename + /// retires the old path; a deletion still counts the file as changed. + #[test] + fn changed_files_are_counted_once_and_follow_renames() { + let mut scope: FileCategories = BTreeMap::new(); + for _ in 0..3 { + record_changed_files( + &mut scope, + &prepared( + vec![change(ChangeAction::Modify, "a.go", "a.go")], + vec![Category::Source], + ), + ); + } + record_changed_files( + &mut scope, + &prepared( + vec![ + change(ChangeAction::Modify, "a.go", "b.go"), + change(ChangeAction::Delete, "img.png", ""), + ], + vec![Category::Source, Category::Image], + ), + ); + assert_eq!( + scope, + BTreeMap::from([ + ("b.go".to_string(), Category::Source), + ("img.png".to_string(), Category::Image), + ]) + ); + let counts = category_counts(&scope); + assert_eq!((counts.source, counts.image, counts.total()), (1, 1, 2)); + } + + /// The latest classification of a path wins (text that became binary). + #[test] + fn latest_category_wins() { + let mut scope: FileCategories = BTreeMap::new(); + record_changed_files( + &mut scope, + &prepared( + vec![change(ChangeAction::Insert, "", "blob.dat")], + vec![Category::Source], + ), + ); + record_changed_files( + &mut scope, + &prepared( + vec![change(ChangeAction::Modify, "blob.dat", "blob.dat")], + vec![Category::Binary], + ), + ); + assert_eq!(category_counts(&scope).binary, 1); + assert_eq!(category_counts(&scope).source, 0); + } + + /// `--max-changes-per-commit 2` over `c1 (alice: a, b) ; c2 (bob: a, b, + /// c) ; c3 (alice: a)`: Bob's three-change commit is dropped before the + /// walk, as devs and burndown drop it. + #[test] + fn max_changes_per_commit_drops_the_oversized_commit() { + use cf_gitlib::testutil::{commit_files_as, init_repo}; + const T0: i64 = 1_609_459_200; + const DAY: i64 = 86_400; + let test = init_repo().expect("init"); + commit_files_as( + &test, + "alice", + "a@x", + "c1", + T0, + &[("a", b"1\n"), ("b", b"1\n")], + ) + .expect("c1"); + commit_files_as( + &test, + "bob", + "b@x", + "c2", + T0 + DAY, + &[("a", b"2\n"), ("b", b"2\n"), ("c", b"2\n")], + ) + .expect("c2"); + commit_files_as(&test, "alice", "a@x", "c3", T0 + 2 * DAY, &[("a", b"3\n")]).expect("c3"); + let path = test.path().to_str().expect("utf-8"); + let metrics = |extra: &[&str]| { + let argv: Vec<&str> = ["codefang", "run", "-a", "history/file-history"] + .into_iter() + .chain(extra.iter().copied()) + .chain([path]) + .collect(); + let matches = crate::build_codefang_command() + .try_get_matches_from(argv) + .expect("run args parse"); + file_history_run_metrics(matches.subcommand_matches("run").expect("run")) + .expect("file-history report") + }; + let commits = |m: &cf_file_history::ComputedMetrics| -> Vec<(String, i64)> { + let mut v: Vec<(String, i64)> = m + .file_churn + .iter() + .map(|f| (f.path.clone(), f.commit_count)) + .collect(); + v.sort(); + v + }; + + let all = metrics(&[]); + assert_eq!( + commits(&all), + [ + ("a".to_string(), 3), + ("b".to_string(), 2), + ("c".to_string(), 1) + ] + ); + + let capped = metrics(&["--max-changes-per-commit", "2"]); + assert_eq!( + commits(&capped), + [("a".to_string(), 2), ("b".to_string(), 1)] + ); + assert_eq!(capped.aggregate.total_contributors, 1, "{capped:?}"); + } + + #[test] + fn tick_bounds_are_rfc3339_utc() { + let bounds = tick_bounds(&BTreeMap::from([(3, (1_609_459_200, 1_609_462_800))])); + assert_eq!( + bounds[&3], + ( + "2021-01-01T00:00:00Z".to_string(), + "2021-01-01T01:00:00Z".to_string() + ) + ); + } + + /// The analyzer id the fold tests run. + const ID: &str = "history/file-history"; + + /// The file-history walk folds per chunk: folding the chunk fixture + /// (a rename and a delete after the files' first commits, a merge, a + /// file first written by the dropped oversized commit) in chunks of every + /// size finishes to the one-pass walk, and that is what the walk returns + /// for any `--chunk-size`. + #[test] + fn the_walk_folds_in_chunks_to_the_one_pass_value() { + let test = chunk_fixture(); + let run = open_run(&test, ID, &[]); + let max_changes = run.max_changes; + let whole = assert_chunk_invariant( + &run, + file_history_prepare(&run), + |prep| prep.raw_change_count > max_changes, + || FileHistoryFold::new(run.first_parent), + FileHistoryFold::consume, + |fold, _| fold.finish(&run), + ); + let positions: Vec<usize> = whole.commits.iter().map(|c| c.pos).collect(); + assert_eq!(positions.len(), run.hashes.len() - 1, "{positions:?}"); + assert!(!positions.contains(&OVERSIZED_POS), "{positions:?}"); + for size in ["1", "4"] { + let chunked = file_history_run(&run_matches(&test, ID, &["--chunk-size", size])); + assert_eq!(chunked.as_ref(), Some(&whole), "--chunk-size {size}"); + } + } +} diff --git a/crates/cf-commands/src/handlers/history/fold_testutil.rs b/crates/cf-commands/src/handlers/history/fold_testutil.rs new file mode 100644 index 00000000..477d605b --- /dev/null +++ b/crates/cf-commands/src/handlers/history/fold_testutil.rs @@ -0,0 +1,236 @@ +//! Test support for the per-analyzer fold tests +//! (`specs/frds/FRD-bounded-memory-history.md`): one repository exercising +//! every chunk edge, and a driver that folds an analyzer's REAL pre-pass +//! products through [`walk_chunks`] in chunks of every size, checking each +//! finished value against the one-pass value and the live products against +//! the chunk size. + +use std::fmt::Debug; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use cf_gitlib::testutil::{init_repo, CommitBuilder, TestRepo}; +use cf_gitlib::Hash; + +use super::{walk_chunks, ChunkPlan, CommitStamp, HistoryRun, StampSummary}; + +/// Base instant of the fixture: 2021-01-01T00:00:00Z. +const T0: i64 = 1_609_459_200; +/// One day between fixture commits (one default tick each). +const DAY: i64 = 86_400; +/// The fixture developers. +const ALICE: (&str, &str) = ("alice", "alice@example.com"); +const BOB: (&str, &str) = ("bob", "bob@example.com"); +/// Alice under her full name (an email alias). +const ALICE_SMITH: (&str, &str) = ("Alice Smith", "alice@example.com"); +/// Alice under another email (a name alias). +const ALICE_ELSEWHERE: (&str, &str) = ("alice", "alice@elsewhere.org"); +/// The fixture's `--max-changes-per-commit` cap: its oversized commit +/// changes one file more than this. +const FIXTURE_CAP: &str = "3"; +/// Files the oversized commit writes (one above [`FIXTURE_CAP`]). +const OVERSIZED_FILES: usize = 4; +/// The window position of the oversized commit. +pub(crate) const OVERSIZED_POS: usize = 6; +/// The chunk sizes every fold runs with (the window length is added per +/// run): one commit per chunk; two; three, so the merge and the oversized +/// commit open chunks; seven, so the oversized commit closes the first one. +const CHUNK_SIZES: [usize; 4] = [1, 2, 3, 7]; +/// A blob the binary detector rejects (NUL bytes after a PNG signature). +const PNG_V1: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x01"; +/// The binary blob, changed. +const PNG_V2: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x02"; + +/// `n` numbered lines. +fn lines(n: usize) -> Vec<u8> { + (1..=n) + .map(|i| format!("line {i}\n")) + .collect::<String>() + .into_bytes() +} + +/// A Go source body with `n` one-line functions. +fn go_source(n: usize) -> Vec<u8> { + let mut s = String::from("package main\n\n"); + for i in 0..n { + s.push_str(&format!("func f{i}() int {{ return {i} }}\n")); + } + s.into_bytes() +} + +/// A ten-commit window with every edge a chunk boundary can split: +/// +/// | pos | commit | author | edit | +/// |-----|--------|--------|------| +/// | 0 | c1 | alice | `a.go`, `b.txt`, binary `logo.png` | +/// | 1 | side | bob | side line off c1: `c.txt`, `b.txt` | +/// | 2 | c3 | Alice Smith (email alias) | `a.go` | +/// | 3 | merge | alice | merges side | +/// | 4 | rename | bob | `a.go` → `src/a.go` | +/// | 5 | empty | bob | no change | +/// | 6 | oversized | bob | four new files (above the cap) | +/// | 7 | delete | alice (name alias) | deletes `b.txt` | +/// | 8 | binary | alice | `logo.png`, `src/a.go` | +/// | 9 | tail | bob | `c.txt`, `d0.txt` | +pub(crate) fn chunk_fixture() -> TestRepo { + let test = init_repo().expect("init"); + let at = |n: i64| T0 + n * DAY; + let c1 = CommitBuilder::new(&test, "c1", at(0)) + .author(ALICE.0, ALICE.1) + .write("a.go", &go_source(2)) + .write("b.txt", &lines(3)) + .write("logo.png", PNG_V1) + .commit() + .expect("c1"); + let side = CommitBuilder::new(&test, "side", at(1)) + .author(BOB.0, BOB.1) + .parent(c1) + .detached() + .write("c.txt", &lines(2)) + .write("b.txt", &lines(4)) + .commit() + .expect("side"); + CommitBuilder::new(&test, "c3", at(2)) + .author(ALICE_SMITH.0, ALICE_SMITH.1) + .write("a.go", &go_source(3)) + .commit() + .expect("c3"); + CommitBuilder::new(&test, "merge", at(3)) + .author(ALICE.0, ALICE.1) + .merge_parent(side) + .write("c.txt", &lines(2)) + .write("b.txt", &lines(4)) + .commit() + .expect("merge"); + CommitBuilder::new(&test, "rename", at(4)) + .author(BOB.0, BOB.1) + .rename("a.go", "src/a.go") + .commit() + .expect("rename"); + CommitBuilder::new(&test, "empty", at(5)) + .author(BOB.0, BOB.1) + .commit() + .expect("empty"); + let mut oversized = CommitBuilder::new(&test, "oversized", at(6)).author(BOB.0, BOB.1); + for i in 0..OVERSIZED_FILES { + oversized = oversized.write(&format!("d{i}.txt"), &lines(i + 1)); + } + oversized.commit().expect("oversized"); + CommitBuilder::new(&test, "delete", at(7)) + .author(ALICE_ELSEWHERE.0, ALICE_ELSEWHERE.1) + .delete("b.txt") + .commit() + .expect("delete"); + CommitBuilder::new(&test, "binary", at(8)) + .author(ALICE.0, ALICE.1) + .write("logo.png", PNG_V2) + .write("src/a.go", &go_source(4)) + .commit() + .expect("binary"); + CommitBuilder::new(&test, "tail", at(9)) + .author(BOB.0, BOB.1) + .write("c.txt", &lines(5)) + .write("d0.txt", &lines(2)) + .commit() + .expect("tail"); + test +} + +/// The parsed `run` matches for `codefang run -a <id> +/// --max-changes-per-commit 3 <extra> <repo>`. +pub(crate) fn run_matches(test: &TestRepo, id: &str, extra: &[&str]) -> clap::ArgMatches { + let path = test.path().to_str().expect("utf-8 path"); + let argv: Vec<&str> = [ + "codefang", + "run", + "-a", + id, + "--max-changes-per-commit", + FIXTURE_CAP, + ] + .into_iter() + .chain(extra.iter().copied()) + .chain([path]) + .collect(); + crate::build_codefang_command() + .try_get_matches_from(argv) + .expect("run args parse") + .subcommand_matches("run") + .expect("run") + .clone() +} + +/// The opened run of [`run_matches`]. +pub(crate) fn open_run(test: &TestRepo, id: &str, extra: &[&str]) -> HistoryRun { + HistoryRun::open(&run_matches(test, id, extra)).expect("open run") +} + +/// One live product's slot in a counter: a fold test pairs every product +/// with a ticket, so it observes how many products the walk holds at once. +struct Ticket(Arc<AtomicUsize>); + +impl Ticket { + fn new(live: &Arc<AtomicUsize>) -> Self { + live.fetch_add(1, Ordering::SeqCst); + Self(Arc::clone(live)) + } +} + +impl Drop for Ticket { + fn drop(&mut self) { + self.0.fetch_sub(1, Ordering::SeqCst); + } +} + +/// Folds `run` in chunks of `size` through an analyzer's REAL pre-pass +/// (`prepare`), drop gate (`oversized`) and fold (`consume` per kept commit +/// into `fold`), asserting that no more than one chunk of products is ever +/// alive and that every product is dropped; returns the folded state and +/// the identity summary for the analyzer's `finish`. +fn fold_in_chunks<P: Send, F>( + run: &HistoryRun, + size: usize, + prepare: &(impl Fn(&cf_gitlib::Repository, Hash) -> Option<P> + Sync), + oversized: &impl Fn(&P) -> bool, + mut fold: F, + consume: &impl Fn(&mut F, &CommitStamp, P), +) -> (F, StampSummary) { + let live = Arc::new(AtomicUsize::new(0)); + let summary = walk_chunks( + run, + ChunkPlan::sized(size), + |repo, hash| prepare(repo, hash).map(|p| (Ticket::new(&live), p)), + |(_, product)| oversized(product), + |stamp, (ticket, product)| { + let now = live.load(Ordering::SeqCst); + assert!(now <= size, "{now} live products with chunk size {size}"); + consume(&mut fold, stamp, product); + drop(ticket); + }, + ) + .expect("walk"); + assert_eq!(live.load(Ordering::SeqCst), 0, "every product dropped"); + (fold, summary) +} + +/// Folding `run` in chunks of every [`CHUNK_SIZES`] size and of the window +/// length finishes to the one-pass value, holding at most one chunk of +/// products at a time; returns the one-pass value. +pub(crate) fn assert_chunk_invariant<P: Send, F, R: PartialEq + Debug>( + run: &HistoryRun, + prepare: impl Fn(&cf_gitlib::Repository, Hash) -> Option<P> + Sync, + oversized: impl Fn(&P) -> bool, + new: impl Fn() -> F, + consume: impl Fn(&mut F, &CommitStamp, P), + finish: impl Fn(F, StampSummary) -> R, +) -> R { + let folded = |size: usize| { + let (fold, summary) = fold_in_chunks(run, size, &prepare, &oversized, new(), &consume); + finish(fold, summary) + }; + let whole = folded(ChunkPlan::whole_window().size); + for size in CHUNK_SIZES.into_iter().chain([run.hashes.len()]) { + assert_eq!(folded(size), whole, "chunk size {size}"); + } + whole +} diff --git a/crates/cf-commands/src/handlers/history/imports.rs b/crates/cf-commands/src/handlers/history/imports.rs new file mode 100644 index 00000000..19c5b975 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/imports.rs @@ -0,0 +1,212 @@ +//! `history/imports` over the shared history walk. +//! +//! Per commit: the filtered tree diff, then for every Insert/Modify change's +//! After blob a gated UAST parse and [`cf_imports::extract_imports`] with +//! the parser's language for the file and the blob bytes (Kotlin reads its +//! import statements back from the source). The shared identity/tick stamps +//! key the 4-level `author → language → import → tick → count` map; the +//! [`cf_imports::HistoryReport`] (tick size, author index, the map keyed by +//! developer name) is the ONE value json/yaml/bin encode. + +use std::collections::BTreeMap; +use std::sync::Arc; + +use cf_analyzers_plumbing::author_missing_id; +use cf_gitlib::changes::ChangeAction; +use cf_imports::history::{add_entries_to_map, aggregate_import_counts, ImportEntry, ImportsMap}; +use cf_imports::HistoryReport; + +use super::super::uast_walk::{uast_results, ChangeParses, ParseOutcome, Side, UastAnalyzer}; +use super::{CommitStamp, StampSummary}; + +/// Computes the `history/imports` [`cf_imports::HistoryReport`] over the +/// commit stream (the format-independent report value). +#[must_use] +pub fn imports_run_metrics(sub: &clap::ArgMatches) -> Option<HistoryReport> { + let walk = imports_walk(sub)?; + let unmatched = walk.summary.unmatched_slot as i64; + let mut imports = ImportsMap::new(); + for c in &walk.commits { + let author = if c.author_id == author_missing_id() { + unmatched + } else { + c.author_id + }; + add_entries_to_map(&mut imports, &c.entries, author, c.tick); + } + Some(HistoryReport { + tick_size_secs: walk.tick_size_secs, + author_index: walk.summary.names.clone(), + imports, + }) +} + +/// Aggregated per-import usage counts over the same report as +/// [`imports_run_metrics`]: total count per import path across every +/// developer, language and tick (the plot's input). +#[must_use] +pub fn imports_run_usage_counts(sub: &clap::ArgMatches) -> Option<BTreeMap<String, i64>> { + let report = imports_run_metrics(sub)?; + Some(aggregate_import_counts(&report.imports)) +} + +/// One walked commit's imports products plus the shared stamps. +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) struct ImportsCommit { + /// Position in the ORIGINAL window (oversized-dropped commits count). + pub pos: usize, + /// Full hex hash. + pub hash: String, + /// Extracted `(lang, import)` entries (empty ⇒ no ndjson line, no + /// timeseries entry). + pub entries: Vec<ImportEntry>, + /// Author id. + pub author_id: i64, + /// Tick. + pub tick: i64, + /// Committer time, Unix seconds. + pub when: i64, + /// Committer UTC-offset minutes. + pub offset_min: i32, +} + +impl ImportsCommit { + /// The record of one kept commit: its stamps and its import entries. + pub(crate) fn new(stamp: &CommitStamp, entries: Vec<ImportEntry>) -> Self { + Self { + pos: stamp.pos, + hash: stamp.hex.clone(), + entries, + author_id: stamp.author_id, + tick: stamp.tick, + when: stamp.when, + offset_min: stamp.offset_min, + } + } +} + +/// The `history/imports` walk: the per-commit records plus the identity +/// state that names the authors and the configured tick size. +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) struct ImportsWalk { + /// One entry per walked commit, oldest first. + pub commits: Vec<ImportsCommit>, + /// The identity state the commits were stamped with. + pub summary: StampSummary, + /// `--tick-size` in seconds. + pub tick_size_secs: i64, +} + +/// The `history/imports` walk of the run's UAST pass: all imports formats +/// consume THIS one walk. +pub(crate) fn imports_walk(sub: &clap::ArgMatches) -> Option<Arc<ImportsWalk>> { + uast_results(sub, UastAnalyzer::Imports)?.imports +} + +/// One change's `history/imports` contribution: the `(lang, import)` entries +/// extracted from a surviving After tree, each tagged with the parser's +/// language for the file, appended to the commit's entries (so they follow +/// its change order). +pub(crate) fn imports_change(parses: &ChangeParses<'_>, entries: &mut Vec<ImportEntry>) { + let change = parses.change(); + if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { + return; + } + let ParseOutcome::Parsed(root) = parses.parse(Side::After) else { + return; + }; + let Some(blob) = parses.blob(Side::After) else { + return; + }; + let lang = parses.parser().get_language(&change.to.name); + for imp in cf_imports::extract_imports(root, &lang, &blob.data) { + entries.push(ImportEntry { + lang: lang.clone(), + import: imp.path, + }); + } +} + +/// Per-commit imports NDJSON records: `data` is `{"authorID", "entries": +/// [{Lang, Import}]}`; commits with no entries emit no line. +#[must_use] +pub fn imports_ndjson_records( + sub: &clap::ArgMatches, +) -> Option<Vec<super::super::history_formats::NdjsonRecord>> { + use cf_gojson::{GoMap, GoValue}; + + let walk = imports_walk(sub)?; + let mut records = Vec::new(); + for c in &walk.commits { + if c.entries.is_empty() { + continue; + } + let mut data = GoMap::new_map(); + data.insert("authorID".to_string(), GoValue::Int(c.author_id)); + let entries: Vec<GoValue> = c + .entries + .iter() + .map(|e| { + let mut m = GoMap::new_struct(); + m.insert("Lang".to_string(), GoValue::Str(e.lang.clone())); + m.insert("Import".to_string(), GoValue::Str(e.import.clone())); + GoValue::Object(m) + }) + .collect(); + data.insert("entries".to_string(), GoValue::Array(entries)); + records.push(super::super::history_formats::NdjsonRecord { + pos: c.pos, + hash: c.hash.clone(), + tick: c.tick, + author_id: c.author_id, + time_secs: c.when, + tz_offset_min: c.offset_min, + data: GoValue::Object(data), + }); + } + Some(records) +} + +/// The imports contribution to the merged `--format timeseries` document: per +/// entry-bearing commit `{"import_count": n, "languages": {lang: count}}`. +#[must_use] +pub fn imports_timeseries_contribution( + sub: &clap::ArgMatches, +) -> Option<super::super::history_formats::TimeSeriesContribution> { + use cf_gojson::{GoMap, GoValue}; + + let walk = imports_walk(sub)?; + let mut per_commit = Vec::new(); + let mut commit_meta = Vec::new(); + for c in &walk.commits { + if c.entries.is_empty() { + continue; + } + let mut languages: BTreeMap<&str, i64> = BTreeMap::new(); + for e in &c.entries { + *languages.entry(e.lang.as_str()).or_insert(0) += 1; + } + let mut entry = GoMap::new_map(); + entry.insert( + "import_count".to_string(), + GoValue::Int(c.entries.len() as i64), + ); + let mut langs = GoMap::new_map(); + for (lang, n) in &languages { + langs.insert((*lang).to_string(), GoValue::Int(*n)); + } + entry.insert("languages".to_string(), GoValue::Object(langs)); + per_commit.push((c.hash.clone(), GoValue::Object(entry))); + commit_meta.push(( + c.hash.clone(), + c.tick, + crate::handlers::format_rfc3339_offset(c.when, c.offset_min), + String::new(), + )); + } + Some(super::super::history_formats::TimeSeriesContribution { + flag: "imports-per-dev", + per_commit, + commit_meta, + }) +} diff --git a/crates/cf-commands/src/handlers/history/mod.rs b/crates/cf-commands/src/handlers/history/mod.rs new file mode 100644 index 00000000..bf946583 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/mod.rs @@ -0,0 +1,537 @@ +//! The SHARED history-walk core every `history/*` analyzer runs through, plus +//! one submodule per analyzer owning that analyzer's per-commit product and +//! report shaping. +//! +//! The shared pieces live here so they exist exactly once: +//! +//! * [`HistoryOptions`] — the validated run flags (`--head`, `--limit`, +//! `--first-parent`, `--since`, `--tick-size`, `--max-changes-per-commit`, +//! `--exact-signatures`, `--people-dict`, `--anonymize`, the `--languages` +//! / vendor / generated change filter); +//! * [`HistoryRun`] — the opened repository, the commit window (oldest-first), +//! the change filter, and the identity/tick configuration; +//! * [`Stamper`] — the ONE identity + tick assignment, one commit at a time +//! in window order (author ids first-seen oldest-first, ticks monotonic); +//! * [`walk_chunks`] — the chunked walk driver: the pure per-commit pre-pass +//! (tree diff, line stats, parses) runs on a worker pool +//! ([`pool::prepare_in_order`]) at most one [`ChunkPlan`] chunk ahead of +//! the fold, and each kept commit is stamped and folded into the walk's +//! reduce state in window order, then its product is dropped. The output is +//! a pure function of the repository and the flags — never of the host CPU +//! count or the chunk size; +//! * [`Gated`] / [`gated_product`] — the `--max-changes-per-commit` drop gate +//! every walk applies to its RAW tree diff before stamping. +//! +//! The analyzer MATH lives in the cf-* crates the submodules call; report +//! bytes route through cf-gojson / cf-goyaml / cf-reportutil. + +pub mod anomaly; +mod budget; +mod chunks; +pub mod devs; +pub mod file_history; +#[cfg(test)] +pub(crate) mod fold_testutil; +pub mod imports; +mod pool; +pub mod quality; +pub mod sentiment; +pub mod typos; + +pub use anomaly::{anomaly_ndjson_records, anomaly_run_metrics, anomaly_timeseries_contribution}; +pub use devs::{ + devs_ndjson_records, devs_run_metrics, devs_run_report, devs_run_report_bin, + devs_run_report_yaml, devs_timeseries_contribution, +}; +pub use file_history::{ + file_history_ndjson_records, file_history_report_value, file_history_run_metrics, + file_history_timeseries_contribution, +}; +pub use imports::{ + imports_ndjson_records, imports_run_metrics, imports_run_usage_counts, + imports_timeseries_contribution, +}; +pub use quality::{quality_metrics, quality_ndjson_records, quality_timeseries_contribution}; +pub use sentiment::{ + sentiment_metrics, sentiment_ndjson_records, sentiment_timeseries_contribution, +}; +pub use typos::{ + typos_ndjson_records, typos_report_data, typos_run_report, typos_run_report_bin, + typos_run_report_yaml, +}; + +pub(crate) use budget::{limit_native_memory, other_walks_follow_the_uast_pass, WalkCost}; +pub(crate) use chunks::{fold_planned, walk_chunks, ChunkPlan, StampSummary, Stamper}; + +use cf_analyzers_plumbing::{IdentityDetector, Signature}; +use cf_gitlib::Hash; + +use crate::handlers::{ + history_filter, history_since_spec, load_history_commit_hashes, run_repo_path, HistoryFilter, + HistoryWindow, SinceSpec, +}; +use crate::pipeline::HandlerError; + +thread_local! { + /// One [`cf_uast::Parser`] per worker thread, reused across every commit that + /// thread processes in a pooled ([`pool::prepare_in_order`]) UAST walk. `Parser::new()` + /// registers a lazy parser for every embedded language mapping (hundreds of + /// bloom inserts), so constructing it per commit dominated the parallel + /// runtime. A thread-local amortizes that construction to once per thread + /// while keeping each thread its OWN parser (tree-sitter parsers are not + /// thread-safe, so the parser is never shared ACROSS threads). + static UAST_PARSER: cf_uast::Parser = cf_uast::Parser::new(); +} + +/// Runs `f` with this worker thread's reusable [`cf_uast::Parser`] (see +/// [`UAST_PARSER`]). +pub(crate) fn with_uast_parser<R>(f: impl FnOnce(&cf_uast::Parser) -> R) -> R { + UAST_PARSER.with(|p| f(p)) +} + +/// Default `--max-changes-per-commit` cap (`0` on the flag). +const DEFAULT_MAX_CHANGES_PER_COMMIT: usize = 10_000; + +/// The validated history run flags. +#[derive(Debug, Clone)] +pub(crate) struct HistoryOptions { + /// `--head`: the window is exactly `[HEAD]`. + pub head: bool, + /// `--limit`: the N newest commits (`0` = unlimited). + pub limit: usize, + /// `--first-parent` as the user set it (burndown forces its own walk). + pub first_parent: bool, + /// `--since`. + pub since: SinceSpec, + /// `--tick-size` in hours (validated `> 0`). + pub tick_size_hours: i64, + /// `--max-changes-per-commit` (`0` ⇒ the default cap). + pub max_changes: usize, + /// `--exact-signatures`. + pub exact_signatures: bool, + /// `--people-dict` path when set. + pub people_dict: Option<String>, + /// `--anonymize`. + pub anonymize: bool, + /// The shared change filter (`--languages`, vendor / generated policy). + pub filter: HistoryFilter, +} + +/// Reads an `i64` flag that may be absent from a reduced command tree. +pub(crate) fn flag_i64(sub: &clap::ArgMatches, name: &str, default: i64) -> i64 { + sub.try_get_one::<i64>(name) + .ok() + .flatten() + .copied() + .unwrap_or(default) +} + +/// Reads an `f64` flag that may be absent from a reduced command tree. +pub(crate) fn flag_f64(sub: &clap::ArgMatches, name: &str, default: f64) -> f64 { + sub.try_get_one::<f64>(name) + .ok() + .flatten() + .copied() + .unwrap_or(default) +} + +/// Reads a boolean dynamic flag (registered as `--flag[=<bool>]`). +fn flag_bool(sub: &clap::ArgMatches, name: &str) -> bool { + sub.try_get_one::<bool>(name) + .ok() + .flatten() + .copied() + .unwrap_or(false) +} + +/// Reads a string dynamic flag; empty ⇒ `None`. +fn flag_string(sub: &clap::ArgMatches, name: &str) -> Option<String> { + sub.try_get_one::<String>(name) + .ok() + .flatten() + .filter(|s| !s.is_empty()) + .cloned() +} + +impl HistoryOptions { + /// Parses and validates the history flags. + /// + /// # Errors + /// + /// [`HandlerError::InvalidFlag`] for a negative `--limit`, a non-positive + /// `--tick-size`, a negative `--max-changes-per-commit`, or an unknown + /// `--languages` token (the anomaly flags are validated by + /// [`validate_history_run`]). + pub(crate) fn from_matches(sub: &clap::ArgMatches) -> Result<Self, HandlerError> { + let limit = flag_i64(sub, "limit", 0); + if limit < 0 { + return Err(HandlerError::InvalidFlag(format!( + "invalid --limit: must be >= 0, got {limit}" + ))); + } + let tick_size_hours = flag_i64( + sub, + "tick-size", + cf_analyzers_plumbing::DEFAULT_TICK_SIZE_HOURS, + ); + if tick_size_hours <= 0 { + return Err(HandlerError::InvalidFlag(format!( + "invalid --tick-size: must be > 0, got {tick_size_hours}" + ))); + } + let max_changes = flag_i64(sub, "max-changes-per-commit", 0); + if max_changes < 0 { + return Err(HandlerError::InvalidFlag(format!( + "invalid --max-changes-per-commit: must be >= 0, got {max_changes}" + ))); + } + let filter = history_filter(sub).map_err(HandlerError::InvalidFlag)?; + Ok(Self { + head: sub.get_flag("head"), + limit: limit as usize, + first_parent: sub.get_flag("first-parent"), + since: history_since_spec(sub), + tick_size_hours, + max_changes: if max_changes == 0 { + DEFAULT_MAX_CHANGES_PER_COMMIT + } else { + max_changes as usize + }, + exact_signatures: flag_bool(sub, "exact-signatures"), + people_dict: flag_string(sub, "people-dict"), + anonymize: flag_bool(sub, "anonymize"), + filter, + }) + } + + /// The identity detector configured by `--exact-signatures` and + /// `--people-dict`. + fn identity(&self) -> std::io::Result<IdentityDetector> { + let mut identity = IdentityDetector::new(); + identity.exact_signatures = self.exact_signatures; + if let Some(path) = &self.people_dict { + identity.load_people_dict(path)?; + } + Ok(identity) + } +} + +/// Validates everything a history run needs BEFORE any walk: the flags, the +/// repository, the `--since` spec against it, the `--people-dict` file, and +/// the shotness DSL expressions. +/// +/// # Errors +/// +/// The [`HandlerError`] naming the first failing input. +pub(crate) fn validate_history_run(sub: &clap::ArgMatches) -> Result<(), HandlerError> { + let opts = HistoryOptions::from_matches(sub)?; + ChunkPlan::from_matches(sub)?; + budget::memory_budget_flag(sub)?; + validate_positive_flags(sub)?; + anomaly::validate_anomaly_flags(sub)?; + let path = run_repo_path(sub); + let repo = cf_gitlib::Repository::open(&path) + .map_err(|_| HandlerError::CannotOpenRepository(path.clone()))?; + if let SinceSpec::Active(spec) = &opts.since { + repo.resolve_time(spec).map_err(|e| { + HandlerError::InvalidFlag(format!("cannot resolve --since {spec:?}: {e}")) + })?; + } + if let Some(dict) = &opts.people_dict { + opts.identity().map_err(|e| { + HandlerError::InvalidFlag(format!("cannot read --people-dict {dict}: {e}")) + })?; + } + sentiment::validate_sentiment_flags(sub)?; + crate::handlers::shotness_run::validate_shotness_dsl(sub)?; + Ok(()) +} + +/// Analyzer flags whose non-positive values have no meaning: the burndown +/// band and sample sizes and the typo distance. +const POSITIVE_FLAGS: &[&str] = &["granularity", "sampling", "typos-max-distance"]; + +/// Rejects a non-positive value of every [`POSITIVE_FLAGS`] flag (it used to +/// be replaced by the default without a word). +/// +/// # Errors +/// +/// [`HandlerError::InvalidFlag`] naming the first offending flag. +fn validate_positive_flags(sub: &clap::ArgMatches) -> Result<(), HandlerError> { + for flag in POSITIVE_FLAGS { + if let Some(value) = sub.try_get_one::<i64>(flag).ok().flatten().copied() { + if value <= 0 { + return Err(HandlerError::InvalidFlag(format!( + "invalid --{flag}: must be > 0, got {value}" + ))); + } + } + } + Ok(()) +} + +/// One history run: the repository, the oldest-first commit window and the +/// per-run configuration every walk reads. +pub(crate) struct HistoryRun { + /// The repository path as given on the command line. + pub path: String, + /// The opened repository (main-thread handle). + pub repo: cf_gitlib::Repository, + /// The commit window, oldest-first. + pub hashes: Vec<Hash>, + /// The shared change filter. + pub filter: HistoryFilter, + /// The effective first-parent mode of THIS walk: a merge is treated as an + /// ordinary single-parent commit when set. + pub first_parent: bool, + /// `--max-changes-per-commit` cap: commits whose raw tree diff exceeds it + /// are dropped before any analyzer sees them. + pub max_changes: usize, + tick_size_hours: i64, + anonymize: bool, + identity_seed: IdentityDetector, +} + +impl HistoryRun { + /// Opens the run with the user's `--first-parent` setting. + pub(crate) fn open(sub: &clap::ArgMatches) -> Option<Self> { + let opts = HistoryOptions::from_matches(sub).ok()?; + let first_parent = opts.first_parent; + Self::open_with(sub, opts, first_parent) + } + + /// Opens the run with first-parent simplification FORCED on — the burndown + /// walk, whose line-survival tracking follows the main line only. This + /// never leaks into any other analyzer's walk. + pub(crate) fn open_first_parent(sub: &clap::ArgMatches) -> Option<Self> { + let opts = HistoryOptions::from_matches(sub).ok()?; + Self::open_with(sub, opts, true) + } + + fn open_with(sub: &clap::ArgMatches, opts: HistoryOptions, first_parent: bool) -> Option<Self> { + let path = run_repo_path(sub); + let repo = cf_gitlib::Repository::open(&path).ok()?; + let hashes = load_history_commit_hashes( + &repo, + &HistoryWindow { + head: opts.head, + limit: opts.limit, + first_parent, + since: opts.since.clone(), + }, + )?; + let identity_seed = opts.identity().ok()?; + Some(Self { + path, + repo, + hashes, + filter: opts.filter, + first_parent, + max_changes: opts.max_changes, + tick_size_hours: opts.tick_size_hours, + anonymize: opts.anonymize, + identity_seed, + }) + } + + /// The configured tick size in hours. + pub(crate) const fn tick_size_hours(&self) -> i64 { + self.tick_size_hours + } + + /// Worker count for the pure per-commit pre-pass. Only the WORK is + /// distributed; every reduce runs in chronological order, so this never + /// reaches the output. + pub(crate) fn workers() -> usize { + std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get) + } +} + +/// The run-level stamps of one walked commit: the identity and tick the +/// shared core assigned, plus the commit facts every per-commit record emits. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CommitStamp { + /// Position in the ORIGINAL window (oversized-dropped commits count). + pub pos: usize, + /// The commit hash. + pub hash: Hash, + /// The commit hash, 40 hex digits. + pub hex: String, + /// The tick (`--tick-size` buckets, monotonic). + pub tick: i64, + /// The author id (loose or exact identity, `--people-dict` aware). + pub author_id: i64, + /// Committer time, unix seconds. + pub when: i64, + /// Committer UTC-offset minutes. + pub offset_min: i32, + /// Number of parents. + pub num_parents: usize, +} + +/// Whether a commit with `num_parents` is a merge for a walk in `first_parent` +/// mode: under first-parent simplification a merge is visited as an ordinary +/// single-parent commit (its diff against `parent(0)` is a real change set), +/// so it is NOT treated as a merge. Free function so the `!Sync` run handle +/// never has to be captured by the parallel pre-pass. +pub(crate) const fn is_merge(num_parents: usize, first_parent: bool) -> bool { + num_parents > 1 && !first_parent +} + +/// The identity-detector view of a commit's author. +pub(crate) fn author_signature(commit: &cf_gitlib::commit::Commit<'_>) -> Signature { + let gsig = commit.author(); + Signature { + name: gsig.name, + email: gsig.email, + when_unix: gsig.when.seconds(), + } +} + +/// Computes a commit's tree changes against its first parent (root commit → +/// the full initial tree) — the ONE diff every history walk starts from. +pub(crate) fn commit_tree_changes( + repo: &cf_gitlib::Repository, + commit: &cf_gitlib::commit::Commit<'_>, +) -> Option<cf_gitlib::changes::Changes> { + use cf_gitlib::changes::{initial_tree_changes, tree_diff}; + let new_tree = commit.tree().ok()?; + if commit.num_parents() > 0 { + let parent = commit.parent(0).ok()?; + let old_tree = parent.tree().ok()?; + tree_diff(repo, Some(&old_tree), Some(&new_tree)).ok() + } else { + initial_tree_changes(repo, Some(&new_tree)).ok() + } +} + +/// One commit's pre-pass result behind the `--max-changes-per-commit` gate: +/// the RAW (pre-filter) tree-diff change count the gate reads, and the +/// analyzer's product over the FILTERED changes. An oversized commit carries +/// the default product (it does no per-file work) and is dropped before +/// stamping, exactly as every other walk drops it. +pub(crate) struct Gated<P> { + /// RAW (pre-filter) tree-diff change count. + pub raw_change_count: usize, + /// The analyzer's per-commit product. + pub product: P, +} + +impl<P> Gated<P> { + /// Whether the commit's raw diff exceeds `max_changes`, so no analyzer + /// sees it. + pub(crate) const fn oversized(&self, max_changes: usize) -> bool { + self.raw_change_count > max_changes + } +} + +/// The gated pre-pass of a walk whose product is a pure function of the +/// commit and its FILTERED changes: the raw tree diff against the first +/// parent, the drop gate on its size, the shared change filter, then +/// `compute`. `None` on a fatal git error. +pub(crate) fn gated_product<P: Default>( + repo: &cf_gitlib::Repository, + hash: Hash, + filter: &HistoryFilter, + max_changes: usize, + compute: impl FnOnce(&cf_gitlib::commit::Commit<'_>, &[cf_gitlib::changes::Change]) -> P, +) -> Option<Gated<P>> { + let commit = repo.lookup_commit(hash).ok()?; + let mut changes = commit_tree_changes(repo, &commit)?; + let raw_change_count = changes.len(); + if raw_change_count > max_changes { + return Some(Gated { + raw_change_count, + product: P::default(), + }); + } + filter.retain_changes(repo, &mut changes); + Some(Gated { + raw_change_count, + product: compute(&commit, &changes), + }) +} + +/// Runs `compute` for every hash in parallel across `workers` OS threads, +/// returning the results in the SAME order as `hashes` (or `None` if any commit +/// hits a fatal git error, mirroring the sequential `.ok()?` contract). +/// +/// The whole-window form of [`pool::prepare_in_order`]: every product is +/// kept, so use it only for products that are small per commit (burndown's +/// raw change counts); walks fold through [`walk_chunks`] instead. +pub(crate) fn parallel_prepare<T: Send>( + repo_path: &str, + hashes: &[Hash], + workers: usize, + compute: impl Fn(&cf_gitlib::Repository, Hash) -> Option<T> + Sync, +) -> Option<Vec<T>> { + let mut out = Vec::with_capacity(hashes.len()); + pool::prepare_in_order( + repo_path, + hashes, + workers, + hashes.len(), + compute, + |_, product| { + out.push(product); + Some(()) + }, + )?; + Some(out) +} + +/// Per-change `(added, removed, changed)` line stats for a Modify change over +/// the libgit2 line-diff op stream (`diff_blob_line_ops`): a Delete run +/// immediately followed by an Insert run is reclassified as "changed" lines +/// (counted in neither added nor removed), which is the churn model every +/// line-stat consumer (devs, file-history, anomaly) shares. +pub(crate) fn compute_diff_line_stats( + repo: &cf_gitlib::Repository, + from: Hash, + to: Hash, + old_lines: i64, +) -> (i64, i64, i64) { + use cf_gitlib::diff::{diff_blob_line_ops, LineOp}; + let Ok(ops) = diff_blob_line_ops(repo.native(), from, to, old_lines) else { + return (0, 0, 0); + }; + let mut added = 0i64; + let mut removed = 0i64; + let mut changed = 0i64; + let mut removed_pending = 0i64; + for op in &ops { + match *op { + LineOp::Equal(_) => { + removed += removed_pending; + removed_pending = 0; + } + LineOp::Insert(delta) => { + if removed_pending > delta { + changed += delta; + removed += removed_pending - delta; + } else { + changed += removed_pending; + added += delta - removed_pending; + } + removed_pending = 0; + } + LineOp::Delete(n) => { + removed_pending = n; + } + } + } + removed += removed_pending; + (added, removed, changed) +} + +/// Detects the language of a changed file for the per-language breakdowns: +/// `""` for a binary blob, else the full enry cascade over the path AND the +/// blob content (`cf-langpath`), so `.h` resolves to C/C++/Objective-C by +/// content and an extensionless script by its shebang. `None` (no strategy +/// matched) maps to `""`, the "Other" bucket. +pub(crate) fn detect_language(name: &str, data: &[u8]) -> String { + if cf_textutil::is_binary(data) { + return String::new(); + } + cf_langpath::language_by_path_with_content(name, data).unwrap_or_default() +} diff --git a/crates/cf-commands/src/handlers/history/pool.rs b/crates/cf-commands/src/handlers/history/pool.rs new file mode 100644 index 00000000..32b9389a --- /dev/null +++ b/crates/cf-commands/src/handlers/history/pool.rs @@ -0,0 +1,525 @@ +//! The history worker pool: one fixed set of worker threads prepares a +//! walk's per-commit products while the calling thread folds them in window +//! order. +//! +//! Workers claim short runs of consecutive commits from a shared cursor +//! instead of owning a fixed slice: commit costs are heavily skewed (one +//! vendoring or rename-heavy commit can outweigh thousands of others, and such +//! commits cluster), so a static split makes the whole window wait for its +//! unluckiest slice. A run keeps the claiming worker's libgit2 object cache +//! warm (consecutive commits share most of their trees), and a worker that +//! finds nothing left to claim takes the unstarted back half of another +//! worker's run, so a cluster of slow commits is shared out instead of +//! serialized. A worker may claim a commit only while it is less than +//! `window` positions ahead of the fold, so at most `window` products are +//! alive at once, and the window slides one commit at a time: there is no +//! barrier where every worker waits for the slowest commit of a chunk. Each +//! worker opens its repository once and keeps its thread-local state +//! (parsers, caches) warm for the whole walk. + +use std::collections::BTreeMap; +use std::ops::Range; +use std::sync::{Condvar, Mutex, MutexGuard, PoisonError}; + +use cf_gitlib::Hash; + +/// The largest run of consecutive commits one claim takes. Measured on the +/// 31,923-commit kubernetes prefix (`history/anomaly`, 24 workers, 3000-commit +/// window): claiming one commit at a time ran 65 s, runs of 8 ran 43 s, runs +/// of 32 ran 31 s; consecutive commits share most of their trees, which the +/// claiming worker's libgit2 object cache then still holds. +pub(crate) const MAX_BLOCK: usize = 32; + +/// Claims per worker that fit in one window, at least: smaller runs keep +/// every worker fed near the end of the window. +const BLOCKS_PER_WORKER: usize = 4; + +/// The commits one claim takes for `workers` workers sharing a `window`: +/// a quarter of each worker's share of the window, between one commit and +/// [`MAX_BLOCK`]. +pub(crate) fn block_size(window: usize, workers: usize) -> usize { + (window / workers.max(1).saturating_mul(BLOCKS_PER_WORKER)).clamp(1, MAX_BLOCK) +} + +/// The pool state shared by the workers and the folding thread. +struct Flight<T> { + /// The next window position a worker may claim. + next: usize, + /// Every position below this one has been folded. + folded: usize, + /// Prepared products waiting for their turn, by window position. + ready: BTreeMap<usize, T>, + /// The walk can no longer succeed (a repository that does not open, a + /// failed or panicking commit, a failed fold): workers stop claiming and + /// the fold stops waiting. + failed: bool, + /// Per worker, the claimed commits it has not started yet. + queued: Vec<Range<usize>>, +} + +/// One walk's pool: the window it prepares and the state guarding it. +struct Pool<'h, T> { + hashes: &'h [Hash], + /// Products alive at once, at most. + window: usize, + /// Commits one claim takes, at most ([`block_size`]). + block: usize, + flight: Mutex<Flight<T>>, + /// Signalled when the fold advances or the walk fails (workers wait for + /// room in the window). + room: Condvar, + /// Signalled when a product arrives or the walk fails (the fold waits for + /// its next position). + arrived: Condvar, +} + +impl<'h, T> Pool<'h, T> { + fn new(hashes: &'h [Hash], window: usize, workers: usize) -> Self { + Self { + hashes, + window, + block: block_size(window, workers), + flight: Mutex::new(Flight { + next: 0, + folded: 0, + ready: BTreeMap::new(), + failed: false, + queued: vec![0..0; workers], + }), + room: Condvar::new(), + arrived: Condvar::new(), + } + } + + /// The state lock. A poisoned lock still holds consistent state: every + /// critical section is a few field updates that cannot panic halfway. + fn lock(&self) -> MutexGuard<'_, Flight<T>> { + self.flight.lock().unwrap_or_else(PoisonError::into_inner) + } + + /// Marks the walk failed and wakes everyone waiting on it. + fn fail(&self) { + self.lock().failed = true; + self.room.notify_all(); + self.arrived.notify_all(); + } + + /// The next commit for worker `me`: the front of its own run; else a new + /// run from the cursor, inside the window; else the unstarted back half + /// of the longest run another worker holds. Waits while the window is + /// full and nothing can be taken; `None` once nothing is left to claim or + /// the walk failed. + fn claim(&self, me: usize) -> Option<usize> { + let mut flight = self.lock(); + loop { + if flight.failed { + return None; + } + if let Some(pos) = flight.queued[me].next() { + return Some(pos); + } + let limit = flight + .folded + .saturating_add(self.window) + .min(self.hashes.len()); + if flight.next < limit { + let start = flight.next; + let end = start.saturating_add(self.block).min(limit); + flight.next = end; + flight.queued[me] = start + 1..end; + if end - start > 1 { + // The rest of this run is work an idle worker may share. + self.room.notify_one(); + } + return Some(start); + } + if let Some(pos) = steal(&mut flight.queued, me) { + return Some(pos); + } + if flight.next >= self.hashes.len() { + return None; + } + flight = self + .room + .wait(flight) + .unwrap_or_else(PoisonError::into_inner); + } + } + + /// Hands a prepared product to the fold; a failed preparation fails the + /// walk. + fn deliver(&self, pos: usize, product: Option<T>) { + match product { + Some(product) => { + self.lock().ready.insert(pos, product); + self.arrived.notify_one(); + } + None => self.fail(), + } + } + + /// The product at `pos` once it is prepared; `None` when the walk failed. + fn take(&self, pos: usize) -> Option<T> { + let mut flight = self.lock(); + loop { + if let Some(product) = flight.ready.remove(&pos) { + return Some(product); + } + if flight.failed { + return None; + } + flight = self + .arrived + .wait(flight) + .unwrap_or_else(PoisonError::into_inner); + } + } + + /// Records that `pos` is folded, making room for one more claim. + fn advance(&self, pos: usize) { + self.lock().folded = pos + 1; + self.room.notify_one(); + } +} + +/// Takes the unstarted back half of the longest run another worker holds +/// (its owner keeps the front, whose trees its cache is about to hold) and +/// makes it `me`'s run; returns its first commit, or `None` when no other +/// worker holds an unstarted commit. +fn steal(queued: &mut [Range<usize>], me: usize) -> Option<usize> { + let victim = (0..queued.len()) + .filter(|&w| w != me) + .max_by_key(|&w| queued[w].len()) + .filter(|&w| !queued[w].is_empty())?; + let run = &mut queued[victim]; + let mid = run.start + run.len() / 2; + let stolen = mid..run.end; + run.end = mid; + queued[me] = stolen.start + 1..stolen.end; + Some(stolen.start) +} + +/// Fails the walk when a worker unwinds, so neither the fold nor the other +/// workers wait for a product that will never arrive. +struct UnwindGuard<'p, 'h, T>(&'p Pool<'h, T>); + +impl<T> Drop for UnwindGuard<'_, '_, T> { + fn drop(&mut self) { + if std::thread::panicking() { + self.0.fail(); + } + } +} + +/// Prepares every commit of `hashes` on `workers` threads, at most `window` +/// products alive at once, and folds each product on the calling thread in +/// window order. `compute` must be a pure per-commit function (it reads the +/// repository but shares no mutable state), so the fold sees the same +/// sequence for any worker count and window. +/// +/// `None` when a repository does not open, a commit's preparation returns +/// `None`, or `fold` returns `None` (the `.ok()?` contract of every history +/// walk); a panicking commit panics the caller with its own payload. +pub(crate) fn prepare_in_order<T: Send>( + repo_path: &str, + hashes: &[Hash], + workers: usize, + window: usize, + compute: impl Fn(&cf_gitlib::Repository, Hash) -> Option<T> + Sync, + mut fold: impl FnMut(usize, T) -> Option<()>, +) -> Option<()> { + if hashes.is_empty() { + return Some(()); + } + let window = window.max(1); + // More workers than window positions would only wait for room. + let workers = workers.clamp(1, hashes.len().min(window)); + let pool = Pool::new(hashes, window, workers); + let (pool, compute) = (&pool, &compute); + std::thread::scope(|s| { + let handles: Vec<_> = (0..workers) + .map(|me| s.spawn(move || work(pool, me, repo_path, compute))) + .collect(); + let result = fold_in_order(pool, &mut fold); + if result.is_none() { + pool.fail(); + } + for handle in handles { + if let Err(panic) = handle.join() { + std::panic::resume_unwind(panic); + } + } + result + }) +} + +/// Worker `me`: open the repository once, then prepare claimed commits until +/// none are left. +fn work<T>( + pool: &Pool<'_, T>, + me: usize, + repo_path: &str, + compute: &(impl Fn(&cf_gitlib::Repository, Hash) -> Option<T> + Sync), +) { + let _unwind = UnwindGuard(pool); + let Ok(repo) = cf_gitlib::Repository::open(repo_path) else { + pool.fail(); + return; + }; + while let Some(pos) = pool.claim(me) { + pool.deliver(pos, compute(&repo, pool.hashes[pos])); + } +} + +/// The fold side: every position in order, each as soon as it is prepared. +fn fold_in_order<T>( + pool: &Pool<'_, T>, + fold: &mut impl FnMut(usize, T) -> Option<()>, +) -> Option<()> { + for pos in 0..pool.hashes.len() { + let product = pool.take(pos)?; + fold(pos, product)?; + pool.advance(pos); + } + Some(()) +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::time::{Duration, Instant}; + + use cf_gitlib::testutil::{init_repo, CommitBuilder, TestRepo}; + + use super::*; + + /// 2021-01-01T00:00:00Z. + const T0: i64 = 1_609_459_200; + /// Commits in the fixture history. + const COMMITS: usize = 6; + /// Worker threads every test runs with, independent of the host's CPUs. + const WORKERS: usize = 2; + /// How long a deliberately stalled commit waits before giving up. + const STALL_LIMIT: Duration = Duration::from_secs(10); + /// Polling period of a stalled commit. + const POLL: Duration = Duration::from_millis(1); + + /// A linear history of [`COMMITS`] commits, oldest first. + fn linear_history() -> (TestRepo, Vec<Hash>) { + let test = init_repo().expect("init"); + let hashes = (0..COMMITS) + .map(|i| { + CommitBuilder::new(&test, &format!("c{i}"), T0 + i as i64) + .write("f.txt", format!("v{i}\n").as_bytes()) + .commit() + .expect("commit") + }) + .collect(); + (test, hashes) + } + + fn path(test: &TestRepo) -> &str { + test.path().to_str().expect("utf-8 path") + } + + /// Waits until `ready` holds; `false` once [`STALL_LIMIT`] passes. + fn wait_for(ready: impl Fn() -> bool) -> bool { + let deadline = Instant::now() + STALL_LIMIT; + while !ready() { + if Instant::now() > deadline { + return false; + } + std::thread::sleep(POLL); + } + true + } + + /// The `(position, product)` pairs in the order the fold saw them. + fn folded<T: Send>( + test: &TestRepo, + hashes: &[Hash], + window: usize, + compute: impl Fn(&cf_gitlib::Repository, Hash) -> Option<T> + Sync, + ) -> Option<Vec<(usize, T)>> { + let mut seen = Vec::new(); + prepare_in_order( + path(test), + hashes, + WORKERS, + window, + compute, + |pos, product| { + seen.push((pos, product)); + Some(()) + }, + )?; + Some(seen) + } + + /// Products reach the fold in window order even when later commits + /// finish first. + #[test] + fn products_reach_the_fold_in_window_order() { + let (test, hashes) = linear_history(); + let seen = folded(&test, &hashes, 3, |_, hash| { + let pos = hashes.iter().position(|h| *h == hash)?; + std::thread::sleep(POLL * (COMMITS - pos) as u32); + Some(hash) + }); + let expected: Vec<(usize, Hash)> = hashes.iter().copied().enumerate().collect(); + assert_eq!(seen, Some(expected)); + } + + /// A slow commit never holds the commits queued behind it hostage: an + /// idle worker takes the next unclaimed commit. The first commit finishes + /// only once every other commit is prepared, which a static split (the + /// first worker owning the first half of the window) can never satisfy. + #[test] + fn a_slow_commit_does_not_stall_the_commits_queued_behind_it() { + let (test, hashes) = linear_history(); + let prepared = AtomicUsize::new(0); + let seen = folded(&test, &hashes, COMMITS, |_, hash| { + if hash == hashes[0] && !wait_for(|| prepared.load(Ordering::SeqCst) == COMMITS - 1) { + return None; + } + prepared.fetch_add(1, Ordering::SeqCst); + Some(hash) + }); + assert_eq!(seen.map(|s| s.len()), Some(COMMITS)); + } + + /// The window bounds the products in flight; it is not a barrier. Once + /// the oldest commit is folded, an idle worker starts the commit one + /// window ahead while a slow commit is still being prepared: with a + /// window of two, commit 1 finishes only after commit 2 is prepared, + /// which a chunk {2, 3} waiting for chunk {0, 1} can never satisfy. + #[test] + fn a_slow_commit_does_not_stall_the_next_window() { + let (test, hashes) = linear_history(); + let third = AtomicBool::new(false); + let seen = folded(&test, &hashes, 2, |_, hash| { + if hash == hashes[1] && !wait_for(|| third.load(Ordering::SeqCst)) { + return None; + } + if hash == hashes[2] { + third.store(true, Ordering::SeqCst); + } + Some(hash) + }); + assert_eq!(seen.map(|s| s.len()), Some(COMMITS)); + } + + /// A commit whose preparation fails fails the walk. + #[test] + fn a_failed_commit_fails_the_walk() { + let (test, hashes) = linear_history(); + let seen = folded(&test, &hashes, 2, |_, hash| { + (hash != hashes[2]).then_some(hash) + }); + assert!(seen.is_none()); + } + + /// A fold that fails stops the walk, and no worker runs ahead of the + /// window: with a window of one, only the two folded commits were ever + /// prepared. + #[test] + fn a_failed_fold_stops_the_walk() { + let (test, hashes) = linear_history(); + let prepared = AtomicUsize::new(0); + let mut folds = 0; + let result = prepare_in_order( + path(&test), + &hashes, + WORKERS, + 1, + |_, hash| { + prepared.fetch_add(1, Ordering::SeqCst); + Some(hash) + }, + |pos, _| { + folds += 1; + (pos != 1).then_some(()) + }, + ); + assert!(result.is_none()); + assert_eq!(folds, 2); + assert_eq!(prepared.load(Ordering::SeqCst), 2); + } + + /// A repository no worker can open fails the walk instead of hanging it. + #[test] + fn an_unopenable_repository_fails_the_walk() { + let (test, hashes) = linear_history(); + let missing = test.path().join("missing"); + let result = prepare_in_order( + missing.to_str().expect("utf-8 path"), + &hashes, + WORKERS, + 2, + |_, hash| Some(hash), + |_, _| Some(()), + ); + assert!(result.is_none()); + } + + /// A panicking commit panics the walk with its own message instead of + /// hanging the fold that waits for it. + #[test] + #[should_panic(expected = "commit 1 exploded")] + fn a_panicking_commit_panics_the_walk() { + let (test, hashes) = linear_history(); + let _ = folded(&test, &hashes, 2, |_, hash| { + assert!(hash != hashes[1], "commit 1 exploded"); + Some(hash) + }); + } + + /// A claim is about a quarter of each worker's share of the window, at + /// least one commit and at most [`MAX_BLOCK`]. + #[test] + fn a_claim_is_a_quarter_of_each_workers_share_of_the_window() { + assert_eq!(block_size(3000, 24), 31); + assert_eq!(block_size(COMMITS, WORKERS), 1); + assert_eq!(block_size(1, 1), 1); + assert_eq!(block_size(usize::MAX, 24), MAX_BLOCK); + assert_eq!(block_size(1_000_000, 0), MAX_BLOCK); + } + + /// Commits of the window this long. + const LONG_WINDOW: usize = 64; + + /// Without slow commits a worker prepares runs of consecutive commits + /// (its libgit2 object cache then still holds the previous commit's + /// trees): the preparing thread changes far less often than a per-commit + /// hand-off between two equally fast workers would make it. + #[test] + fn a_worker_prepares_runs_of_consecutive_commits() { + let test = init_repo().expect("init"); + let hashes: Vec<Hash> = (0..LONG_WINDOW) + .map(|i| { + CommitBuilder::new(&test, &format!("c{i}"), T0 + i as i64) + .write("f.txt", format!("v{i}\n").as_bytes()) + .commit() + .expect("commit") + }) + .collect(); + let seen = folded(&test, &hashes, LONG_WINDOW, |_, _| { + std::thread::sleep(POLL); + Some(std::thread::current().id()) + }) + .expect("walk"); + let switches = seen.windows(2).filter(|w| w[0].1 != w[1].1).count(); + assert!( + switches <= LONG_WINDOW / 4, + "{switches} thread switches over {LONG_WINDOW} commits" + ); + } + + /// An empty window folds nothing and succeeds. + #[test] + fn an_empty_window_folds_nothing() { + let (test, _) = linear_history(); + let seen = folded(&test, &[], 2, |_, hash| Some(hash)); + assert_eq!(seen, Some(Vec::new())); + } +} diff --git a/crates/cf-commands/src/handlers/history/quality.rs b/crates/cf-commands/src/handlers/history/quality.rs new file mode 100644 index 00000000..6201ee57 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/quality.rs @@ -0,0 +1,200 @@ +//! `history/quality` over the shared history walk. +//! +//! Per commit: the filtered tree diff, then for every Insert/Modify change's +//! After blob a gated UAST parse ([`super::super::uast_walk::ChangeParses`]) +//! and one [`cf_quality::analyze_file`] sample per PARSED file (the four +//! component analyzers over the UAST and the blob bytes — the static +//! analyzers' numbers). A failed parse contributes nothing. The shared +//! identity/tick stamps bucket the samples per tick for +//! `cf_quality::compute_all_metrics`. + +use std::collections::BTreeMap; +use std::sync::Arc; + +use cf_gitlib::changes::ChangeAction; +use cf_quality::{compute_all_metrics, ReportData, TickBounds, TickQuality}; + +use super::super::uast_walk::{uast_results, ChangeParses, ParseOutcome, Side, UastAnalyzer}; +use super::CommitStamp; + +/// Runs the `history/quality` walk and returns the typed +/// [`cf_quality::ComputedMetrics`] — the single report value behind every +/// output format — or `None` if the repository cannot be walked. +#[must_use] +pub fn quality_metrics(sub: &clap::ArgMatches) -> Option<cf_quality::ComputedMetrics> { + let walk = quality_walk(sub)?; + + // Per-tick merged quality + bounds (committer-time min/max), folding each + // commit's per-file samples into its tick in walk order. + let mut tick_quality: BTreeMap<i64, TickQuality> = BTreeMap::new(); + let mut tick_when: BTreeMap<i64, (i64, i64)> = BTreeMap::new(); + for c in walk.iter() { + tick_when + .entry(c.tick) + .and_modify(|(lo, hi)| { + *lo = (*lo).min(c.when); + *hi = (*hi).max(c.when); + }) + .or_insert((c.when, c.when)); + tick_quality.entry(c.tick).or_default().merge(&c.tq); + } + + let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); + for (tick, (lo, hi)) in &tick_when { + tick_bounds.insert( + *tick, + TickBounds { + start_time: cf_analyze::metadata::format_rfc3339_utc(*lo), + end_time: cf_analyze::metadata::format_rfc3339_utc(*hi), + }, + ); + } + + let input = ReportData { + tick_quality, + tick_bounds, + }; + Some(compute_all_metrics(&input)) +} + +/// One walked commit's quality products plus the shared stamps. +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) struct QualityCommit { + /// Position in the ORIGINAL window (oversized-dropped commits count). + pub pos: usize, + /// Full hex hash. + pub hash: String, + /// This commit's per-file quality samples. + pub tq: TickQuality, + /// Tick. + pub tick: i64, + /// Author id. + pub author_id: i64, + /// Committer time, Unix seconds. + pub when: i64, + /// Committer UTC-offset minutes. + pub offset_min: i32, +} + +impl QualityCommit { + /// The record of one kept commit: its stamps and its per-file samples. + pub(crate) fn new(stamp: &CommitStamp, tq: TickQuality) -> Self { + Self { + pos: stamp.pos, + hash: stamp.hex.clone(), + tq, + tick: stamp.tick, + author_id: stamp.author_id, + when: stamp.when, + offset_min: stamp.offset_min, + } + } +} + +/// The `history/quality` records of the run's UAST pass: per-commit +/// `TickQuality` plus the stamps, in walk order. Every quality format +/// consumes THIS one walk. +pub(crate) fn quality_walk(sub: &clap::ArgMatches) -> Option<Arc<Vec<QualityCommit>>> { + uast_results(sub, UastAnalyzer::Quality)?.quality +} + +/// One change's `history/quality` contribution: a [`cf_quality::FileSample`] +/// of the PARSED After tree, pushed onto the commit's samples (so a commit's +/// samples follow its change order). A deleted, gate-skipped or unparsable +/// file contributes nothing: `files_analyzed` counts parsed files only. +pub(crate) fn quality_change(parses: &ChangeParses<'_>, tq: &mut TickQuality) { + let change = parses.change(); + if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { + return; + } + let ParseOutcome::Parsed(root) = parses.parse(Side::After) else { + return; + }; + // The blob was read by the parse that just succeeded. + let Some(blob) = parses.blob(Side::After) else { + return; + }; + tq.push(&cf_quality::analyze_file(root, &blob.data)); +} + +/// Serializes one per-commit [`TickQuality`] as the per-commit record struct +/// (declaration field order, empty slices → null). +fn quality_tq_value(tq: &TickQuality) -> cf_gojson::GoValue { + use cf_gojson::{GoMap, GoValue}; + fn floats(v: &[f64]) -> GoValue { + if v.is_empty() { + GoValue::Null + } else { + GoValue::Array(v.iter().map(|f| GoValue::Float(*f)).collect()) + } + } + fn ints(v: &[i64]) -> GoValue { + if v.is_empty() { + GoValue::Null + } else { + GoValue::Array(v.iter().map(|i| GoValue::Int(*i)).collect()) + } + } + let mut m = GoMap::new_struct(); + m.insert("Complexities".to_string(), floats(&tq.complexities)); + m.insert("Cognitives".to_string(), floats(&tq.cognitives)); + m.insert("MaxComplexities".to_string(), ints(&tq.max_complexities)); + m.insert("Functions".to_string(), ints(&tq.functions)); + m.insert("HalsteadVolumes".to_string(), floats(&tq.halstead_volumes)); + m.insert("HalsteadEfforts".to_string(), floats(&tq.halstead_efforts)); + m.insert("DeliveredBugs".to_string(), floats(&tq.delivered_bugs)); + m.insert("CommentScores".to_string(), floats(&tq.comment_scores)); + m.insert("DocCoverages".to_string(), floats(&tq.doc_coverages)); + m.insert("CohesionScores".to_string(), floats(&tq.cohesion_scores)); + GoValue::Object(m) +} + +/// Per-commit quality NDJSON records: every commit emits a line. +#[must_use] +pub fn quality_ndjson_records( + sub: &clap::ArgMatches, +) -> Option<Vec<super::super::history_formats::NdjsonRecord>> { + let walk = quality_walk(sub)?; + Some( + walk.iter() + .map(|c| super::super::history_formats::NdjsonRecord { + pos: c.pos, + hash: c.hash.clone(), + tick: c.tick, + author_id: c.author_id, + time_secs: c.when, + tz_offset_min: c.offset_min, + data: quality_tq_value(&c.tq), + }) + .collect(), + ) +} + +/// The quality contribution to the merged `--format timeseries` document: per +/// commit the summary map (`cf_quality::commit_summary`). +#[must_use] +pub fn quality_timeseries_contribution( + sub: &clap::ArgMatches, +) -> Option<super::super::history_formats::TimeSeriesContribution> { + let walk = quality_walk(sub)?; + let mut per_commit = Vec::new(); + let mut commit_meta = Vec::new(); + for c in walk.iter() { + let summary = cf_quality::commit_summary(&c.tq); + per_commit.push(( + c.hash.clone(), + cf_quality::serialize::commit_summary_value(&summary), + )); + commit_meta.push(( + c.hash.clone(), + c.tick, + crate::handlers::format_rfc3339_offset(c.when, c.offset_min), + String::new(), + )); + } + Some(super::super::history_formats::TimeSeriesContribution { + flag: "quality", + per_commit, + commit_meta, + }) +} diff --git a/crates/cf-commands/src/handlers/history/sentiment.rs b/crates/cf-commands/src/handlers/history/sentiment.rs new file mode 100644 index 00000000..231f8d47 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/sentiment.rs @@ -0,0 +1,318 @@ +//! `history/sentiment` over the shared history walk. +//! +//! Per commit: the filtered tree diff, then for every Insert/Modify change a +//! gated UAST parse of BOTH sides, `Comment` node collection and +//! `merge_comments` (group by line, merge adjacent, strip delimiters, drop +//! short / license / low-letter comments) per side, and the multiset +//! difference After − Before ([`cf_sentiment::new_or_changed`]): only the +//! comments the commit introduced or edited are its product. The shared +//! identity/tick stamps bucket them per tick for +//! `cf_sentiment::compute_all_metrics`, which scores only the ticks that +//! carry at least one comment. + +use std::collections::BTreeMap; +use std::sync::Arc; + +use cf_gitlib::changes::ChangeAction; +use cf_sentiment::analyzer::{merge_comments, CommentNode}; +use cf_sentiment::{ + compute_all_metrics, new_or_changed, ReportData, TickBounds, + DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH, +}; +use cf_uast_node::UAST_COMMENT; + +use super::super::uast_walk::{uast_results, ChangeParses, ParseOutcome, Side, UastAnalyzer}; +use super::CommitStamp; +use crate::pipeline::HandlerError; + +/// The `--min-comment-len` flag name. +const MIN_COMMENT_LEN_FLAG: &str = "min-comment-len"; + +/// Reads `--min-comment-len`; absent ⇒ the default. +fn min_comment_len_raw(sub: &clap::ArgMatches) -> i64 { + sub.try_get_one::<i64>(MIN_COMMENT_LEN_FLAG) + .ok() + .flatten() + .copied() + .unwrap_or(DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH) +} + +/// Validates `--min-comment-len` (`>= 0`; `0` disables the length filter). +/// +/// # Errors +/// +/// [`HandlerError::InvalidFlag`] for a negative value. +pub(crate) fn validate_sentiment_flags(sub: &clap::ArgMatches) -> Result<(), HandlerError> { + let value = min_comment_len_raw(sub); + if value < 0 { + return Err(HandlerError::InvalidFlag(format!( + "invalid --{MIN_COMMENT_LEN_FLAG}: must be >= 0, got {value}" + ))); + } + Ok(()) +} + +/// The effective minimum comment byte length: `--min-comment-len`, with the +/// default standing in for a value the preflight rejects. +pub(crate) fn sentiment_min_comment_len(sub: &clap::ArgMatches) -> i64 { + let value = min_comment_len_raw(sub); + if value < 0 { + DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH + } else { + value + } +} + +/// Computes the typed sentiment [`cf_sentiment::ComputedMetrics`] — the single +/// report value behind every output format — or `None` if the repository +/// cannot be walked. +#[must_use] +pub fn sentiment_metrics(sub: &clap::ArgMatches) -> Option<cf_sentiment::ComputedMetrics> { + let walk = sentiment_walk(sub)?; + + let mut comments_by_commit: BTreeMap<String, Vec<String>> = BTreeMap::new(); + let mut commits_by_tick: BTreeMap<i64, Vec<String>> = BTreeMap::new(); + let mut tick_when: BTreeMap<i64, (i64, i64)> = BTreeMap::new(); + for c in walk.iter() { + commits_by_tick + .entry(c.tick) + .or_default() + .push(c.hash.clone()); + tick_when + .entry(c.tick) + .and_modify(|(lo, hi)| { + *lo = (*lo).min(c.when); + *hi = (*hi).max(c.when); + }) + .or_insert((c.when, c.when)); + comments_by_commit.insert(c.hash.clone(), c.comments.clone()); + } + + let mut tick_bounds: BTreeMap<i64, TickBounds> = BTreeMap::new(); + for (tick, (lo, hi)) in &tick_when { + tick_bounds.insert( + *tick, + TickBounds { + start_time: cf_analyze::metadata::format_rfc3339_utc(*lo), + end_time: cf_analyze::metadata::format_rfc3339_utc(*hi), + }, + ); + } + + let input = ReportData::from_commit_data(&comments_by_commit, commits_by_tick, tick_bounds); + Some(compute_all_metrics(&input)) +} + +/// One walked commit's sentiment products plus the shared stamps. +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) struct SentimentCommit { + /// Position in the ORIGINAL window (oversized-dropped commits count). + pub pos: usize, + /// Full hex hash. + pub hash: String, + /// The merged+filtered comments this commit introduced or changed + /// (possibly empty). + pub comments: Vec<String>, + /// Tick. + pub tick: i64, + /// Author id. + pub author_id: i64, + /// Committer time, Unix seconds. + pub when: i64, + /// Committer UTC-offset minutes. + pub offset_min: i32, +} + +impl SentimentCommit { + /// The record of one kept commit: its stamps and its new or changed + /// comments. + pub(crate) fn new(stamp: &CommitStamp, comments: Vec<String>) -> Self { + Self { + pos: stamp.pos, + hash: stamp.hex.clone(), + comments, + tick: stamp.tick, + author_id: stamp.author_id, + when: stamp.when, + offset_min: stamp.offset_min, + } + } +} + +/// The `history/sentiment` records of the run's UAST pass: every sentiment +/// format consumes THIS one walk. +pub(crate) fn sentiment_walk(sub: &clap::ArgMatches) -> Option<Arc<Vec<SentimentCommit>>> { + uast_results(sub, UastAnalyzer::Sentiment)?.sentiment +} + +/// One change's `history/sentiment` contribution: for a surviving +/// Insert/Modify change, the merged+filtered comments of the After side that +/// are NEW or CHANGED relative to the Before side (an Insert has no Before +/// side), appended to the commit's comments (so they follow its change +/// order). +pub(crate) fn sentiment_change( + parses: &ChangeParses<'_>, + min_len: i64, + comments: &mut Vec<String>, +) { + let change = parses.change(); + if matches!(change.action, ChangeAction::Delete) || change.to.hash.is_zero() { + return; + } + let after = side_comments(parses, Side::After, min_len); + if after.is_empty() { + return; + } + let before = side_comments(parses, Side::Before, min_len); + comments.extend(new_or_changed(&after, &before)); +} + +/// The merged+filtered comments of one change side (empty for the missing +/// side of an Insert, a gate-skipped blob, or a failed non-shell parse). +fn side_comments(parses: &ChangeParses<'_>, side: Side, min_len: i64) -> Vec<String> { + let entry = parses.entry(side); + if entry.hash.is_zero() { + return Vec::new(); + } + let mut comment_nodes: Vec<CommentNode> = Vec::new(); + match parses.parse(side) { + ParseOutcome::Parsed(root) => { + collect_comment_nodes(root, UAST_COMMENT, &mut comment_nodes); + } + // Shell scripts register for a grammar the UAST loader does not + // vendor, so they pass `is_supported` but fail to parse; their + // `#`-comments are extracted line-wise instead (the failed parse + // read the blob). + ParseOutcome::Failed if is_shell_path(&entry.name) => { + if let Some(blob) = parses.blob(side) { + extract_shell_comment_nodes(&blob.data, &mut comment_nodes); + } + } + ParseOutcome::Failed | ParseOutcome::Skipped => {} + } + merge_comments(&comment_nodes, min_len) +} + +/// Per-commit sentiment NDJSON records: EVERY commit emits a line whose `data` +/// is a `Comments` array (possibly empty, never null). +#[must_use] +pub fn sentiment_ndjson_records( + sub: &clap::ArgMatches, +) -> Option<Vec<super::super::history_formats::NdjsonRecord>> { + use cf_gojson::{GoMap, GoValue}; + + let walk = sentiment_walk(sub)?; + Some( + walk.iter() + .map(|c| { + let comments: Vec<GoValue> = + c.comments.iter().map(|s| GoValue::Str(s.clone())).collect(); + let mut data = GoMap::new_struct(); + data.insert("Comments".to_string(), GoValue::Array(comments)); + super::super::history_formats::NdjsonRecord { + pos: c.pos, + hash: c.hash.clone(), + tick: c.tick, + author_id: c.author_id, + time_secs: c.when, + tz_offset_min: c.offset_min, + data: GoValue::Object(data), + } + }) + .collect(), + ) +} + +/// The sentiment contribution to the merged `--format timeseries` document: +/// per commit `{"comment_count": n, "sentiment"?: f32}` (`sentiment` only when +/// the commit introduced or changed a comment). +#[must_use] +pub fn sentiment_timeseries_contribution( + sub: &clap::ArgMatches, +) -> Option<super::super::history_formats::TimeSeriesContribution> { + use cf_gojson::{GoMap, GoValue}; + use cf_sentiment::model::f32_float; + + let walk = sentiment_walk(sub)?; + let mut per_commit = Vec::new(); + let mut commit_meta = Vec::new(); + for c in walk.iter() { + commit_meta.push(( + c.hash.clone(), + c.tick, + crate::handlers::format_rfc3339_offset(c.when, c.offset_min), + String::new(), + )); + let mut entry = GoMap::new_map(); + entry.insert( + "comment_count".to_string(), + GoValue::Int(c.comments.len() as i64), + ); + if !c.comments.is_empty() { + entry.insert( + "sentiment".to_string(), + f32_float(cf_sentiment::scorer::compute_sentiment(&c.comments)), + ); + } + per_commit.push((c.hash.clone(), GoValue::Object(entry))); + } + Some(super::super::history_formats::TimeSeriesContribution { + flag: "sentiment", + per_commit, + commit_meta, + }) +} + +/// Recursively collects UAST nodes whose type is `Comment` into `out` +/// (preorder: the node itself before its children). +fn collect_comment_nodes( + node: &cf_uast_node::Node, + comment_type: &str, + out: &mut Vec<CommentNode>, +) { + if node.node_type == comment_type { + let (start_line, end_line) = match &node.pos { + Some(p) => (p.start_line as i64, p.end_line as i64), + // Nodes without a position cannot be grouped by line. + None => (-1, -1), + }; + if start_line >= 0 { + out.push(CommentNode { + start_line, + end_line, + token: node.token.clone(), + }); + } + } + for child in &node.children { + collect_comment_nodes(child, comment_type, out); + } +} + +/// Whether `name` is a shell-script path handled by the bash-comment fallback. +fn is_shell_path(name: &str) -> bool { + name.rsplit('.') + .next() + .is_some_and(|ext| ext.eq_ignore_ascii_case("sh")) + && name.contains('.') +} + +/// Extracts `#`-comment nodes from shell-script `content`: one node per +/// `#`-introduced line (including shebangs), spanning the `#` to end-of-line, +/// with `start_line == end_line` (1-based). +fn extract_shell_comment_nodes(content: &[u8], out: &mut Vec<CommentNode>) { + let text = String::from_utf8_lossy(content); + for (idx, line) in text.split('\n').enumerate() { + let line = line.strip_suffix('\r').unwrap_or(line); + let trimmed = line.trim_start(); + if !trimmed.starts_with('#') { + continue; + } + let lineno = (idx + 1) as i64; + out.push(CommentNode { + start_line: lineno, + end_line: lineno, + token: trimmed.to_string(), + }); + } +} diff --git a/crates/cf-commands/src/handlers/history/typos.rs b/crates/cf-commands/src/handlers/history/typos.rs new file mode 100644 index 00000000..22add025 --- /dev/null +++ b/crates/cf-commands/src/handlers/history/typos.rs @@ -0,0 +1,341 @@ +//! `history/typos` over the shared history walk. +//! +//! Per commit: the filtered tree diff; for every Modify change whose blobs +//! differ and are text, the libgit2 line-diff op stream feeds +//! [`cf_typos::candidate_line_pairs`] (equal-sized Delete/Insert runs whose +//! lines pass the Levenshtein prefilter), both UAST sides are parsed, and +//! [`cf_typos::identifier_pair`] turns the identifiers of each focused line +//! pair into the `(wrong → correct)` fix the commit made. The walk's +//! occurrences, in chronological order, are the report input +//! ([`cf_typos::ReportData`]); the crate derives the metrics and every +//! machine format encodes that ONE value (`typo_list` keeps the first fix of +//! each pair, `patterns` counts every occurrence, `line` is 1-based). + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use cf_alg_levenshtein::Context as LevenshteinContext; +use cf_gitlib::changes::ChangeAction; +use cf_gitlib::diff::LineOp; +use cf_typos::{DiffOp, Hash as TypoHash, ReportData, Typo}; + +use super::super::uast_walk::{uast_results, ChangeParses, ParseOutcome, Side, UastAnalyzer}; +use super::CommitStamp; + +/// The analyzer id. +const ID: &str = "history/typos"; +/// The yaml section name. +const NAME: &str = "history/typos"; + +/// `history/typos --format json` bytes (compact, no trailing newline). +#[must_use] +pub fn typos_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + typos_report_bytes(sub, crate::formats::FORMAT_JSON) +} + +/// `history/typos --format yaml` bytes: the `codefang (v2)` header, the +/// `history/typos:` section name and the report value. +#[must_use] +pub fn typos_run_report_yaml(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + typos_report_bytes(sub, crate::formats::FORMAT_YAML) +} + +/// `history/typos --format bin` bytes: the CFB1 envelope over the json +/// value. +#[must_use] +pub fn typos_run_report_bin(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + typos_report_bytes(sub, crate::formats::FORMAT_BIN_ALIAS) +} + +/// The one report value in `format`, or `None` when the repository cannot be +/// walked. +fn typos_report_bytes(sub: &clap::ArgMatches, format: &str) -> Option<Vec<u8>> { + let report = typos_report_data(sub)?; + let value = cf_typos::metrics_report_value(&report); + super::super::serialize_history_metrics(format, ID, NAME, &value, &value).ok() +} + +/// Builds the `history/typos` [`cf_typos::ReportData`]: every typo-fix +/// occurrence of the walk, chronological (the single report input every +/// output format derives its metrics from). Returns `None` when the +/// repository cannot be walked. +#[must_use] +pub fn typos_report_data(sub: &clap::ArgMatches) -> Option<ReportData> { + let walk = typos_walk(sub)?; + Some(ReportData { + typos: walk.iter().flat_map(|c| c.typos.iter().cloned()).collect(), + }) +} + +/// One walked commit's typos products plus the shared stamps. One entry per +/// walked commit, in walk order. +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) struct TyposCommit { + /// Position in the ORIGINAL window (oversized-dropped commits count). + pub pos: usize, + /// This commit's detected typos, in detection order. + pub typos: Vec<Typo>, + /// Tick. + pub tick: i64, + /// Author id. + pub author_id: i64, + /// Committer time, Unix seconds. + pub when: i64, + /// Committer UTC-offset minutes. + pub offset_min: i32, +} + +impl TyposCommit { + /// The record of one kept commit: its stamps and its typo fixes. + pub(crate) fn new(stamp: &CommitStamp, typos: Vec<Typo>) -> Self { + Self { + pos: stamp.pos, + typos, + tick: stamp.tick, + author_id: stamp.author_id, + when: stamp.when, + offset_min: stamp.offset_min, + } + } +} + +/// The `history/typos` records of the run's UAST pass: every typos format +/// consumes THIS walk. +pub(crate) fn typos_walk(sub: &clap::ArgMatches) -> Option<Arc<Vec<TyposCommit>>> { + uast_results(sub, UastAnalyzer::Typos)?.typos +} + +/// The effective `--typos-max-distance`: the flag when set (the preflight +/// rejects a non-positive value), else the crate default. +pub(crate) fn typos_max_distance(sub: &clap::ArgMatches) -> i64 { + let v = sub + .try_get_one::<i64>("typos-max-distance") + .ok() + .flatten() + .copied() + .unwrap_or(0); + if v <= 0 { + cf_typos::DEFAULT_MAXIMUM_ALLOWED_TYPO_DISTANCE as i64 + } else { + v + } +} + +/// The per-commit `history/typos` context: the commit every fix names, the +/// distance cap, and the Levenshtein scratch (pure scratch: results are +/// state-independent, so one context per commit equals one per walk). +pub(crate) struct TyposContext { + commit: TypoHash, + max_distance: usize, + lctx: LevenshteinContext, +} + +impl TyposContext { + /// The context of commit `hash` under `--typos-max-distance`. + pub(crate) fn new(hash: cf_gitlib::Hash, max_distance: i64) -> Self { + Self { + commit: TypoHash(hash.0), + max_distance: usize::try_from(max_distance) + .unwrap_or(cf_typos::DEFAULT_MAXIMUM_ALLOWED_TYPO_DISTANCE), + lctx: LevenshteinContext::new(), + } + } +} + +/// One change's `history/typos` contribution: the typo fixes a Modify change +/// made, in detection order, appended to the commit's fixes (so they follow +/// its change order). Only a Modify whose two text blobs differ and whose +/// two sides parse can fix a typo. +pub(crate) fn typos_change( + parses: &ChangeParses<'_>, + context: &mut TyposContext, + typos: &mut Vec<Typo>, +) { + let change = parses.change(); + if !matches!(change.action, ChangeAction::Modify) || change.from.hash == change.to.hash { + return; + } + let Some(blob_before) = parses.blob(Side::Before) else { + return; + }; + let Some(blob_after) = parses.blob(Side::After) else { + return; + }; + if blob_before.is_binary() || blob_after.is_binary() || blob_before.data == blob_after.data { + return; + } + let ParseOutcome::Parsed(before) = parses.parse(Side::Before) else { + return; + }; + let ParseOutcome::Parsed(after) = parses.parse(Side::After) else { + return; + }; + let old_lines = blob_before.count_lines().map_or(0, |n| n as i64); + let ops: Vec<DiffOp> = cf_gitlib::diff::diff_blob_line_ops( + parses.repo().native(), + change.from.hash, + change.to.hash, + old_lines, + ) + .unwrap_or_default() + .iter() + .map(diff_op) + .collect(); + let sides = ModifySides { + file: &change.to.name, + before, + after, + lines_before: split_lines(&blob_before.data), + lines_after: split_lines(&blob_after.data), + }; + push_line_pair_fixes(&sides, &ops, context, typos); +} + +/// The two parsed sides of one Modify change. +struct ModifySides<'a> { + /// The After path (the file a fix is reported in). + file: &'a str, + /// The Before UAST. + before: &'a cf_uast_node::Node, + /// The After UAST. + after: &'a cf_uast_node::Node, + /// The Before blob's raw (unstripped) lines; candidate line indices index + /// into them. + lines_before: Vec<&'a [u8]>, + /// The After blob's raw lines. + lines_after: Vec<&'a [u8]>, +} + +/// Appends the fixes of one Modify change: the equal-sized Delete/Insert line +/// pairs of `ops` that pass the Levenshtein prefilter, each turned into the +/// `(wrong → correct)` identifier fix its two lines make. +fn push_line_pair_fixes( + sides: &ModifySides<'_>, + ops: &[DiffOp], + context: &mut TyposContext, + typos: &mut Vec<Typo>, +) { + let pairs = cf_typos::candidate_line_pairs( + ops, + &sides.lines_before, + &sides.lines_after, + context.max_distance, + &mut context.lctx, + ); + if pairs.is_empty() { + return; + } + let focused_before: HashSet<usize> = pairs.iter().map(|p| p.before).collect(); + let focused_after: HashSet<usize> = pairs.iter().map(|p| p.after).collect(); + let removed = collect_identifiers_on_lines(sides.before, &focused_before); + let added = collect_identifiers_on_lines(sides.after, &focused_after); + + for pair in &pairs { + let (Some(nb), Some(na)) = (removed.get(&pair.before), added.get(&pair.after)) else { + continue; + }; + if let Some(fix) = + cf_typos::identifier_pair(nb, na, context.max_distance, &mut context.lctx) + { + typos.push(Typo { + wrong: fix.wrong, + correct: fix.correct, + file: sides.file.to_string(), + commit: context.commit, + line: pair.after as i64 + 1, + }); + } + } +} + +/// Maps a libgit2 line-diff op onto the crate's diff op. +fn diff_op(op: &LineOp) -> DiffOp { + let count = |n: i64| usize::try_from(n).unwrap_or(0); + match *op { + LineOp::Equal(n) => DiffOp::Equal(count(n)), + LineOp::Insert(n) => DiffOp::Insert(count(n)), + LineOp::Delete(n) => DiffOp::Delete(count(n)), + } +} + +/// Per-commit typos NDJSON records: only commits with detected typos emit a +/// line; `data` is the typo list (each `{Wrong, Correct, File, Commit, Line}` +/// with `Commit` as 20 bytes and `Line` 1-based). +#[must_use] +pub fn typos_ndjson_records( + sub: &clap::ArgMatches, +) -> Option<Vec<super::super::history_formats::NdjsonRecord>> { + use cf_gojson::{GoMap, GoValue}; + + let walk = typos_walk(sub)?; + let mut records = Vec::new(); + for c in walk.iter() { + if c.typos.is_empty() { + continue; + } + let items: Vec<GoValue> = c + .typos + .iter() + .map(|t| { + let mut m = GoMap::new_struct(); + m.insert("Wrong".to_string(), GoValue::Str(t.wrong.clone())); + m.insert("Correct".to_string(), GoValue::Str(t.correct.clone())); + m.insert("File".to_string(), GoValue::Str(t.file.clone())); + m.insert( + "Commit".to_string(), + GoValue::Array( + t.commit + .0 + .iter() + .map(|b| GoValue::Int(i64::from(*b))) + .collect(), + ), + ); + m.insert("Line".to_string(), GoValue::Int(t.line)); + GoValue::Object(m) + }) + .collect(); + records.push(super::super::history_formats::NdjsonRecord { + pos: c.pos, + hash: c.typos[0].commit.string(), + tick: c.tick, + author_id: c.author_id, + time_secs: c.when, + tz_offset_min: c.offset_min, + data: GoValue::Array(items), + }); + } + Some(records) +} + +/// Splits on `\n`, dropping the newline; a trailing newline yields a final +/// empty element. +fn split_lines(data: &[u8]) -> Vec<&[u8]> { + data.split(|&b| b == b'\n').collect() +} + +/// Groups the identifier tokens of `root` by their 0-based start line, +/// keeping only the focused lines. +fn collect_identifiers_on_lines( + root: &cf_uast_node::Node, + focused: &HashSet<usize>, +) -> HashMap<usize, Vec<String>> { + use cf_uast_node::UAST_IDENTIFIER; + let mut result: HashMap<usize, Vec<String>> = HashMap::new(); + root.visit_pre_order(|n| { + if n.node_type != UAST_IDENTIFIER { + return; + } + let Some(line) = n + .pos + .as_ref() + .and_then(|pos| (pos.start_line as usize).checked_sub(1)) + else { + return; + }; + if focused.contains(&line) { + result.entry(line).or_default().push(n.token.clone()); + } + }); + result +} diff --git a/crates/cf-commands/src/handlers/history_formats.rs b/crates/cf-commands/src/handlers/history_formats.rs index 8a85bcf4..91330e66 100644 --- a/crates/cf-commands/src/handlers/history_formats.rs +++ b/crates/cf-commands/src/handlers/history_formats.rs @@ -1,208 +1,284 @@ -//! Centralized history-phase output for the streaming/derived formats — -//! `text`, `ndjson`, `timeseries` (and `timeseries+ndjson`) — the Rust port of -//! the reference `analyze.OutputHistoryResults` + `analyze.StreamingSink` + -//! `analyze.BuildMergedTimeSeriesDirect` trio. +//! Centralized history-phase output for the whole-selection formats — `text`, +//! `compact`, `ndjson`, `timeseries` and `timeseries --ndjson`. //! //! Unlike json/yaml/bin (per-analyzer encodings dispatched through the -//! registry), the reference implementation routes these three formats through ONE history-phase output -//! function over the whole selected leaf set: +//! registry), these formats are rendered over the whole selected analyzer +//! set: //! -//! - `text`: `PrintHeader` once, then per leaf `"<Name>:\n"` + -//! `leaf.Serialize(res, "text", w)`. Only the leaves wired with a -//! `SerializeTextFn` hook support text (sentiment, shotness, burndown, -//! couples, devs, file-history); the rest fall through -//! `writeMetricsToFormat`'s default arm and the run FAILS with -//! `serialization error for <Name>: unsupported format: text` AFTER the -//! header + section name were already written to stdout (exit 1). -//! - `ndjson`: one compact JSON line per non-nil per-commit TC -//! (`{"hash","tick","author_id","timestamp","analyzer","data"}`), written in -//! walk order during the pipeline; the final report is NOT emitted. -//! - `timeseries`: a merged `codefang.timeseries.v1` document built from the -//! per-commit summaries of every leaf implementing -//! `CommitTimeSeriesProvider`, ordered by `commits_by_tick` + `commit_meta`. +//! - `text`: the version header once, then per analyzer `"<id>:\n"` + the +//! analyzer's human summary ([`super::history_text`]). +//! - `compact`: one line per analyzer — its id, `Info` and its key figures — +//! rendered by the compact renderer the static analyzers use. +//! - `ndjson`: one compact JSON line per per-commit record +//! (`{"hash","tick","author_id","timestamp","analyzer","data"}`), every +//! analyzer's records merged by commit position (selection order within a +//! commit). +//! - `timeseries`: a merged `codefang.timeseries.v1` document of the +//! per-commit summaries of every analyzer, in commit order; with `--ndjson` +//! its `commits` array, one compact line per commit. //! -//! This module owns the per-format byte shaping; the per-analyzer per-commit -//! payloads come from the walk functions in [`super::history`] and friends. - -use cf_gojson::GoValue; - -use crate::pipeline::RunContext; - -/// A history-phase failure that occurs AFTER bytes were already streamed to -/// stdout (the reference implementation writes the text header/section names before the per-leaf -/// `Serialize` call can fail). The caller must emit `partial` to stdout, the -/// `Error: <message>` line to stderr, and exit 1 — exactly cobra's error path. -pub struct PartialFailure { - /// Bytes the reference implementation would have already written to stdout before the error. - pub partial: Vec<u8>, - /// The error text after the `Error: ` prefix. - pub message: String, -} +//! Every renderer builds the WHOLE output before returning it: an analyzer +//! that produces no report is a [`HandlerError`] and nothing reaches stdout. -/// Result of a centralized history-format run: full stdout bytes on success, -/// [`PartialFailure`] when the reference implementation errors mid-stream. `None` means this module has -/// no implementation for the selection yet — the caller falls through to the -/// per-id pipeline (and its dispatch-blocked diagnostic), preserving the -/// previous behavior for unported combinations. -pub type HistoryFormatResult = Option<Result<Vec<u8>, PartialFailure>>; +use std::collections::{BTreeSet, HashMap, HashSet}; -/// The reference `HistoryAnalyzer.Name()` for each leaf id (the text/yaml section header -/// and the error-message subject). Most leaves default to the descriptor id; -/// shotness/couples/file-history/imports override it. -#[must_use] -pub fn leaf_name(id: &str) -> Option<&'static str> { - Some(match id { - "history/imports" => "ImportsPerDeveloper", - "history/typos" => "history/typos", - "history/couples" => "Couples", - "history/shotness" => "Shotness", - "history/devs" => "history/devs", - "history/anomaly" => "TemporalAnomaly", - "history/quality" => "history/quality", - "history/sentiment" => "history/sentiment", - "history/file-history" => "FileHistoryAnalysis", - "history/burndown" => "history/burndown", - _ => return None, +use cf_gojson::{GoMap, GoValue, MapOrigin}; + +use crate::formats::{ + FORMAT_COMPACT, FORMAT_JSON, FORMAT_NDJSON, FORMAT_TEXT, FORMAT_TIMESERIES, + FORMAT_TIMESERIES_NDJSON, +}; +use crate::pipeline::{HandlerError, RunContext}; + +/// The [`HandlerError::NoReport`] for analyzer `id` in `format`. +fn no_report(id: &str, format: &str) -> HandlerError { + HandlerError::NoReport { + analyzer: id.to_string(), + format: format.to_string(), + } +} + +/// Computes `render` for every selected analyzer — concurrently, after the +/// ONE shared UAST walk of the co-selected heavy analyzers is warmed — and +/// returns the results in selection order, or the first analyzer's +/// [`HandlerError::NoReport`]. +fn render_each<T: Send>( + ctx: &RunContext, + ids: &[String], + format: &str, + render: impl Fn(&str, &RunContext) -> Option<T> + Sync, +) -> Result<Vec<T>, HandlerError> { + super::uast_walk::prewarm(ctx.matches); + super::run_concurrent(ids.len(), super::ANALYZER_CONCURRENCY, |i| { + render(&ids[i], ctx) }) + .into_iter() + .zip(ids) + .map(|(result, id)| result.ok_or_else(|| no_report(id, format))) + .collect() } -/// Appends the reference `analyze.PrintHeader` (`codefang (v2):` / version / hash). +// --------------------------------------------------------------------------- +// text +// --------------------------------------------------------------------------- + +/// Appends the `codefang (v2):` / version / hash header. fn print_header(out: &mut Vec<u8>) { out.extend_from_slice(b"codefang (v2):\n"); out.extend_from_slice(format!(" version: {}\n", cf_version::DEFAULT_BINARY).as_bytes()); out.extend_from_slice(format!(" hash: {}\n", cf_version::BINARY_GIT_HASH).as_bytes()); } -/// Whether the leaf's report is non-nil for this run — the reference implementation only prints the -/// `"<Name>:\n"` section (and only then can fail serialization) when -/// `results[leaf] != nil`, which requires actually running the analysis. -/// Reuses the same walk the json/yaml formats use, so the text path performs -/// the identical work the reference implementation does before erroring. -fn leaf_report_exists(id: &str, ctx: &RunContext) -> bool { - let sub = ctx.matches; - match id { - "history/imports" => super::history::imports_run_metrics(sub).is_some(), - "history/typos" => super::history::typos_report_data(sub).is_some(), - "history/anomaly" => super::history::anomaly_run_metrics(sub).is_some(), - "history/quality" => super::history::quality_metrics(sub).is_some(), - _ => false, - } -} - -/// The text serializer for the leaves wired with the reference implementation's `SerializeTextFn` hook -/// (the [`super::history_text`] renderers). `None` for the four leaves with no -/// reference hook, making [`history_text`] take the unsupported-format error path. +/// The text body of one analyzer ([`super::history_text`]). fn leaf_text(id: &str, ctx: &RunContext) -> Option<Vec<u8>> { let sub = ctx.matches; match id { - "history/sentiment" => super::history_text::sentiment_text(sub), - "history/shotness" => super::history_text::shotness_text(sub), "history/burndown" => super::history_text::burndown_text(sub), - "history/couples" => super::history_text::couples_text(sub), "history/devs" => super::history_text::devs_text(sub), + "history/couples" => super::history_text::couples_text(sub), "history/file-history" => super::history_text::file_history_text(sub), + "history/shotness" => super::history_text::shotness_text(sub), + "history/quality" => super::history_text::quality_text(sub), + "history/sentiment" => super::history_text::sentiment_text(sub), + "history/typos" => super::history_text::typos_text(sub), + "history/anomaly" => super::history_text::anomaly_text(sub), + "history/imports" => super::history_text::imports_text(sub), _ => None, } } -/// Whether the reference implementation wires a `SerializeTextFn` for this leaf (text is a supported -/// format). The other four history leaves fall through `writeMetricsToFormat` -/// to `unsupported format: text`. -fn leaf_supports_text(id: &str) -> bool { - matches!( - id, - "history/sentiment" - | "history/shotness" - | "history/burndown" - | "history/couples" - | "history/devs" - | "history/file-history" - ) +/// The centralized `--format text` history path: the header once, then per +/// analyzer its id as the section line and its text body. +/// +/// # Errors +/// +/// [`HandlerError::NoReport`] naming the first analyzer without a report; +/// nothing is rendered then. +pub fn history_text(ctx: &RunContext, ids: &[String]) -> Result<Vec<u8>, HandlerError> { + let bodies = render_each(ctx, ids, FORMAT_TEXT, leaf_text)?; + let mut out = Vec::new(); + print_header(&mut out); + for (id, body) in ids.iter().zip(bodies) { + out.extend_from_slice(format!("{id}:\n").as_bytes()); + out.extend_from_slice(&body); + } + Ok(out) } -/// The centralized `--format text` history path (the reference `OutputHistoryResults` -/// with `format == "text"`): header once, then per leaf the section name and -/// the leaf's text serialization. A leaf without a text hook aborts the run -/// with the reference implementation's exact `serialization error` message AFTER the header + its -/// section name were written. -#[must_use] -pub fn history_text(ctx: &RunContext, ids: &[String]) -> HistoryFormatResult { - // Every selected leaf must be one this module can either render or - // faithfully fail; otherwise fall through to the per-id pipeline. (The - // co-selected heavy leaves share ONE memoized UAST walk: the first leaf's - // walk call computes it, the rest re-read it.) - for id in ids { - leaf_name(id)?; - if leaf_supports_text(id) { - // Reference-supported leaf whose Rust text renderer is not ported yet: - // fall through (dispatch-blocked) rather than mis-serialize. - leaf_text(id, ctx)?; +// --------------------------------------------------------------------------- +// compact +// --------------------------------------------------------------------------- + +/// The report key whose scalar entries become a history analyzer's compact +/// figures. +const COMPACT_AGGREGATE_KEY: &str = "aggregate"; +/// The score every history compact section carries (info-only, rendered +/// `Info`): history reports are measurements, not graded sections. +const COMPACT_INFO_SCORE: f64 = -1.0; +/// Decimals a fractional compact figure keeps. +const COMPACT_DECIMALS: usize = 2; +/// Largest magnitude a float may have to print as an integer. +const COMPACT_MAX_INTEGRAL: f64 = 1e15; + +/// One compact figure as a human reads it: integers as-is, a float with at +/// most [`COMPACT_DECIMALS`] decimals (trailing zeros dropped), booleans as +/// words. `None` for a non-scalar value. +fn compact_figure(v: &GoValue) -> Option<String> { + Some(match v { + GoValue::Int(n) => n.to_string(), + GoValue::Uint(n) => n.to_string(), + GoValue::Bool(b) => b.to_string(), + GoValue::Float(f) if f.fract() == 0.0 && f.abs() < COMPACT_MAX_INTEGRAL => { + format!("{f:.0}") } - } + GoValue::Float(f) => { + let fixed = format!("{f:.COMPACT_DECIMALS$}"); + fixed + .trim_end_matches('0') + .trim_end_matches('.') + .to_string() + } + _ => return None, + }) +} - let mut out = Vec::new(); - print_header(&mut out); +/// The scalar `(key, figure)` pairs of the report's `aggregate` object +/// (falling back to the top-level scalars). +fn aggregate_figures(report: &GoValue) -> Vec<(String, String)> { + let scalars = |m: &GoMap| -> Vec<(String, String)> { + m.iter() + .filter_map(|(k, v)| Some((k.clone(), compact_figure(v)?))) + .collect() + }; + report + .as_map() + .map(|root| { + root.get(COMPACT_AGGREGATE_KEY) + .and_then(GoValue::as_map) + .map_or_else(|| scalars(root), scalars) + }) + .unwrap_or_default() +} - for id in ids { - let name = leaf_name(id).expect("checked above"); - if leaf_supports_text(id) { - let body = leaf_text(id, ctx).expect("checked above"); - out.extend_from_slice(format!("{name}:\n").as_bytes()); - out.extend_from_slice(&body); - } else { - // The reference implementation runs the full analysis; only a non-nil report prints the - // section name and reaches the failing Serialize call. - if !leaf_report_exists(id, ctx) { - return Some(Err(PartialFailure { - partial: out, - message: format!("analyzer history phase failed for {id}"), - })); - } - out.extend_from_slice(format!("{name}:\n").as_bytes()); - return Some(Err(PartialFailure { - partial: out, - message: format!("serialization error for {name}: unsupported format: text"), - })); - } +/// The compact figures of one analyzer: the `aggregate` scalars of its JSON +/// report; imports (whose report has no aggregate) summarizes its usage map. +fn leaf_compact_figures(id: &str, ctx: &RunContext) -> Option<Vec<(String, String)>> { + if id == "history/imports" { + let report = super::history::imports_run_metrics(ctx.matches)?; + let summary = super::history_text::imports_summary(&report); + return Some(vec![ + ("developers".to_string(), summary.developers.to_string()), + ("languages".to_string(), summary.languages.to_string()), + ( + "distinct_imports".to_string(), + summary.distinct_imports.to_string(), + ), + ("usages".to_string(), summary.usages.to_string()), + ]); } + let registry = super::default_registry(); + let bytes = (registry.lookup(id)?.run)(ctx, FORMAT_JSON).ok()?; + let report = cf_analyze::json_parse::parse(&bytes).ok()?; + Some(aggregate_figures(&report)) +} + +/// The compact section of one analyzer: its id (padded to `title_width` so +/// the figures of a multi-analyzer run line up) as title, an info-only score +/// and the figures joined `key=value` as the status the compact line shows. +fn compact_section(id: &str, title_width: usize, figures: &[(String, String)]) -> GoValue { + let status = figures + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::<Vec<_>>() + .join(" "); + let mut section = GoMap::new(MapOrigin::Struct); + section.push("title", GoValue::Str(format!("{id:<title_width$}"))); + section.push("status", GoValue::Str(status)); + section.push("score", GoValue::Float(COMPACT_INFO_SCORE)); + GoValue::Map(section) +} - Some(Ok(out)) +/// The centralized `--format compact` history path: one line per analyzer, +/// rendered by the shared compact renderer. +/// +/// # Errors +/// +/// [`HandlerError::NoReport`] naming the first analyzer without a report. +pub fn history_compact(ctx: &RunContext, ids: &[String]) -> Result<Vec<u8>, HandlerError> { + let figures = render_each(ctx, ids, FORMAT_COMPACT, leaf_compact_figures)?; + let title_width = ids.iter().map(|id| id.chars().count()).max().unwrap_or(0); + let sections = ids + .iter() + .zip(&figures) + .map(|(id, f)| compact_section(id, title_width, f)) + .collect(); + let mut root = GoMap::new(MapOrigin::Struct); + root.push("sections", GoValue::Array(sections)); + Ok(super::section_render::render_compact_report(&GoValue::Map( + root, + ))) } // --------------------------------------------------------------------------- // timeseries // --------------------------------------------------------------------------- -/// One leaf's contribution to the merged timeseries: its `Flag()` plus the -/// per-commit summary values (the reference `ExtractCommitTimeSeries`, keyed by hash) and -/// the ordering data its report carries (`commits_by_tick` + `commit_meta`). +/// Ordered commit metadata of a contribution: `(hash, tick, +/// rfc3339_timestamp, author)`, oldest first. +pub type CommitMeta = (String, i64, String, String); + +/// One analyzer's contribution to the merged timeseries: its flag plus the +/// per-commit summary values (keyed by hash) and the commit order of its walk. pub struct TimeSeriesContribution { - /// The reference `Flag()` — the analyzer key inside each merged commit object. + /// The analyzer key inside each merged commit object. pub flag: &'static str, - /// Per-commit summary values keyed by full hex hash (reference: provider data). + /// Per-commit summary values keyed by full hex hash. pub per_commit: Vec<(String, GoValue)>, - /// Ordered commit metadata derived from the report's `commits_by_tick` + - /// `commit_meta`: `(hash, tick, rfc3339_timestamp, author)` in tick order. - /// Empty when the leaf's report has no `commits_by_tick` (e.g. devs). - pub commit_meta: Vec<(String, i64, String, String)>, + /// The walk's commits, oldest first (possibly only the data-bearing + /// ones). + pub commit_meta: Vec<CommitMeta>, +} + +/// One compact JSON line per data-bearing commit of `contrib`: the merged +/// commit object `{author, hash, <flag>, tick, timestamp}` with the +/// analyzer's per-commit value under its flag. +#[must_use] +pub fn timeseries_ndjson_lines(contrib: &TimeSeriesContribution) -> Vec<u8> { + commit_lines(&merged_commits(&[contrib])) } -/// Per-leaf timeseries data source. `None` body when the leaf is not ported -/// yet. A leaf that has NO provider in the reference implementation (typos) returns a contribution with -/// empty `per_commit`, mirroring `collectProviderData` skipping it. +/// The `history/typos` contribution: per commit that fixed typos, the number +/// of fixes and of distinct files they were in. +fn typos_timeseries_contribution(sub: &clap::ArgMatches) -> Option<TimeSeriesContribution> { + let walk = super::history::typos::typos_walk(sub)?; + let mut per_commit = Vec::new(); + let mut commit_meta = Vec::new(); + for c in walk.iter().filter(|c| !c.typos.is_empty()) { + let hash = c.typos[0].commit.string(); + let files: BTreeSet<&str> = c.typos.iter().map(|t| t.file.as_str()).collect(); + let mut entry = GoMap::new_map(); + entry.insert("fixes", GoValue::Int(c.typos.len() as i64)); + entry.insert("files", GoValue::Int(files.len() as i64)); + per_commit.push((hash.clone(), GoValue::Map(entry))); + commit_meta.push(( + hash, + c.tick, + super::format_rfc3339_offset(c.when, c.offset_min), + String::new(), + )); + } + Some(TimeSeriesContribution { + flag: "typos", + per_commit, + commit_meta, + }) +} + +/// Per-analyzer timeseries data source. fn leaf_timeseries(id: &str, ctx: &RunContext) -> Option<TimeSeriesContribution> { let sub = ctx.matches; match id { - // typos: no CommitTimeSeriesProvider and no commits_by_tick — the - // merged document is structurally empty, but the reference implementation still runs the walk. - "history/typos" => { - super::history::typos_report_data(sub)?; - Some(TimeSeriesContribution { - flag: "typos", - per_commit: Vec::new(), - commit_meta: Vec::new(), - }) - } + "history/typos" => typos_timeseries_contribution(sub), "history/devs" => super::history::devs_timeseries_contribution(sub), "history/anomaly" => super::history::anomaly_timeseries_contribution(sub), "history/imports" => super::history::imports_timeseries_contribution(sub), @@ -216,156 +292,161 @@ fn leaf_timeseries(id: &str, ctx: &RunContext) -> Option<TimeSeriesContribution> } } -/// The centralized `--format timeseries` history path: collect provider data -/// from every leaf (flag-sorted, the reference `collectProviderData`), order commits by -/// the first report carrying `commits_by_tick`, and emit the merged +/// The centralized `--format timeseries` history path: every analyzer's +/// contribution (flag-sorted) merged per commit into the /// `codefang.timeseries.v1` document (2-space-indented JSON + trailing -/// newline, the reference `WriteMergedTimeSeries`). With `ndjson_lines` (the -/// `timeseries+ndjson` composition) each merged commit object is emitted as -/// one compact line instead. -#[must_use] +/// newline). With `ndjson_lines` (`--format timeseries --ndjson`) the +/// document's `commits` array is emitted instead, one compact line per +/// commit. +/// +/// # Errors +/// +/// An invalid `--tick-size`, or [`HandlerError::NoReport`] naming the first +/// analyzer without a report. pub fn history_timeseries( ctx: &RunContext, ids: &[String], ndjson_lines: bool, -) -> HistoryFormatResult { - // The co-selected heavy leaves share ONE memoized UAST walk: the first - // leaf's contribution computes it, the rest re-read it. - let mut contribs = Vec::with_capacity(ids.len()); - for id in ids { - contribs.push(leaf_timeseries(id, ctx)?); - } - // The reference implementation sorts leaves by Flag() before collecting provider data. +) -> Result<Vec<u8>, HandlerError> { + let format = if ndjson_lines { + FORMAT_TIMESERIES_NDJSON + } else { + FORMAT_TIMESERIES + }; + let tick_size_hours = + super::history::HistoryOptions::from_matches(ctx.matches)?.tick_size_hours; + let mut contribs = render_each(ctx, ids, format, leaf_timeseries)?; contribs.sort_by(|a, b| a.flag.cmp(b.flag)); - - // Ordering comes from the FIRST leaf (selection order) whose report has - // commits_by_tick; commit_meta enriches timestamps/authors. - let commit_meta: &[(String, i64, String, String)] = contribs - .iter() - .find(|c| !c.commit_meta.is_empty()) - .map_or(&[], |c| c.commit_meta.as_slice()); - - // active = leaves with non-empty provider data, flag-sorted. - let active: Vec<&TimeSeriesContribution> = contribs - .iter() - .filter(|c| !c.per_commit.is_empty()) - .collect(); - - let bytes = render_merged_timeseries(&active, commit_meta, ndjson_lines); - Some(Ok(bytes)) -} - -/// Builds the merged-timeseries bytes (the reference `BuildMergedTimeSeriesDirect` + -/// `WriteMergedTimeSeries`/`WriteTimeSeriesNDJSON`). -fn render_merged_timeseries( - active: &[&TimeSeriesContribution], - commit_meta: &[(String, i64, String, String)], - ndjson_lines: bool, -) -> Vec<u8> { - use cf_gojson::{GoMap, MapOrigin}; - - // The set of hashes any analyzer contributed. - let mut in_set = std::collections::HashSet::new(); - for c in active { - for (h, _) in &c.per_commit { - in_set.insert(h.as_str()); - } - } - - // Ordered commits: commit_meta order filtered to the contributed set. - let mut commits: Vec<GoValue> = Vec::new(); - for (hash, tick, ts, author) in commit_meta { - if !in_set.contains(hash.as_str()) { - continue; - } - // The reference implementation marshals MergedCommitData through a map[string]any — keys are - // SORTED by encoding/json (author, hash, <flags...>, tick, timestamp - // interleave alphabetically). - let mut m = GoMap::new(MapOrigin::Map); - m.push("hash", GoValue::Str(hash.clone())); - m.push("timestamp", GoValue::Str(ts.clone())); - m.push("author", GoValue::Str(author.clone())); - m.push("tick", GoValue::Int(*tick)); - for c in active { - if let Some((_, v)) = c.per_commit.iter().find(|(h, _)| h == hash) { - m.push(c.flag, v.clone()); - } - } - commits.push(GoValue::Map(m)); + let refs: Vec<&TimeSeriesContribution> = contribs.iter().collect(); + let commits = merged_commits(&refs); + if ndjson_lines { + return Ok(commit_lines(&commits)); } - // Top level is a STRUCT (field order fixed by the reference json tags). + // Top level is a STRUCT (fixed field order). let mut root = GoMap::new(MapOrigin::Struct); root.push( "version", GoValue::Str("codefang.timeseries.v1".to_string()), ); - root.push("tick_size_hours", GoValue::Int(24)); + root.push("tick_size_hours", GoValue::Int(tick_size_hours)); root.push( "analyzers", GoValue::Array( - active + contribs .iter() + .filter(|c| !c.per_commit.is_empty()) .map(|c| GoValue::Str(c.flag.to_string())) .collect(), ), ); - - if ndjson_lines { - // One compact line per merged commit object. - let mut out = Vec::new(); - for c in commits { - out.extend_from_slice(&cf_gojson::marshal(&c)); - out.push(b'\n'); - } - return out; - } - root.push("commits", GoValue::Array(commits)); let mut out = cf_gojson::Encoder::indented(" ").encode_to_vec(&GoValue::Map(root)); - // The reference `json.Encoder.Encode` appends a newline after the document. out.push(b'\n'); + Ok(out) +} + +/// One compact JSON line per merged commit object. +fn commit_lines(commits: &[GoValue]) -> Vec<u8> { + let mut out = Vec::new(); + for c in commits { + out.extend_from_slice(&cf_gojson::marshal(c)); + out.push(b'\n'); + } out } +/// The commit order of the merged document: the longest walk's commits +/// (ties → flag order), then every commit only a shorter walk lists (e.g. a +/// typo-fixing commit when only data-bearing walks are selected), each +/// placed after the last commit of its tick or earlier. +fn commit_order<'a>(contribs: &[&'a TimeSeriesContribution]) -> Vec<&'a CommitMeta> { + let Some(base) = contribs + .iter() + .enumerate() + .max_by(|(ia, a), (ib, b)| { + a.commit_meta + .len() + .cmp(&b.commit_meta.len()) + .then_with(|| ib.cmp(ia)) + }) + .map(|(_, c)| c) + else { + return Vec::new(); + }; + let mut order: Vec<&CommitMeta> = base.commit_meta.iter().collect(); + let mut seen: HashSet<&str> = order.iter().map(|m| m.0.as_str()).collect(); + for c in contribs { + for meta in &c.commit_meta { + if seen.insert(meta.0.as_str()) { + let at = order.partition_point(|m| m.1 <= meta.1); + order.insert(at, meta); + } + } + } + order +} + +/// The merged commit objects: per commit in [`commit_order`] that any +/// contribution has a value for, `{author, hash, <flag>…, tick, timestamp}` +/// (map-origin: keys byte-sorted on encode). Each contribution's values are +/// indexed by hash once, so the merge is linear in the commit count. +fn merged_commits(contribs: &[&TimeSeriesContribution]) -> Vec<GoValue> { + let active: Vec<(&'static str, HashMap<&str, &GoValue>)> = contribs + .iter() + .filter(|c| !c.per_commit.is_empty()) + .map(|c| { + let values = c.per_commit.iter().map(|(h, v)| (h.as_str(), v)).collect(); + (c.flag, values) + }) + .collect(); + commit_order(contribs) + .into_iter() + .filter_map(|(hash, tick, ts, author)| { + let mut values = active + .iter() + .filter_map(|(flag, by_hash)| Some((*flag, *by_hash.get(hash.as_str())?))) + .peekable(); + values.peek()?; + let mut m = GoMap::new(MapOrigin::Map); + m.push("hash", GoValue::Str(hash.clone())); + m.push("timestamp", GoValue::Str(ts.clone())); + m.push("author", GoValue::Str(author.clone())); + m.push("tick", GoValue::Int(*tick)); + for (flag, v) in values { + m.push(flag, v.clone()); + } + Some(GoValue::Map(m)) + }) + .collect() +} + // --------------------------------------------------------------------------- // ndjson // --------------------------------------------------------------------------- -/// One per-commit TC record (the reference `analyze.TC` after runner stamping): the -/// commit identity plus the analyzer-specific `data` payload. +/// One per-commit record: the commit identity plus the analyzer-specific +/// `data` payload. pub struct NdjsonRecord { - /// The commit's consume position in the walk (0-based). For leaves the reference implementation - /// forks across workers this drives the drain order; sequential leaves - /// emit in walk order regardless. + /// The commit's position in the walk (0-based); drives the merged emit + /// order across leaves. pub pos: usize, /// Full hex commit hash. pub hash: String, - /// Tick index (TicksSinceStart). + /// Tick index. pub tick: i64, /// Identity-detector author id. pub author_id: i64, - /// Commit author time, Unix seconds. + /// Committer time, Unix seconds. pub time_secs: i64, - /// Commit author timezone offset, minutes. + /// Committer timezone offset, minutes. pub tz_offset_min: i32, /// The analyzer-specific payload (`data`); records with no payload are - /// never constructed (the reference implementation skips nil-Data TCs). + /// never constructed. pub data: GoValue, } -/// Whether the reference implementation forks this leaf across `LeafWorkers` (declared `Sequential: -/// false` and `analyze.Parallelizable`). Forked leaves' TCs reach the NDJSON -/// sink drained worker-by-worker: consume position `p` goes to worker `p % W` -/// (`W` = [`super::leaf_worker_count`]), so the emitted line order is the -/// commits stably reordered by `(p % W, p)` — oracle-verified on -/// anomaly@hercules. `burndown` and `devs` declare `Sequential: true` and emit -/// in plain walk order. -fn leaf_forked(id: &str) -> bool { - !matches!(id, "history/burndown" | "history/devs") -} - -/// Per-leaf ndjson record source. `None` when the leaf is not ported yet. +/// Per-leaf ndjson record source. fn leaf_ndjson(id: &str, ctx: &RunContext) -> Option<Vec<NdjsonRecord>> { let sub = ctx.matches; match id { @@ -383,10 +464,9 @@ fn leaf_ndjson(id: &str, ctx: &RunContext) -> Option<Vec<NdjsonRecord>> { } } -/// The reference `Flag()` per leaf id (the `"analyzer"` field of each ndjson line and the -/// analyzer key in the merged timeseries). -#[must_use] -pub fn leaf_flag(id: &str) -> Option<&'static str> { +/// The `"analyzer"` field of each ndjson line and the analyzer key in the +/// merged timeseries, per analyzer id. +fn leaf_flag(id: &str) -> Option<&'static str> { Some(match id { "history/imports" => "imports-per-dev", "history/typos" => "typos", @@ -402,24 +482,16 @@ pub fn leaf_flag(id: &str) -> Option<&'static str> { }) } -/// The centralized `--format ndjson` history path: one -/// compact JSON line per non-nil TC, fields `hash`/`tick`/`author_id`/ -/// `timestamp`/`analyzer`/`data` in struct order, RFC3339 timestamp. -#[must_use] -pub fn history_ndjson(ctx: &RunContext, ids: &[String]) -> HistoryFormatResult { - use cf_gojson::{GoMap, MapOrigin}; - - // Emit model (oracle-verified against the live reference binary): the - // SEQUENTIAL leaves (burndown, devs — `Sequential: true`) write their TC - // line for each commit DURING the walk, so with several sequential leaves - // selected their lines interleave per commit in selection order - // (`burndown,devs` → b,d,b,d,…; `devs,burndown` → d,b,d,b,…). The FORKED - // leaves drain worker-by-worker after their fork completes, so their lines - // follow the sequential stream (each forked leaf's records stably - // reordered by `(pos % W, pos)`). The relative order of DIFFERENT forked - // leaves is Go-scheduler-dependent (measured Go-nondeterministic); we emit - // them in selection order, which is inside the reference's own observed - // variance. +/// The centralized `--format ndjson` history path: one compact JSON line per +/// record, fields `hash`/`tick`/`author_id`/`timestamp`/`analyzer`/`data` in +/// struct order, RFC3339 timestamp. Every leaf's records are merged by commit +/// position; within one commit the lines follow the selection order — a pure +/// function of the repository, the flags and the selection. +/// +/// # Errors +/// +/// [`HandlerError::NoReport`] naming the first analyzer without records. +pub fn history_ndjson(ctx: &RunContext, ids: &[String]) -> Result<Vec<u8>, HandlerError> { let serialize = |out: &mut Vec<u8>, flag: &str, r: NdjsonRecord| { let mut m = GoMap::new(MapOrigin::Struct); m.push("hash", GoValue::Str(r.hash)); @@ -435,28 +507,23 @@ pub fn history_ndjson(ctx: &RunContext, ids: &[String]) -> HistoryFormatResult { out.push(b'\n'); }; - // The co-selected heavy leaves share ONE memoized UAST walk: the first - // leaf's records compute it, the rest re-read it. - let mut out = Vec::new(); - - // Sequential leaves: gather each leaf's walk-ordered records, then merge - // them per commit position (selection order breaks ties within a commit — - // the first-selected sequential leaf's line for a commit precedes the - // second's, matching the reference per-commit consume loop). - let mut seq_streams: Vec<( + let records = render_each(ctx, ids, FORMAT_NDJSON, |id, ctx| { + Some((leaf_flag(id)?, leaf_ndjson(id, ctx)?)) + })?; + let mut streams: Vec<( &'static str, std::iter::Peekable<std::vec::IntoIter<NdjsonRecord>>, - )> = Vec::new(); - for id in ids { - if leaf_forked(id) { - continue; - } - let flag = leaf_flag(id)?; - seq_streams.push((flag, leaf_ndjson(id, ctx)?.into_iter().peekable())); - } + )> = records + .into_iter() + .map(|(flag, r)| (flag, r.into_iter().peekable())) + .collect(); + + let mut out = Vec::new(); loop { - let mut best: Option<(usize, usize)> = None; // (stream index, pos) - for (i, (_, it)) in seq_streams.iter_mut().enumerate() { + // The stream whose head has the lowest position; ties resolve to the + // first-selected leaf. + let mut best: Option<(usize, usize)> = None; + for (i, (_, it)) in streams.iter_mut().enumerate() { if let Some(head) = it.peek() { if best.map_or(true, |(_, bp)| head.pos < bp) { best = Some((i, head.pos)); @@ -464,23 +531,193 @@ pub fn history_ndjson(ctx: &RunContext, ids: &[String]) -> HistoryFormatResult { } } let Some((i, _)) = best else { break }; - let (flag, it) = &mut seq_streams[i]; + let (flag, it) = &mut streams[i]; let head = it.next().expect("peeked head exists"); serialize(&mut out, flag, head); } + Ok(out) +} - // Forked leaves: each leaf's records after its worker-drain reorder. - for id in ids { - if !leaf_forked(id) { - continue; - } - let flag = leaf_flag(id)?; - let mut records = leaf_ndjson(id, ctx)?; - let w = super::leaf_worker_count(); - records.sort_by_key(|r| (r.pos % w, r.pos)); - for r in records { - serialize(&mut out, flag, r); +#[cfg(test)] +mod tests { + use super::*; + + /// Commits in the large-merge fixture (well above the 2000 the quadratic + /// lookup made slow). + const MANY_COMMITS: usize = 2_500; + + fn meta(i: usize, tick: i64) -> CommitMeta { + ( + format!("{i:040x}"), + tick, + format!("2021-01-01T00:00:{:02}Z", i % 60), + String::new(), + ) + } + + fn value(n: usize) -> GoValue { + let mut m = GoMap::new_map(); + m.insert("n", GoValue::Int(n as i64)); + GoValue::Map(m) + } + + /// The merge as the quadratic implementation computed it: per commit of + /// the base walk, a linear search of every contribution. + fn naive_merge(base: &[CommitMeta], contribs: &[&TimeSeriesContribution]) -> Vec<GoValue> { + base.iter() + .filter_map(|(hash, tick, ts, author)| { + let mut m = GoMap::new(MapOrigin::Map); + m.push("hash", GoValue::Str(hash.clone())); + m.push("timestamp", GoValue::Str(ts.clone())); + m.push("author", GoValue::Str(author.clone())); + m.push("tick", GoValue::Int(*tick)); + let mut any = false; + for c in contribs { + if let Some((_, v)) = c.per_commit.iter().find(|(h, _)| h == hash) { + m.push(c.flag, v.clone()); + any = true; + } + } + any.then_some(GoValue::Map(m)) + }) + .collect() + } + + /// Two contributions over 2500 commits: one with a value on every + /// commit (and the full commit order), one on every third commit (and + /// only its own commits). The hash-indexed merge is byte-identical to the + /// linear-search merge, for the document and for the ndjson lines. + #[test] + fn indexed_merge_equals_the_linear_search_merge_on_many_commits() { + let all: Vec<CommitMeta> = (0..MANY_COMMITS).map(|i| meta(i, i as i64 / 7)).collect(); + let dense = TimeSeriesContribution { + flag: "devs", + per_commit: all + .iter() + .enumerate() + .map(|(i, m)| (m.0.clone(), value(i))) + .rev() + .collect(), + commit_meta: all.clone(), + }; + let sparse = TimeSeriesContribution { + flag: "typos", + per_commit: all + .iter() + .step_by(3) + .map(|m| (m.0.clone(), value(1))) + .collect(), + commit_meta: all.iter().step_by(3).cloned().collect(), + }; + let contribs = [&dense, &sparse]; + let merged = merged_commits(&contribs); + assert_eq!(merged.len(), MANY_COMMITS); + assert_eq!(merged, naive_merge(&all, &contribs)); + assert_eq!( + commit_lines(&merged), + commit_lines(&naive_merge(&all, &contribs)) + ); + assert_eq!( + timeseries_ndjson_lines(&sparse), + commit_lines(&naive_merge(&sparse.commit_meta, &[&sparse])) + ); + } + + /// A commit only a shorter walk lists is placed by its tick, after the + /// longer walk's commits of that tick. + #[test] + fn commits_only_a_shorter_walk_lists_are_placed_by_tick() { + let long = TimeSeriesContribution { + flag: "imports-per-dev", + per_commit: vec![(meta(0, 0).0, value(0)), (meta(2, 3).0, value(2))], + commit_meta: vec![meta(0, 0), meta(2, 3)], + }; + let short = TimeSeriesContribution { + flag: "typos", + per_commit: vec![(meta(1, 0).0, value(1))], + commit_meta: vec![meta(1, 0)], + }; + let hashes: Vec<String> = commit_order(&[&short, &long]) + .iter() + .map(|m| m.0.clone()) + .collect(); + assert_eq!(hashes, [meta(0, 0).0, meta(1, 0).0, meta(2, 3).0]); + assert!(commit_order(&[]).is_empty()); + } + + #[test] + fn compact_figures_are_rounded_for_reading() { + assert_eq!(compact_figure(&GoValue::Int(-3)).as_deref(), Some("-3")); + assert_eq!(compact_figure(&GoValue::Uint(7)).as_deref(), Some("7")); + assert_eq!(compact_figure(&GoValue::Float(2.0)).as_deref(), Some("2")); + assert_eq!( + compact_figure(&GoValue::Float(100.0 / 3.0)).as_deref(), + Some("33.33") + ); + assert_eq!(compact_figure(&GoValue::Float(0.5)).as_deref(), Some("0.5")); + assert_eq!( + compact_figure(&GoValue::Bool(true)).as_deref(), + Some("true") + ); + assert_eq!(compact_figure(&GoValue::Str("x".into())), None); + } + + /// The compact titles of a multi-analyzer run are padded to one width. + #[test] + fn compact_titles_share_one_width() { + let ids = ["history/devs", "history/file-history"]; + let lines = ids.map(|id| { + let section = compact_section(id, 20, &[("n".to_string(), "1".to_string())]); + let mut root = GoMap::new(MapOrigin::Struct); + root.push("sections", GoValue::Array(vec![section])); + String::from_utf8(super::super::section_render::render_compact_report( + &GoValue::Map(root), + )) + .expect("utf-8") + }); + let column = |l: &str| l.find('[').expect("bar"); + assert_eq!(column(&lines[0]), column(&lines[1]), "{lines:?}"); + assert!(lines[0].contains("Info n=1"), "{lines:?}"); + } + + /// An analyzer without a report is a named error — never a partial + /// document — for every whole-selection format. + #[test] + fn a_missing_report_is_a_named_error_with_no_output() { + let dir = tempfile::tempdir().expect("tempdir"); + let argv = [ + "codefang", + "run", + "-a", + "history/devs", + dir.path().to_str().expect("utf-8"), + ]; + let matches = crate::build_codefang_command() + .try_get_matches_from(argv) + .expect("run args parse"); + let sub = matches.subcommand_matches("run").expect("run"); + let ctx = RunContext::from_matches(sub); + let ids = vec!["history/devs".to_string()]; + let renders: [(&str, Result<Vec<u8>, HandlerError>); 5] = [ + (FORMAT_TEXT, history_text(&ctx, &ids)), + (FORMAT_COMPACT, history_compact(&ctx, &ids)), + (FORMAT_NDJSON, history_ndjson(&ctx, &ids)), + (FORMAT_TIMESERIES, history_timeseries(&ctx, &ids, false)), + ( + FORMAT_TIMESERIES_NDJSON, + history_timeseries(&ctx, &ids, true), + ), + ]; + for (format, result) in renders { + match result { + Err(HandlerError::NoReport { + analyzer, + format: f, + }) => { + assert_eq!((analyzer.as_str(), f.as_str()), ("history/devs", format)); + } + other => panic!("{format}: expected NoReport, got {other:?}"), + } } } - Some(Ok(out)) } diff --git a/crates/cf-commands/src/handlers/history_plot.rs b/crates/cf-commands/src/handlers/history_plot.rs index 728bd61a..eca72c8b 100644 --- a/crates/cf-commands/src/handlers/history_plot.rs +++ b/crates/cf-commands/src/handlers/history_plot.rs @@ -1,42 +1,31 @@ -//! `--format plot` history-phase orchestration — the Rust analogue of the reference implementation's -//! plot pipeline (the reference `executePlotPipeline` → `FinalizeToStore` → -//! `enrichAnomalyFromStore` → `renderFromStore` = the reference `runRender`). +//! `--format plot` history-phase orchestration. //! -//! Flow (per the reference `runRender` over the temp `FileReportStore`): +//! Flow: //! -//! 1. every selected history leaf writes its structured store kinds — here -//! each leaf's section data is computed directly from the analyzer crates' -//! metric surfaces (the same values the byte-verified json/yaml/bin reports -//! encode), so no store round-trip is needed; -//! 2. `StorePlotSectionsFor(id)` builds the per-analyzer sections; -//! `RenderAnalyzerPage(safeID, id, sections)` writes `<flag>.html` (the -//! store id is the analyzer `Flag()`, slash-free, so `safeID == id`) and -//! collects its [`PageMeta`] (`{ID: flag, Title: flag}`); -//! 3. `RenderIndex(pages)` writes `index.html`; -//! 4. `writeRenderReportJSON` writes `report.json` — -//! `{"analyzer_ids": [...], "pages": [...]}`, two-space-indented -//! `encoding/json` with the trailing newline, 0o640, atomic temp+rename. -//! In a MIXED static+history run this overwrites the static phase's -//! `report.json` (the reference implementation's history phase runs second), and `rebuildPlotIndex` -//! then rescans the directory for the unified, title-sorted index. +//! 1. every selected history analyzer's page sections are computed from its +//! typed metrics (the same values its json/yaml/bin reports encode) — +//! concurrently, after the shared UAST walk is warmed; +//! 2. each analyzer with sections renders `<flag>.html` (the analyzer flag, +//! e.g. `devs`, `imports-per-dev`, is the page id and title) and adds a +//! [`PageMeta`] card; +//! 3. `index.html` lists the cards; +//! 4. `report.json` records `{"analyzer_ids": [...], "pages": [...]}`. In a +//! MIXED static+history run it replaces the static phase's `report.json`, +//! and the index is rebuilt from a directory scan so it lists both phases' +//! pages, title-sorted. //! -//! # Ordering -//! -//! The reference implementation's `analyzer_ids` order is the store-manifest write order — the pipeline -//! leaf order, which is MAP-RANDOM in the reference implementation (verified: three star runs produced -//! three different orders). The deterministic Rust stand-in is the separate- -//! phase emit order ([`crate::handlers::HISTORY_PHASE_EMIT_ORDER`]); the -//! oracle measures the reference implementation's variance and compares those files structurally. +//! Pages render in the separate-phase emit order +//! ([`HISTORY_PLOT_ANALYZERS`]), so the output does not depend on the order +//! of the selection. use std::collections::BTreeMap; -use std::fs; use std::io; -use std::path::Path; use cf_anomaly::enrich::ExtractedSeries; -use cf_gojson::{Encoder, GoMap, GoValue, MapOrigin}; -use cf_plotpage::{MultiPageRenderer, PageMeta, Section, Theme}; +use cf_gojson::{GoMap, GoValue, MapOrigin}; +use cf_plotpage::{PageMeta, Section}; +use crate::handlers::plot::{create_output_dir, page_renderer, write_report_json_atomic}; use crate::handlers::plot_sections::{ history_anomaly, history_burndown, history_couples, history_devs, history_file_history, history_imports, history_quality, history_sentiment, history_shotness, history_typos, @@ -44,13 +33,6 @@ use crate::handlers::plot_sections::{ use crate::handlers::{burndown_ndjson, couples_run, history, shotness_run}; use crate::pipeline::RunContext; -/// Project title shown on every plot page. -const PLOT_PAGE_TITLE: &str = "Codefang"; - -/// report.json file mode. -#[cfg(unix)] -const REPORT_JSON_MODE: u32 = 0o640; - /// One plot-capable history analyzer: registry id, store id (the analyzer /// `Flag()` — page filename and title), and its section builder. The builder /// receives the full selected-flag set so the anomaly leaf can reproduce the @@ -61,8 +43,8 @@ pub struct HistoryPlotAnalyzer { /// Store id / page name (the analyzer `Flag()`, e.g. `devs`, /// `imports-per-dev`). pub flag: &'static str, - /// Builds the page sections; `None` mirrors a reference section-renderer error - /// (analyzer skipped from the page set but kept in `analyzer_ids`). + /// Builds the page sections; `None` when the analyzer produced no report + /// (it gets no page but stays in `analyzer_ids`). pub sections: fn(&RunContext, &[&'static str]) -> Option<Vec<Section>>, } @@ -77,7 +59,8 @@ fn sentiment_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Ve } fn shotness_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Vec<Section>> { - let report = shotness_run::shotness_run_report_data(ctx.matches)?; + // The pages show the code that exists: ended entities stay out. + let report = shotness_run::shotness_run_report_data(ctx.matches)?.live(); Some(history_shotness::sections(&report.nodes, &report.counters)) } @@ -85,7 +68,6 @@ fn couples_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Vec< let data = couples_run::couples_plot_data(ctx.matches)?; Some(history_couples::sections( &data.file_coupling, - &data.dev_names, &data.ownership, )) } @@ -102,15 +84,10 @@ fn typos_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Vec<Se } fn anomaly_sections(ctx: &RunContext, selected: &[&'static str]) -> Option<Vec<Section>> { - let metrics = if ctx.head() { - history::anomaly_head_report(ctx.matches)? - } else { - history::anomaly_run_metrics(ctx.matches)? - }; + let metrics = history::anomaly_run_metrics(ctx.matches)?; - // The reference `enrichAnomalyFromStore` → `runStoreEnrichment`: cross-analyzer - // anomalies over the OTHER selected analyzers' store time series. Only - // quality and sentiment register store extractors. + // Cross-analyzer enrichment: anomalies over the OTHER selected + // analyzers' time series. Only quality and sentiment provide one. let mut extracted: BTreeMap<String, ExtractedSeries> = BTreeMap::new(); if selected.contains(&"quality") { if let Some(qm) = history::quality_metrics(ctx.matches) { @@ -160,32 +137,8 @@ fn anomaly_sections(ctx: &RunContext, selected: &[&'static str]) -> Option<Vec<S } fn burndown_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Vec<Section>> { - if ctx.head() { - // Head pipeline: one TC at tick 0 — the global dense history is a - // single `[total_current_lines]` sample; endTime is the HEAD commit's - // committer time (the only TC timestamp). - let metrics = history::burndown_head_metrics(ctx.matches)?; - let repo = cf_gitlib::Repository::open(&ctx.path).ok()?; - let head = repo.head().ok()?; - let commit = repo.lookup_commit(head).ok()?; - let end_time_ns = commit - .committer() - .when - .seconds() - .saturating_mul(1_000_000_000); - let chart_data = history_burndown::ChartData { - global_history: vec![vec![metrics.aggregate.total_current_lines]], - sampling: 30, - granularity: 30, - tick_size_ns: 24 * 3_600_000_000_000, - end_time_ns, - project_name: burndown_ndjson::repo_base_name(&ctx.path), - }; - return Some(history_burndown::sections(&chart_data, &metrics)); - } - let agg = burndown_ndjson::burndown_run_aggregate(ctx.matches)?; - // The reference `buildChartData`: clamp negative dense values to zero. + // Clamp negative dense values to zero. let mut dense = agg.global_dense.clone(); for row in &mut dense { for v in row.iter_mut() { @@ -196,8 +149,8 @@ fn burndown_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Vec } let chart_data = history_burndown::ChartData { global_history: dense, - sampling: agg.sampling, - granularity: agg.granularity, + sampling: agg.grid.sampling, + granularity: agg.grid.granularity, tick_size_ns: agg.tick_size_ns, end_time_ns: agg.end_time_ns, project_name: agg.project_name.clone(), @@ -206,20 +159,7 @@ fn burndown_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Vec } fn devs_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option<Vec<Section>> { - // `--head` WITHOUT first-parent: the sequential TreeDiff has no - // predecessor for the lone non-root HEAD commit, so the devs leaf consumes - // an empty TC — the closed-form empty metrics (`devs_head_metrics`). - // WITH first-parent (forced whenever burndown is co-selected, e.g. the `*` - // selection), TreeDiff diffs HEAD against `parent(0)` and the full - // walk-path math applies — `devs_run_metrics`' walk already handles the - // single-HEAD window (verified against the live reference plot: head+first-parent - // carries the HEAD diff's languages and DMP line stats; head alone is - // empty). - let metrics = if ctx.head() && !crate::handlers::effective_first_parent(ctx.matches) { - history::devs_head_metrics(ctx.matches)? - } else { - history::devs_run_metrics(ctx.matches)? - }; + let metrics = history::devs_run_metrics(ctx.matches)?; Some(history_devs::sections(&metrics)) } @@ -232,7 +172,7 @@ fn file_history_sections(ctx: &RunContext, _selected: &[&'static str]) -> Option } /// The plot-capable history analyzers in the separate-phase emit order -/// (the reference `pl.Leaves` order is map-random; see the module docs). +/// (see the module docs). pub const HISTORY_PLOT_ANALYZERS: &[HistoryPlotAnalyzer] = &[ HistoryPlotAnalyzer { id: "history/quality", @@ -290,9 +230,9 @@ pub const HISTORY_PLOT_ANALYZERS: &[HistoryPlotAnalyzer] = &[ /// page set + `report.json` into `output_dir`. When `mixed` (the static plot /// phase already wrote into the same directory), the final index is rebuilt /// from the directory scan, title-sorted across both -/// phases. Returns `None` when any selected id has no plot entry (caller falls -/// through to the dispatch-blocked diagnostic), `Some(0)` on success, -/// `Some(1)` on an I/O failure. +/// phases. Returns `None` when any selected id has no plot entry (the caller +/// reports that no pages were produced), `Some(0)` on success, `Some(1)` on +/// an I/O failure. #[must_use] pub fn run_history_plot( ctx: &RunContext, @@ -301,7 +241,7 @@ pub fn run_history_plot( mixed: bool, ) -> Option<i32> { // Resolve the selection against the registry, keeping the canonical emit - // order (the reference implementation's leaf order is map-random; the oracle measures the variance). + // order. let selected: Vec<&HistoryPlotAnalyzer> = HISTORY_PLOT_ANALYZERS .iter() .filter(|entry| history_ids.iter().any(|id| id == entry.id)) @@ -320,8 +260,8 @@ pub fn run_history_plot( } } -/// Renders pages + index + report.json (the reference `runRender` [+ `rebuildPlotIndex` -/// for mixed runs]). +/// Renders pages + index + report.json (+ the rebuilt cross-phase index of a +/// mixed run). fn render_history_plot( ctx: &RunContext, selected: &[&HistoryPlotAnalyzer], @@ -330,12 +270,7 @@ fn render_history_plot( mixed: bool, ) -> io::Result<()> { create_output_dir(output_dir)?; - - let renderer = MultiPageRenderer { - output_dir: output_dir.to_string(), - title: PLOT_PAGE_TITLE.to_string(), - theme: Theme::Dark, - }; + let renderer = page_renderer(output_dir); // Pre-compute the ONE shared UAST walk for the co-selected heavy history // analyzers (imports/quality/sentiment/shotness/typos) — one task — then @@ -351,8 +286,8 @@ fn render_history_plot( let mut pages: Vec<PageMeta> = Vec::new(); for (entry, sections) in selected.iter().zip(all_sections) { - // The reference `renderOneAnalyzer`: a section-build error skips the page (the - // analyzer stays in analyzer_ids); empty sections still render a page. + // An analyzer without a report gets no page (it stays in + // analyzer_ids); empty sections still render a page. let Some(sections) = sections else { continue; }; @@ -368,32 +303,17 @@ fn render_history_plot( write_render_report_json(output_dir, selected_flags, &pages)?; if mixed { - // The reference `rebuildPlotIndex`: rescan the output dir and regenerate the - // unified title-sorted index across both phases. + // Rescan the output dir and regenerate the unified title-sorted index + // across both phases. renderer.rebuild_index()?; } Ok(()) } -/// The reference `os.MkdirAll(outputDir, 0o750)` (reference `renderDirPerm`). -fn create_output_dir(output_dir: &str) -> io::Result<()> { - let existed = Path::new(output_dir).is_dir(); - fs::create_dir_all(output_dir)?; - #[cfg(unix)] - if !existed { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(output_dir, fs::Permissions::from_mode(0o750))?; - } - #[cfg(not(unix))] - let _ = existed; - Ok(()) -} - -/// The reference `writeRenderReportJSON`: `{"analyzer_ids": [...], "pages": -/// [{"ID", "Title", "Description"}...]}` — two-space indent, trailing newline, -/// atomic 0o640 write. `PageMeta` has no json tags, so the reference field names are -/// capitalized. +/// `report.json` of the history phase: `{"analyzer_ids": [...], "pages": +/// [{"ID", "Title", "Description"}...]}` (capitalized page keys), written by +/// [`write_report_json_atomic`]. fn write_render_report_json( output_dir: &str, analyzer_ids: &[&'static str], @@ -420,18 +340,5 @@ fn write_render_report_json( }) .collect(); root.push("pages", GoValue::Array(page_values)); - - let bytes = Encoder::indented(" ") - .with_trailing_newline(true) - .encode(&GoValue::Map(root)); - - let final_path = Path::new(output_dir).join("report.json"); - let tmp_path = Path::new(output_dir).join(".report.json.tmp"); - fs::write(&tmp_path, &bytes)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(&tmp_path, fs::Permissions::from_mode(REPORT_JSON_MODE))?; - } - fs::rename(&tmp_path, &final_path) + write_report_json_atomic(output_dir, &GoValue::Map(root)) } diff --git a/crates/cf-commands/src/handlers/history_text.rs b/crates/cf-commands/src/handlers/history_text.rs index 13208e7d..7609d265 100644 --- a/crates/cf-commands/src/handlers/history_text.rs +++ b/crates/cf-commands/src/handlers/history_text.rs @@ -1,30 +1,23 @@ -//! Per-leaf `--format text` bodies for the six history leaves the reference implementation wires with a -//! `SerializeTextFn` hook — sentiment, shotness, burndown, couples, devs, -//! file-history. Reproduces each leaf's reference text renderer, drawing on -//! the shared [`cf_terminal`] helpers. +//! Per-analyzer `--format text` bodies: a human summary (a title box, the +//! key figures, the top items) for every `history/*` analyzer, drawn with the +//! shared [`cf_terminal`] helpers. //! -//! Each function returns ONLY the bytes the leaf's reference `Serialize(result, -//! "text", w)` call writes; the `codefang (v2):` header and the `"<Name>:\n"` -//! section line are emitted by [`super::history_formats::history_text`]. +//! Each function returns ONLY the analyzer's body; the `codefang (v2):` +//! header and the `"<id>:\n"` section line are emitted by +//! [`super::history_formats::history_text`], which renders every selected +//! analyzer before anything is written. //! //! # Width / color / padding semantics //! -//! * Colors are unconditional: the reference implementation never checks isatty — `terminal.NewConfig` -//! only honors the `NO_COLOR` env var — so the bytes contain ANSI escapes -//! even when piped. Width comes from `COLUMNS` (default 80). Both are read -//! through [`cf_terminal::Config::new`]. -//! * `terminal.PadRight` / `TruncateWithEllipsis` / `DrawHeader` measure -//! string length in BYTES, while the reference `fmt` width specifiers -//! (`%-*s`, `%6s`, …) pad to a minimum number of RUNES -//! (`utf8.RuneCountInString`) — e.g. `"máximo cuadros"` (15 bytes, 14 runes) -//! gets 4 padding spaces under `%-18s` but only 3 under `PadRight(_, 18)`. -//! Rust `format!` width counts `char`s, which equals the reference implementation's rune count, so -//! `fmt`-style padding maps to `format!("{:<w$}")` and the byte-length -//! helpers stay in [`cf_terminal`]. -//! * reference float verbs (`%.1f`, `%5.1f`, `%3.0f`, `%.2f`) and Rust `{:.1}` / -//! `{:5.1}` / `{:3.0}` / `{:.2}` both emit the correctly-rounded fixed -//! decimal of the binary value with ties-to-even — identical bytes. -//! The reference `float32` operands are widened exactly (`f64::from`). +//! * Colors follow `NO_COLOR` only (never isatty), so piped bytes carry ANSI +//! escapes unless `NO_COLOR` (or `--no-color`) is set. Width comes from +//! `COLUMNS` (default 80). Both are read through [`cf_terminal::Config::new`]. +//! * [`cf_terminal::pad_right`] / [`truncate_with_ellipsis`] / +//! [`draw_header`] measure BYTES, while `format!` width specifiers +//! (`{:<18}`) pad to a number of `char`s, so a non-ASCII label is padded +//! by `format!`, never by the byte-length helpers. +//! * Rankings sort on an explicit total key (the ranked value, then the name) +//! with a stable sort, so equal values always list in the same order. use std::fmt::Write as _; @@ -37,8 +30,7 @@ use cf_terminal::{ // Shared helpers // --------------------------------------------------------------------------- -/// The reference `formatUint`: groups of three -/// digits joined by `,` via tail recursion (`formatUint(n/1000) + "," + %03d`). +/// `n` with its digits grouped in threes by `,` (`1234567` → `1,234,567`). fn format_uint_thousands(n: u64) -> String { if n < 1000 { return n.to_string(); @@ -46,8 +38,7 @@ fn format_uint_thousands(n: u64) -> String { format!("{},{:03}", format_uint_thousands(n / 1000), n % 1000) } -/// The reference `formatInt` / `formatInt64`: -/// thousands separators, `-` prefix for negatives. +/// [`format_uint_thousands`] for a signed value (`-` prefix for negatives). fn format_int_thousands(n: i64) -> String { if n < 0 { return format!("-{}", format_uint_thousands(n.unsigned_abs())); @@ -55,9 +46,8 @@ fn format_int_thousands(n: i64) -> String { format_uint_thousands(n as u64) } -/// The reference `filepath.Base` for the slash-separated repo paths shotness emits: -/// empty → `"."`, trailing slashes stripped, all-slash → `"/"`, else the last -/// path element. +/// The last element of a slash-separated repository path: empty → `"."`, +/// trailing slashes stripped, all-slash → `"/"`. fn go_filepath_base(path: &str) -> &str { if path.is_empty() { return "."; @@ -72,29 +62,46 @@ fn go_filepath_base(path: &str) -> &str { } } +/// Writes a blue section title and the separator under it. +fn write_section_title(s: &mut String, cfg: Config, title: &str) { + let _ = writeln!(s, " {}", cfg.colorize(title, Color::Blue)); + let _ = writeln!(s, " {}", draw_separator(cfg.width - 4)); +} + +/// Writes the gray `and N more...` line under a truncated list (nothing when +/// `remaining` is zero). +fn write_more(s: &mut String, cfg: Config, remaining: usize) { + if remaining > 0 { + let _ = writeln!( + s, + " {}", + cfg.colorize(&format!(" and {remaining} more..."), Color::Gray) + ); + } +} + // --------------------------------------------------------------------------- // sentiment // --------------------------------------------------------------------------- -/// `SentimentPositiveThreshold`. +/// Score at/above which a tick is positive. const SENTIMENT_POSITIVE_THRESHOLD: f64 = 0.6; -/// `SentimentNegativeThreshold`. +/// Score at/below which a tick is negative. const SENTIMENT_NEGATIVE_THRESHOLD: f64 = 0.4; -/// `termWidth` — sentiment renders at a FIXED width of 60, -/// ignoring `Config.Width`. +/// Sentiment renders at a FIXED width of 60, ignoring `Config::width`. const SENTIMENT_TERM_WIDTH: i64 = 60; -/// `terminalBarWidth`. +/// Score / distribution bar width. const SENTIMENT_BAR_WIDTH: i64 = 20; -/// `terminalLabelWidth + labelPaddingExtra`. +/// Distribution label column width (label + emoji padding). const SENTIMENT_DIST_LABEL_WIDTH: i64 = 18 + 4; -/// `maxRiskPeriodsToShow`. +/// Risk periods listed before `... and N more`. const SENTIMENT_MAX_RISK: usize = 5; -/// `sparklineLabelGap`. +/// Gap between the `neg` and `pos` sparkline labels. const SENTIMENT_SPARK_GAP: usize = 14; -/// `sparklineChars`. +/// The eight sparkline levels, lowest first. const SPARKLINE_CHARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; -/// `sentimentColor`. +/// Green for a positive score, red for a negative one, else yellow. fn sentiment_color(score: f64) -> Color { if score >= SENTIMENT_POSITIVE_THRESHOLD { Color::Green @@ -105,7 +112,7 @@ fn sentiment_color(score: f64) -> Color { } } -/// `sentimentLabel`. +/// The emoji of a score's class. fn sentiment_label(score: f64) -> &'static str { if score >= SENTIMENT_POSITIVE_THRESHOLD { "😊" @@ -116,8 +123,9 @@ fn sentiment_label(score: f64) -> &'static str { } } -/// The `history/sentiment` text body — the reference `generateText` (sentiment/ -/// reference dispatch) → `RenderTerminal`. +/// The `history/sentiment` text body: the average score, the positive / +/// neutral / negative tick distribution, the trend, a sparkline timeline and +/// the low-sentiment risk periods. #[must_use] pub fn sentiment_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { let metrics = super::history::sentiment_metrics(sub)?; @@ -131,8 +139,8 @@ pub fn sentiment_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { )); s.push_str("\n\n"); - // renderSummarySection. The Blue colorize wraps the - // trailing newline, so the reset lands at the start of the next line. + // Summary. The Blue colorize wraps the trailing newline, so the reset + // lands at the start of the next line. s.push_str(&cfg.colorize(" Summary\n", Color::Blue)); s.push_str(&draw_separator(SENTIMENT_TERM_WIDTH)); s.push('\n'); @@ -159,7 +167,7 @@ pub fn sentiment_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { ); s.push('\n'); - // renderDistributionSection. + // Distribution of the scored ticks. if metrics.aggregate.total_ticks != 0 { s.push_str(&cfg.colorize(" Distribution\n", Color::Blue)); s.push_str(&draw_separator(SENTIMENT_TERM_WIDTH)); @@ -200,7 +208,7 @@ pub fn sentiment_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { s.push('\n'); } - // renderTrendSection. + // Trend. if !metrics.trend.trend_direction.is_empty() { s.push_str(&cfg.colorize(" Trend\n", Color::Blue)); s.push_str(&draw_separator(SENTIMENT_TERM_WIDTH)); @@ -233,12 +241,12 @@ pub fn sentiment_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { s.push('\n'); } - // renderSparklineSection. + // Sparkline timeline. if !metrics.time_series.is_empty() { s.push_str(&cfg.colorize(" Sentiment Timeline\n", Color::Blue)); s.push_str(&draw_separator(SENTIMENT_TERM_WIDTH)); s.push('\n'); - // buildSparkline: idx = max(int(min(score*8, 7)), 0). + // One of eight levels: idx = max(int(min(score*8, 7)), 0). let mut sparkline = String::new(); for ts in &metrics.time_series { let score = f64::from(ts.sentiment); @@ -258,8 +266,8 @@ pub fn sentiment_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { s.push('\n'); } - // renderRiskSection. The colorize wraps the newline, so - // each reset code starts the FOLLOWING line. + // Risk periods. The colorize wraps the newline, so each reset code + // starts the FOLLOWING line. if !metrics.low_sentiment_periods.is_empty() { s.push_str(&cfg.colorize(" Risk Periods\n", Color::Blue)); s.push_str(&draw_separator(SENTIMENT_TERM_WIDTH)); @@ -296,7 +304,7 @@ pub fn sentiment_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { // shotness // --------------------------------------------------------------------------- -/// `formatNodeLabel`: `"name (base(file))"`. +/// `"name (base(file))"`, or the bare name without a file. fn shotness_node_label(name: &str, file: &str) -> String { if file.is_empty() { return name.to_string(); @@ -304,7 +312,7 @@ fn shotness_node_label(name: &str, file: &str) -> String { format!("{} ({})", name, go_filepath_base(file)) } -/// `riskLevelColor`. +/// Red for HIGH, yellow for MEDIUM, else green. fn shotness_risk_color(level: &str) -> Color { match level { "HIGH" => Color::Red, @@ -313,14 +321,15 @@ fn shotness_risk_color(level: &str) -> Color { } } -/// The `history/shotness` text body — the reference `generateText`. +/// The `history/shotness` text body: the summary, the hottest functions, the +/// risk assessment and the strongest function couplings. #[must_use] pub fn shotness_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - const BAR_WIDTH: i64 = 20; // textBarWidth - const LABEL_WIDTH: i64 = 24; // textLabelWidth - const HALF_LABEL: i64 = 12; // textHalfLabel - const MAX_ROWS: usize = 10; // textMaxHot / textMaxCouplings / textMaxHotspots - const SUMMARY_LABEL_WIDTH: usize = 22; // summaryLabelWidth + const BAR_WIDTH: i64 = 20; + const LABEL_WIDTH: i64 = 24; + const HALF_LABEL: i64 = 12; + const MAX_ROWS: usize = 10; + const SUMMARY_LABEL_WIDTH: usize = 22; let metrics = super::shotness_run::shotness_run_metrics(sub)?; let cfg = Config::new(); @@ -335,9 +344,7 @@ pub fn shotness_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { )); s.push_str("\n\n"); - // writeSummarySection. - let _ = writeln!(s, " {}", cfg.colorize("Summary", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Summary"); let _ = writeln!( s, " {:<SUMMARY_LABEL_WIDTH$} {}", @@ -379,21 +386,27 @@ pub fn shotness_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { "Hot Nodes", cfg.colorize(&agg.hot_nodes.to_string(), hot_color) ); + if metrics.ended_tracked { + let _ = writeln!( + s, + " {:<SUMMARY_LABEL_WIDTH$} {}", + "Ended Nodes", agg.ended_nodes + ); + } - // writeHottestFunctions. if !metrics.node_hotness.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("Hottest Functions", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Hottest Functions"); let shown = metrics.node_hotness.len().min(MAX_ROWS); for n in &metrics.node_hotness[..shown] { let label = truncate_with_ellipsis(&shotness_node_label(&n.name, &n.file), LABEL_WIDTH); let bar = draw_progress_bar(n.hotness_score, BAR_WIDTH); - // hotnessColor: inverted score. + // A hotter node is colored as a lower score. let score_color = color_for_score(1.0 - n.hotness_score); + let ended = if n.ended { ", ended" } else { "" }; let _ = writeln!( s, - " {:<width$} [{}] {} ({} changes)", + " {:<width$} [{}] {} ({} changes{ended})", label, bar, cfg.colorize(&format!("{:.1}", n.hotness_score), score_color), @@ -413,11 +426,9 @@ pub fn shotness_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { } } - // writeRiskNodes. if !metrics.hotspot_nodes.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("Risk Assessment", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Risk Assessment"); let shown = metrics.hotspot_nodes.len().min(MAX_ROWS); for n in &metrics.hotspot_nodes[..shown] { let label = truncate_with_ellipsis(&shotness_node_label(&n.name, &n.file), LABEL_WIDTH); @@ -445,16 +456,14 @@ pub fn shotness_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { } } - // writeStrongestCouplings. if !metrics.node_coupling.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("Strongest Couplings", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Strongest Couplings"); let shown = metrics.node_coupling.len().min(MAX_ROWS); for c in &metrics.node_coupling[..shown] { let left = truncate_with_ellipsis(&c.node1_name, HALF_LABEL); let right = truncate_with_ellipsis(&c.node2_name, HALF_LABEL); - // couplingStrengthColor: inverted strength. + // A stronger coupling is colored as a lower score. let strength_color = color_for_score(1.0 - c.strength); let _ = writeln!( s, @@ -487,19 +496,26 @@ pub fn shotness_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { // burndown // --------------------------------------------------------------------------- -/// One display band of `buildAgeBands`. +/// One display band of the age distribution. struct AgeBand { label: &'static str, lines: i64, } -/// `buildAgeBands`: groups dense history bands (band -/// `i` ≈ age `i+1` months) into at most five labeled age buckets, dropping -/// empty ones. -fn build_age_bands(breakdown: &[i64], num_bands: i64) -> Vec<AgeBand> { - if num_bands == 0 { - return Vec::new(); - } +/// Days per month in the age labels. +const DAYS_PER_MONTH: i64 = 30; +/// Hours per day in the age labels. +const HOURS_PER_DAY: i64 = 24; + +/// The age in months of band `index` of `num_bands` at the last sample: band +/// 0 is the oldest, the newest band is one granularity old. +fn band_age_months(index: i64, num_bands: i64, granularity: i64, tick_size_hours: i64) -> i64 { + (num_bands - index) * granularity * tick_size_hours / HOURS_PER_DAY / DAYS_PER_MONTH +} + +/// Groups the last sample's bands into at most five labeled age buckets by +/// [`band_age_months`], dropping empty ones. +fn build_age_bands(breakdown: &[i64], granularity: i64, tick_size_hours: i64) -> Vec<AgeBand> { // (maxMonths, label); maxMonths 0 = unbounded. const BUCKETS: [(i64, &str); 5] = [ (1, "< 1 month"), @@ -508,19 +524,17 @@ fn build_age_bands(breakdown: &[i64], num_bands: i64) -> Vec<AgeBand> { (12, "6-12 months"), (0, "> 12 months"), ]; + let num_bands = breakdown.len() as i64; let mut lines = [0i64; BUCKETS.len()]; for (i, &val) in breakdown.iter().enumerate() { if val <= 0 { continue; } - let age_months = i as i64 + 1; - let mut bucket_idx = BUCKETS.len() - 1; - for (j, (max_months, _)) in BUCKETS.iter().enumerate() { - if *max_months > 0 && age_months <= *max_months { - bucket_idx = j; - break; - } - } + let age_months = band_age_months(i as i64, num_bands, granularity, tick_size_hours); + let bucket_idx = BUCKETS + .iter() + .position(|(max_months, _)| *max_months > 0 && age_months <= *max_months) + .unwrap_or(BUCKETS.len() - 1); lines[bucket_idx] += val; } BUCKETS @@ -531,33 +545,37 @@ fn build_age_bands(breakdown: &[i64], num_bands: i64) -> Vec<AgeBand> { .collect() } -/// The `history/burndown` text body — the reference `generateText`. +/// The `history/burndown` text body: the summary, the code-age distribution +/// of the last sample and, with `--burndown-people`, the top developers by +/// surviving lines. #[must_use] pub fn burndown_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - const BAR_WIDTH: i64 = 20; // textBarWidth - const LABEL_WIDTH: i64 = 14; // textLabelWidth - const DEV_NAME_WIDTH: i64 = 16; // textDevNameWidth - const MAX_DEVS: usize = 5; // textMaxDevs - - let metrics = super::burndown_ndjson::burndown_run_metrics(sub)?; + const BAR_WIDTH: i64 = 20; + const LABEL_WIDTH: i64 = 14; + const DEV_NAME_WIDTH: i64 = 16; + const MAX_DEVS: usize = 5; + const NANOS_PER_HOUR: i64 = 3_600 * 1_000_000_000; + + let run = super::burndown_ndjson::burndown_run_aggregate(sub)?; + let metrics = &run.metrics; let cfg = Config::new(); let w = cfg.width; let mut s = String::new(); let agg = &metrics.aggregate; - // extractProjectName: the run path's ticksToReport - // never sets report["ProjectName"], so the title always - // falls back to "project". + let project = if run.project_name.is_empty() { + "project" + } else { + run.project_name.as_str() + }; s.push_str(&draw_header( - "Burndown: project", + &format!("Burndown: {project}"), &format!("{}d", agg.analysis_period_days), w, )); s.push_str("\n\n"); - // writeSummary. - let _ = writeln!(s, " {}", cfg.colorize("Summary", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Summary"); let survival_pct = agg.overall_survival_rate; let survival_color = color_for_score(survival_pct); let bar = draw_progress_bar(survival_pct, BAR_WIDTH); @@ -581,19 +599,16 @@ pub fn burndown_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { cfg.colorize(&format!("{:.1}%", survival_pct * 100.0), survival_color) ); - // writeAgeDistribution: the section title prints before the - // empty-sample early return. - if !metrics.global_survival.is_empty() { + if let Some(last_sample) = metrics.global_survival.last() { s.push('\n'); - let _ = writeln!( - s, - " {}", - cfg.colorize("Code Age Distribution", Color::Blue) - ); - let _ = writeln!(s, " {}", draw_separator(w - 4)); - let last_sample = &metrics.global_survival[metrics.global_survival.len() - 1]; + write_section_title(&mut s, cfg, "Code Age Distribution"); if last_sample.total_lines != 0 { - for band in build_age_bands(&last_sample.band_breakdown, agg.num_bands) { + let tick_size_hours = run.tick_size_ns / NANOS_PER_HOUR; + for band in build_age_bands( + &last_sample.band_breakdown, + run.grid.granularity, + tick_size_hours, + ) { let pct = band.lines as f64 / last_sample.total_lines as f64; let _ = writeln!( s, @@ -604,86 +619,34 @@ pub fn burndown_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { } } - // writeTopDevelopers. The run path never carries people - // histories (peopleNumber == 0 in ticksToReport), so developer_survival is - // empty and the section is skipped; the port still mirrors the reference logic - // over the report's GoValue rows for fidelity. - let dev_rows: Vec<(i64, String, i64, f64)> = metrics - .developer_survival - .as_deref() - .unwrap_or(&[]) - .iter() - .filter_map(|v| { - let m = match v { - cf_gojson::GoValue::Map(m) => m, - _ => return None, - }; - let get = |k: &str| { - m.iter() - .find(|(key, _)| key.as_str() == k) - .map(|(_, val)| val) - }; - let id = match get("id") { - Some(cf_gojson::GoValue::Int(i)) => *i, - _ => 0, - }; - let name = match get("name") { - Some(cf_gojson::GoValue::Str(n)) => n.clone(), - _ => String::new(), - }; - let current = match get("current_lines") { - Some(cf_gojson::GoValue::Int(i)) => *i, - _ => 0, - }; - let rate = match get("survival_rate") { - Some(cf_gojson::GoValue::Float(f)) => *f, - Some(cf_gojson::GoValue::Int(i)) => *i as f64, - _ => 0.0, - }; - Some((id, name, current, rate)) - }) - .collect(); - if !dev_rows.is_empty() { + // developer_survival is already sorted by surviving lines (id on a tie). + if !metrics.developer_survival.is_empty() { s.push('\n'); - let _ = writeln!( - s, - " {}", - cfg.colorize("Top Developers (by surviving lines)", Color::Blue) - ); - let _ = writeln!(s, " {}", draw_separator(w - 4)); - // The reference implementation sorts a copy with sort.Slice (unstable pdqsort) by CurrentLines - // descending; replicate the exact permutation for ties. - let mut devs = dev_rows; - super::go_sort::slice(&mut devs, |a, b| a.2 > b.2); + write_section_title(&mut s, cfg, "Top Developers (by surviving lines)"); + let devs = &metrics.developer_survival; let shown = devs.len().min(MAX_DEVS); - for (id, name, current_lines, survival_rate) in &devs[..shown] { - let name = if name.is_empty() { - format!("dev#{id}") + for dev in &devs[..shown] { + let name = if dev.name.is_empty() { + format!("dev#{}", dev.id) } else { - name.clone() + dev.name.clone() }; let name = truncate_with_ellipsis(&name, DEV_NAME_WIDTH); - let survival_color = color_for_score(*survival_rate); - let bar = draw_progress_bar(*survival_rate, BAR_WIDTH); + let survival_color = color_for_score(dev.survival_rate); + let bar = draw_progress_bar(dev.survival_rate, BAR_WIDTH); let _ = writeln!( s, " {:<16} {:8} [{}] {}", name, - current_lines, + dev.current_lines, bar, - cfg.colorize(&format!("{:.1}%", survival_rate * 100.0), survival_color) - ); - } - if devs.len() > MAX_DEVS { - let _ = writeln!( - s, - " {}", cfg.colorize( - &format!(" and {} more...", devs.len() - MAX_DEVS), - Color::Gray + &format!("{:.1}%", dev.survival_rate * 100.0), + survival_color ) ); } + write_more(&mut s, cfg, devs.len().saturating_sub(MAX_DEVS)); } s.push('\n'); @@ -694,7 +657,7 @@ pub fn burndown_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { // couples // --------------------------------------------------------------------------- -/// `colorForStrength`. +/// Red from 70% coupling, yellow from 40%, else green. fn couples_strength_color(strength: f64) -> Color { if strength >= 0.7 { Color::Red @@ -705,13 +668,12 @@ fn couples_strength_color(strength: f64) -> Color { } } -/// `formatPct`: `%.0f%%` of `v * 100`. +/// `v` as a whole percentage (`0.654` → `65%`). fn couples_format_pct(v: f64) -> String { format!("{:.0}%", v * 100.0) } -/// `writeCoupleRows`: one titled section of coupling -/// pairs `(left, right, count, strength)`. +/// One titled section of coupling pairs `(left, right, count, strength)`. fn couples_write_rows( s: &mut String, cfg: Config, @@ -719,10 +681,9 @@ fn couples_write_rows( rows: &[(String, String, i64, f64)], max_rows: usize, ) { - const NAME_WIDTH: i64 = 25; // textNameWidth + const NAME_WIDTH: i64 = 25; - let _ = writeln!(s, " {}", cfg.colorize(title, Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(cfg.width - 4)); + write_section_title(s, cfg, title); let shown = rows.len().min(max_rows); for (left, right, count, strength) in &rows[..shown] { let left = truncate_with_ellipsis(left, NAME_WIDTH); @@ -740,23 +701,57 @@ fn couples_write_rows( ) ); } - if rows.len() > max_rows { + write_more(s, cfg, rows.len().saturating_sub(max_rows)); +} + +/// The files ranked by ownership risk: fewest contributors first, then by +/// path — a stable sort on a total key, so equal counts always list in path +/// order. +fn rank_ownership_by_risk( + ownership: &[cf_couples::FileOwnershipData], +) -> Vec<&cf_couples::FileOwnershipData> { + let mut ranked: Vec<&cf_couples::FileOwnershipData> = ownership.iter().collect(); + ranked.sort_by(|a, b| { + a.contributors + .cmp(&b.contributors) + .then_with(|| a.file.cmp(&b.file)) + }); + ranked +} + +/// The "File Ownership Risk" section: the `max_rows` riskiest files with +/// their line and contributor counts, a single-owner file flagged `!!`. +fn couples_write_ownership( + s: &mut String, + cfg: Config, + ownership: &[cf_couples::FileOwnershipData], + max_rows: usize, +) { + const OWNERSHIP_WIDTH: i64 = 25 * 2 + 3; + + write_section_title(s, cfg, "File Ownership Risk"); + let ranked = rank_ownership_by_risk(ownership); + for fo in ranked.iter().take(max_rows) { + let file = truncate_with_ellipsis(&fo.file, OWNERSHIP_WIDTH); + let risk = if fo.contributors <= 1 { + cfg.colorize(" !!", Color::Red) + } else { + String::new() + }; let _ = writeln!( s, - " {}", - cfg.colorize( - &format!(" and {} more...", rows.len() - max_rows), - Color::Gray - ) + " {:<53} {:5} lines {} contributors{}", + file, fo.lines, fo.contributors, risk ); } + write_more(s, cfg, ranked.len().saturating_sub(max_rows)); } -/// The `history/couples` text body — the reference `generateText`. +/// The `history/couples` text body: the summary, the top file and developer +/// couples and the file-ownership risk (fewest contributors first). #[must_use] pub fn couples_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - const MAX_ROWS: usize = 7; // textMaxFileCouples / textMaxDevCouples / textMaxOwnership - const OWNERSHIP_WIDTH: i64 = 25 * 2 + 3; // textOwnershipWidth + const MAX_ROWS: usize = 7; let metrics = super::couples_run::couples_run_metrics(sub)?; let cfg = Config::new(); @@ -771,9 +766,7 @@ pub fn couples_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { )); s.push_str("\n\n"); - // writeCouplesSummary. - let _ = writeln!(s, " {}", cfg.colorize("Summary", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Summary"); let _ = writeln!( s, " {:<22} {:<12}{:<22} {}", @@ -791,7 +784,6 @@ pub fn couples_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { couples_format_pct(agg.avg_coupling_strength) ); - // writeFileCouples. if !metrics.file_coupling.is_empty() { s.push('\n'); let rows: Vec<(String, String, i64, f64)> = metrics @@ -802,7 +794,6 @@ pub fn couples_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { couples_write_rows(&mut s, cfg, "Top File Couples", &rows, MAX_ROWS); } - // writeDevCouples. if !metrics.developer_coupling.is_empty() { s.push('\n'); let rows: Vec<(String, String, i64, f64)> = metrics @@ -820,40 +811,9 @@ pub fn couples_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { couples_write_rows(&mut s, cfg, "Top Developer Couples", &rows, MAX_ROWS); } - // writeOwnershipRisk. if !metrics.file_ownership.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("File Ownership Risk", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); - // SortOwnershipByRisk: sort.Slice (unstable - // pdqsort) on a copy by contributors ascending — go_sort replicates the - // tie permutation. - let mut sorted = metrics.file_ownership.clone(); - super::go_sort::slice(&mut sorted, |a, b| a.contributors < b.contributors); - let shown = sorted.len().min(MAX_ROWS); - for fo in &sorted[..shown] { - let file = truncate_with_ellipsis(&fo.file, OWNERSHIP_WIDTH); - let risk = if fo.contributors <= 1 { - cfg.colorize(" !!", Color::Red) - } else { - String::new() - }; - let _ = writeln!( - s, - " {:<53} {:5} lines {} contributors{}", - file, fo.lines, fo.contributors, risk - ); - } - if sorted.len() > MAX_ROWS { - let _ = writeln!( - s, - " {}", - cfg.colorize( - &format!(" and {} more...", sorted.len() - MAX_ROWS), - Color::Gray - ) - ); - } + couples_write_ownership(&mut s, cfg, &metrics.file_ownership, MAX_ROWS); } s.push('\n'); @@ -864,8 +824,8 @@ pub fn couples_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { // devs // --------------------------------------------------------------------------- -/// `findPrimaryLanguage`: the language with -/// the most added lines, `"Other"` when none (or when the winner is unnamed). +/// The language with the most added lines, `"Other"` when none (or when the +/// winner is unnamed). fn devs_primary_language(dev: &cf_devs::DeveloperData) -> &str { let mut primary = "Other"; let mut max_lines = 0i64; @@ -882,8 +842,7 @@ fn devs_primary_language(dev: &cf_devs::DeveloperData) -> &str { primary } -/// `riskToColor`: CRITICAL/HIGH → red, MEDIUM → yellow, -/// else green (pkg/metrics RiskLevel strings). +/// CRITICAL / HIGH → red, MEDIUM → yellow, else green. fn devs_risk_color(level: &str) -> Color { match level { "CRITICAL" | "HIGH" => Color::Red, @@ -892,12 +851,13 @@ fn devs_risk_color(level: &str) -> Color { } } -/// The `history/devs` text body — the reference `generateText`. +/// The `history/devs` text body: the summary, the top contributors, the +/// per-language bus-factor risk and the churn totals. #[must_use] pub fn devs_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - const MAX_CONTRIBUTORS: usize = 7; // textMaxContributors - const MAX_BUS_FACTORS: usize = 7; // textMaxBusFactors - const DEV_NAME_WIDTH: i64 = 18; // textDevNameWidth + const MAX_CONTRIBUTORS: usize = 7; + const MAX_BUS_FACTORS: usize = 7; + const DEV_NAME_WIDTH: i64 = 18; let metrics = super::history::devs_run_metrics(sub)?; let cfg = Config::new(); @@ -912,9 +872,7 @@ pub fn devs_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { )); s.push_str("\n\n"); - // writeSummarySection. - let _ = writeln!(s, " {}", cfg.colorize("Summary", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Summary"); let _ = writeln!( s, " {:<22} {}", @@ -946,12 +904,11 @@ pub fn devs_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { format_int_thousands(agg.total_languages) ); - // writeContributors. The empty Colorize calls still emit the - // color-code + reset pairs around the +/- counters. + // Top contributors. The empty colorize calls emit the color-code + + // reset pairs around the +/- counters. if !metrics.developers.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("Top Contributors", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Top Contributors"); let shown = metrics.developers.len().min(MAX_CONTRIBUTORS); for dev in &metrics.developers[..shown] { let name = if dev.name.is_empty() { @@ -976,26 +933,16 @@ pub fn devs_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { cfg.colorize(primary_lang, Color::Gray) ); } - if metrics.developers.len() > MAX_CONTRIBUTORS { - let _ = writeln!( - s, - " {}", - cfg.colorize( - &format!( - " and {} more...", - metrics.developers.len() - MAX_CONTRIBUTORS - ), - Color::Gray - ) - ); - } + write_more( + &mut s, + cfg, + metrics.developers.len().saturating_sub(MAX_CONTRIBUTORS), + ); } - // writeBusFactorRisk. if !metrics.busfactor.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("Bus Factor Risk", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Bus Factor Risk"); let shown = metrics.busfactor.len().min(MAX_BUS_FACTORS); for bf in &metrics.busfactor[..shown] { let lang = truncate_with_ellipsis(&bf.language, DEV_NAME_WIDTH); @@ -1012,26 +959,16 @@ pub fn devs_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { bf.total_contributors ); } - if metrics.busfactor.len() > MAX_BUS_FACTORS { - let _ = writeln!( - s, - " {}", - cfg.colorize( - &format!( - " and {} more...", - metrics.busfactor.len() - MAX_BUS_FACTORS - ), - Color::Gray - ) - ); - } + write_more( + &mut s, + cfg, + metrics.busfactor.len().saturating_sub(MAX_BUS_FACTORS), + ); } - // writeChurnSummary. if !metrics.churn.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("Churn Summary", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Churn Summary"); let mut total_added = 0i64; let mut total_removed = 0i64; for c in &metrics.churn { @@ -1062,20 +999,19 @@ pub fn devs_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { // file-history // --------------------------------------------------------------------------- -/// `buildBar`: `int(pct/100*30)` clamped to -/// `[0, 30]` pipes. +/// `int(pct / 100 * max_width)` pipes, clamped to `[0, max_width]`. fn file_history_build_bar(pct: f64, max_width: i64) -> String { let filled = ((pct / 100.0 * max_width as f64) as i64).clamp(0, max_width); "|".repeat(filled as usize) } -/// The `history/file-history` text body — the reference `generateText` -///. +/// The `history/file-history` text body: the summary, the file composition +/// by category and the most modified files. #[must_use] pub fn file_history_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - const MAX_FILES: usize = 10; // textMaxFiles - const BAR_WIDTH: i64 = 30; // textBarWidth - const CATEGORY_WIDTH: usize = 16; // textCategoryWidth + const MAX_FILES: usize = 10; + const BAR_WIDTH: i64 = 30; + const CATEGORY_WIDTH: usize = 16; let metrics = super::history::file_history_run_metrics(sub)?; let cfg = Config::new(); @@ -1090,9 +1026,7 @@ pub fn file_history_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { )); s.push_str("\n\n"); - // writeFileSummary. - let _ = writeln!(s, " {}", cfg.colorize("Summary", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Summary"); let _ = writeln!(s, " {:<26} {}", "Total Files", agg.total_files); let _ = writeln!(s, " {:<26} {}", "Total Commits", agg.total_commits); let _ = writeln!( @@ -1107,11 +1041,10 @@ pub fn file_history_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { ); let _ = writeln!(s, " {:<26} {}", "High Churn Files", agg.high_churn_files); - // writeComposition, iterating AllCategories in canonical order. + // Composition, in the canonical category order. if !metrics.composition.breakdown.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("File Composition", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "File Composition"); for cat in cf_file_history::ALL_CATEGORIES { let count = metrics .composition @@ -1140,11 +1073,9 @@ pub fn file_history_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { } } - // writeTopFiles. if !metrics.file_churn.is_empty() { s.push('\n'); - let _ = writeln!(s, " {}", cfg.colorize("Most Modified Files", Color::Blue)); - let _ = writeln!(s, " {}", draw_separator(w - 4)); + write_section_title(&mut s, cfg, "Most Modified Files"); let limit = metrics.file_churn.len().min(MAX_FILES); for f in &metrics.file_churn[..limit] { let _ = writeln!(s, " {:4} commits {}", f.commit_count, f.path); @@ -1161,3 +1092,587 @@ pub fn file_history_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { s.push('\n'); Some(s.into_bytes()) } + +// --------------------------------------------------------------------------- +// Summary rows shared by quality / typos / anomaly / imports +// --------------------------------------------------------------------------- + +/// Label column width of the quality / typos / anomaly / imports summaries. +const SUMMARY_LABEL_WIDTH: usize = 26; +/// Rows a top-items section lists before `and N more...`. +const MAX_TOP_ROWS: usize = 10; +/// Characters of an RFC 3339 timestamp that form its date. +const RFC3339_DATE_LEN: usize = 10; +/// Hex digits of a commit hash shown in a text row. +const SHORT_HASH_LEN: usize = 7; +/// Column width of a `wrong → correct` typo pair. +const TYPO_PAIR_WIDTH: usize = 36; +/// Column width of an import path. +const IMPORT_NAME_WIDTH: usize = 40; +/// Column width of a developer name. +const DEVELOPER_NAME_WIDTH: usize = 20; + +/// Writes one `label value` summary row. +fn write_summary_row(s: &mut String, label: &str, value: impl std::fmt::Display) { + let _ = writeln!(s, " {label:<SUMMARY_LABEL_WIDTH$} {value}"); +} + +/// The title box and the `Summary` section heading every summary opens with. +fn summary_opening(cfg: Config, title: &str, subtitle: &str) -> String { + let mut s = draw_header(title, subtitle, cfg.width); + s.push_str("\n\n"); + write_section_title(&mut s, cfg, "Summary"); + s +} + +/// The date part of an RFC 3339 timestamp (the whole string when shorter). +fn rfc3339_date(ts: &str) -> &str { + ts.get(..RFC3339_DATE_LEN).unwrap_or(ts) +} + +// --------------------------------------------------------------------------- +// quality +// --------------------------------------------------------------------------- + +/// The `history/quality` text body: the aggregate complexity, Halstead, +/// comment and cohesion figures and the ticks with the highest p95 +/// complexity. +#[must_use] +pub fn quality_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + let metrics = super::history::quality_metrics(sub)?; + Some(render_quality(&metrics, Config::new()).into_bytes()) +} + +/// Renders a quality report (see [`quality_text`]). +fn render_quality(metrics: &cf_quality::ComputedMetrics, cfg: Config) -> String { + let agg = &metrics.aggregate; + let mut s = summary_opening(cfg, "Code Quality", &format!("{} ticks", agg.total_ticks)); + write_summary_row(&mut s, "Ticks Analyzed", agg.total_ticks); + write_summary_row(&mut s, "Files Analyzed", agg.total_files_analyzed); + write_summary_row( + &mut s, + "Complexity (median)", + format!("{:.2}", agg.complexity_median_mean), + ); + write_summary_row( + &mut s, + "Complexity (p95)", + format!("{:.2}", agg.complexity_p95_mean), + ); + write_summary_row( + &mut s, + "Halstead Volume (median)", + format!("{:.2}", agg.halstead_vol_median_mean), + ); + write_summary_row( + &mut s, + "Delivered Bugs", + format!("{:.2}", agg.total_delivered_bugs), + ); + write_summary_row( + &mut s, + "Comment Score", + format!( + "{:.2} (min {:.2})", + agg.comment_score_mean_mean, agg.min_comment_score + ), + ); + write_summary_row( + &mut s, + "Cohesion", + format!( + "{:.2} (min {:.2})", + agg.cohesion_mean_mean, agg.min_cohesion + ), + ); + + if !metrics.time_series.is_empty() { + s.push('\n'); + write_section_title(&mut s, cfg, "Most Complex Ticks"); + let mut ranked: Vec<&cf_quality::TimeSeriesEntry> = metrics.time_series.iter().collect(); + ranked.sort_by(|a, b| { + b.stats + .complexity_p95 + .total_cmp(&a.stats.complexity_p95) + .then_with(|| a.tick.cmp(&b.tick)) + }); + for e in ranked.iter().take(MAX_TOP_ROWS) { + let _ = writeln!( + s, + " tick {:<5} p95 {:>7.2} max {:>4.0} {:>4} files {}", + e.tick, + e.stats.complexity_p95, + e.stats.complexity_max, + e.stats.files_analyzed, + rfc3339_date(&e.start_time) + ); + } + write_more(&mut s, cfg, ranked.len().saturating_sub(MAX_TOP_ROWS)); + } + + s.push('\n'); + s +} + +// --------------------------------------------------------------------------- +// typos +// --------------------------------------------------------------------------- + +/// The `history/typos` text body: the fix counts, each `wrong → correct` +/// pair at its first fix, the recurring pairs and the files with the most +/// fixes. +#[must_use] +pub fn typos_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + let report = super::history::typos_report_data(sub)?; + Some(render_typos(&report, Config::new()).into_bytes()) +} + +/// Renders a typos report (see [`typos_text`]). +fn render_typos(report: &cf_typos::ReportData, cfg: Config) -> String { + use cf_typos::metrics::{ + compute_aggregate, compute_file_typos, compute_typo_list, compute_typo_patterns, + }; + + let agg = compute_aggregate(report); + let mut s = summary_opening(cfg, "Typos", &format!("{} fixes", agg.total_typos)); + write_summary_row(&mut s, "Typo Fixes", agg.total_typos); + write_summary_row(&mut s, "Unique Pairs", agg.unique_patterns); + write_summary_row(&mut s, "Affected Files", agg.affected_files); + write_summary_row(&mut s, "Affected Commits", agg.affected_commits); + + let list = compute_typo_list(report); + if !list.is_empty() { + s.push('\n'); + write_section_title(&mut s, cfg, "Typo Fixes (first occurrence)"); + for t in list.iter().take(MAX_TOP_ROWS) { + let pair = format!("{} → {}", t.wrong, t.correct); + let short = t.commit.get(..SHORT_HASH_LEN).unwrap_or(&t.commit); + let _ = writeln!( + s, + " {pair:<TYPO_PAIR_WIDTH$} {}:{} {}", + t.file, + t.line, + cfg.colorize(short, Color::Gray) + ); + } + write_more(&mut s, cfg, list.len().saturating_sub(MAX_TOP_ROWS)); + } + + let patterns = compute_typo_patterns(report); + if !patterns.is_empty() { + s.push('\n'); + write_section_title(&mut s, cfg, "Recurring Pairs"); + for p in patterns.iter().take(MAX_TOP_ROWS) { + let pair = format!("{} → {}", p.wrong, p.correct); + let _ = writeln!(s, " {pair:<TYPO_PAIR_WIDTH$} ×{}", p.frequency); + } + write_more(&mut s, cfg, patterns.len().saturating_sub(MAX_TOP_ROWS)); + } + + let files = compute_file_typos(report); + if !files.is_empty() { + s.push('\n'); + write_section_title(&mut s, cfg, "Files"); + for f in files.iter().take(MAX_TOP_ROWS) { + let _ = writeln!(s, " {:4} fixes {}", f.typo_count, f.file); + } + write_more(&mut s, cfg, files.len().saturating_sub(MAX_TOP_ROWS)); + } + + s.push('\n'); + s +} + +// --------------------------------------------------------------------------- +// anomaly +// --------------------------------------------------------------------------- + +/// The `history/anomaly` text body: the detection settings, the per-tick +/// baselines and the anomalous ticks, most severe first. +#[must_use] +pub fn anomaly_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + let metrics = super::history::anomaly_run_metrics(sub)?; + Some(render_anomaly(&metrics, Config::new()).into_bytes()) +} + +/// Renders an anomaly report (see [`anomaly_text`]). +fn render_anomaly(metrics: &cf_anomaly::model::ComputedMetrics, cfg: Config) -> String { + let agg = &metrics.aggregate; + let mut s = summary_opening( + cfg, + "Temporal Anomalies", + &format!("{} ticks", agg.total_ticks), + ); + write_summary_row(&mut s, "Ticks Analyzed", agg.total_ticks); + write_summary_row( + &mut s, + "Anomalies", + format!( + "{} ({:.1}% of ticks)", + agg.total_anomalies, agg.anomaly_rate + ), + ); + write_summary_row( + &mut s, + "Z-Score Threshold", + format!("{:.2}", f64::from(agg.threshold)), + ); + write_summary_row(&mut s, "Window", format!("{} ticks", agg.window_size)); + let baselines = [ + ("Net Churn / Tick", agg.churn_mean, agg.churn_stddev), + ("Files / Tick", agg.files_mean, agg.files_stddev), + ( + "Languages / Tick", + agg.lang_diversity_mean, + agg.lang_diversity_stddev, + ), + ( + "Authors / Tick", + agg.author_count_mean, + agg.author_count_stddev, + ), + ]; + for (label, mean, stddev) in baselines { + write_summary_row(&mut s, label, format!("{mean:.2} ± {stddev:.2}")); + } + + if !metrics.anomalies.is_empty() { + s.push('\n'); + write_section_title(&mut s, cfg, "Anomalies (most severe first)"); + for a in metrics.anomalies.iter().take(MAX_TOP_ROWS) { + let m = &a.metrics; + let _ = writeln!( + s, + " tick {:<5} |z| {} churn {:+} (+{} / -{}) {} files {} authors", + a.tick, + cfg.colorize(&format!("{:>6.2}", a.max_abs_z_score), Color::Red), + m.net_churn, + m.lines_added, + m.lines_removed, + m.files_changed, + m.author_count + ); + } + write_more( + &mut s, + cfg, + metrics.anomalies.len().saturating_sub(MAX_TOP_ROWS), + ); + } + + s.push('\n'); + s +} + +// --------------------------------------------------------------------------- +// imports +// --------------------------------------------------------------------------- + +/// The headline figures of a `history/imports` report. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ImportsSummary { + /// Developers who added at least one import. + pub developers: usize, + /// Distinct languages the imports were found in. + pub languages: usize, + /// Distinct import paths. + pub distinct_imports: usize, + /// Import additions summed over developers, languages and ticks. + pub usages: i64, +} + +/// The headline figures of `report` (the text summary and the compact line). +pub(crate) fn imports_summary(report: &cf_imports::HistoryReport) -> ImportsSummary { + let counts = cf_imports::history::aggregate_import_counts(&report.imports); + let languages: std::collections::BTreeSet<&str> = report + .imports + .values() + .flat_map(|langs| langs.keys().map(String::as_str)) + .collect(); + ImportsSummary { + developers: report.imports.len(), + languages: languages.len(), + distinct_imports: counts.len(), + usages: counts.values().sum(), + } +} + +/// The `history/imports` text body: the import counts, the most used +/// imports and the developers who added the most distinct imports. +#[must_use] +pub fn imports_text(sub: &clap::ArgMatches) -> Option<Vec<u8>> { + let report = super::history::imports_run_metrics(sub)?; + Some(render_imports(&report, Config::new()).into_bytes()) +} + +/// The developers ranked by the number of distinct imports they added +/// (descending, then by name): `(name, distinct imports, languages)`. +fn rank_importers(report: &cf_imports::HistoryReport) -> Vec<(String, usize, String)> { + let mut ranked: Vec<(String, usize, String)> = report + .imports + .iter() + .map(|(&author, langs)| { + // `author_index` holds `name|email` signatures; show the name. + let name = usize::try_from(author) + .ok() + .and_then(|i| report.author_index.get(i)) + .and_then(|n| n.split('|').next()) + .filter(|n| !n.is_empty()) + .map_or_else(|| format!("dev#{author}"), str::to_string); + let distinct = langs.values().map(std::collections::BTreeMap::len).sum(); + let names: Vec<&str> = langs.keys().map(String::as_str).collect(); + (name, distinct, names.join(", ")) + }) + .collect(); + ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); + ranked +} + +/// Renders an imports report (see [`imports_text`]). +fn render_imports(report: &cf_imports::HistoryReport, cfg: Config) -> String { + let summary = imports_summary(report); + let mut s = summary_opening( + cfg, + "Imports", + &format!("{} developers", summary.developers), + ); + write_summary_row(&mut s, "Developers", summary.developers); + write_summary_row(&mut s, "Languages", summary.languages); + write_summary_row(&mut s, "Distinct Imports", summary.distinct_imports); + write_summary_row(&mut s, "Import Usages", summary.usages); + + let counts = cf_imports::history::aggregate_import_counts(&report.imports); + let (names, uses) = cf_imports::history::top_imports(&counts); + if !names.is_empty() { + s.push('\n'); + write_section_title(&mut s, cfg, "Most Used Imports"); + for (name, n) in names.iter().zip(&uses).take(MAX_TOP_ROWS) { + let _ = writeln!(s, " {name:<IMPORT_NAME_WIDTH$} {n:>6}"); + } + write_more(&mut s, cfg, counts.len().saturating_sub(MAX_TOP_ROWS)); + } + + let importers = rank_importers(report); + if !importers.is_empty() { + s.push('\n'); + write_section_title(&mut s, cfg, "Top Importers"); + for (name, distinct, langs) in importers.iter().take(MAX_TOP_ROWS) { + let _ = writeln!( + s, + " {name:<DEVELOPER_NAME_WIDTH$} {distinct:>4} imports {}", + cfg.colorize(langs, Color::Gray) + ); + } + write_more(&mut s, cfg, importers.len().saturating_sub(MAX_TOP_ROWS)); + } + + s.push('\n'); + s +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A colorless 80-column configuration. + const PLAIN: Config = Config { + width: 80, + no_color: true, + }; + + fn ownership(file: &str, contributors: i32) -> cf_couples::FileOwnershipData { + cf_couples::FileOwnershipData { + file: file.to_string(), + lines: 1, + contributors, + top_contributor: String::new(), + } + } + + /// Equal contributor counts rank by path, whatever the input order. + #[test] + fn ownership_risk_ranks_by_contributors_then_path() { + let input = [ + ownership("z.go", 1), + ownership("m.go", 3), + ownership("a.go", 1), + ownership("b.go", 2), + ]; + let mut reversed = input.clone(); + reversed.reverse(); + for list in [&input[..], &reversed[..]] { + let files: Vec<&str> = rank_ownership_by_risk(list) + .iter() + .map(|f| f.file.as_str()) + .collect(); + assert_eq!(files, ["a.go", "z.go", "b.go", "m.go"]); + } + } + + #[test] + fn write_more_is_silent_without_remaining_rows() { + let mut s = String::new(); + write_more(&mut s, PLAIN, 0); + assert!(s.is_empty()); + write_more(&mut s, PLAIN, 3); + assert_eq!(s, " and 3 more...\n"); + } + + fn typo(wrong: &str, correct: &str, file: &str, line: i64) -> cf_typos::Typo { + cf_typos::Typo { + wrong: wrong.to_string(), + correct: correct.to_string(), + file: file.to_string(), + commit: cf_typos::Hash([0xab; 20]), + line, + } + } + + #[test] + fn typos_text_lists_pairs_recurrences_and_files() { + let report = cf_typos::ReportData { + typos: vec![ + typo("recieve", "receive", "a.go", 3), + typo("recieve", "receive", "b.go", 7), + typo("lenght", "length", "a.go", 9), + ], + }; + let text = render_typos(&report, PLAIN); + assert!(text.contains("Typo Fixes 3"), "{text}"); + assert!(text.contains("Unique Pairs 2"), "{text}"); + assert!(text.contains("recieve → receive"), "{text}"); + assert!(text.contains("a.go:3 abababa"), "{text}"); + assert!(text.contains("×2"), "{text}"); + assert!(text.contains(" 2 fixes a.go"), "{text}"); + } + + #[test] + fn an_empty_typos_report_is_only_the_summary() { + let text = render_typos(&cf_typos::ReportData::default(), PLAIN); + assert!(text.contains("Typo Fixes 0"), "{text}"); + assert!(!text.contains("Recurring Pairs"), "{text}"); + assert!(!text.contains("\n Files\n"), "{text}"); + } + + #[test] + fn anomaly_text_lists_the_anomalous_ticks() { + let metrics = cf_anomaly::model::ComputedMetrics { + anomalies: vec![cf_anomaly::model::Record { + tick: 9, + max_abs_z_score: 3.8, + metrics: cf_anomaly::model::RawMetrics { + files_changed: 2, + lines_added: 40, + lines_removed: 4, + net_churn: 36, + language_diversity: 1, + author_count: 1, + }, + ..cf_anomaly::model::Record::default() + }], + aggregate: cf_anomaly::model::AggregateData { + total_ticks: 3, + total_anomalies: 1, + anomaly_rate: 100.0 / 3.0, + threshold: 2.0, + window_size: 20, + ..cf_anomaly::model::AggregateData::default() + }, + ..cf_anomaly::model::ComputedMetrics::default() + }; + let text = render_anomaly(&metrics, PLAIN); + assert!(text.contains("1 (33.3% of ticks)"), "{text}"); + assert!( + text.contains("Window 20 ticks"), + "{text}" + ); + assert!( + text.contains("tick 9 |z| 3.80 churn +36 (+40 / -4) 2 files 1 authors"), + "{text}" + ); + } + + #[test] + fn imports_text_ranks_imports_and_importers() { + let mut imports = cf_imports::ImportsMap::new(); + let ticks = |n: i64| std::collections::BTreeMap::from([(0, n)]); + imports.insert( + 0, + std::collections::BTreeMap::from([( + "go".to_string(), + std::collections::BTreeMap::from([ + ("fmt".to_string(), ticks(1)), + ("os".to_string(), ticks(2)), + ]), + )]), + ); + imports.insert( + 1, + std::collections::BTreeMap::from([( + "python".to_string(), + std::collections::BTreeMap::from([("os".to_string(), ticks(1))]), + )]), + ); + let report = cf_imports::HistoryReport { + tick_size_secs: 86_400, + author_index: vec!["alice|a@x".to_string(), "bob|b@x".to_string()], + imports, + }; + assert_eq!( + imports_summary(&report), + ImportsSummary { + developers: 2, + languages: 2, + distinct_imports: 2, + usages: 4, + } + ); + let text = render_imports(&report, PLAIN); + let os = text.find("\n os ").expect("os row"); + let fmt = text.find("\n fmt ").expect("fmt row"); + assert!(os < fmt, "os (3 uses) ranks above fmt (1):\n{text}"); + assert!( + text.contains("alice 2 imports go"), + "{text}" + ); + assert!( + text.contains("bob 1 imports python"), + "{text}" + ); + } + + #[test] + fn quality_text_ranks_ticks_by_p95_complexity() { + let entry = |tick: i64, p95: f64| cf_quality::TimeSeriesEntry { + tick, + start_time: "2021-01-05T00:00:00Z".to_string(), + end_time: String::new(), + stats: cf_quality::TickStats { + complexity_p95: p95, + files_analyzed: 1, + ..cf_quality::TickStats::default() + }, + }; + let metrics = cf_quality::ComputedMetrics { + time_series: vec![entry(0, 1.0), entry(4, 7.5), entry(9, 7.5)], + aggregate: cf_quality::AggregateData { + total_ticks: 3, + total_files_analyzed: 3, + ..cf_quality::AggregateData::default() + }, + }; + let text = render_quality(&metrics, PLAIN); + assert!(text.contains("Files Analyzed 3"), "{text}"); + let rows: Vec<&str> = text + .lines() + .filter(|l| l.trim_start().starts_with("tick ")) + .collect(); + assert_eq!(rows.len(), 3, "{text}"); + assert!( + rows[0].contains("tick 4 ") && rows[0].contains("2021-01-05"), + "{text}" + ); + assert!(rows[1].contains("tick 9 "), "{text}"); + assert!(rows[2].contains("tick 0 "), "{text}"); + } +} diff --git a/crates/cf-commands/src/handlers/plot.rs b/crates/cf-commands/src/handlers/plot.rs index a585fbc1..c7300e9c 100644 --- a/crates/cf-commands/src/handlers/plot.rs +++ b/crates/cf-commands/src/handlers/plot.rs @@ -1,18 +1,16 @@ -//! `--format plot` static-phase orchestration — the Rust analogue of the reference -//! `runStaticPlotAnalyzers` → `StaticService.FormatPlotPages` flow. +//! `--format plot` static-phase orchestration, plus the output-directory +//! helpers the history plot phase ([`super::history_plot`]) shares. //! -//! Flow (matching the reference implementation): +//! Flow: //! //! 1. run the selected static analyzers and gather each one's AGGREGATED RAW -//! report (the `analyze.Report` value — the same map `report.json` carries); -//! 2. `RenderPlotPages`: for each analyzer with a registered section renderer, -//! render `<output>/<id-with-dashes>.html` (page title = the full analyzer -//! id) and collect its [`PageMeta`]; -//! 3. `RenderIndex`: render `<output>/index.html` from the collected metas; -//! 4. `writeReportJSON`: write `<output>/report.json` — the results map keyed -//! by the analyzer's short name, two-space-indented `encoding/json` with -//! the `Encoder.Encode` trailing newline, 0o640 file mode, atomic -//! temp+rename. +//! report (the same map `report.json` carries); +//! 2. for each analyzer with a registered section renderer, render +//! `<output>/<id-with-dashes>.html` (page title = the full analyzer id) and +//! collect its [`PageMeta`]; +//! 3. render `<output>/index.html` from the collected metas; +//! 4. write `<output>/report.json` — the results map keyed by the analyzer's +//! short name ([`write_report_json_atomic`]). use std::fs; use std::io; @@ -28,22 +26,21 @@ use crate::handlers::{ }; use crate::pipeline::RunContext; -/// Project title shown on every plot page (the reference `plotPageTitle`). +/// Project title shown on every plot page. const PLOT_PAGE_TITLE: &str = "Codefang"; -/// Output directory mode (the reference `plotDirPerm`). +/// Mode of an output directory a plot run creates. #[cfg(unix)] const PLOT_DIR_MODE: u32 = 0o750; -/// report.json file mode (the reference `reportJSONPerm`). +/// `report.json` file mode. #[cfg(unix)] const REPORT_JSON_MODE: u32 = 0o640; /// One plot-capable static analyzer: its registry id, the short name keying -/// the `report.json` results map (reference: analyzer `Name()`), the builder of its -/// aggregated raw report, and its optional section renderer. Analyzers whose -/// sections are not yet ported keep `sections: None` — the reference implementation renders no page for -/// them but still includes their report in `report.json`. +/// the `report.json` results map, the builder of its aggregated raw report, +/// and its optional section renderer. An analyzer with `sections: None` +/// renders no page but still has its report in `report.json`. pub struct PlotAnalyzer { /// Full analyzer id (`static/complexity`). pub id: &'static str, @@ -83,11 +80,9 @@ fn composition_raw(ctx: &RunContext) -> Option<GoValue> { static_json::composition_raw_report_value(&ctx.path, &super::static_filter(ctx).ok()?) } -/// The plot-capable static analyzers in registry order (reference: -/// `defaultUASTAnalyzers ++ defaultRawFileAnalyzers`: clones, -/// complexity, comments, halstead, cohesion, imports, then composition). One -/// line per analyzer; see `plot_sections/mod.rs` for the porting recipe. -/// Composition registers no plot sections in the reference implementation (no page; report.json only). +/// The plot-capable static analyzers in registry order: clones, complexity, +/// comments, halstead, cohesion, imports, then composition (no page; +/// `report.json` only). pub const PLOT_ANALYZERS: &[PlotAnalyzer] = &[ PlotAnalyzer { id: "static/clones", @@ -137,14 +132,11 @@ pub const PLOT_ANALYZERS: &[PlotAnalyzer] = &[ /// the page set + `report.json` into `output_dir`. Returns `None` when any /// selected id has no plot entry yet (caller falls through to the /// dispatch-blocked diagnostic), `Some(0)` on success, `Some(1)` on an I/O -/// failure (reference: surfaces the render error through cobra, rc 1). +/// failure (the error is printed as `Error: <cause>`). #[must_use] pub fn run_static_plot(ctx: &RunContext, static_ids: &[String], output_dir: &str) -> Option<i32> { - // Resolve every selected id to its plot entry in SELECTION order — the reference implementation - // renders pages (and collects the index metas) in the resolved - // `analyzerNames` order (reference `AnalyzerNamesByID(analyzerIDs)` → - // `RenderPlotPages` ranges that list), which is the CLI selection order; a - // glob selection arrives here already expanded in registry order. + // Pages (and the index cards) follow the SELECTION order; a glob + // selection arrives here already expanded in registry order. let mut selected: Vec<&PlotAnalyzer> = Vec::new(); for id in static_ids { let entry = PLOT_ANALYZERS.iter().find(|entry| entry.id == id)?; @@ -154,10 +146,9 @@ pub fn run_static_plot(ctx: &RunContext, static_ids: &[String], output_dir: &str return None; } - // Run the analyzers (reference: service.AnalyzeFolder over the shared folder - // walk). The static analyzers are independent single-threaded folder walks, - // so COMPUTE their reports concurrently and keep the deterministic - // selection order for rendering/writing below. + // The static analyzers are independent folder walks: COMPUTE their + // reports concurrently and keep the selection order for rendering and + // writing below. let reports: Vec<Option<GoValue>> = crate::handlers::run_concurrent( selected.len(), crate::handlers::ANALYZER_CONCURRENCY, @@ -184,15 +175,10 @@ fn render_plot_output( output_dir: &str, ) -> io::Result<()> { create_output_dir(output_dir)?; + let renderer = page_renderer(output_dir); - let renderer = MultiPageRenderer { - output_dir: output_dir.to_string(), - title: PLOT_PAGE_TITLE.to_string(), - theme: Theme::Dark, - }; - - // RenderPlotPages: pages only for analyzers with a section renderer whose - // sections build successfully (the reference implementation skips section errors silently). + // Pages only for analyzers with a section renderer whose sections build + // (an analyzer whose sections fail keeps its report.json entry). let mut pages: Vec<PageMeta> = Vec::new(); for (entry, (_, report)) in selected.iter().zip(results) { let Some(section_fn) = entry.sections else { @@ -211,12 +197,25 @@ fn render_plot_output( } renderer.render_index(&pages)?; - write_report_json(results, output_dir) + let mut map = GoMap::new(MapOrigin::Map); + for (name, report) in results { + map.push(*name, report.clone()); + } + write_report_json_atomic(output_dir, &GoValue::Map(map)) +} + +/// The dark-themed multi-page renderer writing into `output_dir`. +pub(crate) fn page_renderer(output_dir: &str) -> MultiPageRenderer { + MultiPageRenderer { + output_dir: output_dir.to_string(), + title: PLOT_PAGE_TITLE.to_string(), + theme: Theme::Dark, + } } -/// The reference `os.MkdirAll(outputDir, 0o750)` — apply the reference mode to directories this -/// run creates; pre-existing directories keep their mode (as in the reference implementation). -fn create_output_dir(output_dir: &str) -> io::Result<()> { +/// Creates `output_dir` (and its parents); a directory this run creates gets +/// mode 0o750, a pre-existing one keeps its mode. +pub(crate) fn create_output_dir(output_dir: &str) -> io::Result<()> { let existed = Path::new(output_dir).is_dir(); fs::create_dir_all(output_dir)?; #[cfg(unix)] @@ -229,17 +228,12 @@ fn create_output_dir(output_dir: &str) -> io::Result<()> { Ok(()) } -/// The reference `writeReportJSON`: the results map (analyzer short name -/// → raw report) as two-space-indented JSON + the `Encoder.Encode` trailing -/// newline, written atomically (temp file + rename) with mode 0o640. -fn write_report_json(results: &[(&'static str, GoValue)], output_dir: &str) -> io::Result<()> { - let mut map = GoMap::new(MapOrigin::Map); - for (name, report) in results { - map.push(*name, report.clone()); - } +/// Writes `value` to `<output_dir>/report.json` as two-space-indented JSON +/// with a trailing newline, atomically (temp file + rename), mode 0o640. +pub(crate) fn write_report_json_atomic(output_dir: &str, value: &GoValue) -> io::Result<()> { let bytes = Encoder::indented(" ") .with_trailing_newline(true) - .encode(&GoValue::Map(map)); + .encode(value); let final_path = Path::new(output_dir).join("report.json"); let tmp_path = Path::new(output_dir).join(".report.json.tmp"); diff --git a/crates/cf-commands/src/handlers/plot_sections/cohesion.rs b/crates/cf-commands/src/handlers/plot_sections/cohesion.rs index a9bbbfa5..efef0b2a 100644 --- a/crates/cf-commands/src/handlers/plot_sections/cohesion.rs +++ b/crates/cf-commands/src/handlers/plot_sections/cohesion.rs @@ -12,8 +12,6 @@ use cf_plotpage::echarts::{ }; use cf_plotpage::{build_pie_chart, get_chart_palette, ChartOpts, Hint, Section, Theme}; -use crate::handlers::go_sort; - /// Reference plot-section constants. const EMPTY_CHART_HEIGHT: &str = "400px"; const PIE_RADIUS: &str = "60%"; @@ -306,9 +304,11 @@ fn box_plot_chart(report: &GoValue) -> Chart { return empty_box_plot(); } - // sort.Slice by median ascending (reference: pdqsort; equal-median ties measured). - go_sort::slice(&mut groups, |a, b| { - percentile(&a.scores, P_MEDIAN) < percentile(&b.scores, P_MEDIAN) + // The least cohesive directories (lowest median) first, then by label. + groups.sort_by(|a, b| { + percentile(&a.scores, P_MEDIAN) + .total_cmp(&percentile(&b.scores, P_MEDIAN)) + .then_with(|| a.label.cmp(&b.label)) }); groups.truncate(MAX_DIRECTORIES); diff --git a/crates/cf-commands/src/handlers/plot_sections/comments.rs b/crates/cf-commands/src/handlers/plot_sections/comments.rs index 72a9017b..451109e5 100644 --- a/crates/cf-commands/src/handlers/plot_sections/comments.rs +++ b/crates/cf-commands/src/handlers/plot_sections/comments.rs @@ -12,8 +12,6 @@ use cf_plotpage::echarts::{ }; use cf_plotpage::{build_pie_chart, get_chart_palette, ChartOpts, Hint, Section, Theme}; -use crate::handlers::go_sort; - /// Reference plot-section constants. const TOP_FUNCTIONS_LIMIT: usize = 20; const X_AXIS_ROTATE: f64 = 45.0; @@ -153,10 +151,14 @@ fn function_coverage_chart(report: &GoValue) -> Option<Chart> { return Some(empty_comments_chart()); } - // mapx.SortAndLimit by lines desc (the reference implementation's unstable sort.Slice over the - // walk-order input — go_sort reproduces the tie permutation). + // The longest functions first, then by name (stable: equal names keep + // the report order). let mut sorted = functions.clone(); - go_sort::slice(&mut sorted, |a, b| lines_value(a) > lines_value(b)); + sorted.sort_by(|a, b| { + lines_value(b) + .cmp(&lines_value(a)) + .then_with(|| function_name(a).cmp(function_name(b))) + }); sorted.truncate(TOP_FUNCTIONS_LIMIT); let co = ChartOpts::default_dark(); diff --git a/crates/cf-commands/src/handlers/plot_sections/complexity.rs b/crates/cf-commands/src/handlers/plot_sections/complexity.rs index 35e9cce2..426ccd43 100644 --- a/crates/cf-commands/src/handlers/plot_sections/complexity.rs +++ b/crates/cf-commands/src/handlers/plot_sections/complexity.rs @@ -5,6 +5,9 @@ //! renderer does: the bar chart over the top-20 functions by cyclomatic //! complexity, the cyclomatic-vs-cognitive scatter, and the distribution pie. +use cf_complexity::thresholds::{ + distribution_bucket, COGNITIVE_HIGH, CYCLOMATIC_HIGH, DIST_LABEL_MODERATE, DIST_LABEL_SIMPLE, +}; use cf_gojson::{GoMap, GoValue}; use cf_plotpage::echarts::{ AxisLabel, AxisLine, BarData, Chart, ChartKind, Grid, ItemStyle, LineStyle, MarkLineItem, @@ -12,8 +15,6 @@ use cf_plotpage::echarts::{ }; use cf_plotpage::{build_pie_chart, get_chart_palette, ChartOpts, Hint, Section, Theme}; -use crate::handlers::static_complexity::go_pdqsort; - /// Reference plot-section constants. const TOP_FUNCTIONS_LIMIT: usize = 20; const X_AXIS_ROTATE: f64 = 45.0; @@ -21,15 +22,19 @@ const EMPTY_CHART_HEIGHT: &str = "400px"; const PIE_RADIUS: &str = "60%"; const SCATTER_SYMBOL_SIZE: i64 = 15; const NESTING_MULTIPLIER: i64 = 3; -const CYCLOMATIC_YELLOW_LINE: i64 = 5; -const CYCLOMATIC_RED_LINE: i64 = 10; -const COGNITIVE_RED_LINE: i64 = 15; const UNKNOWN_NAME: &str = "unknown"; -/// Plot display labels for the distribution pie. -const PLOT_LABEL_SIMPLE: &str = "Simple"; -const PLOT_LABEL_MODERATE: &str = "Moderate"; -const PLOT_LABEL_COMPLEX: &str = "Complex"; +/// The three-bucket pie keys of [`distribution_bucket`]. +const BUCKET_SIMPLE: &str = "simple"; +const BUCKET_MODERATE: &str = "moderate"; +/// The pie merges the crate's `Complex (11-20)` and `Very Complex (>20)` +/// terminal buckets into one slice. +const PIE_LABEL_COMPLEX: &str = "Complex (>10)"; + +/// Bar / pie colors per [`distribution_bucket`] key. +const COLOR_SIMPLE: &str = "#91cc75"; +const COLOR_MODERATE: &str = "#fac858"; +const COLOR_COMPLEX: &str = "#ee6666"; /// The reference `analyze.ReportFunctionListWithFallback(report, "functions", /// "function_complexity")`: the function maps under either key. @@ -94,14 +99,13 @@ fn format_plot_label(f: &GoMap) -> String { format!("{}:{}", go_filepath_base(sf), name) } -/// The reference `getComplexityColor`. +/// The bar color of a function: its [`distribution_bucket`] (the crate's +/// `<= 5` simple / `<= 10` moderate / else complex classification). fn complexity_color(complexity: i64) -> &'static str { - if complexity <= CYCLOMATIC_YELLOW_LINE { - "#91cc75" - } else if complexity <= CYCLOMATIC_RED_LINE { - "#fac858" - } else { - "#ee6666" + match distribution_bucket(complexity) { + BUCKET_SIMPLE => COLOR_SIMPLE, + BUCKET_MODERATE => COLOR_MODERATE, + _ => COLOR_COMPLEX, } } @@ -177,11 +181,13 @@ fn bar_chart(report: &GoValue) -> Option<Chart> { return Some(empty_bar_chart()); } - // mapx.SortAndLimit: copy, sort.Slice (the reference implementation's unstable pdqsort — reproduced - // exactly by go_pdqsort so the tie order in the top 20 matches), truncate. + // Top-N by cyclomatic complexity, descending. The raw report lists its + // functions in the crate's total order (cyclomatic desc, cognitive desc, + // nesting desc, name, file, line), so a STABLE sort on the cyclomatic key + // keeps ties in that deterministic order. let mut sorted = functions.clone(); - go_pdqsort(&mut sorted, &|a: &&GoMap, b: &&GoMap| { - get_int(a, "cyclomatic_complexity") > get_int(b, "cyclomatic_complexity") + sorted.sort_by(|a, b| { + get_int(b, "cyclomatic_complexity").cmp(&get_int(a, "cyclomatic_complexity")) }); sorted.truncate(TOP_FUNCTIONS_LIMIT); @@ -340,11 +346,11 @@ fn scatter_chart(report: &GoValue) -> Option<Chart> { series.mark_lines = vec![ MarkLineItem::XAxis { name: "Cyclomatic warning".to_string(), - value: GoValue::Int(CYCLOMATIC_RED_LINE), + value: GoValue::Int(CYCLOMATIC_HIGH), }, MarkLineItem::YAxis { name: "Cognitive warning".to_string(), - value: GoValue::Int(COGNITIVE_RED_LINE), + value: GoValue::Int(COGNITIVE_HIGH), }, ]; @@ -361,27 +367,22 @@ fn pie_chart(report: &GoValue) -> Chart { return empty_pie_chart(); } - // stats.Distribution over classifyComplexityForPlot. + // The crate's three-bucket distribution over every function. let mut simple = 0i64; let mut moderate = 0i64; let mut complex_count = 0i64; for f in &functions { - let cc = get_int(f, "cyclomatic_complexity"); - if cc <= CYCLOMATIC_YELLOW_LINE { - simple += 1; - } else if cc <= CYCLOMATIC_RED_LINE { - moderate += 1; - } else { - complex_count += 1; + match distribution_bucket(get_int(f, "cyclomatic_complexity")) { + BUCKET_SIMPLE => simple += 1, + BUCKET_MODERATE => moderate += 1, + _ => complex_count += 1, } } - // Distribution keys (kept for parity with the reference label constants). - let _ = (PLOT_LABEL_SIMPLE, PLOT_LABEL_MODERATE, PLOT_LABEL_COMPLEX); let palette = get_chart_palette(Theme::Dark); let pie_data = vec![ cf_plotpage::echarts::PieData { - name: "Simple (1-5)".to_string(), + name: DIST_LABEL_SIMPLE.to_string(), value: Some(GoValue::Int(simple)), item_style: Some(ItemStyle { color: palette.semantic.good.to_string(), @@ -390,7 +391,7 @@ fn pie_chart(report: &GoValue) -> Chart { ..cf_plotpage::echarts::PieData::default() }, cf_plotpage::echarts::PieData { - name: "Moderate (6-10)".to_string(), + name: DIST_LABEL_MODERATE.to_string(), value: Some(GoValue::Int(moderate)), item_style: Some(ItemStyle { color: palette.semantic.warning.to_string(), @@ -399,7 +400,7 @@ fn pie_chart(report: &GoValue) -> Chart { ..cf_plotpage::echarts::PieData::default() }, cf_plotpage::echarts::PieData { - name: "Complex (>10)".to_string(), + name: PIE_LABEL_COMPLEX.to_string(), value: Some(GoValue::Int(complex_count)), item_style: Some(ItemStyle { color: palette.semantic.bad.to_string(), diff --git a/crates/cf-commands/src/handlers/plot_sections/halstead.rs b/crates/cf-commands/src/handlers/plot_sections/halstead.rs index 85186c78..2666601d 100644 --- a/crates/cf-commands/src/handlers/plot_sections/halstead.rs +++ b/crates/cf-commands/src/handlers/plot_sections/halstead.rs @@ -7,14 +7,16 @@ //! `ErrInvalidFunctionsData` — the empty-result report). use cf_gojson::{GoMap, GoValue}; +use cf_halstead::thresholds::{ + VolumeLevel, BUGS_FAIR, BUGS_POOR, DIFFICULTY_COMPLEX, EFFORT_HIGH, EFFORT_MEDIUM, VOLUME_HIGH, + VOLUME_VERY_HIGH, +}; use cf_plotpage::echarts::{ AxisLabel, AxisLine, BarData, Chart, ChartKind, ItemStyle, LineStyle, ScatterData, SplitLine, XAxis, YAxis, }; use cf_plotpage::{build_pie_chart, get_chart_palette, ChartOpts, Hint, Section, Theme}; -use crate::handlers::go_sort; - /// Reference plot-section constants. const TOP_FUNCTIONS_LIMIT: usize = 12; const X_AXIS_ROTATE: f64 = 45.0; @@ -23,13 +25,15 @@ const PIE_RADIUS: &str = "60%"; const SCATTER_SYMBOL_SIZE: i64 = 12; const MAX_SYMBOL_SIZE: i64 = 45; const BUGS_MULTIPLIER: f64 = 10.0; -const VOLUME_LOW: f64 = 100.0; -const VOLUME_MEDIUM: f64 = 1000.0; -const VOLUME_HIGH: f64 = 5000.0; -const EFFORT_LOW: f64 = 1000.0; -const EFFORT_MEDIUM: f64 = 10000.0; -const DIFFICULTY_MEDIUM: f64 = 15.0; -const DIFFICULTY_HIGH: f64 = 30.0; +/// Difficulty at which the risk scatter places a point in the high-risk +/// series — a plot-only band above the crate's `Complex` bound +/// ([`DIFFICULTY_COMPLEX`]), which marks medium risk here. +const SCATTER_DIFFICULTY_HIGH: f64 = 30.0; + +/// Bar colors per effort band ([`EFFORT_MEDIUM`] / [`EFFORT_HIGH`]). +const COLOR_LOW: &str = "#91cc75"; +const COLOR_MEDIUM: &str = "#fac858"; +const COLOR_HIGH: &str = "#ee6666"; /// The reference `analyze.ReportFunctionListWithFallback(report, "functions", /// "function_halstead")`. @@ -61,14 +65,15 @@ fn function_name(f: &GoMap) -> &str { } } -/// The reference `getEffortColor`. +/// The bar color of a function: the crate's effort bands (lower-inclusive, +/// as `cf_halstead::thresholds::effort_assessment` classifies them). fn effort_color(effort: f64) -> &'static str { - if effort <= EFFORT_LOW { - "#91cc75" - } else if effort <= EFFORT_MEDIUM { - "#fac858" + if effort >= EFFORT_HIGH { + COLOR_HIGH + } else if effort >= EFFORT_MEDIUM { + COLOR_MEDIUM } else { - "#ee6666" + COLOR_LOW } } @@ -114,7 +119,7 @@ pub fn sections(report: &GoValue) -> Option<Vec<Section>> { Hint { title: "How to interpret:".to_string(), items: vec![ - "<strong>Low (≤100)</strong> = usually easy to maintain".to_string(), + "<strong>Low (<100)</strong> = usually easy to maintain".to_string(), "<strong>High / Very High</strong> concentration means decomposition debt".to_string(), ], }, @@ -129,11 +134,13 @@ fn effort_bar_chart(report: &GoValue) -> Option<Chart> { return Some(empty_halstead_chart()); } - // mapx.SortAndLimit by effort desc (unstable sort.Slice over the walk-order - // input — go_sort reproduces the tie permutation). + // The highest effort first, then by name (stable: equal names keep the + // report order). let mut sorted = functions.clone(); - go_sort::slice(&mut sorted, |a, b| { - float_value(a, "effort") > float_value(b, "effort") + sorted.sort_by(|a, b| { + float_value(b, "effort") + .total_cmp(&float_value(a, "effort")) + .then_with(|| function_name(a).cmp(function_name(b))) }); sorted.truncate(TOP_FUNCTIONS_LIMIT); @@ -190,11 +197,13 @@ fn effort_bar_chart(report: &GoValue) -> Option<Chart> { Some(bar) } -/// Scatter risk zones (reference `classifyScatterRisk`). +/// Scatter risk zones: high from the crate's very-high volume, the plot's +/// high difficulty band or a `poor` bug estimate; medium from high volume, +/// complex difficulty or a `fair` bug estimate. fn classify_scatter_risk(volume: f64, difficulty: f64, bugs: f64) -> u8 { - if volume >= VOLUME_HIGH || difficulty >= DIFFICULTY_HIGH || bugs >= 1.0 { + if volume >= VOLUME_VERY_HIGH || difficulty >= SCATTER_DIFFICULTY_HIGH || bugs >= BUGS_POOR { 2 // riskHigh - } else if volume >= VOLUME_MEDIUM || difficulty >= DIFFICULTY_MEDIUM || bugs >= 0.3 { + } else if volume >= VOLUME_HIGH || difficulty >= DIFFICULTY_COMPLEX || bugs >= BUGS_FAIR { 1 // riskMedium } else { 0 // riskLow @@ -306,36 +315,32 @@ fn volume_pie_chart(report: &GoValue) -> Chart { return empty_halstead_pie(); } - let (mut low, mut medium, mut high, mut very_high) = (0i64, 0i64, 0i64, 0i64); + // The crate's volume bands ([`VolumeLevel::of`]), one slice each. + let mut counts = [0i64; VolumeLevel::ALL.len()]; for f in &functions { - let volume = float_value(f, "volume"); - if volume <= VOLUME_LOW { - low += 1; - } else if volume <= VOLUME_MEDIUM { - medium += 1; - } else if volume <= VOLUME_HIGH { - high += 1; - } else { - very_high += 1; - } + counts[VolumeLevel::of(float_value(f, "volume")).index()] += 1; } let palette = get_chart_palette(Theme::Dark); - let item = |name: &str, value: i64, color: &str| cf_plotpage::echarts::PieData { - name: name.to_string(), - value: Some(GoValue::Int(value)), - item_style: Some(ItemStyle { - color: color.to_string(), - ..ItemStyle::default() - }), - ..cf_plotpage::echarts::PieData::default() - }; - let pie_data = vec![ - item("Low (≤100)", low, palette.semantic.good), - item("Medium (101-1000)", medium, palette.primary[1]), - item("High (1001-5000)", high, palette.semantic.warning), - item("Very High (>5000)", very_high, palette.semantic.bad), + let colors = [ + palette.semantic.good, + palette.primary[1], + palette.semantic.warning, + palette.semantic.bad, ]; + let pie_data = VolumeLevel::ALL + .iter() + .zip(colors) + .map(|(level, color)| cf_plotpage::echarts::PieData { + name: level.bucket_label().to_string(), + value: Some(GoValue::Int(counts[level.index()])), + item_style: Some(ItemStyle { + color: color.to_string(), + ..ItemStyle::default() + }), + ..cf_plotpage::echarts::PieData::default() + }) + .collect(); build_pie_chart(None, "Volume", pie_data, PIE_RADIUS) } @@ -437,11 +442,24 @@ mod tests { assert!(!json.contains("Medium risk")); } + /// The pie buckets are the crate's volume bands with the crate's labels. #[test] - fn pie_volume_buckets_match_go() { + fn pie_volume_buckets_are_the_crate_bands() { let json = volume_pie_chart(&raw_report()).option_json(); assert!(json.contains( - "{\"name\":\"Low (≤100)\",\"value\":1,\"itemStyle\":{\"color\":\"#22c55e\"}},{\"name\":\"Medium (101-1000)\",\"value\":0,\"itemStyle\":{\"color\":\"#38bdf8\"}},{\"name\":\"High (1001-5000)\",\"value\":0,\"itemStyle\":{\"color\":\"#eab308\"}},{\"name\":\"Very High (>5000)\",\"value\":1,\"itemStyle\":{\"color\":\"#ef4444\"}}" - )); + "{\"name\":\"Low (<100)\",\"value\":1,\"itemStyle\":{\"color\":\"#22c55e\"}},{\"name\":\"Medium (100-1000)\",\"value\":0,\"itemStyle\":{\"color\":\"#38bdf8\"}},{\"name\":\"High (1000-5000)\",\"value\":0,\"itemStyle\":{\"color\":\"#eab308\"}},{\"name\":\"Very High (>=5000)\",\"value\":1,\"itemStyle\":{\"color\":\"#ef4444\"}}" + ), "{json}"); + } + + /// A function exactly at a band bound belongs to the higher band, as in + /// `cf_halstead::thresholds`. + #[test] + fn bounds_are_lower_inclusive_like_the_crate() { + assert_eq!(effort_color(EFFORT_MEDIUM), COLOR_MEDIUM); + assert_eq!(effort_color(EFFORT_HIGH), COLOR_HIGH); + assert_eq!(classify_scatter_risk(VOLUME_HIGH, 0.0, 0.0), 1); + assert_eq!(classify_scatter_risk(VOLUME_VERY_HIGH, 0.0, 0.0), 2); + assert_eq!(classify_scatter_risk(0.0, DIFFICULTY_COMPLEX, 0.0), 1); + assert_eq!(classify_scatter_risk(0.0, 0.0, BUGS_POOR), 2); } } diff --git a/crates/cf-commands/src/handlers/plot_sections/history_anomaly.rs b/crates/cf-commands/src/handlers/plot_sections/history_anomaly.rs index 55812538..a825aa83 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_anomaly.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_anomaly.rs @@ -91,7 +91,7 @@ pub fn sections(metrics: &ComputedMetrics, external_summaries: &[ExternalSummary "Anomalies indicate sudden deviations from the rolling average".to_string(), "Investigate anomaly ticks for large refactors, bulk imports, or regressions" .to_string(), - "Adjust --anomaly-threshold to tune sensitivity (lower = more sensitive)" + "The Z-score threshold (2.0) sets the sensitivity (lower = more sensitive)" .to_string(), "Adjust --anomaly-window to change the rolling baseline period".to_string(), ], diff --git a/crates/cf-commands/src/handlers/plot_sections/history_burndown.rs b/crates/cf-commands/src/handlers/plot_sections/history_burndown.rs index 5f4420eb..61620e66 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_burndown.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_burndown.rs @@ -385,29 +385,47 @@ fn aggregate_by_year(data: &ChartData) -> Option<(Vec<i64>, Vec<Vec<f64>>)> { Some((years, year_data)) } -/// The reference `bandLabel`. +/// The label of band `band_idx`: its age at the end of the window — band 0 +/// is the oldest, the newest band is one granularity old — in months, or in +/// years once it reaches a year. fn band_label(band_idx: i64, data: &ChartData) -> String { - let upper_ticks = (band_idx + 1) * data.granularity; - let age_ns = upper_ticks * data.tick_size_ns; - let age_days = age_ns as f64 / NS_PER_HOUR as f64 / HOURS_PER_DAY as f64; - let age_months = ((age_days as i64) / DAYS_PER_MONTH).max(1); - - let max_band_idx = data.global_history[0].len() as i64 - 1; - let max_days = ((max_band_idx + 1) * data.granularity * data.tick_size_ns) as f64 - / NS_PER_HOUR as f64 - / HOURS_PER_DAY as f64; - let max_months = (max_days as i64) / DAYS_PER_MONTH; - - if max_months >= MONTHS_PER_YEAR && data.end_time_ns != 0 { - return year_of_ns(data.end_time_ns - age_ns).to_string(); + let num_bands = data.global_history[0].len() as i64; + let age_ticks = (num_bands - band_idx) * data.granularity; + let age_days = age_ticks * data.tick_size_ns / NS_PER_HOUR / HOURS_PER_DAY; + let age_months = (age_days / DAYS_PER_MONTH).max(1); + if age_months >= MONTHS_PER_YEAR { + return format!("{}y", age_months / MONTHS_PER_YEAR); } + format!("{age_months}mo") +} - if max_months >= MONTHS_PER_YEAR { - let y = age_months / MONTHS_PER_YEAR; - if y > 0 { - return format!("{y}y"); +#[cfg(test)] +mod tests { + use super::*; + + fn chart(bands: usize, granularity: i64) -> ChartData { + ChartData { + global_history: vec![vec![1; bands]], + sampling: granularity, + granularity, + tick_size_ns: 24 * NS_PER_HOUR, + end_time_ns: 0, + project_name: String::new(), } } - format!("{age_months}mo") + /// Band 0 is the oldest code and the last band the newest. + #[test] + fn band_labels_age_from_oldest_to_newest() { + let data = chart(11, 30); + assert_eq!(band_label(0, &data), "11mo"); + assert_eq!(band_label(5, &data), "6mo"); + assert_eq!(band_label(10, &data), "1mo"); + let yearly = chart(30, 30); + assert_eq!(band_label(0, &yearly), "2y"); + assert_eq!(band_label(29, &yearly), "1mo"); + // A sub-month band still reads as one month. + let fine = chart(3, 5); + assert_eq!(band_label(2, &fine), "1mo"); + } } diff --git a/crates/cf-commands/src/handlers/plot_sections/history_couples.rs b/crates/cf-commands/src/handlers/plot_sections/history_couples.rs index 7c88f807..3e036dd4 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_couples.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_couples.rs @@ -1,11 +1,6 @@ -//! `history/couples` plot sections. -//! (`GenerateStoreSections` → `buildStoreSections` over the `file_coupling`, -//! `dev_matrix` and `ownership` store kinds). -//! -//! The dev-coupling heatmap section is gated on the `dev_matrix` names, which -//! every `run` pipeline leaves EMPTY (the aggregator's `reversedNames` comes -//! only from a preloaded people dict) — so, like the live reference pages, only the -//! file-couples bar and the ownership pie can render. +//! `history/couples` plot sections: the top file couples bar chart over the +//! report's `file_coupling` list and the ownership pie over its +//! `file_ownership` list. use cf_couples::{bucket_ownership, FileCouplingData, FileOwnershipData}; use cf_gojson::GoValue; @@ -13,7 +8,7 @@ use cf_plotpage::echarts::PieData; use cf_plotpage::echarts::{AxisLabel, BarData, Chart, ChartKind, ItemStyle, Label, XAxis, YAxis}; use cf_plotpage::{build_pie_chart, get_chart_palette, ChartOpts, Hint, Section, Theme}; -/// Reference couples plot-section constants. +/// Couples plot-section constants. const LABEL_FONT_SIZE: i64 = 10; const INNER_LABEL_SIZE: f64 = 9.0; const BAR_CHART_HEIGHT: &str = "500px"; @@ -21,12 +16,10 @@ const PIE_RADIUS: &str = "65%"; const MAX_FILE_COUPLES: usize = 20; const MAX_PATH_LEN: usize = 30; -/// The reference `buildStoreSections` over the store kinds. `dev_names` is the -/// `dev_matrix` record's name list (empty on every run pipeline; the heatmap -/// section is skipped exactly as the reference implementation skips it). +/// Builds the couples sections: the top file couples bar (when any pair +/// co-changed) and the ownership pie (when any file is owned). pub fn sections( file_coupling: &[FileCouplingData], - dev_names: &[String], ownership: &[FileOwnershipData], ) -> Vec<Section> { let mut result = Vec::new(); @@ -49,11 +42,6 @@ pub fn sections( }); } - // Section 2 (developer coupling heatmap) requires dev_matrix names; the - // run pipelines never populate them, so this is unreachable in practice — - // the gate is kept for reference parity. - debug_assert!(dev_names.is_empty(), "run pipelines never carry dev names"); - if let Some(chart) = build_ownership_pie_chart(ownership) { result.push(Section { title: "File Ownership Distribution".to_string(), @@ -75,7 +63,7 @@ pub fn sections( result } -/// The reference `truncatePath`. +/// Keeps the trailing `MAX_PATH_LEN` characters of a long path behind `...`. fn truncate_path(path: &str) -> String { if path.len() <= MAX_PATH_LEN { return path.to_string(); @@ -83,8 +71,8 @@ fn truncate_path(path: &str) -> String { format!("...{}", &path[path.len() - MAX_PATH_LEN + 3..]) } -/// The reference `buildFileCouplingBarChartFromData`: horizontal bar of the top couples -/// (reversed into ascending display order); `None` when there are no pairs. +/// Horizontal bar of the top couples (reversed into ascending display +/// order); `None` when there are no pairs. fn build_file_coupling_bar_chart(couples: &[FileCouplingData]) -> Option<Chart> { if couples.is_empty() { return None; @@ -158,8 +146,8 @@ fn build_file_coupling_bar_chart(couples: &[FileCouplingData]) -> Option<Chart> Some(bar) } -/// The reference `buildOwnershipPieChartFromData`: bucketed contributor counts; `None` -/// when there is no ownership data. +/// Pie of the bucketed contributor counts; `None` when there is no ownership +/// data. fn build_ownership_pie_chart(ownership: &[FileOwnershipData]) -> Option<Chart> { if ownership.is_empty() { return None; diff --git a/crates/cf-commands/src/handlers/plot_sections/history_devs.rs b/crates/cf-commands/src/handlers/plot_sections/history_devs.rs index fd6d368b..67a09cf0 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_devs.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_devs.rs @@ -16,8 +16,9 @@ use cf_plotpage::{ build_bar_chart, build_line_chart, BarSeries, ChartOpts, Hint, LineSeries, Section, SeriesValue, }; -use crate::handlers::go_sort; -use crate::handlers::plot_sections::history_shared::{format_number, format_signed_number}; +use crate::handlers::plot_sections::history_shared::{ + by_value_then_name, format_number, format_signed_number, +}; /// Reference devs plot/dashboard constants. const MAX_DEVS: usize = 20; @@ -318,9 +319,8 @@ fn create_workload_tab(metrics: &ComputedMetrics) -> Box<dyn Renderable> { return Box::new(Text::new("No workload data available")); } - // buildTreemapNodes. the reference implementation iterates the langDevs map randomly before the - // value-descending sorts; language-name-ascending is the deterministic - // stand-in (ties at equal Value are reference-variant). + // Developers grouped by primary language; every level ranks by value + // (descending), then by name. let mut lang_devs: std::collections::BTreeMap<&str, Vec<TreeMapNode>> = std::collections::BTreeMap::new(); let mut lang_totals: std::collections::BTreeMap<&str, i64> = std::collections::BTreeMap::new(); @@ -336,14 +336,14 @@ fn create_workload_tab(metrics: &ComputedMetrics) -> Box<dyn Renderable> { let mut root_nodes: Vec<TreeMapNode> = Vec::with_capacity(lang_devs.len()); for (lang, mut dev_nodes) in lang_devs { - go_sort::slice(&mut dev_nodes, |a, b| a.value > b.value); + dev_nodes.sort_by(by_value_then_name); root_nodes.push(TreeMapNode { name: lang.to_string(), value: lang_totals[lang], children: dev_nodes, }); } - go_sort::slice(&mut root_nodes, |a, b| a.value > b.value); + root_nodes.sort_by(by_value_then_name); let co = ChartOpts::default_dark(); let mut tm = Chart::new(ChartKind::TreeMap); @@ -424,7 +424,7 @@ fn create_languages_tab(metrics: &ComputedMetrics, top_langs: &[String]) -> Box< .iter() .map(|d| (d, contribution(d))) .collect(); - go_sort::slice(&mut scored, |a, b| a.1 > b.1); + scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.name.cmp(&b.0.name))); scored.truncate(TOP_DEVS_FOR_RADAR); let indicators: Vec<cf_plotpage::echarts::Indicator> = top_langs diff --git a/crates/cf-commands/src/handlers/plot_sections/history_file_history.rs b/crates/cf-commands/src/handlers/plot_sections/history_file_history.rs index f54e253e..b6babd77 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_file_history.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_file_history.rs @@ -1,7 +1,6 @@ -//! `history/file-history` plot sections. -//! (`GenerateStoreSections` → `buildStoreSections` over the `file_churn` and -//! `composition` store kinds, which the run's `ComputedMetrics` carries as -//! `file_churn` / `composition_ts`). +//! `history/file-history` plot sections: the most-modified-files bar over +//! the report's `file_churn` list and the composition-over-time stacked area +//! over its `composition_ts` (distinct files changed per tick, by category). use cf_file_history::{CompositionTimeSeriesEntry, FileChurnData}; use cf_plotpage::echarts::{AxisLabel, AxisLine, LineStyle, XAxis}; @@ -10,15 +9,12 @@ use cf_plotpage::{ LineSeries, Section, SeriesValue, Theme, }; -use crate::handlers::go_sort; - -/// file_Reference history plot-section constants. +/// File-history plot-section constants. const TOP_FILES_LIMIT: usize = 20; const X_AXIS_ROTATE: f64 = 60.0; const COMPOSITION_AREA_ALPHA: f64 = 0.5; -/// The reference `AllCategories` order (file_history `Category` constants) with the -/// per-category chart colors (`categoryColors`). +/// The canonical category order with the per-category chart colors. const CATEGORY_COLORS: &[(&str, &str)] = &[ ("source", "#4CAF50"), ("documentation", "#2196F3"), @@ -30,8 +26,8 @@ const CATEGORY_COLORS: &[(&str, &str)] = &[ ("binary", "#F44336"), ]; -/// The reference `GenerateStoreSections` → `buildStoreSections`: zero churn AND zero -/// composition yield zero sections. +/// Builds the file-history sections; zero churn AND zero composition yield +/// zero sections. pub fn sections( file_churn: &[FileChurnData], composition_ts: &[CompositionTimeSeriesEntry], @@ -86,12 +82,15 @@ pub fn sections( sections } -/// The reference `buildBarChartFromChurnData`: re-sort (the reference `sort.Slice`, commit-count -/// descending over the churn-score-sorted input), cut to the top 20, one -/// "Commits" series, rotated X labels. +/// Bar of the top 20 files by commit count (then path), one "Commits" +/// series, rotated X labels. fn build_bar_chart_from_churn(file_churn: &[FileChurnData]) -> Chart { let mut churn: Vec<&FileChurnData> = file_churn.iter().collect(); - go_sort::slice(&mut churn, |a, b| a.commit_count > b.commit_count); + churn.sort_by(|a, b| { + b.commit_count + .cmp(&a.commit_count) + .then_with(|| a.path.cmp(&b.path)) + }); let limit = churn.len().min(TOP_FILES_LIMIT); let top = &churn[..limit]; @@ -109,8 +108,7 @@ fn build_bar_chart_from_churn(file_churn: &[FileChurnData]) -> Chart { let co = ChartOpts::default_dark(); let mut chart = build_bar_chart(Some(&co), &labels, &series, "Commits"); - // The reference implementation's follow-up WithXAxisOpts replaces the X axis with the rotated-label - // variant (no name, interval "0"). + // The rotated-label X axis (no name, interval "0"). chart.x_axis = XAxis { axis_label: Some(AxisLabel { rotate: X_AXIS_ROTATE, @@ -131,9 +129,9 @@ fn build_bar_chart_from_churn(file_churn: &[FileChurnData]) -> Chart { chart } -/// The reference `buildCompositionChartFromTS`: per-category stacked area line over the -/// pre-computed time series; categories with no positive values are skipped; -/// `None` when nothing plots. +/// Per-category stacked area line over the composition time series; +/// categories with no positive values are skipped; `None` when nothing +/// plots. fn build_composition_chart_from_ts(ts: &[CompositionTimeSeriesEntry]) -> Option<Chart> { if ts.is_empty() { return None; diff --git a/crates/cf-commands/src/handlers/plot_sections/history_imports.rs b/crates/cf-commands/src/handlers/plot_sections/history_imports.rs index 96da72f1..d1ff877b 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_imports.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_imports.rs @@ -1,38 +1,29 @@ -//! `history/imports` (`imports-per-dev`) plot sections (`GenerateStoreSections` → -//! `buildImportsStoreSections` over the `import_usage` store kind). +//! `history/imports` (`imports-per-dev`) plot sections: the top imports by +//! total usage across every developer, language and tick. //! -//! The store records are the reference `topImports(aggregateImportCounts(merged))`: the -//! per-import total counts sorted count-descending and cut to the top 20. The -//! caller passes the name-ascending aggregated counts -//! (`imports_run_usage_counts`); the pdqsort descending sort + cut happen -//! here, mirroring `WriteToStore`. +//! The caller passes the aggregated per-import counts +//! (`imports_run_usage_counts`); [`cf_imports::history::top_imports`] ranks +//! them (count descending, name ascending) and cuts the top 20. +use std::collections::BTreeMap; + +use cf_imports::history::top_imports; use cf_plotpage::echarts::{ AxisLabel, AxisLine, BarData, Chart, ChartKind, ItemStyle, LineStyle, XAxis, }; use cf_plotpage::{get_chart_palette, ChartOpts, Hint, Section, Theme}; -use crate::handlers::go_sort; - -/// imports the reference `topImportsLimit`. -const TOP_IMPORTS_LIMIT: usize = 20; -/// imports the reference `xAxisRotate`. +/// Rotation of the x-axis import labels. const X_AXIS_ROTATE: f64 = 60.0; -/// The reference `GenerateStoreSections`: zero records yield zero sections. -pub fn sections(usage_counts: &[(String, i64)]) -> Vec<Section> { - // The reference `topImports`: count-descending `sort.Slice` over the aggregate map's - // (random-order) entries, then the top-20 cut. The name-ascending input is - // the deterministic stand-in for the map order; ties are reference-variant. - let mut records: Vec<(String, i64)> = usage_counts.to_vec(); - go_sort::slice(&mut records, |a, b| a.1 > b.1); - records.truncate(TOP_IMPORTS_LIMIT); - - if records.is_empty() { +/// The plot sections: zero imports yield zero sections. +pub fn sections(usage_counts: &BTreeMap<String, i64>) -> Vec<Section> { + let (labels, data) = top_imports(usage_counts); + if labels.is_empty() { return Vec::new(); } - let chart = build_bar_chart_from_usage_records(&records); + let chart = build_usage_bar_chart(&labels, &data); vec![Section { title: "Top Imports Usage".to_string(), @@ -51,9 +42,8 @@ pub fn sections(usage_counts: &[(String, i64)]) -> Vec<Section> { }] } -/// The reference `buildBarChartFromUsageRecords` → `createImportsBarChart` (+ the -/// follow-up `WithXAxisOpts` override, which re-sets the identical axis). -fn build_bar_chart_from_usage_records(records: &[(String, i64)]) -> Chart { +/// One "Usage" bar per import label. +fn build_usage_bar_chart(labels: &[String], data: &[i64]) -> Chart { let co = ChartOpts::default_dark(); let palette = get_chart_palette(Theme::Dark); @@ -80,14 +70,11 @@ fn build_bar_chart_from_usage_records(records: &[(String, i64)]) -> Chart { ..XAxis::default() }; bar.y_axis = co.y_axis("Usage Count"); + bar.set_x_axis_labels(labels); - let labels: Vec<String> = records.iter().map(|(name, _)| name.clone()).collect(); - bar.set_x_axis_labels(&labels); - - let data = cf_gojson::GoValue::Array( - records - .iter() - .map(|(_, count)| { + let values = cf_gojson::GoValue::Array( + data.iter() + .map(|count| { BarData { value: Some(cf_gojson::GoValue::Int(*count)), ..BarData::default() @@ -96,7 +83,7 @@ fn build_bar_chart_from_usage_records(records: &[(String, i64)]) -> Chart { }) .collect(), ); - let series = bar.add_series("Usage", data); + let series = bar.add_series("Usage", values); series.item_style = Some(ItemStyle { color: palette.primary[1].to_string(), ..ItemStyle::default() diff --git a/crates/cf-commands/src/handlers/plot_sections/history_shared.rs b/crates/cf-commands/src/handlers/plot_sections/history_shared.rs index d98f7fb7..dbaca98b 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_shared.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_shared.rs @@ -4,6 +4,14 @@ //! components. use cf_plotpage::components::{BadgeColor, GridLayout, Renderable, Stat}; +use cf_plotpage::echarts::TreeMapNode; + +/// Treemap ranking: the larger value first, then by name — a total order, +/// so equal values list identically on every run. +#[must_use] +pub fn by_value_then_name(a: &TreeMapNode, b: &TreeMapNode) -> std::cmp::Ordering { + b.value.cmp(&a.value).then_with(|| a.name.cmp(&b.name)) +} /// The reference `fmt.Sprintf("%.<prec>f", v)` (round-half-to-even is NOT what the reference implementation does — /// `fmt` rounds half away from zero via strconv shortest-then-fixed; Rust's diff --git a/crates/cf-commands/src/handlers/plot_sections/history_shotness.rs b/crates/cf-commands/src/handlers/plot_sections/history_shotness.rs index 72f92864..98bc2cc8 100644 --- a/crates/cf-commands/src/handlers/plot_sections/history_shotness.rs +++ b/crates/cf-commands/src/handlers/plot_sections/history_shotness.rs @@ -1,6 +1,7 @@ -//! `history/shotness` plot sections. -//! (`GenerateStoreSections` over the `node_data` store kind, which is exactly -//! the run report's key-sorted `(NodeSummary, Counter)` pairs). +//! `history/shotness` plot sections over the run report's key-sorted +//! `(NodeSummary, Counter)` pairs (`shotness_run_report_data`): the file → +//! entity treemap, the entity coupling heatmap and the top-entities bar +//! chart. use std::collections::HashMap; @@ -13,7 +14,7 @@ use cf_plotpage::echarts::{ use cf_plotpage::{get_chart_palette, ChartOpts, Hint, Section, Theme}; use cf_shotness::NodeSummary; -use crate::handlers::go_sort; +use crate::handlers::plot_sections::history_shared::by_value_then_name; /// Reference shotness plot-section constants. const TOP_N_NODES: usize = 20; @@ -28,7 +29,7 @@ const BORDER_WIDTH_1: f64 = 1.0; const BORDER_WIDTH_2: f64 = 2.0; const MIN_HEAT_MAP_NODES: usize = 2; -/// The reference `GenerateStoreSections`: zero nodes yield zero sections. +/// The three sections; zero nodes yield zero sections. pub fn sections(nodes: &[NodeSummary], counters: &[HashMap<usize, i64>]) -> Vec<Section> { if nodes.is_empty() { return Vec::new(); @@ -139,9 +140,8 @@ fn create_tree_map( counters: &[HashMap<usize, i64>], co: &ChartOpts, ) -> Chart { - // buildFileHierarchy: per-file child nodes + totals. the reference implementation iterates the maps - // randomly when building rootNodes; file-name-ascending is the - // deterministic stand-in (ties at equal Value are reference-variant). + // Per-file child nodes + totals; every level ranks by value + // (descending), then by name. let mut file_map: std::collections::BTreeMap<&str, Vec<TreeMapNode>> = std::collections::BTreeMap::new(); let mut file_totals: std::collections::BTreeMap<&str, i64> = std::collections::BTreeMap::new(); @@ -164,14 +164,14 @@ fn create_tree_map( let mut root_nodes: Vec<TreeMapNode> = Vec::with_capacity(file_map.len()); for (file, mut children) in file_map { - go_sort::slice(&mut children, |a, b| a.value > b.value); + children.sort_by(by_value_then_name); root_nodes.push(TreeMapNode { name: go_filepath_base(file).to_string(), value: file_totals[file], children, }); } - go_sort::slice(&mut root_nodes, |a, b| a.value > b.value); + root_nodes.sort_by(by_value_then_name); root_nodes.truncate(MAX_FILES); let mut tm = Chart::new(ChartKind::TreeMap); @@ -249,7 +249,12 @@ fn create_heat_map( }); } } - go_sort::slice(&mut actives, |a, b| a.count > b.count); + actives.sort_by(|a, b| { + b.count + .cmp(&a.count) + .then_with(|| a.name.cmp(&b.name)) + .then_with(|| a.idx.cmp(&b.idx)) + }); actives.truncate(TOP_N_NODES); if actives.len() < MIN_HEAT_MAP_NODES { @@ -404,8 +409,7 @@ fn create_bar_chart( .enumerate() .map(|(idx, counter)| { let mut coupled: i64 = 0; - // The reference implementation ranges the counter map randomly; addition is - // order-independent. + // Addition is independent of the map's iteration order. for (other, val) in counter { if *other != idx && *val > 0 { coupled += *val; @@ -418,7 +422,12 @@ fn create_bar_chart( } }) .collect(); - go_sort::slice(&mut scores, |a, b| a.self_count > b.self_count); + // Stable: equal counts and names keep the node order. + scores.sort_by(|a, b| { + b.self_count + .cmp(&a.self_count) + .then_with(|| a.name.cmp(&b.name)) + }); scores.truncate(TOP_N_NODES); let labels: Vec<String> = scores.iter().map(|s| s.name.clone()).collect(); diff --git a/crates/cf-commands/src/handlers/section_render.rs b/crates/cf-commands/src/handlers/section_render.rs index 03cb5423..31a623e0 100644 --- a/crates/cf-commands/src/handlers/section_render.rs +++ b/crates/cf-commands/src/handlers/section_render.rs @@ -1,30 +1,23 @@ -//! Terminal `text` / `compact` rendering for the static analyzers. +//! Terminal `text` / `compact` rendering over a section report tree. //! -//! Mirrors the reference `StaticService.FormatText` / `FormatCompact` -//! (`DefaultStaticRenderer.RenderText` / `RenderCompact`). Rather than re-derive -//! each analyzer's section data, this adapter reads the **same** JSON section -//! tree every static handler already builds for `--format json` -//! (`*_report_value` → `renderer.JSONReport` shape: `{overall_score_label, -//! sections:[{title,score_label,status,metrics,distribution?,issues,score}], -//! overall_score}`) and feeds it to the ported [`cf_renderer::DefaultStaticRenderer`]. -//! -//! Because the JSON path is byte-identical to the reference binary (DESIGN tier-0), the terminal -//! formats are guaranteed to render from the exact same metrics / distribution / -//! issue values and order as the reference implementation's own `ReportSection`, so wherever the reference implementation's terminal -//! output is byte-deterministic the Rust output matches it byte-for-byte. Where -//! the reference implementation's terminal output is itself nondeterministic (e.g. a map-order-dependent -//! status message), the differential oracle falls back to a structural realcheck -//! and this deterministic, non-empty rendering satisfies it. +//! Rather than re-derive each analyzer's section data, this adapter reads the +//! **same** JSON section tree every static handler already builds for +//! `--format json` (`{overall_score_label, sections:[{title, score_label, +//! status, metrics, distribution?, issues, score}], overall_score}`) and feeds +//! it to [`cf_renderer::DefaultStaticRenderer`], so the terminal formats show +//! exactly the metrics, distribution and issues the JSON report carries, in +//! the same order. The history `--format compact` lines +//! ([`super::history_formats::history_compact`]) are rendered through the same +//! tree. use cf_gojson::{GoMap, GoValue}; use cf_renderer::analyze::{DistributionItem, Issue, Metric, ReportSection}; use cf_renderer::DefaultStaticRenderer; /// A [`ReportSection`] backed by an analyzer's already-built JSON section -/// `GoValue` (the `renderer.SectionToJSON` shape). The issue list is stored in -/// the reference implementation's exact render order, so [`Self::top_issues`] is a prefix slice and -/// [`Self::all_issues`] the whole list — matching the reference implementation's `TopIssues(n)` / -/// `AllIssues` over the pre-sorted slice. +/// `GoValue`. The issue list is stored in render order, so +/// [`Self::top_issues`] is a prefix slice and [`Self::all_issues`] the whole +/// list. struct JsonSection { title: String, score: f64, @@ -147,8 +140,8 @@ fn parse_section(section: &GoValue) -> Option<JsonSection> { }) } -/// Reads the `sections` array of a `renderer.JSONReport` tree into renderer -/// sections, preserving order (the executive summary + multi-analyzer text/compact +/// Reads the `sections` array of a report tree into renderer sections, +/// preserving order (the executive summary + multi-analyzer text/compact /// depend on it). fn sections_from_report(report: &GoValue) -> Vec<JsonSection> { let GoValue::Map(rm) = report else { @@ -160,13 +153,12 @@ fn sections_from_report(report: &GoValue) -> Vec<JsonSection> { .collect() } -/// The reference implementation honors `NO_COLOR` (`terminal.NewConfig`: `os.Getenv("NO_COLOR") != ""`). +/// Whether `NO_COLOR` is set (`--no-color` sets it for the run). fn no_color() -> bool { std::env::var_os("NO_COLOR").is_some() } -/// `--format compact` bytes for a `renderer.JSONReport` tree (reference: -/// `FormatCompact`: one single-line section render each, trailing `\n`). +/// `--format compact` bytes for a report tree: one line per section. #[must_use] pub fn render_compact_report(report: &GoValue) -> Vec<u8> { let sections = sections_from_report(report); @@ -176,9 +168,9 @@ pub fn render_compact_report(report: &GoValue) -> Vec<u8> { .into_bytes() } -/// `--format text` bytes for a `renderer.JSONReport` tree (the reference `FormatText`: -/// optional executive summary when ≥2 sections, then a blank line + full render -/// per section). +/// `--format text` bytes for a report tree: an executive summary when there +/// are two or more sections, then a blank line + the full render of each +/// section. #[must_use] pub fn render_text_report(report: &GoValue) -> Vec<u8> { let sections = sections_from_report(report); diff --git a/crates/cf-commands/src/handlers/shotness_run.rs b/crates/cf-commands/src/handlers/shotness_run.rs index 77782e96..032652b5 100644 --- a/crates/cf-commands/src/handlers/shotness_run.rs +++ b/crates/cf-commands/src/handlers/shotness_run.rs @@ -1,119 +1,203 @@ -//! Real `history/shotness` over the general history pipeline. +//! `history/shotness` over the shared history core. //! -//! Port of the reference streaming shotness pipeline (the reference `initHistoryPipeline` / -//! initHeadOnly → Runner → shotness `Analyzer.Consume` -//! (`handleInsertion`/`handleModification`/`handleDeletion`, diff-driven -//! line→node attribution) → aggregator (`accumulateNodes` / -//! `computeCouplingPairs`) → `ticksToReport` (`buildReportFromMerged`) → -//! `ComputeAllMetrics`). +//! Per commit, in the pure parallel pre-pass of the history core: the +//! filtered tree diff against the first parent and, change by change, the +//! gated UAST parse of the changed blobs through the change's [`ChangeParses`] +//! (the shared 256 KiB cap), the entity extraction (structural DSL → name +//! DSL → qualified name) and the line diff — the [`ShotnessChangeProduct`]s, +//! which keep only the extracted entities and the diff, never a tree. Then +//! ONE sequential fold in chronological order ([`ShotnessFold`]) turns +//! the products into each commit's [`CommitEffects`] over the live +//! [`ShotnessState`]: an entity is born on insertion, touched once per commit +//! whose hunks overlap its line span, moved by a file rename and ended when +//! its declaration or file disappears. Every format replays the SAME effect +//! records: the report is the state after the last commit, ndjson / +//! timeseries list the per-commit touches. //! -//! ## Node identity in the streaming pipeline (measured reference-binary wobble) +//! Entity identity is `(type, qualified name, file)`: the name DSL result +//! prefixed by the names of the enclosing declarations +//! (`Outer.Inner.method`) and, for a method declared with a receiver +//! (`func (a *A) String()`), by the receiver type (`A.String`), so same-named +//! methods on different types are different entities. //! -//! The reference analyzer registers extracted nodes in a name-keyed map -//! (`res map[name]node`) and attributes every diff-touched line's spanning -//! node through `reverseNodeMap` (node identity → registered name), i.e. each -//! touched node is attributed to its OWN name (hercules -//! `addNode(reversedNodesBefore[UniqueKey(node)], node, toName)`). The live Go -//! binary's per-commit records confirm this: one commit attributes MULTIPLE -//! distinct names per file per diff side (e.g. hercules 4d3f9500 touches -//! `Function_groupStatus` AND `Function_loc` in `analyser.go`), which is -//! impossible under any single-winner collapse. +//! Merge commits are skipped (their diff against the first parent re-shows +//! the side line that was already walked); under `--first-parent` a merge is +//! an ordinary commit and counts once. //! -//! The reference binary's reverse map keys do collide for a minority of nodes, -//! and Go's randomized map iteration then picks the surviving name at random — -//! measured: repeated Go runs at the same argv differ in a small fraction of -//! change counts / member nodes (content-level nondeterminism, so the compat -//! oracle downgrades these cells to its structural contract). No deterministic -//! port can be byte-identical to that wobble; this port attributes every -//! touched node to its own registered name (the collision-free reference -//! behavior, and Go's measured majority behavior). All accumulation, coupling, -//! metric, and serialization logic is the byte-exact cf-shotness port. -//! -//! Output bytes route through `cf-gojson` (the reference `encoding/json` parity); never -//! `serde_json`. +//! Output bytes route through `cf-gojson`; never `serde_json`. use std::collections::{BTreeMap, BTreeSet}; +use std::sync::Arc; -use cf_analyzers_plumbing::identity_detector::IdentityDetector; use cf_gitlib::changes::ChangeAction; -use cf_pathpolicy::Options as PathPolicyOptions; -use cf_shotness::aggregate::{ - accumulate_nodes, build_report_from_merged, compute_coupling_pairs, merge_nodes_into, TickNodes, +use cf_shotness::{ + compute_all_metrics, coupling_pair_count, touched_entities, CommitEffects, EntityEffect, + NodeSummary, ShotnessState, DEFAULT_SHOTNESS_DSL_NAME, DEFAULT_SHOTNESS_DSL_STRUCT, +}; +use cf_uast_node::dsl::Query; +use cf_uast_node::{ + Node, ROLE_NAME, ROLE_TYPE, UAST_CLASS, UAST_ENUM, UAST_FUNCTION, UAST_IDENTIFIER, + UAST_INTERFACE, UAST_METHOD, UAST_PARAMETER, UAST_STRUCT, }; -use cf_shotness::{compute_all_metrics, NodeSummary}; -use cf_uast_node::Node; - -use crate::handlers::{floor_tick_secs, run_repo_path}; - -/// `UASTChangesAnalyzer` spill threshold: a commit with more than this many file -/// changes streams zero UAST changes (the gated parses themselves, including -/// the 256 KiB blob cap, live in [`crate::handlers::uast_walk::CommitParseCache`]). -const SPILL_THRESHOLD: usize = 32; -/// Default DSL selecting structural nodes. -const DSL_STRUCT: &str = r#"filter(.roles has "Function")"#; -/// Default DSL extracting the node name (the reference `DefaultShotnessDSLName`, -/// `.props.name`): resolved directly from the node's `name` property — see -/// [`extract_nodes`]. -const _DSL_NAME: &str = ".props.name"; - -/// Builds the `run --analyzers history/shotness --format json` bytes over the -/// REAL general history pipeline, or `None` if the repository cannot be -/// opened/walked. -pub fn shotness_run_report(sub: &clap::ArgMatches) -> Option<Vec<u8>> { - let metrics = shotness_run_metrics(sub)?; - Some(cf_gojson::marshal(&metrics.to_go_value())) -} - -/// Computes the `history/shotness` [`ComputedMetrics`] over the REAL general -/// history pipeline (one report value shared by every output format), or `None` -/// if the repository cannot be opened/walked. Each `run --format` encoding is -/// just a serializer over this single value (the reference `ToJSON`/`ToYAML`/binary -/// envelope), routed at the handler layer. + +use crate::handlers::history::{is_merge, CommitStamp}; +use crate::handlers::uast_walk::{uast_results, ChangeParses, ParseOutcome, Side, UastAnalyzer}; +use crate::pipeline::HandlerError; + +/// The structural-selection flag. +pub(crate) const FLAG_DSL_STRUCT: &str = "shotness-dsl-struct"; +/// The name-extraction flag. +pub(crate) const FLAG_DSL_NAME: &str = "shotness-dsl-name"; +/// `--shotness-keep-ended`: ended entities stay in the report, marked. +pub(crate) const FLAG_KEEP_ENDED: &str = "shotness-keep-ended"; + +/// Declaration kinds whose names qualify the entities declared inside them. +const QUALIFYING_TYPES: [&str; 6] = [ + UAST_CLASS, + UAST_STRUCT, + UAST_INTERFACE, + UAST_ENUM, + UAST_FUNCTION, + UAST_METHOD, +]; +/// Separator between a qualifier and the name it qualifies. +const QUALIFIER_SEPARATOR: &str = "."; + +/// The `history/shotness` DSL configuration as the user set it: the structural +/// selection (`--shotness-dsl-struct`) and the name extraction +/// (`--shotness-dsl-name`), both validated at construction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ShotnessDsl { + /// The structural query, run against each parsed file root. + pub struct_query: String, + /// The name query, run against each selected node. + pub name_query: String, +} + +impl Default for ShotnessDsl { + fn default() -> Self { + Self { + struct_query: DEFAULT_SHOTNESS_DSL_STRUCT.to_string(), + name_query: DEFAULT_SHOTNESS_DSL_NAME.to_string(), + } + } +} + +impl ShotnessDsl { + /// Reads and validates the two DSL flags. A flag the user did not set + /// (or set empty) resolves to the crate default. + /// + /// # Errors + /// + /// [`HandlerError::InvalidFlag`] naming the flag whose expression does not + /// compile. + pub(crate) fn from_matches(sub: &clap::ArgMatches) -> Result<Self, HandlerError> { + let dsl = Self { + struct_query: dsl_flag(sub, FLAG_DSL_STRUCT, DEFAULT_SHOTNESS_DSL_STRUCT), + name_query: dsl_flag(sub, FLAG_DSL_NAME, DEFAULT_SHOTNESS_DSL_NAME), + }; + dsl.compile()?; + Ok(dsl) + } + + /// Compiles both queries. + /// + /// # Errors + /// + /// [`HandlerError::InvalidFlag`] naming the flag whose expression does not + /// compile. + pub(crate) fn compile(&self) -> Result<CompiledDsl, HandlerError> { + Ok(CompiledDsl { + structure: compile_flag(FLAG_DSL_STRUCT, &self.struct_query)?, + name: compile_flag(FLAG_DSL_NAME, &self.name_query)?, + }) + } +} + +/// Compiles one flag's expression, naming the flag on failure. +fn compile_flag(flag: &str, query: &str) -> Result<Query, HandlerError> { + Query::compile(query) + .map_err(|e| HandlerError::InvalidFlag(format!("invalid --{flag} {query:?}: {e}"))) +} + +/// The flag's value when the user set it (command line or environment) to a +/// non-empty string, else `default` — the registered flag default is NOT +/// consulted, so the crate default stays the effective one. +fn dsl_flag(sub: &clap::ArgMatches, name: &str, default: &str) -> String { + use clap::parser::ValueSource; + let Ok(Some(value)) = sub.try_get_one::<String>(name) else { + return default.to_string(); + }; + match sub.value_source(name) { + Some(ValueSource::CommandLine | ValueSource::EnvVariable) if !value.is_empty() => { + value.clone() + } + _ => default.to_string(), + } +} + +/// Validates the shotness DSL flags (the preflight leg of this analyzer). +/// +/// # Errors +/// +/// [`HandlerError::InvalidFlag`] naming the flag whose expression does not +/// compile. +pub(crate) fn validate_shotness_dsl(sub: &clap::ArgMatches) -> Result<(), HandlerError> { + ShotnessDsl::from_matches(sub).map(|_| ()) +} + +/// Both DSL queries compiled once per worker commit (a [`Query`] is not +/// `Sync`, so the parallel pre-pass compiles from the [`ShotnessDsl`] strings). +pub(crate) struct CompiledDsl { + structure: Query, + name: Query, +} + +/// Computes the `history/shotness` [`cf_shotness::ComputedMetrics`] over the +/// shared history core (one report value shared by every output format), or +/// `None` if the repository cannot be opened/walked. pub fn shotness_run_metrics(sub: &clap::ArgMatches) -> Option<cf_shotness::ComputedMetrics> { let report = shotness_run_report_data(sub)?; Some(compute_all_metrics(&report)) } -/// The raw `history/shotness` report (the reference `ticksToReport`: key-sorted `Nodes` + -/// index-keyed `Counters`) over the same walk as [`shotness_run_metrics`]. The -/// plot path consumes this directly — the reference implementation's store writer streams exactly these -/// `(NodeSummary, Counter)` records (`WriteToStore` → -/// `extractShotnessData(report)`). +/// The raw `history/shotness` report (key-sorted `Nodes` + index-keyed +/// `Counters`): the live state after replaying every walked commit's effects +/// in walk order. The plot path consumes this directly. pub fn shotness_run_report_data(sub: &clap::ArgMatches) -> Option<cf_shotness::ReportData> { let walk = shotness_walk(sub)?; - - // Per-tick accumulator (reference: aggregator `byTick`). - let mut by_tick: BTreeMap<i64, TickNodes> = BTreeMap::new(); - for c in &walk { - if c.touched.is_empty() { - continue; - } - let acc = by_tick.entry(c.tick).or_default(); - accumulate_nodes(acc, &c.touched); - compute_coupling_pairs(acc, &c.touched); - } - - // ticksToReport: merge every tick's node map, then buildReportFromMerged. - let mut merged: TickNodes = TickNodes::new(); - for td in by_tick.values() { - merge_nodes_into(&mut merged, td); + let keep_ended = sub + .try_get_one::<bool>(FLAG_KEEP_ENDED) + .ok() + .flatten() + .copied() + .unwrap_or(false); + let mut state = if keep_ended { + ShotnessState::keeping_ended() + } else { + ShotnessState::new() + }; + for c in walk.iter() { + state.apply(&c.touched); } - - Some(build_report_from_merged(&merged)) + Some(state.report()) } -/// One walked commit's shotness products (the reference `shotness.Consume` TC + runner -/// stamps). `touched` empty ⇔ nil-Data TC (skipped merge / spill / no nodes). -#[derive(Clone)] +/// One walked commit's shotness record: its [`CommitEffects`] plus the shared +/// identity/tick stamps. `touched` empty ⇔ the commit affected no entity +/// (skipped merge / no parseable change). +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub(crate) struct ShotnessCommit { + /// Position in the ORIGINAL window (oversized-dropped commits count): the + /// key the merged ndjson stream orders every analyzer's records by. + pub pos: usize, /// Full hex hash. pub hash: String, - /// Node key → summary for the nodes this commit touched (reference: - /// `CommitData.NodesTouched` keys + summaries; `CountDelta` is always 1). - pub touched: BTreeMap<String, NodeSummary>, - /// TicksSinceStart tick. + /// What the commit did to each entity, keyed by entity key. + pub touched: CommitEffects, + /// The tick. pub tick: i64, - /// Loose-identity author id (walk order). + /// The author id. pub author_id: i64, /// Committer time, Unix seconds. pub when: i64, @@ -121,123 +205,41 @@ pub(crate) struct ShotnessCommit { pub offset_min: i32, } -/// The shared `history/shotness` revwalk: one entry per walked commit, walk -/// order. Every shotness format consumes THIS one walk. -pub(crate) fn shotness_walk(sub: &clap::ArgMatches) -> Option<Vec<ShotnessCommit>> { - // Multi-analyzer runs route through the ONE shared UAST walk (same code, - // one tree diff + one parse per blob per commit across the co-selected - // analyzers); single-analyzer runs keep this direct walk. Only the - // per-commit PARSES/extracts are shared — the cumulative state machine - // below ([`ShotnessReducer`]) runs sequentially in walk order either way. - if let Some(shared) = crate::handlers::uast_walk::shared_shotness_walk(sub) { - return shared; - } - - let path = run_repo_path(sub); - let repo = cf_gitlib::Repository::open(&path).ok()?; - - let head_only = sub.get_flag("head"); - let limit = sub.get_one::<i64>("limit").copied().unwrap_or(0); - let first_parent = crate::handlers::effective_first_parent(sub); - - let hashes: Vec<cf_gitlib::Hash> = if head_only { - vec![repo.head().ok()?] - } else { - // Window: `limit` NEWEST commits oldest-first. - crate::handlers::load_history_commit_hashes( - &repo, - limit, - first_parent, - crate::handlers::history_since_spec(sub), - )? - }; - - let parser = cf_uast::Parser::new(); - // `opts` drives the UAST parse gate only (the reference always-on - // pathfilter); the TreeDiff-level filter is applied to the changes below. - let opts = PathPolicyOptions::default(); - let change_filter = crate::handlers::history_filter(sub).unwrap_or_default(); - let mut identity = IdentityDetector::new(); - - // Cumulative analyzer state + merge dedup (sequential, walk order). - let mut reducer = ShotnessReducer::default(); - - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - - let mut commits: Vec<ShotnessCommit> = Vec::with_capacity(hashes.len()); - - for hash in &hashes { - let commit = repo.lookup_commit(*hash).ok()?; - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); - - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - let mut entry = ShotnessCommit { - hash: hash.to_string(), - touched: BTreeMap::new(), - tick, - author_id, - when, - offset_min: committer_when.offset_minutes(), - }; - - // shouldConsumeCommit: a merge commit is processed only the first time. - if !reducer.should_consume(*hash, commit.num_parents()) { - commits.push(entry); - continue; - } - - // Tree diff against the first parent (root → full initial tree), then - // the SAME per-commit parse/extract/diff product the shared - // multi-analyzer walk computes, fed to the sequential state machine. - let mut changes = crate::handlers::history::commit_tree_changes(&repo, &commit)?; - // TreeDiffAnalyzer.filterChanges (policy + --languages). - change_filter.retain_changes(&repo, &mut changes); - let mut cache = crate::handlers::uast_walk::CommitParseCache::new(&repo, &parser, &opts); - let products = shotness_commit_product(&changes, &mut cache); - entry.touched = reducer.consume(&products); - - commits.push(entry); +impl ShotnessCommit { + /// Whether the commit touched (counted a change on) any entity. + fn touched_any(&self) -> bool { + self.touched.values().any(|e| e.touched) } +} - Some(commits) +/// The `history/shotness` records of the run's UAST pass: one entry per +/// walked commit, walk order. Every shotness format consumes THIS one walk. +pub(crate) fn shotness_walk(sub: &clap::ArgMatches) -> Option<Arc<Vec<ShotnessCommit>>> { + uast_results(sub, UastAnalyzer::Shotness)?.shotness } -/// One change's state-independent shotness inputs: the parsed/extracted node -/// lists and the file diff — everything `Consume`'s `handle*` calls need that -/// does NOT depend on the cumulative analyzer state. Computed per commit by -/// [`shotness_commit_product`] (in the direct walk AND the shared -/// multi-analyzer walk) and consumed sequentially by [`ShotnessReducer`]. +/// One change's state-independent shotness inputs: the extracted entity lists +/// and the file diff — everything the reduce needs that does NOT depend on +/// the cumulative state. Computed per change by [`shotness_change`] and +/// folded per commit, sequentially, by [`ShotnessFold`] — which needs ALL of +/// a commit's products at once (every file-level delete and rename applies +/// before any entity touch), so a commit's products are kept until its fold. pub(crate) enum ShotnessChangeProduct { - /// A Delete change (`handleDeletion` input). + /// A Delete change: every entity of the file ends. Delete { /// The deleted file path. from_name: String, }, - /// An Insert change whose After side parsed (`handleInsertion` input); - /// a failed parse emits no product, exactly as the direct walk skipped - /// the `handle_insertion` call. + /// An Insert change whose After side parsed: every entity is born. Insert { /// The inserted file path. to_name: String, - /// The extracted structural nodes of the After tree. + /// The extracted entities of the After tree. nodes: Vec<ExtractedNode>, }, - /// A Modify change (`handleModification` input). The rename bookkeeping - /// applies unconditionally; `detail` is present only when BOTH sides - /// parsed AND the FileDiff survived its preconditions. + /// A Modify change. The rename bookkeeping applies unconditionally; + /// `detail` is present only when BOTH sides parsed AND the line diff + /// survived its preconditions. Modify { /// The old file path. from_name: String, @@ -255,140 +257,206 @@ pub(crate) struct ShotnessModifyDetail { diff: FileDiff, } -/// Computes one commit's [`ShotnessChangeProduct`]s — the pure -/// (state-independent) half of the reference `shotness.Consume`: gated UAST -/// parses through the per-commit cache, node extraction, and the FileDiff. -/// The spill rule (> 32 changes ⇒ zero UAST changes, NO products — not even -/// deletions) matches the direct walk's pre-loop skip. -pub(crate) fn shotness_commit_product( - changes: &[cf_gitlib::changes::Change], - cache: &mut crate::handlers::uast_walk::CommitParseCache<'_>, -) -> Vec<ShotnessChangeProduct> { - use crate::handlers::uast_walk::ParseOutcome; - - // Spill rule: > 32 changes ⇒ the analyzer sees zero UAST changes. - if changes.len() > SPILL_THRESHOLD { - return Vec::new(); - } - - let mut products: Vec<ShotnessChangeProduct> = Vec::with_capacity(changes.len()); - for change in changes { - match change.action { - ChangeAction::Delete => products.push(ShotnessChangeProduct::Delete { - from_name: change.from.name.clone(), +/// Computes one change's [`ShotnessChangeProduct`] — the pure +/// (state-independent) half of the consume: the gated UAST parses of the +/// change's sides, entity extraction under `dsl`, and the line diff. `None` +/// for an Insert whose After side does not parse (it contributes nothing). +pub(crate) fn shotness_change( + parses: &ChangeParses<'_>, + dsl: &CompiledDsl, +) -> Option<ShotnessChangeProduct> { + let change = parses.change(); + match change.action { + ChangeAction::Delete => Some(ShotnessChangeProduct::Delete { + from_name: change.from.name.clone(), + }), + ChangeAction::Insert => match parses.parse(Side::After) { + ParseOutcome::Parsed(after) => Some(ShotnessChangeProduct::Insert { + to_name: change.to.name.clone(), + nodes: extract_nodes(after, dsl), }), - ChangeAction::Insert => { - if let ParseOutcome::Parsed(after) = &*cache.parse(&change.to.name, change.to.hash) - { - products.push(ShotnessChangeProduct::Insert { - to_name: change.to.name.clone(), - nodes: extract_nodes(after), - }); - } - } - ChangeAction::Modify => { - let before = cache.parse(&change.from.name, change.from.hash); - let after = cache.parse(&change.to.name, change.to.hash); - let detail = match (&*before, &*after) { - (ParseOutcome::Parsed(before), ParseOutcome::Parsed(after)) => { - file_diff(cache, change).map(|diff| ShotnessModifyDetail { - nodes_before: extract_nodes(before), - nodes_after: extract_nodes(after), - diff, - }) - } - _ => None, - }; - products.push(ShotnessChangeProduct::Modify { - from_name: change.from.name.clone(), - to_name: change.to.name.clone(), - detail, - }); - } + ParseOutcome::Failed | ParseOutcome::Skipped => None, + }, + ChangeAction::Modify => { + let detail = match (parses.parse(Side::Before), parses.parse(Side::After)) { + (ParseOutcome::Parsed(before), ParseOutcome::Parsed(after)) => file_diff(parses) + .map(|diff| ShotnessModifyDetail { + nodes_before: extract_nodes(before, dsl), + nodes_after: extract_nodes(after, dsl), + diff, + }), + _ => None, + }; + Some(ShotnessChangeProduct::Modify { + from_name: change.from.name.clone(), + to_name: change.to.name.clone(), + detail, + }) } } - products } -/// The sequential, cumulative half of the reference `shotness.Consume`: the -/// state machine over commits in walk order. Both the direct walk and the -/// shared multi-analyzer walk drive THIS reducer over identical per-commit -/// products, so the cumulative node/file state — and therefore every report — -/// is byte-identical between the two. -#[derive(Default)] -pub(crate) struct ShotnessReducer { - /// Cumulative analyzer state. +/// The sequential half of the walk: turns each commit's products into its +/// [`CommitEffects`] over the live state (file deletes and renames need the +/// entities currently declared in the file), applies them and records them, +/// in walk order. Merges are skipped (their record has no effects) unless the +/// walk is first-parent simplified. +pub(crate) struct ShotnessFold { + /// The live entity state. state: ShotnessState, - /// Merge dedup tracker (the reference `s.merges.SeenOrAdd` over - /// `NumParents() > 1`). - seen_merges: BTreeSet<cf_gitlib::Hash>, + /// Whether the walk simplifies to the first parent (a merge is then an + /// ordinary commit). + first_parent: bool, + /// One record per folded commit. + commits: Vec<ShotnessCommit>, } -impl ShotnessReducer { - /// shouldConsumeCommit: a merge commit is processed only the first time. - pub(crate) fn should_consume(&mut self, hash: cf_gitlib::Hash, num_parents: usize) -> bool { - num_parents <= 1 || self.seen_merges.insert(hash) +impl ShotnessFold { + /// An empty fold for a walk in the given first-parent mode. + pub(crate) fn new(first_parent: bool) -> Self { + Self { + state: ShotnessState::new(), + first_parent, + commits: Vec::new(), + } + } + + /// Folds one kept commit's per-change products, in window order. + pub(crate) fn consume(&mut self, stamp: &CommitStamp, products: &[ShotnessChangeProduct]) { + let touched = if is_merge(stamp.num_parents, self.first_parent) { + CommitEffects::new() + } else { + self.effects(products) + }; + self.commits.push(ShotnessCommit { + pos: stamp.pos, + hash: stamp.hex.clone(), + touched, + tick: stamp.tick, + author_id: stamp.author_id, + when: stamp.when, + offset_min: stamp.offset_min, + }); } - /// Applies one commit's products to the cumulative state and returns the - /// touched-node map (the reference `buildCommitData`: nil unless a known - /// node was touched). - pub(crate) fn consume( - &mut self, - products: &[ShotnessChangeProduct], - ) -> BTreeMap<String, NodeSummary> { - // allNodes: node keys touched in THIS commit (deduped, the reference `allNodes`). - let mut all_nodes: BTreeSet<String> = BTreeSet::new(); + /// The folded records, walk order. + pub(crate) fn finish(self) -> Vec<ShotnessCommit> { + self.commits + } + /// Builds and applies one commit's effects, returning them. + fn effects(&mut self, products: &[ShotnessChangeProduct]) -> CommitEffects { + let mut effects = CommitEffects::new(); + // File-level events first (a rename onto a deleted path must see the + // delete), then the per-entity touches and ends. for product in products { match product { ShotnessChangeProduct::Delete { from_name } => { - self.state.handle_deletion(from_name); + self.end_file(&mut effects, from_name); + } + ShotnessChangeProduct::Modify { + from_name, to_name, .. + } if from_name != to_name => { + self.move_file(&mut effects, from_name, to_name); } + _ => {} + } + } + for product in products { + match product { ShotnessChangeProduct::Insert { to_name, nodes } => { - // Insertion has no diff, so every extracted node is touched - // under its OWN name (the reference implementation iterates - // `res` (name→node) directly). for n in nodes { - self.state - .add_node(&n.name, &n.type_, to_name, &mut all_nodes); + touch(&mut effects, n.summary(to_name)); } } ShotnessChangeProduct::Modify { - from_name, to_name, - detail, + detail: Some(d), + .. } => { - if from_name != to_name { - self.state.apply_rename(from_name, to_name); - } - if let Some(d) = detail { - self.state.apply_diff_edits( - to_name, - &d.nodes_before, - &d.nodes_after, - &d.diff, - &mut all_nodes, - ); + apply_diff_edits(&mut effects, to_name, d); + for removed in removed_declarations(&d.nodes_before, &d.nodes_after) { + end(&mut effects, removed.summary(to_name)); } } + _ => {} } } + self.state.apply(&effects); + effects + } - // buildCommitData: nil unless a known node was touched. - let mut touched: BTreeMap<String, NodeSummary> = BTreeMap::new(); - for key in &all_nodes { - if let Some(ns) = self.state.nodes.get(key) { - touched.insert(key.clone(), ns.summary.clone()); - } + /// Records the end of every entity declared in `file`. + fn end_file(&self, effects: &mut CommitEffects, file: &str) { + for node in self.state.entities_in(file) { + end(effects, node.summary.clone()); + } + } + + /// Records the move of every entity of `from` under `to`. + fn move_file(&self, effects: &mut CommitEffects, from: &str, to: &str) { + for node in self.state.entities_in(from) { + let moved = NodeSummary::new(&node.summary.type_, &node.summary.name, to); + let key = moved.key(); + let effect = effects.entry(key).or_insert_with(|| EntityEffect { + summary: moved, + touched: false, + ended: false, + moved_from: None, + }); + effect.moved_from = Some(node.summary.key()); + // The destination key's previous occupant (a deleted file's + // entity) is replaced by the move, never ended after it. + effect.ended = false; } - touched } } -/// Per-commit shotness NDJSON records (forked leaf): only commits that touched -/// known nodes emit a line; `data` is the reference implementation's `*CommitData` — one `NodesTouched` -/// map (key-sorted) of `NodeDelta{Summary{Type,Name,File}, CountDelta: 1}`. +/// Records a touch of `summary` in the commit's effects. +fn touch(effects: &mut CommitEffects, summary: NodeSummary) { + effects + .entry(summary.key()) + .or_insert_with(|| EntityEffect { + summary, + touched: false, + ended: false, + moved_from: None, + }) + .touched = true; +} + +/// Records the end of `summary` in the commit's effects. +fn end(effects: &mut CommitEffects, summary: NodeSummary) { + effects + .entry(summary.key()) + .or_insert_with(|| EntityEffect { + summary, + touched: false, + ended: false, + moved_from: None, + }) + .ended = true; +} + +/// The Before-side entities whose `(type, name)` no longer exists on the +/// After side: their declarations were removed in this change. +fn removed_declarations<'a>( + before: &'a [ExtractedNode], + after: &[ExtractedNode], +) -> Vec<&'a ExtractedNode> { + let kept: BTreeSet<(&str, &str)> = after + .iter() + .map(|n| (n.type_.as_str(), n.name.as_str())) + .collect(); + before + .iter() + .filter(|n| !kept.contains(&(n.type_.as_str(), n.name.as_str()))) + .collect() +} + +/// Per-commit shotness NDJSON records: only commits that touched an entity +/// emit a line; `data` is one `NodesTouched` map (key-sorted) of +/// `{Summary{Type,Name,File}, CountDelta: 1}`. pub fn shotness_ndjson_records( sub: &clap::ArgMatches, ) -> Option<Vec<crate::handlers::history_formats::NdjsonRecord>> { @@ -396,12 +464,12 @@ pub fn shotness_ndjson_records( let walk = shotness_walk(sub)?; let mut records = Vec::new(); - for (pos, c) in walk.iter().enumerate() { - if c.touched.is_empty() { + for c in walk.iter() { + if !c.touched_any() { continue; } let mut nodes = GoMap::new_map(); - for (key, ns) in &c.touched { + for (key, ns) in touched_entities(&c.touched) { let mut summary = GoMap::new_struct(); summary.insert("Type".to_string(), GoValue::Str(ns.type_.clone())); summary.insert("Name".to_string(), GoValue::Str(ns.name.clone())); @@ -414,7 +482,7 @@ pub fn shotness_ndjson_records( let mut data = GoMap::new_struct(); data.insert("NodesTouched".to_string(), GoValue::Map(nodes)); records.push(crate::handlers::history_formats::NdjsonRecord { - pos, + pos: c.pos, hash: c.hash.clone(), tick: c.tick, author_id: c.author_id, @@ -426,9 +494,8 @@ pub fn shotness_ndjson_records( Some(records) } -/// The shotness contribution to the merged `--format timeseries` document (reference: -/// `shotness.ExtractCommitTimeSeries` over `report["commit_stats"]`): per -/// node-touching commit `{"nodes_touched": n, "coupling_pairs": n*(n-1)/2}`. +/// The shotness contribution to the merged `--format timeseries` document: +/// per entity-touching commit `{"nodes_touched": n, "coupling_pairs": C(n,2)}`. pub fn shotness_timeseries_contribution( sub: &clap::ArgMatches, ) -> Option<crate::handlers::history_formats::TimeSeriesContribution> { @@ -437,16 +504,17 @@ pub fn shotness_timeseries_contribution( let walk = shotness_walk(sub)?; let mut per_commit = Vec::new(); let mut commit_meta = Vec::new(); - for c in &walk { - if c.touched.is_empty() { + for c in walk.iter() { + let n = touched_entities(&c.touched).count(); + if n == 0 { continue; } - let n = c.touched.len() as i64; - // The reference `computeCouplingPairs`: 0 below minCouplingNodes (2), else C(n,2). - let pairs = if n < 2 { 0 } else { n * (n - 1) / 2 }; let mut entry = GoMap::new_map(); - entry.insert("nodes_touched".to_string(), GoValue::Int(n)); - entry.insert("coupling_pairs".to_string(), GoValue::Int(pairs)); + entry.insert("nodes_touched".to_string(), GoValue::Int(n as i64)); + entry.insert( + "coupling_pairs".to_string(), + GoValue::Int(coupling_pair_count(n)), + ); per_commit.push((c.hash.clone(), GoValue::Map(entry))); commit_meta.push(( c.hash.clone(), @@ -462,9 +530,9 @@ pub fn shotness_timeseries_contribution( }) } -/// One extracted structural node: its resolved name, type, and 1-based inclusive -/// line span, used for registration and diff line→node mapping -/// (the reference `extractNodes` / `genLine2Node` / `resolveEndLine`). +/// One extracted entity: its qualified name, type, and 1-based inclusive line +/// span (for the diff line → entity mapping). +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ExtractedNode { name: String, type_: String, @@ -472,56 +540,150 @@ pub(crate) struct ExtractedNode { end_line: usize, } -/// Extracts structural nodes via the struct DSL and resolves each node's name -/// via the name DSL. Returns one entry per distinct name -/// (last-wins on name collision over the deterministic `FindDSL` slice order), -/// each carrying its own name/type/span — the reference `res map[name]node` -/// registration that `reverseNodeMap` inverts for per-node attribution. -fn extract_nodes(root: &Node) -> Vec<ExtractedNode> { - let Ok(structs) = root.find_dsl(DSL_STRUCT) else { - return Vec::new(); - }; +impl ExtractedNode { + /// The entity's identity in `file`. + fn summary(&self, file: &str) -> NodeSummary { + NodeSummary::new(&self.type_, &self.name, file) + } +} - // name → node (last-wins over the deterministic FindDSL slice order). - let mut named: BTreeMap<String, ExtractedNode> = BTreeMap::new(); - - for struct_node in &structs { - // Reference: `nameNodes, err := structNode.FindDSL(".props.name")`. The - // `.props.<key>` field access yields one literal node holding the value of - // the `name` property (the Rust DSL engine lacks the nested-props - // processor, so resolve it directly — identical semantics). the reference implementation's name - // selection: - // if err==nil && len(nameNodes)>0 { name=nameNodes[0].Token; - // if name!="" { use name } } // empty ⇒ skip - // else if structNode.Token!="" { use structNode.Token } // missing key ⇒ fall back - let name = match struct_node.props.get("name") { - Some(prop) => { - if prop.is_empty() { - continue; // present-but-empty ⇒ no fallback, skip - } - prop.clone() - } - None if !struct_node.token.is_empty() => struct_node.token.clone(), - None => continue, - }; +/// A byte span `[start, end)` of a node with position information. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Span { + start: u64, + end: u64, +} + +impl Span { + fn of(n: &Node) -> Option<Self> { + n.pos.as_ref().map(|p| Self { + start: p.start_offset, + end: p.end_offset, + }) + } + + /// Whether `self` covers `other`. + fn contains(self, other: Self) -> bool { + self.start <= other.start && other.end <= self.end + } +} - let (start_line, end_line) = node_span(struct_node); +/// A named declaration that qualifies what is declared inside it. +struct Container { + span: Span, + type_: String, + qualified: String, +} + +/// Extracts the entities of a parsed file: the structural query selects the +/// nodes, the name query names each one, and the enclosing declarations (plus +/// a receiver type) qualify the name. A node without a resolvable name is not +/// an entity. Two selected nodes resolving to the same `(type, name)` are one +/// entity spanning the last one (deterministic over the query's pre-order). +fn extract_nodes(root: &Node, dsl: &CompiledDsl) -> Vec<ExtractedNode> { + let containers = containers(root, &dsl.name); + let mut named: BTreeMap<(String, String), ExtractedNode> = BTreeMap::new(); + for node in dsl.structure.run(root) { + let Some(own) = own_name(&node, &dsl.name) else { + continue; + }; + let name = match Span::of(&node).and_then(|span| enclosing(&containers, span, &node)) { + Some(prefix) => format!("{prefix}{QUALIFIER_SEPARATOR}{own}"), + None => own, + }; + let (start_line, end_line) = node_span(&node); + let type_ = node.node_type.clone(); named.insert( - name.clone(), + (type_.clone(), name.clone()), ExtractedNode { name, - type_: struct_node.node_type.as_str().to_string(), + type_, start_line, end_line, }, ); } - named.into_values().collect() } -/// Resolves a node's 1-based inclusive line span (the reference `pos.StartLine` / -/// `resolveEndLine`: explicit end line if greater, else the max descendant line). +/// The named qualifying declarations of the tree in pre-order, each with its +/// own qualified name. +fn containers(root: &Node, name_query: &Query) -> Vec<Container> { + let mut out: Vec<Container> = Vec::new(); + let mut open: Vec<usize> = Vec::new(); + root.visit_pre_order(|n| { + let Some(span) = Span::of(n) else { + return; + }; + while open.last().is_some_and(|&i| !out[i].span.contains(span)) { + open.pop(); + } + if !QUALIFYING_TYPES.contains(&n.node_type.as_str()) { + return; + } + let Some(own) = own_name(n, name_query) else { + return; + }; + let qualified = match open.last() { + Some(&i) => format!("{}{QUALIFIER_SEPARATOR}{own}", out[i].qualified), + None => own, + }; + out.push(Container { + span, + type_: n.node_type.clone(), + qualified, + }); + open.push(out.len() - 1); + }); + out +} + +/// The qualified name of the deepest container enclosing `node` (a container +/// with the node's own span and type is the node itself). +fn enclosing(containers: &[Container], span: Span, node: &Node) -> Option<String> { + containers + .iter() + .rev() + .find(|c| c.span.contains(span) && !(c.span == span && c.type_ == node.node_type)) + .map(|c| c.qualified.clone()) +} + +/// A node's own name: the receiver type (when declared with one) and the +/// name query's first non-empty result; `None` when the query yields nothing. +fn own_name(node: &Node, name_query: &Query) -> Option<String> { + let name = name_query + .run(node) + .into_iter() + .map(|n| n.token) + .find(|t| !t.is_empty())?; + Some(match receiver_type(node) { + Some(receiver) => format!("{receiver}{QUALIFIER_SEPARATOR}{name}"), + None => name, + }) +} + +/// The receiver type of a method declared as `func (a *A) M()`: a `Parameter` +/// child preceding the name identifier carries the receiver, and the first +/// type-role identifier inside it is its type. Methods declared inside their +/// type (`class A { m() }`) have no receiver parameter and are qualified by +/// the enclosing declaration instead. +fn receiver_type(node: &Node) -> Option<String> { + let name_index = node + .children + .iter() + .position(|c| c.node_type == UAST_IDENTIFIER && c.roles.iter().any(|r| r == ROLE_NAME))?; + let receiver = node.children[..name_index] + .iter() + .find(|c| c.node_type == UAST_PARAMETER)?; + receiver + .find(|d| d.node_type == UAST_IDENTIFIER && d.roles.iter().any(|r| r == ROLE_TYPE)) + .first() + .map(|d| d.token.clone()) + .filter(|t| !t.is_empty()) +} + +/// Resolves a node's 1-based inclusive line span: the explicit end line when +/// greater than the start, else the maximum descendant line. fn node_span(n: &Node) -> (usize, usize) { let Some(pos) = &n.pos else { return (0, 0) }; let start = pos.start_line as usize; @@ -531,21 +693,17 @@ fn node_span(n: &Node) -> (usize, usize) { let mut end = start; n.visit_pre_order(|child| { if let Some(cp) = &child.pos { - let candidate = if cp.end_line > cp.start_line { - cp.end_line - } else { - cp.start_line - }; - if candidate as usize > end { - end = candidate as usize; + let candidate = cp.end_line.max(cp.start_line) as usize; + if candidate > end { + end = candidate; } } }); (start, end) } -/// A line→spanning-node-indices map; `line2node[l-1]` holds -/// the indices (into the `nodes` slice) of nodes whose span covers line `l`. +/// A line → entity-indices map; `line2node[l-1]` holds the indices (into +/// `nodes`) of the entities whose span covers line `l`. fn gen_line2node(nodes: &[ExtractedNode], lines: usize) -> Vec<Vec<usize>> { let mut res: Vec<Vec<usize>> = vec![Vec::new(); lines]; for (i, n) in nodes.iter().enumerate() { @@ -561,7 +719,7 @@ fn gen_line2node(nodes: &[ExtractedNode], lines: usize) -> Vec<Vec<usize>> { res } -/// Diff operation kind (the reference `diffmatchpatch.Operation` as used by FileDiff). +/// Diff operation kind. #[derive(Clone, Copy, PartialEq, Eq)] enum DiffOp { Delete, @@ -569,32 +727,26 @@ enum DiffOp { Equal, } -/// One diff edit: its op and the number of lines it spans -/// (`utf8.RuneCountInString(edit.Text)` over the line-encoded diff text). +/// One diff edit: its op and the number of lines it spans. struct DiffEdit { op: DiffOp, line_count: usize, } -/// Per-file diff: the rune-encoded edits plus the old/new LOC counts that the reference implementation's -/// FileDiff reports as `len(src)` / `len(dst)`. +/// Per-file line diff plus the old/new line counts. struct FileDiff { old_lines_of_code: usize, new_lines_of_code: usize, edits: Vec<DiffEdit>, } -/// Computes the FileDiff for a Modify change, mirroring the reference implementation's `processChange` -/// (skip same-hash / binary, line-diff via the diffmatchpatch port). Returns -/// `None` when the change is not diffed (reference `FileDiff` only diffs Modify changes -/// and skips binary blobs / missing blobs). Blob reads go through the -/// per-commit cache so the parse path and the diff path fetch each blob once. -fn file_diff( - cache: &mut crate::handlers::uast_walk::CommitParseCache<'_>, - change: &cf_gitlib::changes::Change, -) -> Option<FileDiff> { +/// Computes the line diff of a Modify change (same-hash / zero-hash / binary +/// changes are not diffed). Blob reads go through the change's parse scope so +/// the parse path and the diff path fetch each blob once. +fn file_diff(parses: &ChangeParses<'_>) -> Option<FileDiff> { use cf_godiff::{line_diff, Op}; + let change = parses.change(); if change.action != ChangeAction::Modify { return None; } @@ -602,17 +754,16 @@ fn file_diff( { return None; } - let blob_from = cache.blob(change.from.hash)?; - let blob_to = cache.blob(change.to.hash)?; + let blob_from = parses.blob(Side::Before)?; + let blob_to = parses.blob(Side::After)?; if is_binary(&blob_from.data) || is_binary(&blob_to.data) { return None; } - // The reference implementation decodes via string([]byte) (no whitespace stripping at default config). let from = String::from_utf8_lossy(&blob_from.data).into_owned(); let to = String::from_utf8_lossy(&blob_to.data).into_owned(); - // Identical-string fast path (reference: single DiffEqual of "L"*lineCount). + // Identical-string fast path: one Equal run. if from == to { let lc = count_lines(&from); return Some(FileDiff { @@ -625,7 +776,6 @@ fn file_diff( }); } - // FileDiff's DiffTimeout default is 1000ms (>0) ⇒ timeout_active = true. let segments = line_diff(from.as_bytes(), to.as_bytes(), true); let mut edits = Vec::with_capacity(segments.len()); @@ -658,8 +808,7 @@ fn file_diff( }) } -/// Counts lines the way the reference implementation's identical-string fast path does -/// (`strings.Count(s,"\n")` plus one if non-empty without a trailing newline). +/// Counts lines (`\n` count, plus one for an unterminated last line). fn count_lines(s: &str) -> usize { let mut count = s.bytes().filter(|&b| b == b'\n').count(); if !s.is_empty() && s.as_bytes().last() != Some(&b'\n') { @@ -668,146 +817,282 @@ fn count_lines(s: &str) -> usize { count } -/// The reference implementation's `CachedBlob.IsBinary`: a NUL byte in the first 8000 bytes. +/// Binary sniff: a NUL byte in the first 8000 bytes. fn is_binary(data: &[u8]) -> bool { let head = &data[..data.len().min(8000)]; head.contains(&0) } -/// Cumulative shotness analyzer state. -#[derive(Default)] -struct ShotnessState { - /// key → node hotness. - nodes: BTreeMap<String, StateNode>, - /// file → set of keys belonging to that file. - files: BTreeMap<String, BTreeSet<String>>, +/// Walks the diff edits and records the touched entities: a Delete hunk +/// touches the Before-side entities spanning its lines, an Insert hunk the +/// After-side ones. +fn apply_diff_edits(effects: &mut CommitEffects, file: &str, d: &ShotnessModifyDetail) { + let line2node_before = gen_line2node(&d.nodes_before, d.diff.old_lines_of_code); + let line2node_after = gen_line2node(&d.nodes_after, d.diff.new_lines_of_code); + + let mut line_before: usize = 0; + let mut line_after: usize = 0; + for edit in &d.diff.edits { + let size = edit.line_count; + match edit.op { + DiffOp::Delete => { + record_touched( + effects, + &line2node_before, + &d.nodes_before, + line_before, + size, + file, + ); + line_before += size; + } + DiffOp::Insert => { + record_touched( + effects, + &line2node_after, + &d.nodes_after, + line_after, + size, + file, + ); + line_after += size; + } + DiffOp::Equal => { + line_before += size; + line_after += size; + } + } + } } -/// Per-node cumulative state. -struct StateNode { - summary: NodeSummary, - count: i64, +/// Records the entities a hunk spanning `[start, start+size)` (0-based +/// lines) touches. +fn record_touched( + effects: &mut CommitEffects, + line2node: &[Vec<usize>], + nodes: &[ExtractedNode], + start: usize, + size: usize, + file: &str, +) { + for indices in line2node.iter().skip(start).take(size) { + for &idx in indices { + touch(effects, nodes[idx].summary(file)); + } + } } -impl ShotnessState { - /// Registers or increments a node. - fn add_node(&mut self, name: &str, type_: &str, file: &str, all_nodes: &mut BTreeSet<String>) { - let summary = NodeSummary::new(type_, name, file); - let key = summary.key(); - let exists = all_nodes.contains(&key); - all_nodes.insert(key.clone()); +#[cfg(test)] +mod tests { + use super::*; - let count = self.nodes.get(&key).map_or(0, |n| n.count); + fn parse(name: &str, source: &str) -> Node { + cf_uast::Parser::new() + .parse(name, source.as_bytes()) + .expect("parse") + } - if count == 0 { - self.nodes - .insert(key.clone(), StateNode { summary, count: 1 }); - self.files.entry(file.to_string()).or_default().insert(key); - } else if !exists { - if let Some(n) = self.nodes.get_mut(&key) { - n.count = count + 1; - } - } + /// `(type, qualified name, start line, end line)` of every entity. + fn extracted( + name: &str, + source: &str, + dsl: &ShotnessDsl, + ) -> Vec<(String, String, usize, usize)> { + extract_nodes(&parse(name, source), &dsl.compile().expect("compile")) + .into_iter() + .map(|n| (n.type_, n.name, n.start_line, n.end_line)) + .collect() } - /// Removes all nodes associated with a deleted file. - fn handle_deletion(&mut self, from_name: &str) { - if let Some(keys) = self.files.remove(from_name) { - for key in keys { - self.nodes.remove(&key); - } - } + fn row(type_: &str, name: &str, start: usize, end: usize) -> (String, String, usize, usize) { + (type_.to_string(), name.to_string(), start, end) } - /// Walks the diff edits and records touched nodes (the reference `applyDiffEdits` + - /// `recordTouchedNodes`): a Delete hunk touches the Before-side nodes - /// spanning its lines, an Insert hunk the After-side nodes, and each - /// touched node is attributed to its OWN registered name and type - /// (hercules `addNode(reversedNodes[UniqueKey(node)], node, toName)`). - fn apply_diff_edits( - &mut self, - to_name: &str, - nodes_before: &[ExtractedNode], - nodes_after: &[ExtractedNode], - diff: &FileDiff, - all_nodes: &mut BTreeSet<String>, - ) { - let line2node_before = gen_line2node(nodes_before, diff.old_lines_of_code); - let line2node_after = gen_line2node(nodes_after, diff.new_lines_of_code); - - let mut line_before: usize = 0; - let mut line_after: usize = 0; - - for edit in &diff.edits { - let size = edit.line_count; - match edit.op { - DiffOp::Delete => { - self.record_touched( - &line2node_before, - nodes_before, - line_before, - size, - to_name, - all_nodes, - ); - line_before += size; - } - DiffOp::Insert => { - self.record_touched( - &line2node_after, - nodes_after, - line_after, - size, - to_name, - all_nodes, - ); - line_after += size; - } - DiffOp::Equal => { - line_before += size; - line_after += size; - } - } - } + #[test] + fn the_default_dsl_compiles() { + ShotnessDsl::default() + .compile() + .expect("the default shotness DSL compiles"); } - /// Records nodes touched by a hunk spanning `[start, start+size)`: for each - /// line-spanning node, `addNode` is called with that node's own name and - /// type (per-node attribution, the reference `recordTouchedNodes`). - fn record_touched( - &mut self, - line2node: &[Vec<usize>], - nodes: &[ExtractedNode], - start: usize, - size: usize, - file: &str, - all_nodes: &mut BTreeSet<String>, - ) { - for l in start..start + size { - if l < line2node.len() { - for &idx in &line2node[l] { - let node = &nodes[idx]; - let (name, type_) = (node.name.clone(), node.type_.clone()); - self.add_node(&name, &type_, file, all_nodes); - } - } - } + #[test] + fn go_receivers_qualify_methods() { + let src = "package r\n\ntype A struct{}\ntype B struct{}\n\nfunc (a *A) String() string { return \"a\" }\n\nfunc (B) String() string { return \"b\" }\n\nfunc plain() int { return 1 }\n"; + assert_eq!( + extracted("r.go", src, &ShotnessDsl::default()), + vec![ + row("Function", "plain", 10, 10), + row("Method", "A.String", 6, 6), + row("Method", "B.String", 8, 8), + ] + ); } - /// Updates state when a file is renamed. - fn apply_rename(&mut self, old_name: &str, new_name: &str) { - let Some(old_keys) = self.files.remove(old_name) else { - return; + #[test] + fn enclosing_declarations_qualify_nested_entities() { + let src = "def outer():\n def inner():\n return 1\n return inner\n\nclass K:\n def km(self):\n return 3\n class Inner:\n def im(self):\n return 4\n"; + assert_eq!( + extracted("n.py", src, &ShotnessDsl::default()), + vec![ + row("Function", "K.Inner.im", 10, 11), + row("Function", "K.km", 7, 8), + row("Function", "outer", 1, 4), + row("Function", "outer.inner", 2, 3), + ] + ); + } + + #[test] + fn java_methods_are_qualified_by_their_class() { + let src = "public class A {\n public int f() {\n return 1;\n }\n public int g() {\n return 2;\n }\n}\n"; + assert_eq!( + extracted("A.java", src, &ShotnessDsl::default()), + vec![row("Method", "A.f", 2, 4), row("Method", "A.g", 5, 7)] + ); + let classes = ShotnessDsl { + struct_query: r#"rfilter(.roles has "Class")"#.to_string(), + ..ShotnessDsl::default() + }; + assert_eq!( + extracted("A.java", src, &classes), + vec![row("Class", "A", 1, 8)] + ); + } + + #[test] + fn the_name_dsl_is_honored_and_unnamed_nodes_are_skipped() { + let src = + "package l\n\nfunc top() int {\n\tf := func() int { return 1 }\n\treturn f()\n}\n"; + assert_eq!( + extracted("l.go", src, &ShotnessDsl::default()), + vec![row("Function", "top", 3, 6)] + ); + let by_type = ShotnessDsl { + name_query: ".type".to_string(), + ..ShotnessDsl::default() + }; + assert_eq!( + extracted("l.go", src, &by_type), + vec![row("Function", "Function", 3, 6)] + ); + let lambdas = ShotnessDsl { + struct_query: r#"rfilter(.type == "Lambda")"#.to_string(), + ..ShotnessDsl::default() }; - let mut new_keys: BTreeSet<String> = BTreeSet::new(); - for old_key in old_keys { - if let Some(mut node) = self.nodes.remove(&old_key) { - node.summary.file = new_name.to_string(); - let new_key = node.summary.key(); - self.nodes.insert(new_key.clone(), node); - new_keys.insert(new_key); + assert!(extracted("l.go", src, &lambdas).is_empty()); + } + + #[test] + fn removed_declarations_are_the_before_names_missing_after() { + let before = vec![ + ExtractedNode { + name: "f".into(), + type_: "Function".into(), + start_line: 1, + end_line: 2, + }, + ExtractedNode { + name: "g".into(), + type_: "Function".into(), + start_line: 3, + end_line: 4, + }, + ]; + let after = vec![ExtractedNode { + name: "g".into(), + type_: "Function".into(), + start_line: 1, + end_line: 2, + }]; + let removed: Vec<&str> = removed_declarations(&before, &after) + .into_iter() + .map(|n| n.name.as_str()) + .collect(); + assert_eq!(removed, vec!["f"]); + } + + #[test] + fn hunks_touch_the_entities_spanning_their_lines() { + let nodes = vec![ + ExtractedNode { + name: "f".into(), + type_: "Function".into(), + start_line: 1, + end_line: 3, + }, + ExtractedNode { + name: "g".into(), + type_: "Function".into(), + start_line: 5, + end_line: 7, + }, + ]; + // Lines 1-3 equal, line 4 deleted, lines 5-7 kept, then two lines inserted into g. + let detail = ShotnessModifyDetail { + nodes_before: nodes.clone(), + nodes_after: nodes, + diff: FileDiff { + old_lines_of_code: 7, + new_lines_of_code: 8, + edits: vec![ + DiffEdit { + op: DiffOp::Equal, + line_count: 5, + }, + DiffEdit { + op: DiffOp::Insert, + line_count: 1, + }, + DiffEdit { + op: DiffOp::Equal, + line_count: 2, + }, + ], + }, + }; + let mut effects = CommitEffects::new(); + apply_diff_edits(&mut effects, "m.go", &detail); + let touched: Vec<&String> = touched_entities(&effects).map(|(k, _)| k).collect(); + assert_eq!(touched, vec!["Function_g_m.go"]); + } + + #[test] + fn dsl_flags_default_unless_the_user_sets_them() { + let cmd = crate::flags::build_run_command(); + let defaults = cmd.clone().try_get_matches_from(["run"]).unwrap(); + assert_eq!( + ShotnessDsl::from_matches(&defaults).unwrap(), + ShotnessDsl::default() + ); + let set = cmd + .clone() + .try_get_matches_from([ + "run", + "--shotness-dsl-struct", + r#"rfilter(.roles has "Class")"#, + "--shotness-dsl-name", + ".token", + ]) + .unwrap(); + assert_eq!( + ShotnessDsl::from_matches(&set).unwrap(), + ShotnessDsl { + struct_query: r#"rfilter(.roles has "Class")"#.to_string(), + name_query: ".token".to_string(), } - } - self.files.insert(new_name.to_string(), new_keys); + ); + let empty = cmd + .clone() + .try_get_matches_from(["run", "--shotness-dsl-struct", ""]) + .unwrap(); + assert_eq!( + ShotnessDsl::from_matches(&empty).unwrap(), + ShotnessDsl::default() + ); + let bad = cmd + .try_get_matches_from(["run", "--shotness-dsl-name", "map("]) + .unwrap(); + let err = validate_shotness_dsl(&bad).unwrap_err(); + assert!(err.to_string().contains("--shotness-dsl-name"), "{err}"); } } diff --git a/crates/cf-commands/src/handlers/spill.rs b/crates/cf-commands/src/handlers/spill.rs new file mode 100644 index 00000000..5ac9d36e --- /dev/null +++ b/crates/cf-commands/src/handlers/spill.rs @@ -0,0 +1,163 @@ +//! Values spilled to disk: finished results a run no longer needs in memory +//! until a later consumer reads them back. +//! +//! A spill is an unlinked temporary file in the system temp directory +//! (`TMPDIR`): it has no name, so the space is reclaimed when the process +//! exits however it exits. The encoding is the workspace's internal binary +//! codec (`cf_persist::GobCodec`, bincode), which keeps every float's bits. + +use std::fs::File; +use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write}; +use std::marker::PhantomData; +use std::sync::{Mutex, PoisonError}; + +use cf_persist::{Codec, GobCodec}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// Why a value could not be spilled or read back. +#[derive(Debug, thiserror::Error)] +#[error("{op}: {cause}")] +pub(crate) struct SpillError { + /// What was being done. + op: &'static str, + /// The underlying failure. + cause: String, +} + +impl SpillError { + fn new(op: &'static str, cause: impl std::fmt::Display) -> Self { + Self { + op, + cause: cause.to_string(), + } + } +} + +/// A value of type `T` held on disk. +pub(crate) struct Spilled<T> { + /// The unlinked file; reads seek it, so one reader at a time. + file: Mutex<File>, + /// Encoded size. + bytes: u64, + value: PhantomData<fn() -> T>, +} + +impl<T: Serialize + DeserializeOwned + Default> Spilled<T> { + /// Encodes `value` into a new unlinked temporary file. + /// + /// # Errors + /// + /// A [`SpillError`] when the file cannot be created or written (for + /// example a full disk). + pub(crate) fn write(value: &T) -> Result<Self, SpillError> { + let mut file = + tempfile::tempfile().map_err(|e| SpillError::new("create a spill file", e))?; + let mut writer = BufWriter::new(&mut file); + GobCodec::new() + .encode(&mut writer, value) + .map_err(|e| SpillError::new("write a spill file", e))?; + writer + .flush() + .map_err(|e| SpillError::new("write a spill file", e))?; + drop(writer); + let bytes = file + .stream_position() + .map_err(|e| SpillError::new("size a spill file", e))?; + Ok(Self { + file: Mutex::new(file), + bytes, + value: PhantomData, + }) + } + + /// Decodes the whole value again. + /// + /// # Errors + /// + /// A [`SpillError`] when the file cannot be read or decoded. + pub(crate) fn read(&self) -> Result<T, SpillError> { + let mut file = self.file.lock().unwrap_or_else(PoisonError::into_inner); + file.seek(SeekFrom::Start(0)) + .map_err(|e| SpillError::new("rewind a spill file", e))?; + let mut value = T::default(); + GobCodec::new() + .decode(BufReader::new(&mut *file), &mut value) + .map_err(|e| SpillError::new("read a spill file", e))?; + Ok(value) + } + + /// The encoded size, in bytes. + pub(crate) const fn bytes(&self) -> u64 { + self.bytes + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use serde::{Deserialize, Serialize}; + + use super::*; + + #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] + struct Sample { + name: String, + values: Vec<f64>, + nested: BTreeMap<String, Vec<i64>>, + } + + /// Floats whose bits must survive the round trip. + const EDGE_FLOATS: [f64; 6] = [0.0, -0.0, 1e-300, 5e-324, f64::MAX, 0.1]; + + fn sample() -> Sample { + let mut nested = BTreeMap::new(); + nested.insert("b".to_owned(), vec![-3, 7]); + nested.insert("a".to_owned(), Vec::new()); + Sample { + name: "kubernetes".to_owned(), + values: EDGE_FLOATS.to_vec(), + nested, + } + } + + /// A spilled value reads back unchanged, every float to the bit. + #[test] + fn a_spilled_value_reads_back_unchanged() { + let spilled = Spilled::write(&sample()).expect("spill"); + let back = spilled.read().expect("read"); + assert_eq!(back, sample()); + let bits = |v: &[f64]| v.iter().map(|f| f.to_bits()).collect::<Vec<_>>(); + assert_eq!(bits(&back.values), bits(&EDGE_FLOATS)); + } + + /// Every read returns the whole value again. + #[test] + fn a_spilled_value_reads_back_any_number_of_times() { + let spilled = Spilled::write(&sample()).expect("spill"); + for _ in 0..3 { + assert_eq!(spilled.read().expect("read"), sample()); + } + } + + /// Readers on several threads each get the whole value. + #[test] + fn concurrent_reads_each_get_the_whole_value() { + let spilled = Spilled::write(&sample()).expect("spill"); + std::thread::scope(|s| { + for _ in 0..4 { + s.spawn(|| assert_eq!(spilled.read().expect("read"), sample())); + } + }); + } + + /// The spill reports how many bytes it wrote. + #[test] + fn the_spill_reports_its_size() { + let spilled = Spilled::write(&sample()).expect("spill"); + assert!(spilled.bytes() > 0); + let empty = Spilled::write(&Vec::<u8>::new()).expect("spill"); + assert!(spilled.bytes() > empty.bytes()); + } +} diff --git a/crates/cf-commands/src/handlers/static_clones.rs b/crates/cf-commands/src/handlers/static_clones.rs index 70d645d1..88426304 100644 --- a/crates/cf-commands/src/handlers/static_clones.rs +++ b/crates/cf-commands/src/handlers/static_clones.rs @@ -1,67 +1,33 @@ -//! Static-analysis report path for the UAST `static/clones` analyzer. +//! Static-analysis report path for the UAST `static/clones` analyzer: +//! `codefang run --analyzers static/clones --format {json,yaml,bin,compact,text}`. //! -//! Reproduces the reference static folder pipeline for -//! `codefang run --analyzers static/clones --format {json,yaml,bin}`: +//! 1. The shared static walk ([`super::walk_source_files`]) admits every +//! UAST-supported file that passes the run filter (`--languages`, the +//! vendor / generated path policy, the generated-content marker, the +//! read cap); each admitted file is parsed. +//! 2. Per file the [`cf_clones::Visitor`] scans the UAST for function-like +//! nodes (functions, methods, closures) and builds each candidate's +//! profile + `MinHash` signature. +//! 3. The [`cf_clones::Aggregator`] qualifies every candidate as +//! `relative/path.go::name`, builds ONE global LSH index, verifies every +//! candidate pair exactly and emits the [`cf_clones::CloneReport`]. +//! 4. That one report drives every format through the crate's own builders: +//! `json` / `text` ← [`cf_clones::report_section::json_value`], +//! `compact` ← [`cf_clones::report_section::compact`], +//! `yaml` / `bin` ← [`cf_clones::CloneReport::to_metrics_value`], and the +//! plot/`report.json` raw value ← [`cf_clones::CloneReport::to_raw_report`]. //! -//! 1. `StaticService.uastPhase` walks `rootPath` with `filepath.WalkDir` in -//! lexical order (directories recursed except `.git`); every regular file -//! that is UAST-supported (`parser.IsSupported`), matches `--languages` -//! (none here -> all), and is not excluded by `pathpolicy.Exclude` is -//! streamed for analysis. -//! 2. clones is a `VisitorProvider`, so per file the framework runs the -//! single-pass [`cf_clones::Visitor`] (full pre-order traversal): it counts -//! function nodes and exports each surviving function's MinHash signature in -//! the `_func_signatures` report key. The framework then stamps every -//! signature item with `_source_file` (the repo-relative path). -//! 3. The clones [`cf_clones::Aggregator`] folds every per-file report: it sums -//! `total_functions`, qualifies each function name as `sourceFile::name`, -//! builds ONE global LSH index over all signatures, finds cross-file clone -//! pairs (`findClonePairs`, dedup by canonical name key, cap stored pairs at -//! `DefaultMaxClonePairs = 1000` while keeping the exact total count), and -//! emits the aggregate report value. -//! 4. That single report value drives every format: -//! * `json` -> the report becomes ONE `clones.ReportSection` -//! (`renderer.SectionsToJSON`): `overall_score_label` / `sections` -//! (title, score_label, status, metrics, distribution, issues, score) / -//! `overall_score`. Issues are the stored clone pairs sorted by -//! similarity descending; the issue list order is nondeterministic in the reference binary -//! (the stored pair multiset is stable, only the tie order varies), -//! which the differential oracle canonicalizes. -//! * `yaml` / `bin` -> `FormatPerAnalyzer` -> `computeMetricsFromReport` -//! (`ComputedMetrics`: total_functions, total_clone_pairs, clone_ratio, -//! clone_type_distribution, clone_pairs (the stored <=1000), message), -//! marshaled through cf-goyaml / the CFB1 binary envelope. -//! -//! The analyzer MATH (signatures, LSH, pair finding, classification, ratio, -//! section/metrics projection) lives entirely in the cf-clones crate; this -//! module owns only the pipeline-tier folder walk + the serializer routing, -//! exactly as the reference `internal/framework` + `internal/analyzers/analyze` do. - -use std::fs; -use std::path::Path; +//! The analyzer math lives entirely in `cf-clones`; this module owns only the +//! per-file parse and the serializer routing. -use super::StaticFilter; -use cf_analyze::Report; -use cf_clones::aggregator::Aggregator; -use cf_clones::report_section::report_section_json_value; -use cf_clones::Visitor; -use cf_gojson::Encoder; +use super::{SkipReason, StaticFilter}; +use cf_clones::{Aggregator, CloneReport, Visitor}; +use cf_gojson::{Encoder, GoValue}; use cf_uast::Parser; -use cf_uast_node::Node as UastNode; /// Builds the `static/clones --format json` report bytes for `root_path` -/// (`renderer.SectionsToJSON` of the single aggregated clones section), or -/// `None` when the path cannot be read. -#[must_use] -pub fn clones_report_json(root_path: &str) -> Option<Vec<u8>> { - clones_report_json_flags(root_path, &StaticFilter::default(), false) -} - -/// [`clones_report_json`] with the `--per-file` flag applied. The clones -/// aggregator retains NO per-file snapshots (it is not `PerFileModeEnabled` in -/// the reference implementation), but `EnrichWithPerFileData` still initializes -/// EVERY section with an EMPTY `files` array under `--per-file` — so the flag -/// adds `files: []` to the clones section, exactly as the reference emits. +/// (two-space indent, trailing newline), or `None` when the path cannot be +/// read. `per_file` adds one `files` entry per analyzed file. #[must_use] pub fn clones_report_json_flags( root_path: &str, @@ -69,8 +35,6 @@ pub fn clones_report_json_flags( per_file: bool, ) -> Option<Vec<u8>> { let value = clones_report_value_flags(root_path, filter, per_file)?; - // Reference: json.NewEncoder(w).SetIndent("", " ").Encode(report) -> two-space - // indent + one trailing newline. let bytes = Encoder::indented(" ") .with_trailing_newline(true) .encode_to_vec(&value); @@ -78,233 +42,167 @@ pub fn clones_report_json_flags( } /// Builds the `static/clones` `renderer.JSONReport` GoValue (single scored -/// section), shared by the single-analyzer byte path and the multi-analyzer -/// static-JSON merge. `None` when the path cannot be walked. -#[must_use] -pub fn clones_report_value(root_path: &str) -> Option<cf_gojson::GoValue> { - clones_report_value_flags(root_path, &StaticFilter::default(), false) -} - -/// [`clones_report_value`] with the `--per-file` section enrichment (an empty -/// `files` array — see [`clones_report_json_flags`]). +/// section), shared by the single-analyzer JSON path, the `text` render and +/// the multi-analyzer static-JSON merge. With `per_file` the section carries +/// one entry per analyzed file: the file's function count, how many take +/// part in a clone pair, and the stored pairs that involve the file. `None` +/// when the path cannot be walked. #[must_use] pub fn clones_report_value_flags( root_path: &str, filter: &StaticFilter, per_file: bool, -) -> Option<cf_gojson::GoValue> { +) -> Option<GoValue> { let report = aggregate_report_opts(root_path, filter)?; - let value = report_section_json_value(&report); - if per_file { - Some(super::ensure_sections_files_key(value)) - } else { - Some(value) - } + Some(cf_clones::report_section::json_value(&report, per_file)) } /// Builds the `static/clones --format yaml` report bytes for `root_path` -/// (`FormatPerAnalyzer(YAML)` -> `yaml.Marshal(computeMetricsFromReport)`), or -/// `None` when the path cannot be read. +/// (the computed-metrics struct: `total_functions, total_clone_pairs, +/// clone_ratio, clone_type_distribution, clone_pairs, message`), or `None` +/// when the path cannot be read. #[must_use] pub fn clones_report_yaml(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { let report = aggregate_report_opts(root_path, filter)?; - let metrics = cf_clones::Analyzer::new().computed_metrics(&report); - Some(cf_goyaml::marshal(&metrics.to_go_value())) + Some(cf_goyaml::marshal(&report.to_metrics_value())) } -/// Builds the `static/clones --format bin` report bytes for `root_path` -/// (`FormatPerAnalyzer(Binary)` -> CFB1 envelope of `computeMetricsFromReport`), -/// or `None` when the path cannot be read. +/// Builds the `static/clones --format bin` report bytes for `root_path` (the +/// CFB1 envelope of the same computed-metrics struct the YAML format +/// emits), or `None` when the path cannot be read. #[must_use] pub fn clones_report_bin(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { let report = aggregate_report_opts(root_path, filter)?; - let metrics = cf_clones::Analyzer::new().computed_metrics(&report); - cf_reportutil::encode_binary_envelope(&metrics.to_go_value()).ok() + cf_reportutil::encode_binary_envelope(&report.to_metrics_value()).ok() } /// Builds the `static/clones --format compact` report bytes for `root_path` -/// (the reference `StaticService.FormatCompact` -> `DefaultStaticRenderer.RenderCompact`: -/// one single-line section render), or `None` when the path cannot be read. -/// This is the only fully deterministic in the reference binary terminal format for clones — it shows -/// only the title/score-bar/message, never the order-nondeterministic pair list. +/// (one single-line section render), or `None` when the path cannot be read. #[must_use] pub fn clones_report_compact(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { let report = aggregate_report_opts(root_path, filter)?; - Some(cf_clones::report_section::report_section_compact(&report)) + Some(cf_clones::report_section::compact(&report)) } -/// Builds the AGGREGATED RAW `analyze.Report` GoValue for `static/clones` — -/// the value the reference `clones.Aggregator.GetResult()` returns after the -/// folder walk, which is what `--format plot` consumes and what -/// `writeReportJSON` serializes into `report.json`. `filter` carries the run’s -/// path-policy flags. The stored `clone_pairs` ORDER is nondeterministic in the reference binary -/// (LSH candidate iteration); the differential oracle measures and -/// canonicalizes it — the pair multiset and every scalar are deterministic. +/// Builds the aggregated RAW `analyze.Report` GoValue for `static/clones` — +/// what `--format plot` consumes and what `writeReportJSON` serializes into +/// `report.json`. `filter` carries the run's path-policy flags. #[must_use] -pub fn clones_raw_report_value( - root_path: &str, - filter: &StaticFilter, -) -> Option<cf_gojson::GoValue> { - Some(cf_gojson::GoValue::Map(aggregate_report_opts( - root_path, filter, - )?)) +pub fn clones_raw_report_value(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { + let report = aggregate_report_opts(root_path, filter)?; + Some(GoValue::Map(report.to_raw_report())) } -/// Walks `root_path`, runs the clones visitor per file, and folds the per-file -/// signature reports into the cross-file aggregate report value. Returns `None` -/// when the root path does not exist (reference: would surface a walk error). -fn aggregate_report_opts(root_path: &str, filter: &StaticFilter) -> Option<Report> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - +/// Walks `root_path` through the shared static walk, scans every parsed +/// file, and folds the candidates into the cross-file [`CloneReport`] under +/// their root-relative paths. Returns `None` when the root path does not +/// exist; a file that fails to parse is counted as skipped. +fn aggregate_report_opts(root_path: &str, filter: &StaticFilter) -> Option<CloneReport> { let parser = Parser::new(); let mut agg = Aggregator::new(); - - walk(root, root_path, &parser, filter, &mut agg); - - Some(agg.get_result()) -} - -/// Recursively walks `dir` in lexical order, mirroring `filepath.WalkDir`: -/// directories are recursed (except `.git`), files are filtered through parser -/// support + path policy, parsed, run through the clones visitor, and folded -/// into `agg`. -fn walk(dir: &Path, root_path: &str, parser: &Parser, filter: &StaticFilter, agg: &mut Aggregator) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file(dir, root_path, parser, filter, agg); - return; - } - let Ok(read) = fs::read_dir(dir) else { - return; - }; - - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let Ok(file_type) = entry.file_type() else { + for file in super::walk_source_files(root_path, &parser, filter).ok()? { + let Ok(uast_root) = parser.parse(&file.rel_path, &file.bytes) else { + super::note_skipped(&file.path, SkipReason::Unparseable); continue; }; - - if file_type.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; - } - walk(&path, root_path, parser, filter, agg); - continue; - } - - visit_file(&path, root_path, parser, filter, agg); - } -} - -/// Analyzes one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): parser support + path policy filters, parse, -/// clones visitor, and fold into the aggregate. -fn visit_file( - path: &Path, - root_path: &str, - parser: &Parser, - filter: &StaticFilter, - agg: &mut Aggregator, -) { - let path_str = path.to_string_lossy(); - - // ShouldSkipFolderNode: must be UAST-supported. - if !parser.is_supported(&path_str) { - return; - } - // matchesLanguageGlobs, then pathpolicy.Exclude (StaticFilter::skips). - if filter.skips(&path_str) { - return; - } - - let Some(content) = super::read_source_capped(path) else { - return; - }; - let Ok(uast_root) = parser.parse(&path_str, &content) else { - super::note_skipped_file(); - return; - }; - - // Single-pass clones visitor: count functions + export signatures. - let mut visitor = Visitor::new(); - uast_root.visit_pre_order(&mut |n: &UastNode| visitor.on_enter(n)); - - // _source_file stamp = path relative to the analyzed root, then fold the - // stamped per-file signature report into the aggregate. - let source = make_relative_path(&path_str, root_path); - let report = visitor.get_report_with_source(&source); - agg.aggregate(&[(cf_clones::ANALYZER_NAME.to_string(), report)]); -} - -/// The reference `MakeRelativePath`: `filepath.Rel(rootPath, filePath)`. -fn make_relative_path(file_path: &str, root_path: &str) -> String { - if root_path.is_empty() { - return file_path.to_string(); - } - let root = Path::new(root_path); - let file = Path::new(file_path); - match file.strip_prefix(root) { - // filepath.Rel(root, root) == "." (a FILE root stamps "."). - Ok(rel) if rel.as_os_str().is_empty() => ".".to_string(), - Ok(rel) => rel.to_string_lossy().into_owned(), - Err(_) => file_path.to_string(), + agg.add_file(&file.rel_path, Visitor::new().scan(&uast_root)); } + Some(agg.finish()) } #[cfg(test)] mod per_file_tests { use super::*; + const PROCESS_ITEMS: &str = r#"package main + +import "fmt" + +func ProcessItems(items []int) int { + total := 0 + for i, v := range items { + if v%2 == 0 { + total += v * i + } else { + total -= v + } + } + fmt.Println("total", total) + return total +} +"#; + fn fixture() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("a.go"), PROCESS_ITEMS).unwrap(); + std::fs::write(dir.path().join("b.go"), PROCESS_ITEMS).unwrap(); std::fs::write( - dir.path().join("a.go"), + dir.path().join("c.go"), "package main\n\nfunc add(a, b int) int {\n\treturn a + b\n}\n", ) .unwrap(); dir } + fn json(dir: &tempfile::TempDir, per_file: bool) -> String { + let bytes = clones_report_json_flags( + dir.path().to_str().unwrap(), + &StaticFilter::default(), + per_file, + ) + .unwrap(); + String::from_utf8(bytes).unwrap() + } + #[test] - fn per_file_flag_emits_empty_files_array() { + fn per_file_flag_lists_each_files_cloned_functions() { let dir = fixture(); - let bytes = - clones_report_json_flags(dir.path().to_str().unwrap(), &StaticFilter::default(), true) - .unwrap(); - let json = String::from_utf8(bytes).unwrap(); - // Clones retains no per-file data, but the enrichment still initializes - // the section with an EMPTY files array (present, not omitted). + let json = json(&dir, true); + assert!( + json.contains("\"file_path\": \"a.go\""), + "a.go entry missing:\n{json}" + ); assert!( - json.contains("\"files\": []"), - "empty files array missing:\n{json}" + json.contains("\"status\": \"1 of 1 functions in clone pairs\""), + "cloned-function status missing:\n{json}" + ); + assert!( + json.contains("\"file_path\": \"c.go\"") + && json.contains("\"status\": \"No functions in clone pairs\""), + "uncloned file entry missing:\n{json}" ); } #[test] fn no_per_file_flag_omits_files() { let dir = fixture(); - let bytes = clones_report_json_flags( - dir.path().to_str().unwrap(), - &StaticFilter::default(), - false, - ) - .unwrap(); - let json = String::from_utf8(bytes).unwrap(); + let json = json(&dir, false); assert!( !json.contains("\"files\""), "files key must be omitted:\n{json}" ); + assert!(json.contains("\"location\": \"Type-1\""), "{json}"); + } + + #[test] + fn file_root_is_analyzed_as_a_single_file() { + let dir = fixture(); + let file = dir.path().join("a.go"); + let bytes = clones_report_yaml(file.to_str().unwrap(), &StaticFilter::default()).unwrap(); + let yaml = String::from_utf8(bytes).unwrap(); + assert!(yaml.contains("total_functions: 1\n"), "{yaml}"); + // The file root stamps the file's NAME on its per-file entry. + let json = clones_report_json_flags(file.to_str().unwrap(), &StaticFilter::default(), true) + .unwrap(); + let json = String::from_utf8(json).unwrap(); + assert!(json.contains("\"file_path\": \"a.go\""), "{json}"); + } + + #[test] + fn missing_root_yields_none() { + assert!( + clones_report_compact("/nonexistent/codefang-clones", &StaticFilter::default()) + .is_none() + ); } } diff --git a/crates/cf-commands/src/handlers/static_cohesion.rs b/crates/cf-commands/src/handlers/static_cohesion.rs index 10f374a5..ee67e6b1 100644 --- a/crates/cf-commands/src/handlers/static_cohesion.rs +++ b/crates/cf-commands/src/handlers/static_cohesion.rs @@ -1,393 +1,59 @@ -//! Static-analysis report paths for the UAST `static/cohesion` analyzer. +//! Report paths for the `static/cohesion` analyzer. //! -//! Reproduces the reference static pipeline for the single-analyzer -//! `codefang run --analyzers static/cohesion --format {json,yaml,bin}` captures. +//! Pipeline (shared with the other static analyzers): //! -//! Pipeline (mirrors `complexity` and the reference `StaticService.uastPhase`): -//! -//! 1. `filepath.WalkDir` in lexical order — directories recursed (except `.git`), -//! every regular file that is UAST-supported, matches `--languages` (none → -//! all), and is not `pathpolicy.Exclude`d is parsed. -//! 2. Cohesion is a `VisitorProvider`, so the static factory runs it through its -//! `cohesion.Visitor` over a preorder DFS (`MultiAnalyzerTraverser`). This is -//! reproduced by [`cf_cohesion::Analyzer::analyze_visitor`], NOT the -//! `findFunctions` path. Each parsed file yields one per-file `analyze.Report` -//! (an empty-result report when the file has no functions). -//! 3. `StampSourceFile` / `StampLanguage` stamp `_source_file` (path relative to -//! the analyzed root), `_directory` (`filepath.Dir` of it), and `_language` -//! onto the report and every collection item. -//! 4. The cohesion `Aggregator` (`common.Aggregator`) then: -//! * SUMS the count key `total_functions`, -//! * AVERAGES the numeric keys `lcom` / `cohesion_score` / `function_cohesion` -//! over `reportCount` (= number of parsed files, including empty-result ones), -//! * collects the `functions` items deduplicated by the composite key -//! (`_source_file`, `name`) — last write wins — then sorted by `name` -//! (`GetSortedData` sorts on the last identifier key), -//! * builds the `message` from the first numeric average (reference map-iteration -//! order; canonicalized by the harness when it varies). -//! 5. JSON goes through the renderer section (`FormatJSON` → `SectionsToJSON`); -//! YAML/bin go through `FormatPerAnalyzer` → `cohesion.FormatReportYAML` / -//! `FormatReportBinary`, which marshal `ComputedMetrics`. -//! -//! The per-function `functions` table order is nondeterministic in the reference implementation (map -//! dedup + unstable `sort.Slice` on `name`), and the issues list is re-sorted by -//! the FormatFloat string of cohesion (again unstable). The compat harness -//! measures this variance and compares those lists as sorted multisets, so only -//! the report VALUE (the set of functions, their cohesion, the scalars) must -//! match — which is what this handler reproduces. - -use std::collections::BTreeMap; -use std::fs; -use std::path::Path; - -use super::StaticFilter; -use cf_cohesion::report_value::{Report, ReportValue}; -use cf_cohesion::Analyzer; +//! 1. The shared static walk ([`super::walk_source_files`]) admits every +//! UAST-supported file that passes the run filter (`--languages`, path +//! policy, generated-content marker, read cap); each admitted file is +//! parsed. Unreadable, oversized and unparseable files are skipped and +//! counted for the end-of-run `progress: skipped N ...` diagnostic. +//! 2. Each parsed file yields one [`cf_cohesion::FileReport`] via +//! [`cf_cohesion::Analyzer::analyze_file`]. +//! 3. [`cf_cohesion::aggregate`] folds the files: every function is kept, +//! scalars are function-weighted, files without functions carry no data. +//! 4. `json` / `text` / `compact` render the section tree built here (one +//! scored section: metrics, distribution, issues, optional `--per-file` +//! entries); `yaml` / `bin` marshal [`cf_cohesion::ComputedMetrics`]; +//! `plot` consumes the raw aggregated report. + +use super::{SkipReason, StaticFilter}; +use cf_cohesion::aggregator::NO_DATA_MESSAGE; +use cf_cohesion::report_section::{self, Bucket, Issue, SECTION_TITLE}; +use cf_cohesion::{aggregate, Aggregate, AnalyzedFile, Analyzer, ComputedMetrics}; use cf_gojson::{Encoder, GoMap, GoValue, MapOrigin}; use cf_uast::Parser; -// --- Section rendering constants --- - -const SECTION_TITLE: &str = "COHESION"; - const METRIC_TOTAL_FUNCTIONS: &str = "Total Functions"; const METRIC_LCOM: &str = "LCOM Score"; const METRIC_COHESION_SCORE: &str = "Cohesion Score"; const METRIC_FUNCTION_COHESION: &str = "Avg Cohesion"; -const DIST_EXCELLENT_MIN: f64 = 0.6; -const DIST_GOOD_MIN: f64 = 0.4; -const DIST_FAIR_MIN: f64 = 0.3; -const DIST_LABEL_EXCELLENT: &str = "Excellent (>0.6)"; -const DIST_LABEL_GOOD: &str = "Good (0.4-0.6)"; -const DIST_LABEL_FAIR: &str = "Fair (0.3-0.4)"; -const DIST_LABEL_POOR: &str = "Poor (<0.3)"; - -const ISSUE_SEVERITY_FAIR_MAX: f64 = 0.4; -const ISSUE_SEVERITY_POOR_MAX: f64 = 0.3; - -const SEVERITY_GOOD: &str = "good"; -const SEVERITY_FAIR: &str = "fair"; -const SEVERITY_POOR: &str = "poor"; - -const DEFAULT_STATUS_MESSAGE: &str = "No cohesion data available"; - -// Aggregator message thresholds (reference cohesionMessageLabeler). -const MSG_SCORE_HIGH: f64 = 0.7; -const MSG_SCORE_MEDIUM: f64 = 0.4; -const MSG_SCORE_LOW: f64 = 0.3; -const MSG_EXCELLENT: &str = "Excellent overall cohesion across all analyzed code"; -const MSG_GOOD: &str = "Good overall cohesion with room for improvement"; -const MSG_FAIR: &str = "Fair overall cohesion - consider refactoring some functions"; -const MSG_POOR: &str = "Poor overall cohesion - significant refactoring recommended"; - -/// One collected per-function item, carrying the full the reference implementation -/// `convertCohesionFunctionItems` map the raw aggregated -/// report serializes, plus the stamped `_source_file`. -#[derive(Clone)] -struct FnItem { - name: String, - source_file: String, - cohesion: f64, - line_count: i64, - variable_count: i64, - cohesion_assessment: String, - variable_assessment: String, - size_assessment: String, -} - -/// The cross-file aggregated state. -#[derive(Default)] -struct Aggregated { - report_count: i64, - total_functions: i64, - lcom_sum: f64, - cohesion_score_sum: f64, - function_cohesion_sum: f64, - /// Functions keyed by the composite `(_source_file, name)` dedup key; last - /// write wins (reference: buffer overwrite). - functions: BTreeMap<String, FnItem>, - /// Per-file snapshots (the reference `PerFileRetainer` clones of each - /// visitor report) consumed by the `--per-file` section enrichment; walk - /// order. - per_file: Vec<PerFileCohesion>, -} - -/// One retained per-file cohesion report: the visitor result's scalars, -/// per-file `message`, and this file's `(name, cohesion)` function list. -struct PerFileCohesion { - /// Path relative to the analyzed root (the `StampSourceFile` stamp). - rel: String, - total_functions: i64, - lcom: f64, - cohesion_score: f64, - function_cohesion: f64, - message: String, - functions: Vec<(String, f64)>, -} - -impl Aggregated { - fn lcom_avg(&self) -> f64 { - if self.report_count > 0 { - self.lcom_sum / self.report_count as f64 - } else { - 0.0 - } - } - fn cohesion_score_avg(&self) -> f64 { - if self.report_count > 0 { - self.cohesion_score_sum / self.report_count as f64 - } else { - 0.0 - } - } - fn function_cohesion_avg(&self) -> f64 { - if self.report_count > 0 { - self.function_cohesion_sum / self.report_count as f64 - } else { - 0.0 - } - } -} - -/// Walks `root_path`, runs the cohesion visitor per file, and aggregates, or -/// `None` when the path cannot be read. -fn aggregate_opts(root_path: &str, filter: &StaticFilter) -> Option<Aggregated> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } +/// Walks `root_path` through the shared static walk and analyzes every +/// admitted file (a parse failure is skipped and counted), or `None` when +/// the path does not exist. +fn analyze_tree(root_path: &str, filter: &StaticFilter) -> Option<Vec<AnalyzedFile>> { let parser = Parser::new(); let analyzer = Analyzer::new(); - let mut agg = Aggregated::default(); - walk(root, root_path, &parser, filter, &analyzer, &mut agg); - Some(agg) -} - -#[allow(clippy::too_many_arguments)] -fn walk( - dir: &Path, - root_path: &str, - parser: &Parser, - filter: &StaticFilter, - analyzer: &Analyzer, - agg: &mut Aggregated, -) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file(dir, root_path, parser, filter, analyzer, agg); - return; - } - let Ok(read) = fs::read_dir(dir) else { - return; - }; - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let Ok(file_type) = entry.file_type() else { - continue; - }; - if file_type.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; - } - walk(&path, root_path, parser, filter, analyzer, agg); - continue; - } - - visit_file(&path, root_path, parser, filter, analyzer, agg); - } -} - -/// Analyzes one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): parser support + path policy filters, parse, -/// per-file report, and fold into the aggregate. -fn visit_file( - path: &Path, - root_path: &str, - parser: &Parser, - filter: &StaticFilter, - analyzer: &Analyzer, - agg: &mut Aggregated, -) { - let path_str = path.to_string_lossy(); - if !parser.is_supported(&path_str) { - return; - } - if filter.skips(&path_str) { - return; - } - let Some(content) = super::read_source_capped(path) else { - return; - }; - let Ok(uast_root) = parser.parse(&path_str, &content) else { - super::note_skipped_file(); - return; - }; - - // Per-file report via the visitor path (what the static factory uses). - let report = analyzer.analyze_visitor(&uast_root); - - // Every parsed file contributes one report to the averages divisor. - agg.report_count += 1; - - let stamped = make_relative_path(&path_str, root_path); - accumulate(agg, &report, &stamped); - retain_per_file(agg, &report, &stamped); -} - -/// Folds one per-file report into the aggregate (the reference `MetricsProcessor.ProcessReport` -/// + `SpillableDataCollector.CollectFromReport`). -fn accumulate(agg: &mut Aggregated, report: &Report, source_file: &str) { - if let Some(v) = report.get("total_functions").and_then(ReportValue::as_int) { - agg.total_functions += v; - } - if let Some(v) = report.get("lcom").and_then(ReportValue::as_float) { - agg.lcom_sum += v; - } - if let Some(v) = report.get("cohesion_score").and_then(ReportValue::as_float) { - agg.cohesion_score_sum += v; - } - if let Some(v) = report - .get("function_cohesion") - .and_then(ReportValue::as_float) - { - agg.function_cohesion_sum += v; - } - - let Some(functions) = report.get("functions").and_then(ReportValue::as_functions) else { - return; - }; - for fn_map in functions { - let get_str = |key: &str| { - fn_map - .get(key) - .and_then(ReportValue::as_str) - .unwrap_or_default() - .to_string() - }; - let get_int = |key: &str| fn_map.get(key).and_then(ReportValue::as_int).unwrap_or(0); - let name = get_str("name"); - let cohesion = fn_map - .get("cohesion") - .and_then(ReportValue::as_float) - .unwrap_or(0.0); - // Composite dedup key (_source_file, name); the per-file report items do - // not yet carry the stamped source file, so we stamp it here. - let key = format!("{source_file}\u{0}{name}"); - agg.functions.insert( - key, - FnItem { - name, - source_file: source_file.to_string(), - cohesion, - line_count: get_int("line_count"), - variable_count: get_int("variable_count"), - cohesion_assessment: get_str("cohesion_assessment"), - variable_assessment: get_str("variable_assessment"), - size_assessment: get_str("size_assessment"), - }, - ); - } -} - -/// Retains the per-file visitor report for `--per-file` (the reference -/// `PerFileRetainer.Retain` shallow clone, keyed by the stamped path): the -/// scalar metrics, the ANALYZER's per-file `message` (`No functions found` -/// for function-free files), and this file's `(name, cohesion)` functions. -fn retain_per_file(agg: &mut Aggregated, report: &Report, source_file: &str) { - let get_f = |key: &str| { - report - .get(key) - .and_then(ReportValue::as_float) - .unwrap_or(0.0) - }; - let functions: Vec<(String, f64)> = report - .get("functions") - .and_then(ReportValue::as_functions) - .map(|fns| { - fns.iter() - .map(|f| { - ( - f.get("name") - .and_then(ReportValue::as_str) - .unwrap_or_default() - .to_string(), - f.get("cohesion") - .and_then(ReportValue::as_float) - .unwrap_or(0.0), - ) - }) - .collect() + let files = super::walk_source_files(root_path, &parser, filter) + .ok()? + .filter_map(|file| { + let Ok(uast_root) = parser.parse(&file.rel_path, &file.bytes) else { + super::note_skipped(&file.path, SkipReason::Unparseable); + return None; + }; + Some(AnalyzedFile { + path: file.rel_path, + report: analyzer.analyze_file(&uast_root), + }) }) - .unwrap_or_default(); - - agg.per_file.push(PerFileCohesion { - rel: source_file.to_string(), - total_functions: report - .get("total_functions") - .and_then(ReportValue::as_int) - .unwrap_or(0), - lcom: get_f("lcom"), - cohesion_score: get_f("cohesion_score"), - function_cohesion: get_f("function_cohesion"), - message: report - .get("message") - .and_then(ReportValue::as_str) - .unwrap_or_default() - .to_string(), - functions, - }); -} - -/// The collected functions sorted by `name` (the reference `GetSortedData`, last identifier -/// key). Equal-name order is nondeterministic in the reference implementation and canonicalized by the -/// harness; a stable secondary order keeps our output deterministic. -fn sorted_functions(agg: &Aggregated) -> Vec<FnItem> { - let mut out: Vec<FnItem> = agg.functions.values().cloned().collect(); - out.sort_by(|a, b| { - a.name - .cmp(&b.name) - .then_with(|| a.source_file.cmp(&b.source_file)) - }); - out -} - -/// The reference `filepath.Rel(rootPath, filePath)` (flat repos → path under the root). -fn make_relative_path(file_path: &str, root_path: &str) -> String { - if root_path.is_empty() { - return file_path.to_string(); - } - match Path::new(file_path).strip_prefix(Path::new(root_path)) { - // filepath.Rel(root, root) == "." (a FILE root stamps "."). - Ok(rel) if rel.as_os_str().is_empty() => ".".to_string(), - Ok(rel) => rel.to_string_lossy().into_owned(), - Err(_) => file_path.to_string(), - } + .collect(); + Some(files) } -// === JSON (renderer section) === - -/// Builds the `static/cohesion --format json` report bytes, or `None` when the -/// path cannot be read. -#[must_use] -pub fn cohesion_report_json(root_path: &str) -> Option<Vec<u8>> { - cohesion_report_json_flags(root_path, &StaticFilter::default(), false) -} +// === JSON / text / compact (renderer section) === -/// [`cohesion_report_json`] with the `--per-file` flag applied: `per_file` -/// enables the reference implementation's section enrichment -/// (`StaticService.enrichWithPerFileData` → `JSONReport.EnrichWithPerFileData` -/// over the `PerFileRetainer` snapshots — one `JSONFileEntry` per ANALYZED -/// file, function-free files included, keyed into the section's `files` array). +/// The `static/cohesion --format json` report bytes, or `None` when the path +/// does not exist. `per_file` adds one `files` entry per analyzed file. #[must_use] pub fn cohesion_report_json_flags( root_path: &str, @@ -395,445 +61,286 @@ pub fn cohesion_report_json_flags( per_file: bool, ) -> Option<Vec<u8>> { let report = cohesion_report_value_flags(root_path, filter, per_file)?; - let bytes = Encoder::indented(" ") - .with_trailing_newline(true) - .encode_to_vec(&report); - Some(bytes) -} - -/// Builds the `static/cohesion` `renderer.JSONReport` GoValue (single scored -/// section), shared by the single-analyzer byte path and the multi-analyzer -/// static-JSON merge. `None` when the path cannot be walked. -#[must_use] -pub fn cohesion_report_value(root_path: &str) -> Option<GoValue> { - cohesion_report_value_flags(root_path, &StaticFilter::default(), false) + Some( + Encoder::indented(" ") + .with_trailing_newline(true) + .encode_to_vec(&report), + ) } -/// [`cohesion_report_value`] with the `--per-file` section enrichment. +/// The `static/cohesion` single-section report tree +/// (`{overall_score_label, sections, overall_score}`), shared by the +/// single-analyzer JSON path and the multi-analyzer merge. `per_file` adds +/// one entry per analyzed file (function-free files included, with +/// `No functions found`) under the section's `files`. #[must_use] pub fn cohesion_report_value_flags( root_path: &str, filter: &StaticFilter, per_file: bool, ) -> Option<GoValue> { - let agg = aggregate_opts(root_path, filter)?; - // --per-file: one JSONFileEntry per ANALYZED file, in walk order (the - // reference implementation ranges the retainer map here — run-to-run - // random; the oracle's measured-variance canonicalization compares the set). - let file_entries: Option<Vec<GoValue>> = if per_file { - Some(agg.per_file.iter().map(build_file_entry).collect()) - } else { - None - }; + let files = analyze_tree(root_path, filter)?; + let agg = aggregate(&files); + let file_entries = per_file.then(|| files.iter().map(build_file_entry).collect()); Some(build_json_report(&agg, file_entries)) } -/// Builds the `static/cohesion` section tree in the reference implementation's `AggregationModeSummaryOnly` -/// shape (`text` / `compact`): the detailed `functions` collection is a no-op, so -/// the distribution + issues sections (both derived from the collected functions) -/// are absent while the averaged scalar metrics are unchanged. +/// The section tree the `text` / `compact` renderers consume: the same +/// metrics, distribution and issues as the JSON report. #[must_use] pub fn cohesion_report_value_summary(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - let mut agg = aggregate_opts(root_path, filter)?; - agg.functions.clear(); - Some(build_json_report(&agg, None)) + cohesion_report_value_flags(root_path, filter, false) } -/// Builds the AGGREGATED RAW `analyze.Report` GoValue for `static/cohesion` — -/// the value the reference implementation's `cohesion.Aggregator` (a plain `common.Aggregator`) returns -/// from `GetResult()` (`ResultBuilder.BuildCollectionResult`), which is what -/// `--format plot` consumes and what `writeReportJSON` serializes into -/// `report.json`: -/// -/// * `analyzer_name`, `message` (reference keys it off a RANDOM numeric average — -/// measured-nondeterministic; we use `cohesion_score` like the section), -/// * count: `total_functions` (summed; overwrites the collection length), -/// * averages: `lcom` / `cohesion_score` / `function_cohesion`, -/// * `functions`: the dedup-by-`(_source_file, name)` collection sorted by -/// `name` (`GetSortedData`; the equal-name tie order is nondeterministic in the reference binary -/// and canonicalized by the harness), each item the -/// `convertCohesionFunctionItems` map + the `_source_file` stamp only (the -/// base collector never stamps `_language`/`_directory`). -/// -/// With no parsed files the reference implementation returns `createEmptyResult` instead (5 keys, no -/// `analyzer_name`/`functions`). +/// The AGGREGATED RAW cohesion report consumed by `--format plot` and +/// `report.json`: `analyzer_name`, `total_functions`, `lcom`, +/// `cohesion_score`, `function_cohesion`, `message` and the full `functions` +/// table (every function, walk order, `_source_file`-stamped). #[must_use] pub fn cohesion_raw_report_value(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - let agg = aggregate_opts(root_path, filter)?; - - if agg.report_count == 0 { - let mut m = GoMap::new(MapOrigin::Map); - m.push("total_functions", GoValue::Int(0)); - m.push("lcom", GoValue::Float(0.0)); - m.push("cohesion_score", GoValue::Float(1.0)); - m.push("function_cohesion", GoValue::Float(1.0)); - m.push("message", GoValue::Str("No functions found".to_string())); - return Some(GoValue::Map(m)); - } - - let functions: Vec<GoValue> = sorted_functions(&agg) - .into_iter() + let files = analyze_tree(root_path, filter)?; + let agg = aggregate(&files); + let functions: Vec<GoValue> = agg + .functions + .iter() .map(|f| { + let metrics = cf_cohesion::FunctionMetrics { + name: f.name.clone(), + start_line: f.start_line, + line_count: f.line_count, + variable_count: f.variable_count, + cohesion: f.cohesion, + }; let mut m = GoMap::new(MapOrigin::Map); - m.push("name", GoValue::Str(f.name)); - m.push("line_count", GoValue::Int(f.line_count)); - m.push("variable_count", GoValue::Int(f.variable_count)); + m.push("name", GoValue::Str(f.name.clone())); + m.push("start_line", GoValue::Int(f.start_line as i64)); + m.push("line_count", GoValue::Int(f.line_count as i64)); + m.push("variable_count", GoValue::Int(f.variable_count as i64)); m.push("cohesion", GoValue::Float(f.cohesion)); - m.push("cohesion_assessment", GoValue::Str(f.cohesion_assessment)); - m.push("variable_assessment", GoValue::Str(f.variable_assessment)); - m.push("size_assessment", GoValue::Str(f.size_assessment)); - m.push("_source_file", GoValue::Str(f.source_file)); + m.push( + "cohesion_assessment", + GoValue::Str(metrics.cohesion_assessment().to_string()), + ); + m.push( + "variable_assessment", + GoValue::Str(metrics.variable_assessment().to_string()), + ); + m.push( + "size_assessment", + GoValue::Str(metrics.size_assessment().to_string()), + ); + m.push("_source_file", GoValue::Str(f.source_file.clone())); GoValue::Map(m) }) .collect(); let mut m = GoMap::new(MapOrigin::Map); - m.push("analyzer_name", GoValue::Str("cohesion".to_string())); - m.push("total_functions", GoValue::Int(agg.total_functions)); - m.push("functions", GoValue::Array(functions)); m.push( - "message", - GoValue::Str(cohesion_message(agg.cohesion_score_avg()).to_string()), - ); - m.push("lcom", GoValue::Float(agg.lcom_avg())); - m.push("cohesion_score", GoValue::Float(agg.cohesion_score_avg())); - m.push( - "function_cohesion", - GoValue::Float(agg.function_cohesion_avg()), + "analyzer_name", + GoValue::Str(cf_cohesion::ANALYZER_NAME.to_string()), ); + m.push("total_functions", GoValue::Int(agg.total_functions as i64)); + m.push("functions", GoValue::Array(functions)); + m.push("message", GoValue::Str(agg.message.clone())); + m.push("lcom", GoValue::Float(agg.lcom)); + m.push("cohesion_score", GoValue::Float(agg.cohesion_score)); + m.push("function_cohesion", GoValue::Float(agg.function_cohesion)); Some(GoValue::Map(m)) } -/// Aggregator message keyed by the first numeric average. -fn cohesion_message(score: f64) -> &'static str { - if score >= MSG_SCORE_HIGH { - MSG_EXCELLENT - } else if score >= MSG_SCORE_MEDIUM { - MSG_GOOD - } else if score >= MSG_SCORE_LOW { - MSG_FAIR - } else { - MSG_POOR - } -} - -/// Builds one `renderer.JSONFileEntry` for `--per-file` (the reference -/// `SectionToJSONFileEntry` over `cohesion.NewReportSection(perFileReport)`): -/// status/score come from the PER-FILE report (`message` / `cohesion_score`), -/// the metrics are the per-file scalars, and the distribution and -/// value-string-ascending issues cover this file's functions only. -fn build_file_entry(pf: &PerFileCohesion) -> GoValue { - let score = pf.cohesion_score; - let status = if pf.message.is_empty() { - DEFAULT_STATUS_MESSAGE.to_string() - } else { - pf.message.clone() - }; - - let metrics = GoValue::Array(vec![ - metric(METRIC_TOTAL_FUNCTIONS, &pf.total_functions.to_string()), - metric(METRIC_LCOM, &format_float(pf.lcom)), - metric(METRIC_COHESION_SCORE, &format_float(pf.cohesion_score)), - metric( - METRIC_FUNCTION_COHESION, - &format_float(pf.function_cohesion), - ), - ]); - - // Distribution over THIS file's functions (absent for function-free files - // — Distribution() returns nil ⇒ omitempty). - let total = pf.functions.len() as i64; - let mut dist_items = Vec::new(); - if total != 0 { - let mut excellent = 0i64; - let mut good = 0i64; - let mut fair = 0i64; - let mut poor = 0i64; - for (_, coh) in &pf.functions { - if *coh >= DIST_EXCELLENT_MIN { - excellent += 1; - } else if *coh >= DIST_GOOD_MIN { - good += 1; - } else if *coh >= DIST_FAIR_MIN { - fair += 1; - } else { - poor += 1; - } - } - dist_items.push(dist_item( - DIST_LABEL_EXCELLENT, - pct(excellent, total), - excellent, - )); - dist_items.push(dist_item(DIST_LABEL_GOOD, pct(good, total), good)); - dist_items.push(dist_item(DIST_LABEL_FAIR, pct(fair, total), fair)); - dist_items.push(dist_item(DIST_LABEL_POOR, pct(poor, total), poor)); - } - - // Issues: this file's functions sorted by the FormatFloat string ascending - // (the section comparator; name tie-break keeps our output deterministic - // where the reference unstable sort varies). - let mut issues: Vec<&(String, f64)> = pf.functions.iter().collect(); - issues.sort_by(|a, b| { - format_float(a.1) - .cmp(&format_float(b.1)) - .then_with(|| a.0.cmp(&b.0)) - }); - let issue_items: Vec<GoValue> = issues - .iter() - .map(|(name, coh)| { - let mut iss = GoMap::new(MapOrigin::Struct); - iss.push("name", GoValue::Str(name.clone())); - iss.push("location", GoValue::Str(pf.rel.clone())); - iss.push("value", GoValue::Str(format_float(*coh))); - iss.push( - "severity", - GoValue::Str(severity_for_cohesion(*coh).to_string()), - ); - GoValue::Map(iss) - }) - .collect(); +/// One `--per-file` entry: the file's own scalars, distribution and issues; +/// a function-free file reports `No functions found`. +fn build_file_entry(file: &AnalyzedFile) -> GoValue { + let report = &file.report; + let cohesions: Vec<f64> = report.functions.iter().map(|f| f.cohesion).collect(); + let issues = report_section::issues( + report + .functions + .iter() + .map(|f| (f.name.as_str(), file.path.as_str(), f.cohesion)), + ); let mut entry = GoMap::new(MapOrigin::Struct); - entry.push("file_path", GoValue::Str(pf.rel.clone())); - entry.push("score_label", GoValue::Str(score_label(score))); - entry.push("status", GoValue::Str(status)); - entry.push("metrics", metrics); - if !dist_items.is_empty() { - entry.push("distribution", GoValue::Array(dist_items)); - } - entry.push("issues", GoValue::Array(issue_items)); - entry.push("score", GoValue::Float(score)); + entry.push("file_path", GoValue::Str(file.path.clone())); + entry.push( + "score_label", + GoValue::Str(report_section::score_label(report.cohesion_score)), + ); + entry.push("status", GoValue::Str(report.message.clone())); + entry.push( + "metrics", + metrics_value( + report.total_functions, + report.lcom, + report.cohesion_score, + report.function_cohesion, + ), + ); + push_distribution_and_issues( + &mut entry, + &report_section::distribution(&cohesions), + &issues, + ); + entry.push("score", GoValue::Float(report.cohesion_score)); GoValue::Map(entry) } -fn build_json_report(agg: &Aggregated, file_entries: Option<Vec<GoValue>>) -> GoValue { - let cohesion_score = agg.cohesion_score_avg(); - let functions = sorted_functions(agg); - - // status: the cohesion section message. the reference implementation's aggregator picks the message - // from the FIRST numeric average in map-iteration order; the numeric keys are - // {lcom, cohesion_score, function_cohesion} and the harness canonicalizes the - // status when the reference implementation's choice varies run-to-run. We key it on cohesion_score, the - // score the section's NewReportSection also reads. - let status = if agg.report_count == 0 { - DEFAULT_STATUS_MESSAGE.to_string() +/// The single-section report tree of an aggregate. +fn build_json_report(agg: &Aggregate, file_entries: Option<Vec<GoValue>>) -> GoValue { + let cohesions: Vec<f64> = agg.functions.iter().map(|f| f.cohesion).collect(); + let issues = report_section::issues( + agg.functions + .iter() + .map(|f| (f.name.as_str(), f.source_file.as_str(), f.cohesion)), + ); + let status = if agg.has_data() { + agg.message.clone() } else { - cohesion_message(cohesion_score).to_string() + NO_DATA_MESSAGE.to_string() }; + let label = report_section::score_label(agg.cohesion_score); - // ---- metrics ---- - let metrics = GoValue::Array(vec![ - metric(METRIC_TOTAL_FUNCTIONS, &agg.total_functions.to_string()), - metric(METRIC_LCOM, &format!("{:.1}", agg.lcom_avg())), - metric(METRIC_COHESION_SCORE, &format!("{cohesion_score:.1}")), - metric( - METRIC_FUNCTION_COHESION, - &format!("{:.1}", agg.function_cohesion_avg()), - ), - ]); - - // ---- distribution (over the collected functions) ---- - let total = functions.len() as i64; - let mut dist_items = Vec::new(); - if total != 0 { - let mut excellent = 0i64; - let mut good = 0i64; - let mut fair = 0i64; - let mut poor = 0i64; - for f in &functions { - if f.cohesion >= DIST_EXCELLENT_MIN { - excellent += 1; - } else if f.cohesion >= DIST_GOOD_MIN { - good += 1; - } else if f.cohesion >= DIST_FAIR_MIN { - fair += 1; - } else { - poor += 1; - } - } - dist_items.push(dist_item( - DIST_LABEL_EXCELLENT, - pct(excellent, total), - excellent, - )); - dist_items.push(dist_item(DIST_LABEL_GOOD, pct(good, total), good)); - dist_items.push(dist_item(DIST_LABEL_FAIR, pct(fair, total), fair)); - dist_items.push(dist_item(DIST_LABEL_POOR, pct(poor, total), poor)); - } - - // ---- issues: ALL functions sorted by FormatFloat(cohesion) string ascending ---- - let mut issues: Vec<FnItem> = functions.clone(); - issues.sort_by(|a, b| { - format_float(a.cohesion) - .cmp(&format_float(b.cohesion)) - .then_with(|| a.name.cmp(&b.name)) - .then_with(|| a.source_file.cmp(&b.source_file)) - }); - let issue_items: Vec<GoValue> = issues - .iter() - .map(|f| { - let mut iss = GoMap::new(MapOrigin::Struct); - iss.push("name", GoValue::Str(f.name.clone())); - iss.push("location", GoValue::Str(f.source_file.clone())); - iss.push("value", GoValue::Str(format_float(f.cohesion))); - iss.push( - "severity", - GoValue::Str(severity_for_cohesion(f.cohesion).to_string()), - ); - GoValue::Map(iss) - }) - .collect(); - - // ---- section ---- let mut section = GoMap::new(MapOrigin::Struct); section.push("title", GoValue::Str(SECTION_TITLE.to_string())); - section.push("score_label", GoValue::Str(score_label(cohesion_score))); + section.push("score_label", GoValue::Str(label.clone())); section.push("status", GoValue::Str(status)); - section.push("metrics", metrics); - if !dist_items.is_empty() { - section.push("distribution", GoValue::Array(dist_items)); - } - section.push("issues", GoValue::Array(issue_items)); - // --per-file enrichment: `files` sits between `issues` and `score` - // (renderer.JSONSection field order; omitempty without the flag). + section.push( + "metrics", + metrics_value( + agg.total_functions, + agg.lcom, + agg.cohesion_score, + agg.function_cohesion, + ), + ); + push_distribution_and_issues( + &mut section, + &report_section::distribution(&cohesions), + &issues, + ); + // `--per-file` entries sit between `issues` and `score`. if let Some(entries) = file_entries { section.push("files", GoValue::Array(entries)); } - section.push("score", GoValue::Float(cohesion_score)); + section.push("score", GoValue::Float(agg.cohesion_score)); - // ---- report ---- let mut report = GoMap::new(MapOrigin::Struct); - report.push( - "overall_score_label", - GoValue::Str(score_label(cohesion_score)), - ); + report.push("overall_score_label", GoValue::Str(label)); report.push("sections", GoValue::Array(vec![GoValue::Map(section)])); - report.push("overall_score", GoValue::Float(cohesion_score)); + report.push("overall_score", GoValue::Float(agg.cohesion_score)); GoValue::Map(report) } -// === YAML / binary (ComputedMetrics machine format) === +/// The four key metrics, floats rendered with two decimals. +fn metrics_value(total: usize, lcom: f64, score: f64, avg: f64) -> GoValue { + GoValue::Array(vec![ + metric(METRIC_TOTAL_FUNCTIONS, &total.to_string()), + metric(METRIC_LCOM, &report_section::format_metric(lcom)), + metric(METRIC_COHESION_SCORE, &report_section::format_metric(score)), + metric( + METRIC_FUNCTION_COHESION, + &report_section::format_metric(avg), + ), + ]) +} -/// Builds the cross-file aggregated [`Report`] that the machine formats marshal. -fn aggregated_report(agg: &Aggregated) -> Report { - let mut r = Report::new(); - r.insert( - "total_functions".into(), - ReportValue::Int(agg.total_functions), - ); - r.insert("lcom".into(), ReportValue::Float(agg.lcom_avg())); - r.insert( - "cohesion_score".into(), - ReportValue::Float(agg.cohesion_score_avg()), - ); - r.insert( - "function_cohesion".into(), - ReportValue::Float(agg.function_cohesion_avg()), - ); - r.insert( - "message".into(), - ReportValue::Str(cohesion_message(agg.cohesion_score_avg()).to_string()), +/// Pushes the `distribution` (omitted when empty) and `issues` keys. +fn push_distribution_and_issues(target: &mut GoMap, buckets: &[Bucket], issues: &[Issue]) { + if !buckets.is_empty() { + target.push( + "distribution", + GoValue::Array(buckets.iter().map(dist_item).collect()), + ); + } + target.push( + "issues", + GoValue::Array(issues.iter().map(issue_item).collect()), ); +} - let functions: Vec<BTreeMap<String, ReportValue>> = sorted_functions(agg) - .into_iter() - .map(|f| { - // Only `_source_file` is propagated into the collected map items: the reference implementation's - // `convertCohesionFunctionItems` (the `TypedCollection.ToMaps` callback) - // stamps ONLY `analyze.SourceFileKey`, NOT `_directory`/`_language`, - // even though `StampSourceFile`/`StampLanguage` set those on the - // TypedCollection wrapper. So `directory`/`language` stay empty in the - // machine-format output (omitempty drops them). - let mut m: BTreeMap<String, ReportValue> = BTreeMap::new(); - m.insert("name".into(), ReportValue::Str(f.name)); - m.insert("cohesion".into(), ReportValue::Float(f.cohesion)); - m.insert("_source_file".into(), ReportValue::Str(f.source_file)); - m - }) - .collect(); - r.insert("functions".into(), ReportValue::Functions(functions)); - r +fn metric(label: &str, value: &str) -> GoValue { + let mut m = GoMap::new(MapOrigin::Struct); + m.push("label", GoValue::Str(label.to_string())); + m.push("value", GoValue::Str(value.to_string())); + GoValue::Map(m) } -/// Builds the `static/cohesion --format yaml` report bytes, or `None`. -/// -/// The reference `cohesion.FormatReportYAML` = `yaml.Marshal(*ComputedMetrics)`. Routed -/// through the go-compatible `cf-goyaml` serializer over a `cf-gojson::GoValue` -/// tree of the struct (declaration-order fields, `omitempty`, byte-sorted map -/// keys for `distribution`). +fn dist_item(bucket: &Bucket) -> GoValue { + let mut d = GoMap::new(MapOrigin::Struct); + d.push("label", GoValue::Str(bucket.label.to_string())); + d.push("percent", GoValue::Float(bucket.percent)); + d.push("count", GoValue::Int(bucket.count as i64)); + GoValue::Map(d) +} + +fn issue_item(issue: &Issue) -> GoValue { + let mut i = GoMap::new(MapOrigin::Struct); + i.push("name", GoValue::Str(issue.name.clone())); + i.push("location", GoValue::Str(issue.location.clone())); + i.push( + "value", + GoValue::Str(report_section::format_metric(issue.cohesion)), + ); + i.push("severity", GoValue::Str(issue.severity.to_string())); + GoValue::Map(i) +} + +// === YAML / binary (ComputedMetrics machine format) === + +/// The `static/cohesion --format yaml` report bytes, or `None` when the path +/// does not exist. #[must_use] pub fn cohesion_report_yaml(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let agg = aggregate_opts(root_path, filter)?; - let report = aggregated_report(&agg); - let metrics = cf_cohesion::metrics::compute_all_metrics(&report); + let metrics = computed_metrics(root_path, filter)?; Some(cf_goyaml::marshal(&computed_metrics_go_value(&metrics))) } -/// Builds the `static/cohesion --format bin` report bytes, or `None`. -/// -/// The reference `cohesion.FormatReportBinary` = `reportutil.EncodeBinaryEnvelope(metrics)` -/// = `"CFB1"` + u32-LE len + compact `json.Marshal(*ComputedMetrics)`. Routed -/// through `cf-reportutil` over the same `cf-gojson::GoValue` tree. +/// The `static/cohesion --format bin` report bytes (`CFB1` envelope over the +/// compact JSON of [`ComputedMetrics`]), or `None` when the path does not +/// exist. #[must_use] pub fn cohesion_report_bin(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let agg = aggregate_opts(root_path, filter)?; - let report = aggregated_report(&agg); - let metrics = cf_cohesion::metrics::compute_all_metrics(&report); + let metrics = computed_metrics(root_path, filter)?; cf_reportutil::encode_binary_envelope(&computed_metrics_go_value(&metrics)).ok() } -/// Builds the `cf-gojson::GoValue` tree for [`ComputedMetrics`] (reference struct field -/// order + `omitempty`), the single value all machine formats encode. -fn computed_metrics_go_value(m: &cf_cohesion::ComputedMetrics) -> GoValue { +fn computed_metrics(root_path: &str, filter: &StaticFilter) -> Option<ComputedMetrics> { + let files = analyze_tree(root_path, filter)?; + Some(cf_cohesion::metrics::compute_metrics(&aggregate(&files))) +} + +/// The value tree of [`ComputedMetrics`] (struct field order), the single +/// value both machine formats encode. +fn computed_metrics_go_value(m: &ComputedMetrics) -> GoValue { let mut root = GoMap::new(MapOrigin::Struct); - // function_cohesion []FunctionCohesionData - let fc: Vec<GoValue> = m + let function_cohesion: Vec<GoValue> = m .function_cohesion .iter() .map(|f| { let mut o = GoMap::new(MapOrigin::Struct); o.push("name", GoValue::Str(f.name.clone())); - if !f.source_file.is_empty() { - o.push("source_file", GoValue::Str(f.source_file.clone())); - } - if !f.language.is_empty() { - o.push("language", GoValue::Str(f.language.clone())); - } - if !f.directory.is_empty() { - o.push("directory", GoValue::Str(f.directory.clone())); - } + o.push("source_file", GoValue::Str(f.source_file.clone())); o.push("cohesion", GoValue::Float(f.cohesion)); o.push("quality_level", GoValue::Str(f.quality_level.clone())); GoValue::Map(o) }) .collect(); - root.push("function_cohesion", GoValue::Array(fc)); + root.push("function_cohesion", GoValue::Array(function_cohesion)); - // distribution map[string]int (byte-sorted keys via Map origin) let mut dist = GoMap::new(MapOrigin::Map); for (k, v) in &m.distribution { dist.push(k, GoValue::Int(*v)); } root.push("distribution", GoValue::Map(dist)); - // low_cohesion_functions []LowCohesionFunctionData let low: Vec<GoValue> = m .low_cohesion_functions .iter() .map(|f| { let mut o = GoMap::new(MapOrigin::Struct); o.push("name", GoValue::Str(f.name.clone())); - if !f.source_file.is_empty() { - o.push("source_file", GoValue::Str(f.source_file.clone())); - } - if !f.language.is_empty() { - o.push("language", GoValue::Str(f.language.clone())); - } - if !f.directory.is_empty() { - o.push("directory", GoValue::Str(f.directory.clone())); - } + o.push("source_file", GoValue::Str(f.source_file.clone())); o.push("cohesion", GoValue::Float(f.cohesion)); o.push("risk_level", GoValue::Str(f.risk_level.clone())); o.push("recommendation", GoValue::Str(f.recommendation.clone())); @@ -842,91 +349,188 @@ fn computed_metrics_go_value(m: &cf_cohesion::ComputedMetrics) -> GoValue { .collect(); root.push("low_cohesion_functions", GoValue::Array(low)); - // aggregate AggregateData let a = &m.aggregate; - let mut agg_o = GoMap::new(MapOrigin::Struct); - agg_o.push("total_functions", GoValue::Int(a.total_functions)); - agg_o.push("lcom", GoValue::Float(a.lcom)); - agg_o.push("lcom_variant", GoValue::Str(a.lcom_variant.clone())); - agg_o.push("cohesion_score", GoValue::Float(a.cohesion_score)); - agg_o.push("function_cohesion", GoValue::Float(a.function_cohesion)); - agg_o.push("health_score", GoValue::Float(a.health_score)); - agg_o.push("message", GoValue::Str(a.message.clone())); - root.push("aggregate", GoValue::Map(agg_o)); + let mut agg = GoMap::new(MapOrigin::Struct); + agg.push("total_functions", GoValue::Int(a.total_functions)); + agg.push("lcom", GoValue::Float(a.lcom)); + agg.push("lcom_variant", GoValue::Str(a.lcom_variant.clone())); + agg.push("cohesion_score", GoValue::Float(a.cohesion_score)); + agg.push("function_cohesion", GoValue::Float(a.function_cohesion)); + agg.push("health_score", GoValue::Float(a.health_score)); + agg.push("message", GoValue::Str(a.message.clone())); + root.push("aggregate", GoValue::Map(agg)); GoValue::Map(root) } -// === Helpers === +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + /// `f` uses {x, y}, `g` uses {x}: LCOM 0.25, f 0.5, g 1.0. + const SHARE_GO: &str = "package main\n\nfunc f() int {\n\tx := 1\n\ty := 2\n\treturn x + y\n}\n\nfunc g() int {\n\tx := 3\n\treturn x\n}\n"; + /// `p` and `q` are disjoint: LCOM 0.5, both 0.0. + const DISJOINT_GO: &str = + "package main\n\nfunc p1() int {\n\tp := 1\n\treturn p\n}\n\nfunc q1() int {\n\tq := 2\n\treturn q\n}\n"; + const TYPES_GO: &str = "package main\n\ntype Config struct {\n\tName string\n}\n"; -fn metric(label: &str, value: &str) -> GoValue { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("value", GoValue::Str(value.to_string())); - GoValue::Map(m) -} + fn write(dir: &Path, name: &str, content: &str) { + std::fs::write(dir.join(name), content).unwrap(); + } -fn dist_item(label: &str, percent: f64, count: i64) -> GoValue { - let mut d = GoMap::new(MapOrigin::Struct); - d.push("label", GoValue::Str(label.to_string())); - d.push("percent", GoValue::Float(percent)); - d.push("count", GoValue::Int(count)); - GoValue::Map(d) -} + fn section(dir: &Path, per_file: bool) -> GoValue { + let report = + cohesion_report_value_flags(dir.to_str().unwrap(), &StaticFilter::default(), per_file) + .expect("existing path"); + match report.as_map().and_then(|m| m.get("sections")) { + Some(GoValue::Array(sections)) => sections[0].clone(), + other => panic!("no sections in {other:?}"), + } + } -/// The reference `reportutil.Pct`: fraction in [0,1]. -fn pct(count: i64, total: i64) -> f64 { - if total == 0 { - 0.0 - } else { - count as f64 / total as f64 + fn get<'a>(v: &'a GoValue, key: &str) -> &'a GoValue { + v.as_map() + .and_then(|m| m.get(key)) + .unwrap_or_else(|| panic!("key {key} missing in {v:?}")) } -} -/// The reference `reportutil.FormatFloat`: `%.1f`. -fn format_float(v: f64) -> String { - format!("{v:.1}") -} + fn metric_value(section: &GoValue, label: &str) -> String { + let GoValue::Array(metrics) = get(section, "metrics") else { + panic!("metrics is not an array") + }; + metrics + .iter() + .find(|m| get(m, "label").as_str() == Some(label)) + .and_then(|m| get(m, "value").as_str()) + .unwrap_or_else(|| panic!("metric {label} missing")) + .to_string() + } -fn severity_for_cohesion(coh: f64) -> &'static str { - if coh < ISSUE_SEVERITY_POOR_MAX { - SEVERITY_POOR - } else if coh < ISSUE_SEVERITY_FAIR_MAX { - SEVERITY_FAIR - } else { - SEVERITY_GOOD + fn float(v: &GoValue) -> f64 { + match v { + GoValue::Float(f) => *f, + GoValue::Int(i) => *i as f64, + other => panic!("not a number: {other:?}"), + } } -} -/// Renders a score as the `N/10` label. -fn score_label(score: f64) -> String { - let n = (score * 10.0).round() as i64; - format!("{n}/10") -} + /// Scalars are function-weighted over the files WITH functions, never a + /// mean over files, and function-free files are excluded. + #[test] + fn aggregation_is_function_weighted_and_skips_function_free_files() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "share.go", SHARE_GO); + write(dir.path(), "disjoint.go", DISJOINT_GO); + for i in 0..5 { + write(dir.path(), &format!("t{i}.go"), TYPES_GO); + } + let section = section(dir.path(), false); + // (0.25 * 2 + 0.5 * 2) / 4 = 0.375; file-mean would be far lower. + assert_eq!(metric_value(§ion, METRIC_TOTAL_FUNCTIONS), "4"); + assert!((float(get(§ion, "score")) - 0.625).abs() < 1e-9); + assert_eq!(metric_value(§ion, METRIC_LCOM), "0.38"); + // (0.5 + 1.0 + 0 + 0) / 4 + assert_eq!(metric_value(§ion, METRIC_FUNCTION_COHESION), "0.38"); + assert_eq!(get(§ion, "score_label").as_str(), Some("6/10")); + } -#[cfg(test)] -mod per_file_tests { - use super::*; + /// An existing empty directory and a tree of function-free files both + /// yield the "no data" report. + #[test] + fn empty_and_function_free_trees_are_no_data() { + let empty = tempfile::tempdir().unwrap(); + let empty_section = section(empty.path(), false); + assert_eq!( + get(&empty_section, "status").as_str(), + Some(NO_DATA_MESSAGE) + ); + assert_eq!(metric_value(&empty_section, METRIC_TOTAL_FUNCTIONS), "0"); + assert_eq!(get(&empty_section, "score_label").as_str(), Some("0/10")); + assert!(empty_section + .as_map() + .unwrap() + .get("distribution") + .is_none()); + + let structs = tempfile::tempdir().unwrap(); + write(structs.path(), "a.go", TYPES_GO); + write(structs.path(), "b.go", ""); + let structs_section = section(structs.path(), false); + assert_eq!( + get(&structs_section, "status").as_str(), + Some(NO_DATA_MESSAGE) + ); + assert_eq!(float(get(&structs_section, "score")), 0.0); + } - /// Fixture: one file with a function, one function-free file. - fn fixture() -> tempfile::TempDir { - let dir = tempfile::tempdir().expect("tempdir"); - std::fs::write( - dir.path().join("a.go"), - "package main\n\nfunc add(a, b int) int {\n\tc := a + b\n\treturn c\n}\n", - ) - .unwrap(); - std::fs::write( - dir.path().join("types.go"), - "package main\n\ntype Config struct {\n\tName string\n}\n", + /// A path that does not exist is reported to the caller as `None`. + #[test] + fn missing_path_is_none() { + assert!( + cohesion_report_value_flags("/nonexistent/zz", &StaticFilter::default(), false) + .is_none() + ); + assert!(cohesion_report_yaml("/nonexistent/zz", &StaticFilter::default()).is_none()); + } + + /// `text` / `compact` render the same section as JSON: the distribution + /// and the issues are present. + #[test] + fn summary_section_keeps_distribution_and_issues() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "disjoint.go", DISJOINT_GO); + let summary = + cohesion_report_value_summary(dir.path().to_str().unwrap(), &StaticFilter::default()) + .unwrap(); + let full = cohesion_report_value_flags( + dir.path().to_str().unwrap(), + &StaticFilter::default(), + false, ) .unwrap(); - dir + assert_eq!(summary, full); + let section = section(dir.path(), false); + let GoValue::Array(issues) = get(§ion, "issues") else { + panic!("issues missing") + }; + assert_eq!(issues.len(), 2, "both 0.0 functions are issues"); + assert!(section.as_map().unwrap().get("distribution").is_some()); + } + + /// Same-named functions in different files (or types) are all kept in the + /// raw report and the distribution. + #[test] + fn raw_report_keeps_same_named_functions() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", SHARE_GO); + write(dir.path(), "b.go", SHARE_GO); + let raw = cohesion_raw_report_value(dir.path().to_str().unwrap(), &StaticFilter::default()) + .unwrap(); + let GoValue::Array(functions) = get(&raw, "functions") else { + panic!("functions missing") + }; + assert_eq!(functions.len(), 4); + assert_eq!(get(&raw, "total_functions"), &GoValue::Int(4)); + let section = section(dir.path(), false); + let GoValue::Array(dist) = get(§ion, "distribution") else { + panic!("distribution missing") + }; + let total: i64 = dist + .iter() + .map(|d| match get(d, "count") { + GoValue::Int(n) => *n, + other => panic!("count {other:?}"), + }) + .sum(); + assert_eq!(total, 4); } #[test] fn per_file_flag_emits_files_entries() { - let dir = fixture(); + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", SHARE_GO); + write(dir.path(), "types.go", TYPES_GO); let bytes = cohesion_report_json_flags( dir.path().to_str().unwrap(), &StaticFilter::default(), @@ -943,17 +547,20 @@ mod per_file_tests { json.contains("\"file_path\": \"types.go\""), "types.go entry missing:\n{json}" ); - // Per-file status is the ANALYZER's per-file message; a function-free - // file reports the empty-result message. assert!( json.contains("\"status\": \"No functions found\""), "per-file empty-result status missing:\n{json}" ); + assert!( + json.contains("\"value\": \"0.25\""), + "per-file LCOM missing:\n{json}" + ); } #[test] fn no_per_file_flag_omits_files() { - let dir = fixture(); + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "a.go", SHARE_GO); let bytes = cohesion_report_json_flags( dir.path().to_str().unwrap(), &StaticFilter::default(), @@ -965,6 +572,5 @@ mod per_file_tests { !json.contains("\"files\""), "files key must be omitted:\n{json}" ); - assert!(!json.contains("\"file_path\"")); } } diff --git a/crates/cf-commands/src/handlers/static_comments.rs b/crates/cf-commands/src/handlers/static_comments.rs index 96f811ca..737bd24d 100644 --- a/crates/cf-commands/src/handlers/static_comments.rs +++ b/crates/cf-commands/src/handlers/static_comments.rs @@ -1,194 +1,63 @@ -//! Static-analysis YAML report path for the UAST `static/comments` analyzer. -//! -//! Reproduces the reference static pipeline for the single-analyzer -//! `codefang run --analyzers static/comments --format yaml` capture -//! (`StaticService.streamFiles` → `analyzeFile` → `comments.Analyzer.Analyze` → -//! `StampSourceFile`/`StampLanguage` → `comments.Aggregator` (base metrics -//! processor + `DetailedDataCollector`) → `comments.ComputeAllMetrics` → -//! `yaml.Marshal(ComputedMetrics)`). +//! Report paths for the `static/comments` analyzer: the folder walk, the +//! per-file stamping and every serialization (`json` section report, `yaml` +//! / `bin` computed metrics, the raw report behind `plot`, the `text` / +//! `compact` summary). //! //! # Pipeline //! -//! 1. Walk `root_path` lexically (`filepath.WalkDir` order), keeping every -//! regular file the UAST parser supports (`Parser::is_supported`) that is not -//! excluded by the path policy. `--languages` is empty here, so the language -//! glob filter is a no-op. -//! 2. Parse each file → UAST `Node`; run [`cf_comments::Analyzer::analyze`] to -//! get the per-file report map (`comments`/`functions` collections + scalar -//! metrics). -//! 3. Stamp `_source_file` (path relative to `root_path`), `_directory` -//! (`filepath.Dir` of the relative path), and `_language` -//! (`Parser::get_language`) onto every collected comment/function item. -//! 4. Aggregate across files exactly as `common.Aggregator`: -//! - `comments` / `functions` are **concatenated** in file order -//! (`DetailedDataCollector`, no dedup); -//! - count keys (`total_comments`, `good_comments`, `bad_comments`, -//! `total_functions`, `documented_functions`, `total_comment_details`) are -//! summed; -//! - numeric keys (`overall_score`, `good_comments_ratio`, -//! `documentation_coverage`) are summed and divided by the processed-report -//! count (the mean over all parsed files). -//! 5. Feed the aggregated report to the `comments.ComputeAllMetrics` closed form -//! (`ParseReportData` reads only `line` + the stamped source metadata off each -//! collection item; the convert maps carry neither `quality`/`type`/`score` -//! for comments nor `name`/`has_comment`/… for functions, so those per-item -//! fields stay at their zero values, exactly as in the golden capture), and emit -//! `ComputedMetrics` through cf-goyaml (gopkg.in/yaml.v3 parity). - -use std::fs; -use std::path::Path; +//! 1. The shared static walk ([`super::walk_source_files`]) admits every +//! UAST-supported file that passes the run filter (`--languages`, path +//! policy, generated-content marker, read cap). A file root analyzes +//! that one file. +//! 2. Parse each file and run [`cf_comments::Analyzer::analyze_metrics`]. +//! A supported file the parser cannot handle is kept as an empty file +//! (all counters zero) so it still appears under `--per-file`. +//! 3. Stamp every file with its root-relative path, directory and language. +//! 4. Aggregate with [`cf_comments::Aggregator`]: counters are summed and +//! every ratio is Σ/Σ (coverage = Σdocumented / Σfunctions, good ratio = +//! Σgood / Σcomments); the section score and status follow the weighted +//! good ratio. +//! +//! All machine formats share one aggregation, so `json`, `yaml`, `bin`, +//! `text` and `plot` report the same counts. -use super::StaticFilter; +use super::{SourceFile, StaticFilter}; +use cf_comments::{ + comment_preview, Aggregator, CommentCounts, CommentDetail, FileReport, FileStamp, FunctionInfo, +}; use cf_gojson::{GoMap, GoValue, MapOrigin}; use cf_uast::Parser; -/// One collected comment item (post-stamp): the full the reference implementation -/// `convertCommentReportItems` map — the raw aggregated -/// report (plot path / report.json) serializes every key; the YAML -/// `ParseReportData` reads only `line` + the stamped source metadata. -struct CommentItem { - line: i64, - comment: String, - placement: String, - target: String, - assessment: String, - source_file: String, - language: String, - directory: String, -} - -/// One collected function item (post-stamp): the full the reference implementation -/// `convertFunctionReportItems` map. The machine -/// `ComputeAllMetrics` payload reads only the stamped source metadata; the raw -/// aggregated report serializes every key. -struct FunctionItem { - source_file: String, - language: String, - directory: String, - /// The function's name (`function` key). - function_name: String, - /// The function's kind (`type` key, e.g. `"Function"`). - kind: String, - /// The function's line count (`lines` key). - lines: i64, - /// The associated comment type (`comment` key; `"None"` when undocumented). - comment: String, - /// The per-function documentation assessment (`assessment` key, e.g. - /// `"❌ No Comment"`); the JSON section emits an issue for each bad one. - assessment: String, -} - -/// Cross-file accumulator mirroring `comments.Aggregator`. -#[derive(Default)] -struct Aggregated { - comments: Vec<CommentItem>, - functions: Vec<FunctionItem>, - total_comments: i64, - good_comments: i64, - bad_comments: i64, - total_functions: i64, - documented_functions: i64, - total_comment_details: i64, - // Numeric-key running sums + processed-report count (for the mean). - sum_overall_score: f64, - sum_good_comments_ratio: f64, - sum_documentation_coverage: f64, - report_count: i64, - /// Per-file snapshots (the reference `PerFileRetainer` clones of each - /// analyzer report) consumed by the `--per-file` section enrichment; walk - /// order. - per_file: Vec<PerFileComments>, -} - -/// One retained per-file comments report: the analyzer result's scalars, -/// per-file `message`, and this file's undocumented function names. -struct PerFileComments { - /// Path relative to the analyzed root (the `StampSourceFile` stamp). - rel: String, - message: String, - overall_score: f64, - total_comments: i64, - good_comments: i64, - bad_comments: i64, - doc_coverage: f64, - good_ratio: f64, - total_functions: i64, - documented_functions: i64, - /// Function names with the `"❌ No Comment"` assessment. - undocumented: Vec<String>, -} +/// An undocumented declaration spanning at least this many lines is a +/// `HIGH` documentation risk. +const RISK_HIGH_MIN_LINES: i64 = 50; +/// ... at least this many lines is a `MEDIUM` risk; shorter ones are `LOW`. +const RISK_MEDIUM_MIN_LINES: i64 = 10; -impl PerFileComments { - /// The reference `buildEmptyResult` snapshot: what the analyzer reports - /// for a file with no comments (all zeros, the empty-result message). - fn empty(rel: String) -> Self { - PerFileComments { - rel, - message: "No comments found".to_string(), - overall_score: 0.0, - total_comments: 0, - good_comments: 0, - bad_comments: 0, - doc_coverage: 0.0, - good_ratio: 0.0, - total_functions: 0, - documented_functions: 0, - undocumented: Vec::new(), - } - } -} +/// The section title. +const SECTION_TITLE: &str = "COMMENTS"; +/// Issue value / severity for an undocumented declaration. +const ISSUE_VALUE: &str = "undocumented"; +const ISSUE_SEVERITY: &str = "poor"; -/// Builds the `static/comments --format yaml` report bytes for `root_path`, or -/// `None` when the path cannot be read (the caller then falls through to the -/// blocked-dependency sentinel). +/// Builds the `static/comments --format yaml` report bytes for `root_path`. #[must_use] pub fn comments_report_yaml(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let metrics = comments_metrics(root_path, filter)?; - Some(cf_goyaml::marshal(&metrics)) + let agg = aggregate(root_path, filter); + Some(cf_goyaml::marshal(&computed_metrics(&agg))) } -/// Builds the `static/comments --format bin` report bytes for `root_path`, or -/// `None` when the path cannot be read. -/// -/// The CFB1 bin payload is the **same** `ComputedMetrics` value as the yaml -/// sibling (`comments.FormatReportBinary` and `FormatReportYAML` both call -/// `ComputeAllMetrics(report)`), so it reuses [`comments_metrics`] verbatim and -/// only swaps the encoder: cf-reportutil's CFB1 envelope -/// (`reportutil.EncodeBinaryEnvelope`: magic `CFB1` + little-endian u32 payload -/// length + compact `encoding/json` payload) instead of cf-goyaml. +/// Builds the `static/comments --format bin` report bytes for `root_path`: +/// the same computed-metrics value as the yaml sibling inside the CFB1 +/// envelope. `None` when the payload exceeds the envelope's length cap. #[must_use] pub fn comments_report_bin(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let metrics = comments_metrics(root_path, filter)?; - Some( - cf_reportutil::encode_binary_envelope(&metrics) - .expect("comments metrics never exceed the CFB1 length cap"), - ) + let agg = aggregate(root_path, filter); + cf_reportutil::encode_binary_envelope(&computed_metrics(&agg)).ok() } -/// Builds the `static/comments --format json` structured-report bytes for -/// `root_path`, or `None` when the folder cannot be walked. -/// -/// The reference JSON path (`StaticService.FormatJSON` → `comments.CreateReportSection` -/// → `renderer.SectionToJSON` → `json.Encoder.SetIndent(""," ").Encode`) emits -/// a scored COMMENTS section: key metrics (Total/Good/Bad Comments, Doc -/// Coverage, Good Ratio, Total Functions), a Documented/Undocumented -/// distribution, and one info issue per UNDOCUMENTED function (assessment -/// `"❌ No Comment"`), sorted by name. the reference implementation sorts via an unstable `sort.Slice` -/// over functions collected in (parallel/map) order, so the tie order of -/// same-named functions is intrinsically nondeterministic in the reference binary (the project -/// MANIFEST marks static_comments.json nonBinding). We emit the deterministic -/// computation: the section score/labels/metrics/distribution are exact, and the -/// issue list is sorted by name (file-walk order within equal names). -#[must_use] -pub fn comments_report_json(root_path: &str) -> Option<Vec<u8>> { - comments_report_json_flags(root_path, &StaticFilter::default(), false) -} - -/// [`comments_report_json`] with the `--per-file` flag applied: `per_file` -/// enables the reference implementation's section enrichment -/// (`StaticService.enrichWithPerFileData` → `JSONReport.EnrichWithPerFileData` -/// over the `PerFileRetainer` snapshots — one `JSONFileEntry` per ANALYZED -/// file, comment-free files included, keyed into the section's `files` array). +/// Builds the `static/comments --format json` report bytes for `root_path`; +/// `per_file` adds one `files` entry per analyzed file. #[must_use] pub fn comments_report_json_flags( root_path: &str, @@ -203,366 +72,189 @@ pub fn comments_report_json_flags( ) } -/// Builds the `static/comments` `renderer.JSONReport` GoValue (single scored -/// section), shared by the single-analyzer byte path and the multi-analyzer -/// static-JSON merge. `None` when the path cannot be walked. -#[must_use] -pub fn comments_report_value(root_path: &str) -> Option<GoValue> { - comments_report_value_flags(root_path, &StaticFilter::default(), false) -} - -/// [`comments_report_value`] with the `--per-file` section enrichment. +/// Builds the single-section `static/comments` JSON report value, shared by +/// the single-analyzer path and the multi-analyzer static-JSON merge. #[must_use] pub fn comments_report_value_flags( root_path: &str, filter: &StaticFilter, per_file: bool, ) -> Option<GoValue> { - comments_report_value_mode_flags(root_path, filter, false, per_file) + Some(section_report( + &aggregate(root_path, filter), + per_file, + false, + )) } -/// Builds the `static/comments` section tree in the reference implementation's `AggregationModeSummaryOnly` -/// shape (`text` / `compact`): the detailed `comments`/`functions` collections are -/// no-ops, so the top-issues list (undocumented functions, read from `functions`) -/// is absent, while the Documented/Undocumented distribution — computed from the -/// always-on scalar `total_functions`/`documented_functions` counts — and the Key -/// Metrics are unchanged. +/// The `text` / `compact` summary: the section without its issue list. #[must_use] pub fn comments_report_value_summary(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - comments_report_value_mode(root_path, filter, true) -} - -fn comments_report_value_mode( - root_path: &str, - filter: &StaticFilter, - summary_only: bool, -) -> Option<GoValue> { - comments_report_value_mode_flags(root_path, filter, summary_only, false) + Some(section_report(&aggregate(root_path, filter), false, true)) } -fn comments_report_value_mode_flags( - root_path: &str, - filter: &StaticFilter, - summary_only: bool, - per_file: bool, -) -> Option<GoValue> { - let mut agg = comments_aggregate_opts(root_path, filter)?; +// --- section report (json / text / compact) ----------------------------------- - // --per-file: one JSONFileEntry per ANALYZED file, in walk order (the - // reference implementation ranges the retainer map here — run-to-run - // random; the oracle's measured-variance canonicalization compares the set). - let file_entries: Option<Vec<GoValue>> = if per_file { - Some( - agg.per_file - .iter() - .map(|pf| build_file_entry(pf, root_path)) - .collect(), - ) - } else { - None - }; - - if summary_only { - agg.functions.clear(); - } - let report_count = agg.report_count.max(0); - let mean = |sum: f64| { - if report_count > 0 { - sum / report_count as f64 - } else { - 0.0 - } - }; - let overall_score = mean(agg.sum_overall_score); - let good_ratio = mean(agg.sum_good_comments_ratio); - let doc_coverage = mean(agg.sum_documentation_coverage); - - // --- section score / labels --- - let score = overall_score; +fn section_report(agg: &Aggregator, per_file: bool, summary_only: bool) -> GoValue { + let counts = agg.counts(); + let score = counts.overall_score(); let score_label = format_score(score); - // --- status: NewReportSection reads the aggregate `message`. With ZERO - // analyzed files the aggregator returns `buildEmptyResult` (message - // "No comments found"); otherwise we emit the deterministic - // ThresholdLabeler value (matches the common reference output). --- - let status = if report_count == 0 { - "No comments found".to_string() - } else { - comment_message(overall_score) - }; - - // --- metrics (reference KeyMetrics order) --- - let metric = |label: &str, value: String| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("value", GoValue::Str(value)); - GoValue::Map(m) - }; - let metrics = vec![ - metric( - "Total Comments", - cf_reportutil::format_int(agg.total_comments), - ), - metric( - "Good Comments", - cf_reportutil::format_int(agg.good_comments), - ), - metric("Bad Comments", cf_reportutil::format_int(agg.bad_comments)), - metric("Doc Coverage", format_percent(doc_coverage)), - metric("Good Ratio", format_percent(good_ratio)), - metric( - "Total Functions", - cf_reportutil::format_int(agg.total_functions), - ), - ]; - - // --- distribution (Documented / Undocumented); nil when no functions --- - let total_fns = agg.total_functions; - let distribution: Vec<GoValue> = if total_fns == 0 { + let issues = if summary_only { Vec::new() } else { - let documented = agg.documented_functions; - let undocumented = total_fns - documented; - let pct = |c: i64| { - if total_fns == 0 { - 0.0 - } else { - c as f64 / total_fns as f64 - } - }; - let dist = |label: &str, count: i64| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("percent", GoValue::Float(pct(count))); - m.push("count", GoValue::Int(count)); - GoValue::Map(m) - }; - vec![ - dist("Documented", documented), - dist("Undocumented", undocumented), - ] + undocumented_issues(agg.functions()) }; - // --- issues: undocumented functions (assessment "❌ No Comment"), name asc --- - let mut issue_fns: Vec<&FunctionItem> = agg - .functions - .iter() - .filter(|f| f.assessment == "❌ No Comment") - .collect(); - // Reference: mapx.SortAndLimit(buildIssues(), commentNameLess, 0) — unstable - // sort.Slice by Name ascending. Reproduce the pdqsort permutation. - crate::handlers::go_sort::slice(&mut issue_fns, |a, b| a.function_name < b.function_name); - let issues: Vec<GoValue> = issue_fns - .iter() - .map(|f| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(f.function_name.clone())); - m.push("location", GoValue::Str(f.source_file.clone())); - m.push("value", GoValue::Str("undocumented".to_string())); - m.push("severity", GoValue::Str("poor".to_string())); - GoValue::Map(m) - }) - .collect(); - - // --- section (JSONSection field order) --- let mut section = GoMap::new(MapOrigin::Struct); - section.push("title", GoValue::Str("COMMENTS".to_string())); + section.push("title", GoValue::Str(SECTION_TITLE.to_string())); section.push("score_label", GoValue::Str(score_label.clone())); - section.push("status", GoValue::Str(status)); - section.push("metrics", GoValue::Array(metrics)); - if !distribution.is_empty() { - section.push("distribution", GoValue::Array(distribution)); - } + section.push("status", GoValue::Str(counts.message().to_string())); + section.push("metrics", GoValue::Array(key_metrics(&counts))); + push_distribution(&mut section, &counts); section.push("issues", GoValue::Array(issues)); - // --per-file enrichment: `files` sits between `issues` and `score` - // (renderer.JSONSection field order; omitempty without the flag). - if let Some(entries) = file_entries { + if per_file { + let entries = agg.files().iter().map(file_entry).collect(); section.push("files", GoValue::Array(entries)); } section.push("score", GoValue::Float(score)); - // --- top-level JSONReport: single scored section ⇒ overall == section --- let mut root = GoMap::new(MapOrigin::Struct); root.push("overall_score_label", GoValue::Str(score_label)); root.push("sections", GoValue::Array(vec![GoValue::Map(section)])); root.push("overall_score", GoValue::Float(score)); - - Some(GoValue::Map(root)) + GoValue::Map(root) } -/// Builds one `renderer.JSONFileEntry` for `--per-file` (the reference -/// `SectionToJSONFileEntry` over `comments.NewReportSection(perFileReport)`): -/// score/status come from the PER-FILE report (`overall_score` / `message`), -/// the metrics are the per-file scalars, the Documented/Undocumented -/// distribution covers this file's functions, and the issues are this file's -/// undocumented functions sorted by name. -fn build_file_entry(pf: &PerFileComments, root_path: &str) -> GoValue { - let score = pf.overall_score; - let status = if pf.message.is_empty() { - "No comment data available".to_string() - } else { - pf.message.clone() - }; +/// One `--per-file` entry: this file's score, status, metrics, distribution +/// and undocumented declarations. +fn file_entry(file: &FileReport) -> GoValue { + let counts = file.metrics.counts; + let score = counts.overall_score(); + let issues = undocumented_issues(file.metrics.functions.iter().map(|f| (&file.stamp, f))); + + let mut entry = GoMap::new(MapOrigin::Struct); + entry.push("file_path", GoValue::Str(file.stamp.source_file.clone())); + entry.push("score_label", GoValue::Str(format_score(score))); + entry.push("status", GoValue::Str(counts.message().to_string())); + entry.push("metrics", GoValue::Array(key_metrics(&counts))); + push_distribution(&mut entry, &counts); + entry.push("issues", GoValue::Array(issues)); + entry.push("score", GoValue::Float(score)); + GoValue::Map(entry) +} +fn key_metrics(counts: &CommentCounts) -> Vec<GoValue> { let metric = |label: &str, value: String| { let mut m = GoMap::new(MapOrigin::Struct); m.push("label", GoValue::Str(label.to_string())); m.push("value", GoValue::Str(value)); GoValue::Map(m) }; - let metrics = vec![ + vec![ metric( "Total Comments", - cf_reportutil::format_int(pf.total_comments), + cf_reportutil::format_int(counts.total_comments), + ), + metric( + "Good Comments", + cf_reportutil::format_int(counts.good_comments), + ), + metric( + "Bad Comments", + cf_reportutil::format_int(counts.bad_comments), + ), + metric( + "Doc Coverage", + format_percent(counts.documentation_coverage()), ), - metric("Good Comments", cf_reportutil::format_int(pf.good_comments)), - metric("Bad Comments", cf_reportutil::format_int(pf.bad_comments)), - metric("Doc Coverage", format_percent(pf.doc_coverage)), - metric("Good Ratio", format_percent(pf.good_ratio)), + metric("Good Ratio", format_percent(counts.overall_score())), metric( "Total Functions", - cf_reportutil::format_int(pf.total_functions), + cf_reportutil::format_int(counts.total_functions), ), - ]; + ] +} - // Distribution over THIS file's functions (absent for function-free files - // — Distribution() returns nil ⇒ omitempty). - let total_fns = pf.total_functions; - let distribution: Vec<GoValue> = if total_fns == 0 { - Vec::new() - } else { - let documented = pf.documented_functions; - let undocumented = total_fns - documented; - let dist = |label: &str, count: i64| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("percent", GoValue::Float(count as f64 / total_fns as f64)); - m.push("count", GoValue::Int(count)); - GoValue::Map(m) - }; - vec![ - dist("Documented", documented), - dist("Undocumented", undocumented), - ] +/// The Documented / Undocumented distribution; absent without declarations. +fn push_distribution(section: &mut GoMap, counts: &CommentCounts) { + let total = counts.total_functions; + if total == 0 { + return; + } + let dist = |label: &str, count: i64| { + let mut m = GoMap::new(MapOrigin::Struct); + m.push("label", GoValue::Str(label.to_string())); + m.push("percent", GoValue::Float(count as f64 / total as f64)); + m.push("count", GoValue::Int(count)); + GoValue::Map(m) }; + let documented = counts.documented_functions; + section.push( + "distribution", + GoValue::Array(vec![ + dist("Documented", documented), + dist("Undocumented", total - documented), + ]), + ); +} - // Issues: this file's undocumented functions, name ascending. - let mut names: Vec<&String> = pf.undocumented.iter().collect(); - names.sort(); - let issues: Vec<GoValue> = names - .iter() - .map(|name| { +/// One issue per undocumented declaration, sorted by name, then file, then +/// line. +fn undocumented_issues<'a>( + functions: impl Iterator<Item = (&'a FileStamp, &'a FunctionInfo)>, +) -> Vec<GoValue> { + let mut undocumented: Vec<(&FileStamp, &FunctionInfo)> = + functions.filter(|(_, f)| !f.is_documented()).collect(); + undocumented.sort_by_key(|(stamp, f)| (f.name.clone(), stamp.source_file.clone(), f.line)); + undocumented + .into_iter() + .map(|(stamp, f)| { let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str((*name).clone())); - m.push("location", GoValue::Str(pf.rel.clone())); - m.push("value", GoValue::Str("undocumented".to_string())); - m.push("severity", GoValue::Str("poor".to_string())); + m.push("name", GoValue::Str(f.name.clone())); + m.push("location", GoValue::Str(stamp.source_file.clone())); + m.push("value", GoValue::Str(ISSUE_VALUE.to_string())); + m.push("severity", GoValue::Str(ISSUE_SEVERITY.to_string())); GoValue::Map(m) }) - .collect(); - - let mut entry = GoMap::new(MapOrigin::Struct); - // The reference retainer keys the entry by `filepath.Rel(root, stamp)` — - // a DOUBLE relativization (the stamp is already root-relative). With an - // absolute root Rel errors and the stamp passes through unchanged; with a - // relative root the key walks back out of it (`Rel("full","a.go")` → - // `"../a.go"`). Issue locations below keep the plain stamp. - let file_path = go_rel(root_path, &pf.rel).unwrap_or_else(|| pf.rel.clone()); - entry.push("file_path", GoValue::Str(file_path)); - entry.push("score_label", GoValue::Str(format_score(score))); - entry.push("status", GoValue::Str(status)); - entry.push("metrics", GoValue::Array(metrics)); - if !distribution.is_empty() { - entry.push("distribution", GoValue::Array(distribution)); - } - entry.push("issues", GoValue::Array(issues)); - entry.push("score", GoValue::Float(score)); - GoValue::Map(entry) + .collect() } -/// Builds the AGGREGATED RAW `analyze.Report` GoValue for `static/comments` — -/// the value the reference implementation's `comments.Aggregator.GetResult()` returns (the base -/// `BuildCollectionResult` + the `DetailedDataCollector.AddToResult` -/// overwrite), which is what `--format plot` consumes and what -/// `writeReportJSON` serializes into `report.json`: -/// -/// * `analyzer_name`, `message` (`buildMessage` over a random numeric average; -/// all three averages land in the same threshold bucket on real corpora — -/// we key it off `overall_score` like the section), -/// * counts (summed): `total_comments`, `good_comments`, `bad_comments`, -/// `total_functions`, `documented_functions`, `total_comment_details`, -/// * averages: `overall_score`, `good_comments_ratio`, -/// `documentation_coverage`, -/// * `comments` / `functions`: the per-file convert maps concatenated in -/// walk order, each stamped `_source_file`/`_language`/`_directory` -/// (`stampCollectionMetadata`). The base spillable collector contributes an -/// empty `comments` slice (its `line` identifier is an int, never a string), -/// so with zero comments the key stays `[]`; `functions` appears only when -/// the detailed collection is non-empty. -/// -/// With no parsed files the reference implementation returns `buildEmptyResult` instead (8 keys, no -/// `analyzer_name`/collections). +// --- raw report (plot / report.json) ------------------------------------------- + +/// Builds the aggregated raw report value for `static/comments` — what +/// `--format plot` consumes: the counters and ratios, the quality message, +/// and the stamped `comments` / `functions` rows of every file (`functions` +/// only when there is at least one declaration). #[must_use] pub fn comments_raw_report_value(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - let agg = comments_aggregate_opts(root_path, filter)?; - - if agg.report_count == 0 { - let mut m = GoMap::new(MapOrigin::Map); - m.push("total_comments", GoValue::Int(0)); - m.push("good_comments", GoValue::Int(0)); - m.push("bad_comments", GoValue::Int(0)); - m.push("overall_score", GoValue::Float(0.0)); - m.push("total_functions", GoValue::Int(0)); - m.push("documented_functions", GoValue::Int(0)); - m.push("total_comment_details", GoValue::Int(0)); - m.push("message", GoValue::Str("No comments found".to_string())); - return Some(GoValue::Map(m)); - } - - let mean = |sum: f64| { - if agg.report_count > 0 { - sum / agg.report_count as f64 - } else { - 0.0 - } - }; - let overall_score = mean(agg.sum_overall_score); + let agg = aggregate(root_path, filter); + let counts = agg.counts(); - // Stamped item maps (map-origin: keys byte-sort at encode time). - let push_stamps = |m: &mut GoMap, sf: &str, lang: &str, dir: &str| { - m.push("_source_file", GoValue::Str(sf.to_string())); - if !lang.is_empty() { - m.push("_language", GoValue::Str(lang.to_string())); - } - if !dir.is_empty() { - m.push("_directory", GoValue::Str(dir.to_string())); - } - }; let comments: Vec<GoValue> = agg - .comments - .iter() - .map(|c| { + .comments() + .map(|(stamp, c)| { let mut m = GoMap::new(MapOrigin::Map); m.push("line", GoValue::Int(c.line)); - m.push("comment", GoValue::Str(c.comment.clone())); - m.push("placement", GoValue::Str(c.placement.clone())); - m.push("target", GoValue::Str(c.target.clone())); - m.push("assessment", GoValue::Str(c.assessment.clone())); - push_stamps(&mut m, &c.source_file, &c.language, &c.directory); + m.push("comment", GoValue::Str(comment_preview(&c.text))); + m.push("placement", GoValue::Str(c.placement.as_str().to_string())); + m.push("target", GoValue::Str(c.target_name.clone())); + m.push("assessment", GoValue::Str(c.assessment().to_string())); + push_stamps(&mut m, stamp); GoValue::Map(m) }) .collect(); let functions: Vec<GoValue> = agg - .functions - .iter() - .map(|f| { + .functions() + .map(|(stamp, f)| { let mut m = GoMap::new(MapOrigin::Map); - m.push("function", GoValue::Str(f.function_name.clone())); + m.push("function", GoValue::Str(f.name.clone())); m.push("type", GoValue::Str(f.kind.clone())); + m.push("line", GoValue::Int(f.line)); m.push("lines", GoValue::Int(f.lines)); - m.push("comment", GoValue::Str(f.comment.clone())); - m.push("assessment", GoValue::Str(f.assessment.clone())); - push_stamps(&mut m, &f.source_file, &f.language, &f.directory); + m.push("doc_line", GoValue::Int(f.doc_line.unwrap_or(0))); + m.push("assessment", GoValue::Str(f.assessment().to_string())); + push_stamps(&mut m, stamp); GoValue::Map(m) }) .collect(); @@ -573,369 +265,96 @@ pub fn comments_raw_report_value(root_path: &str, filter: &StaticFilter) -> Opti if !functions.is_empty() { m.push("functions", GoValue::Array(functions)); } - m.push("message", GoValue::Str(comment_message(overall_score))); - m.push("total_comments", GoValue::Int(agg.total_comments)); - m.push("good_comments", GoValue::Int(agg.good_comments)); - m.push("bad_comments", GoValue::Int(agg.bad_comments)); - m.push("total_functions", GoValue::Int(agg.total_functions)); + m.push("message", GoValue::Str(counts.message().to_string())); + push_counts(&mut m, &counts); + Some(GoValue::Map(m)) +} + +/// Pushes the file stamp keys of a row (map-origin: keys byte-sort at encode +/// time). +fn push_stamps(m: &mut GoMap, stamp: &FileStamp) { + m.push("_source_file", GoValue::Str(stamp.source_file.clone())); + if !stamp.language.is_empty() { + m.push("_language", GoValue::Str(stamp.language.clone())); + } + if !stamp.directory.is_empty() { + m.push("_directory", GoValue::Str(stamp.directory.clone())); + } +} + +/// Pushes the counters and their ratios. +fn push_counts(m: &mut GoMap, counts: &CommentCounts) { + m.push("total_comments", GoValue::Int(counts.total_comments)); + m.push("good_comments", GoValue::Int(counts.good_comments)); + m.push("bad_comments", GoValue::Int(counts.bad_comments)); + m.push("total_functions", GoValue::Int(counts.total_functions)); m.push( "documented_functions", - GoValue::Int(agg.documented_functions), - ); - m.push( - "total_comment_details", - GoValue::Int(agg.total_comment_details), + GoValue::Int(counts.documented_functions), ); - m.push("overall_score", GoValue::Float(overall_score)); + m.push("overall_score", GoValue::Float(counts.overall_score())); m.push( "good_comments_ratio", - GoValue::Float(mean(agg.sum_good_comments_ratio)), + GoValue::Float(counts.overall_score()), ); m.push( "documentation_coverage", - GoValue::Float(mean(agg.sum_documentation_coverage)), + GoValue::Float(counts.documentation_coverage()), ); - Some(GoValue::Map(m)) } -/// `terminal.FormatScore`: `round(score*10)/10` → `"N/10"`. -fn format_score(score: f64) -> String { - let scaled = (score * 10.0).round() as i64; - format!("{scaled}/10") -} +// --- computed metrics (yaml / bin) -------------------------------------------- -/// `reportutil.FormatPercent`: `"%.1f%%"` of `v*100`. -fn format_percent(v: f64) -> String { - format!("{:.1}%", v * 100.0) -} - -/// Runs the static pipeline over `root_path` and returns the assembled -/// `ComputedMetrics` go-value (the shared report value behind every machine -/// encoding). Returns `None` when the path does not exist. -fn comments_metrics(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - let agg = comments_aggregate_opts(root_path, filter)?; - Some(compute_metrics(&agg)) -} - -/// Runs the static pipeline over `root_path` and returns the cross-file -/// [`Aggregated`] accumulator (shared by the machine-format `ComputeAllMetrics` -/// path and the JSON-section path). Returns `None` when the path is missing. -fn comments_aggregate_opts(root_path: &str, filter: &StaticFilter) -> Option<Aggregated> { - // The reference static pipeline swallows walk errors: a nonexistent (or - // unreadable) root analyzes ZERO files and still emits the empty report - // with exit 0 ("static: complete files=0"), so an empty accumulator — not - // None — is the parity behavior here. - let root = Path::new(root_path); - if !root.exists() { - return Some(Aggregated::default()); - } - let parser = Parser::new(); - - let analyzer = cf_comments::Analyzer::new(); - - let mut files: Vec<std::path::PathBuf> = Vec::new(); - collect_files(root, &parser, filter, &mut files); - // filepath.WalkDir visits entries in lexical (byte-sorted) path order. - files.sort(); - - let mut agg = Aggregated::default(); - - for path in &files { - let Some(content) = super::read_source_capped(path) else { - continue; - }; - let path_str = path.to_string_lossy(); - - // The reference implementation's static pipeline parses EVERY UAST-supported file and runs the - // analyzer; files whose tree has no functions/comments (markdown - // READMEs) yield an empty report that still counts as one file in the - // cross-file averages (`report_count`). Rust may lack a wired grammar - // for some supported extensions (markdown), so a parse failure on a - // supported file is folded as that same empty report — keeping the mean - // denominator (and thus the averaged scalar metrics) byte-identical. - // Stamp metadata exactly as StampSourceFile / StampLanguage. - let stamped = make_relative_path(&path_str, root_path); - - let Ok(node) = parser.parse(&path_str, &content) else { - super::note_skipped_file(); - agg.report_count += 1; - agg.per_file.push(PerFileComments::empty(stamped)); - continue; - }; - let Ok(report) = analyzer.analyze(Some(&node)) else { - agg.report_count += 1; - agg.per_file.push(PerFileComments::empty(stamped)); - continue; - }; - - let directory = parent_dir(&stamped); - let language = parser.get_language(&path_str); - - aggregate_report(&mut agg, &report, &stamped, &directory, &language); - } - - Some(agg) -} - -/// Recursively collects UAST-supported, non-excluded regular files under `dir`. -fn collect_files( - dir: &Path, - parser: &Parser, - filter: &StaticFilter, - out: &mut Vec<std::path::PathBuf>, -) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file(dir, parser, filter, out); - return; - } - let Ok(read) = fs::read_dir(dir) else { - return; - }; - for entry in read.filter_map(Result::ok) { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let Ok(file_type) = entry.file_type() else { - continue; - }; - if file_type.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; // filepath.SkipDir on .git - } - collect_files(&path, parser, filter, out); - continue; - } - visit_file(&path, parser, filter, out); - } -} - -/// Collects one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): parser support + path policy filters. -fn visit_file( - path: &Path, - parser: &Parser, - filter: &StaticFilter, - out: &mut Vec<std::path::PathBuf>, -) { - let path_str = path.to_string_lossy(); - // ShouldSkipFolderNode: skip files the parser does not support. - if !parser.is_supported(&path_str) { - return; - } - // matchesLanguageGlobs: --languages empty → all match (no-op). - // pathpolicy.Exclude(path, nil, opts). - if filter.skips(&path_str) { - return; - } - out.push(path.to_path_buf()); -} - -/// Folds one per-file report into the cross-file accumulator. -fn aggregate_report( - agg: &mut Aggregated, - report: &GoValue, - source_file: &str, - directory: &str, - language: &str, -) { - agg.report_count += 1; - - // DetailedDataCollector: append the file's comment/function items (stamped). - let item_str = |item: &GoValue, key: &str| { - map_get(item, key) - .and_then(as_str) - .unwrap_or_default() - .to_string() - }; - if let Some(items) = map_get(report, "comments").and_then(as_array) { - for item in items { - let line = map_get(item, "line").and_then(as_int).unwrap_or(0); - agg.comments.push(CommentItem { - line, - comment: item_str(item, "comment"), - placement: item_str(item, "placement"), - target: item_str(item, "target"), - assessment: item_str(item, "assessment"), - source_file: source_file.to_string(), - language: language.to_string(), - directory: directory.to_string(), - }); - } - } - if let Some(items) = map_get(report, "functions").and_then(as_array) { - for item in items { - agg.functions.push(FunctionItem { - source_file: source_file.to_string(), - language: language.to_string(), - directory: directory.to_string(), - function_name: item_str(item, "function"), - kind: item_str(item, "type"), - lines: map_get(item, "lines").and_then(as_int).unwrap_or(0), - comment: item_str(item, "comment"), - assessment: item_str(item, "assessment"), - }); - } - } - - // MetricsProcessor: sum count keys, sum numeric keys (mean computed later). - agg.total_comments += scalar_int(report, "total_comments"); - agg.good_comments += scalar_int(report, "good_comments"); - agg.bad_comments += scalar_int(report, "bad_comments"); - agg.total_functions += scalar_int(report, "total_functions"); - agg.documented_functions += scalar_int(report, "documented_functions"); - agg.total_comment_details += scalar_int(report, "total_comment_details"); - - agg.sum_overall_score += scalar_float(report, "overall_score"); - agg.sum_good_comments_ratio += scalar_float(report, "good_comments_ratio"); - agg.sum_documentation_coverage += scalar_float(report, "documentation_coverage"); - - // PerFileRetainer.Retain: snapshot the per-file report for --per-file. - let undocumented: Vec<String> = map_get(report, "functions") - .and_then(as_array) - .map(|items| { - items - .iter() - .filter(|item| { - map_get(item, "assessment").and_then(as_str) == Some("❌ No Comment") - }) - .map(|item| item_str(item, "function")) - .collect() - }) - .unwrap_or_default(); - agg.per_file.push(PerFileComments { - rel: source_file.to_string(), - message: map_get(report, "message") - .and_then(as_str) - .unwrap_or_default() - .to_string(), - overall_score: scalar_float(report, "overall_score"), - total_comments: scalar_int(report, "total_comments"), - good_comments: scalar_int(report, "good_comments"), - bad_comments: scalar_int(report, "bad_comments"), - doc_coverage: scalar_float(report, "documentation_coverage"), - good_ratio: scalar_float(report, "good_comments_ratio"), - total_functions: scalar_int(report, "total_functions"), - documented_functions: scalar_int(report, "documented_functions"), - undocumented, - }); -} - -/// Builds the `comments.ComputedMetrics` GoValue (struct-origin: declaration -/// key order) from the aggregated report, applying the `ComputeAllMetrics` / -/// metric `Compute` transforms and sorts. -fn compute_metrics(agg: &Aggregated) -> GoValue { - let report_count = agg.report_count.max(0); - let mean = |sum: f64| { - if report_count > 0 { - sum / report_count as f64 - } else { - 0.0 - } - }; - let overall_score = mean(agg.sum_overall_score); - let good_comments_ratio = mean(agg.sum_good_comments_ratio); - let documentation_coverage = mean(agg.sum_documentation_coverage); - - // --- comment_quality: sorted by line_number ascending via the reference `sort.Slice` --- - // CommentQualityMetric.Compute calls sort.Slice (unstable pdqsort); the - // equal-line ties must follow pdqsort's permutation, not a stable order. - let mut comment_order: Vec<&CommentItem> = agg.comments.iter().collect(); - crate::handlers::go_sort::slice(&mut comment_order, |a, b| a.line < b.line); - let comment_quality: Vec<GoValue> = comment_order - .iter() - .map(|&c| { - // CommentQualityData declaration order; quality/type/target_name - // empty + score 0 (not present on the convert map) — recommendation - // omitempty (empty → omitted). - let mut m = GoMap::new(MapOrigin::Struct); - m.push("line_number", GoValue::Int(c.line)); - m.push("source_file", GoValue::Str(c.source_file.clone())); - m.push("language", GoValue::Str(c.language.clone())); - m.push("directory", GoValue::Str(c.directory.clone())); - m.push("quality", GoValue::Str(String::new())); - m.push("type", GoValue::Str(String::new())); - m.push("target_name", GoValue::Str(String::new())); - m.push("score", GoValue::Int(0)); - GoValue::Map(m) - }) +/// Builds the computed-metrics value (struct-origin: declaration key order): +/// `comment_quality` (one row per comment), `function_documentation` (one +/// row per declaration), `undocumented_functions` (the undocumented subset +/// with a length-based risk level) and the `aggregate` counters. Rows are +/// sorted by file, then line. +fn computed_metrics(agg: &Aggregator) -> GoValue { + let mut comments: Vec<(&FileStamp, &CommentDetail)> = agg.comments().collect(); + comments.sort_by_key(|(stamp, c)| (stamp.source_file.clone(), c.line)); + let comment_quality: Vec<GoValue> = comments + .into_iter() + .map(|(stamp, c)| comment_quality_row(stamp, c)) .collect(); - // --- function_documentation: sorted by documentation_score asc (sort.Slice) --- - // documentation_score is 0 for every function (the convert map carries no - // `comment_score`), so every key is equal; the emitted order is pdqsort's - // permutation of the equal-key input, reproduced via go_sort. - let mut doc_order: Vec<&FunctionItem> = agg.functions.iter().collect(); - crate::handlers::go_sort::slice(&mut doc_order, |_a, _b| false); - let function_documentation: Vec<GoValue> = doc_order + let mut functions: Vec<(&FileStamp, &FunctionInfo)> = agg.functions().collect(); + functions.sort_by_key(|(stamp, f)| (stamp.source_file.clone(), f.line)); + let function_documentation: Vec<GoValue> = functions .iter() - .map(|&f| { - // !HasComment → status "Undocumented", is_documented false, score 0. - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(String::new())); - m.push("source_file", GoValue::Str(f.source_file.clone())); - m.push("language", GoValue::Str(f.language.clone())); - m.push("directory", GoValue::Str(f.directory.clone())); - m.push("is_documented", GoValue::Bool(false)); - m.push("documentation_score", GoValue::Int(0)); - m.push("status", GoValue::Str("Undocumented".to_string())); - GoValue::Map(m) - }) + .map(|(stamp, f)| function_documentation_row(stamp, f)) .collect(); - - // --- undocumented_functions: every function (none documented), risk MEDIUM --- - // needs_comment false for all → all MEDIUM → all equal RiskPriority; the - // emitted order is pdqsort's permutation of the equal-key input (sort.Slice). - let mut risk_order: Vec<&FunctionItem> = agg.functions.iter().collect(); - crate::handlers::go_sort::slice(&mut risk_order, |_a, _b| false); - let undocumented_functions: Vec<GoValue> = risk_order + let undocumented_functions: Vec<GoValue> = functions .iter() - .map(|&f| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(String::new())); - m.push("source_file", GoValue::Str(f.source_file.clone())); - m.push("language", GoValue::Str(f.language.clone())); - m.push("directory", GoValue::Str(f.directory.clone())); - m.push("needs_comment", GoValue::Bool(false)); - m.push("risk_level", GoValue::Str("MEDIUM".to_string())); - GoValue::Map(m) - }) + .filter(|(_, f)| !f.is_documented()) + .map(|(stamp, f)| undocumented_row(stamp, f)) .collect(); - // --- aggregate --- + let counts = agg.counts(); let mut aggregate = GoMap::new(MapOrigin::Struct); - aggregate.push("total_comments", GoValue::Int(agg.total_comments)); - aggregate.push("good_comments", GoValue::Int(agg.good_comments)); - aggregate.push("bad_comments", GoValue::Int(agg.bad_comments)); - aggregate.push("overall_score", GoValue::Float(overall_score)); - aggregate.push("total_functions", GoValue::Int(agg.total_functions)); + aggregate.push("total_comments", GoValue::Int(counts.total_comments)); + aggregate.push("good_comments", GoValue::Int(counts.good_comments)); + aggregate.push("bad_comments", GoValue::Int(counts.bad_comments)); + aggregate.push("overall_score", GoValue::Float(counts.overall_score())); + aggregate.push("total_functions", GoValue::Int(counts.total_functions)); aggregate.push( "documented_functions", - GoValue::Int(agg.documented_functions), + GoValue::Int(counts.documented_functions), + ); + aggregate.push( + "good_comments_ratio", + GoValue::Float(counts.overall_score()), ); - aggregate.push("good_comments_ratio", GoValue::Float(good_comments_ratio)); aggregate.push( "documentation_coverage", - GoValue::Float(documentation_coverage), + GoValue::Float(counts.documentation_coverage()), ); - aggregate.push("health_score", GoValue::Float(overall_score * 100.0)); - // buildMessage over the overall_score average (the value captured in the - // golden); ThresholdLabeler: ≥0.8 Excellent, ≥0.6 Good, ≥0.4 Fair, else Poor. - // With ZERO analyzed files the aggregator instead returns `buildEmptyResult`, - // whose message is "No comments found". - let message = if report_count == 0 { - "No comments found".to_string() - } else { - comment_message(overall_score) - }; - aggregate.push("message", GoValue::Str(message)); + aggregate.push( + "health_score", + GoValue::Float(counts.overall_score() * 100.0), + ); + aggregate.push("message", GoValue::Str(counts.message().to_string())); - // ComputedMetrics struct order: comment_quality, function_documentation, - // undocumented_functions, aggregate. let mut root = GoMap::new(MapOrigin::Struct); root.push("comment_quality", GoValue::Array(comment_quality)); root.push( @@ -950,159 +369,178 @@ fn compute_metrics(agg: &Aggregated) -> GoValue { GoValue::Map(root) } -/// `common.ThresholdLabeler` for comments (reference thresholds). -fn comment_message(score: f64) -> String { - if score >= 0.8 { - "Excellent comment quality and placement".to_string() - } else if score >= 0.6 { - "Good comment quality with room for improvement".to_string() - } else if score >= 0.4 { - "Fair comment quality - consider improving placement".to_string() - } else { - "Poor comment quality - significant improvement needed".to_string() - } +fn comment_quality_row(stamp: &FileStamp, c: &CommentDetail) -> GoValue { + let quality = if c.is_good() { "good" } else { "bad" }; + let mut m = GoMap::new(MapOrigin::Struct); + m.push("line_number", GoValue::Int(c.line)); + push_location(&mut m, stamp); + m.push("quality", GoValue::Str(quality.to_string())); + m.push("placement", GoValue::Str(c.placement.as_str().to_string())); + m.push("target_type", GoValue::Str(c.target_type.clone())); + m.push("target_name", GoValue::Str(c.target_name.clone())); + m.push("score", GoValue::Float(c.score)); + GoValue::Map(m) } -// --- report-map accessors over cf-gojson GoValue ---------------------------- - -fn map_get<'a>(v: &'a GoValue, key: &str) -> Option<&'a GoValue> { - match v { - GoValue::Map(m) => m.get(key), - _ => None, +fn function_documentation_row(stamp: &FileStamp, f: &FunctionInfo) -> GoValue { + let status = if f.is_documented() { + "Documented" + } else { + "Undocumented" + }; + let mut m = GoMap::new(MapOrigin::Struct); + push_identity(&mut m, f); + push_location(&mut m, stamp); + m.push("is_documented", GoValue::Bool(f.is_documented())); + m.push("doc_line", GoValue::Int(f.doc_line.unwrap_or(0))); + m.push("status", GoValue::Str(status.to_string())); + GoValue::Map(m) +} + +fn undocumented_row(stamp: &FileStamp, f: &FunctionInfo) -> GoValue { + let mut m = GoMap::new(MapOrigin::Struct); + push_identity(&mut m, f); + push_location(&mut m, stamp); + m.push("risk_level", GoValue::Str(risk_level(f.lines).to_string())); + GoValue::Map(m) +} + +fn push_identity(m: &mut GoMap, f: &FunctionInfo) { + m.push("name", GoValue::Str(f.name.clone())); + m.push("type", GoValue::Str(f.kind.clone())); + m.push("line", GoValue::Int(f.line)); + m.push("lines", GoValue::Int(f.lines)); +} + +fn push_location(m: &mut GoMap, stamp: &FileStamp) { + m.push("source_file", GoValue::Str(stamp.source_file.clone())); + m.push("language", GoValue::Str(stamp.language.clone())); + m.push("directory", GoValue::Str(stamp.directory.clone())); +} + +/// Documentation risk of an undocumented declaration, by its length. +fn risk_level(lines: i64) -> &'static str { + if lines >= RISK_HIGH_MIN_LINES { + "HIGH" + } else if lines >= RISK_MEDIUM_MIN_LINES { + "MEDIUM" + } else { + "LOW" } } -fn as_array(v: &GoValue) -> Option<&Vec<GoValue>> { - match v { - GoValue::Array(a) => Some(a), - _ => None, - } +/// `round(score*10)/10` → `"N/10"`. +fn format_score(score: f64) -> String { + let scaled = (score * 10.0).round() as i64; + format!("{scaled}/10") } -fn as_int(v: &GoValue) -> Option<i64> { - match v { - GoValue::Int(n) => Some(*n), - _ => None, - } +/// `"%.1f%%"` of `v*100`. +fn format_percent(v: f64) -> String { + format!("{:.1}%", v * 100.0) } -fn as_str(v: &GoValue) -> Option<&str> { - match v { - GoValue::Str(s) => Some(s.as_str()), - _ => None, - } -} +// --- folder walk + per-file analysis ------------------------------------------ -/// `ParseReportData` reads `total_comments` etc. as the reference `int`; the analyzer emits -/// them as `GoValue::Int`. -fn scalar_int(report: &GoValue, key: &str) -> i64 { - map_get(report, key).and_then(as_int).unwrap_or(0) +/// Runs the static pipeline over `root_path` through the shared static walk +/// and returns the cross-file aggregation. A nonexistent root analyzes zero +/// files (the empty report). +fn aggregate(root_path: &str, filter: &StaticFilter) -> Aggregator { + let mut agg = Aggregator::new(); + let parser = Parser::new(); + let Ok(walk) = super::walk_source_files(root_path, &parser, filter) else { + return agg; + }; + let analyzer = cf_comments::Analyzer::new(); + for file in walk { + let stamp = file_stamp(&file); + let Ok(node) = parser.parse(&file.rel_path, &file.bytes) else { + // A supported extension without a usable grammar (markdown): + // listed as an empty file, never a skip. + agg.add_empty(stamp); + continue; + }; + match analyzer.analyze_metrics(Some(&node), None) { + Ok(metrics) => agg.add(stamp, metrics), + Err(_) => agg.add_empty(stamp), + } + } + agg } -/// Reads a numeric metric key as a float; absent (e.g. the empty result lacks -/// `good_comments_ratio`/`documentation_coverage`) contributes 0 to the sum but -/// the report still counts toward the mean denominator. -fn scalar_float(report: &GoValue, key: &str) -> f64 { - match map_get(report, key) { - Some(GoValue::Float(f)) => *f, - Some(GoValue::Int(n)) => *n as f64, - _ => 0.0, +/// The file's stamp: root-relative path, its directory and its language. +fn file_stamp(file: &SourceFile) -> FileStamp { + FileStamp { + directory: file.directory(), + language: file.language.clone(), + source_file: file.rel_path.clone(), } } -// --- path helpers (filepath.Rel / filepath.Dir parity for this flat case) ---- +#[cfg(test)] +mod tests { + use super::*; -/// `analyze.MakeRelativePath`: the path made relative to `root` (slash-joined). -/// For files directly under `root` this is the bare basename. -fn make_relative_path(path: &str, root: &str) -> String { - let root_trimmed = root.trim_end_matches('/'); - if let Some(rest) = path.strip_prefix(root_trimmed) { - let rest = rest.trim_start_matches('/'); - if !rest.is_empty() { - return rest.to_string(); - } - // filepath.Rel(root, root) == "." (a FILE root stamps "."). - return ".".to_string(); + /// One documented (`add`) and one undocumented (`sub`) function. + const A_GO: &str = "package main\n\n// add sums two ints.\nfunc add(a, b int) int {\n\treturn a + b\n}\n\nfunc sub(a, b int) int {\n\treturn a - b\n}\n"; + /// One documented function out of four. + const B_GO: &str = "package main\n\n// g does g.\nfunc g() int {\n\treturn 1\n}\n\nfunc h() int {\n\treturn 2\n}\n\nfunc i() int {\n\treturn 3\n}\n\nfunc j() int {\n\treturn 4\n}\n"; + /// A parser-supported file with neither declarations nor comments. + const README_MD: &str = "# Title\n\nSome text.\n"; + /// A declaration-free, comment-free Go file. + const EMPTY_GO: &str = "package main\n\nvar version = \"1\"\n"; + + fn write(dir: &tempfile::TempDir, name: &str, content: &str) { + std::fs::write(dir.path().join(name), content).expect("fixture file"); } - path.to_string() -} -/// `filepath.Rel(base, target)` over slash paths — the reference per-file -/// retainer key is `Rel(root, stamp)` where `stamp` is ALREADY root-relative -/// (a double relativization). Returns `None` where Go's `Rel` errors (mixed -/// absolute/relative arguments — i.e. an absolute root with a relative stamp); -/// the caller then keeps `target`, the reference fallback. -fn go_rel(base: &str, target: &str) -> Option<String> { - if base.starts_with('/') != target.starts_with('/') { - return None; - } - fn clean(p: &str) -> Vec<&str> { - p.split('/') - .filter(|c| !c.is_empty() && *c != ".") - .collect() + /// Fixture: `a.go` (1/2 documented) and a declaration-free `empty.go`. + fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + write(&dir, "a.go", A_GO); + write(&dir, "empty.go", EMPTY_GO); + dir } - let b = clean(base); - let t = clean(target); - let common = b.iter().zip(t.iter()).take_while(|(x, y)| x == y).count(); - let mut parts: Vec<&str> = vec![".."; b.len() - common]; - parts.extend_from_slice(&t[common..]); - if parts.is_empty() { - return Some(".".to_string()); + + fn root(dir: &tempfile::TempDir) -> &str { + dir.path().to_str().expect("utf-8 tempdir") } - Some(parts.join("/")) -} -/// `filepath.Dir(stamped)`: the directory portion, or "." when none. -fn parent_dir(stamped: &str) -> String { - match stamped.rfind('/') { - Some(0) => "/".to_string(), - Some(idx) => stamped[..idx].to_string(), - None => ".".to_string(), + fn json(dir: &tempfile::TempDir, per_file: bool) -> String { + let bytes = comments_report_json_flags(root(dir), &StaticFilter::default(), per_file) + .expect("json report"); + String::from_utf8(bytes).expect("utf-8 json") } -} -#[cfg(test)] -mod per_file_tests { - use super::*; + fn yaml(dir: &tempfile::TempDir) -> String { + let bytes = comments_report_yaml(root(dir), &StaticFilter::default()).expect("yaml"); + String::from_utf8(bytes).expect("utf-8 yaml") + } - /// Fixture: one file with a documented + an undocumented function, one - /// function-free file. - fn fixture() -> tempfile::TempDir { - let dir = tempfile::tempdir().expect("tempdir"); - std::fs::write( - dir.path().join("a.go"), - "package main\n\n// add sums two ints.\nfunc add(a, b int) int {\n\treturn a + b\n}\n\nfunc sub(a, b int) int {\n\treturn a - b\n}\n", - ) - .unwrap(); - std::fs::write( - dir.path().join("types.go"), - "package main\n\ntype Config struct {\n\tName string\n}\n", - ) - .unwrap(); - dir + fn metric_value(json: &str, label: &str) -> String { + let needle = format!("\"label\": \"{label}\",\n \"value\": \""); + let start = json + .find(&needle) + .unwrap_or_else(|| panic!("{label} missing:\n{json}")) + + needle.len(); + json[start..].split('"').next().expect("value").to_string() } #[test] fn per_file_flag_emits_files_entries() { let dir = fixture(); - let bytes = comments_report_json_flags( - dir.path().to_str().unwrap(), - &StaticFilter::default(), - true, - ) - .unwrap(); - let json = String::from_utf8(bytes).unwrap(); + let json = json(&dir, true); assert!(json.contains("\"files\""), "files key missing:\n{json}"); assert!( json.contains("\"file_path\": \"a.go\""), "a.go entry missing:\n{json}" ); assert!( - json.contains("\"file_path\": \"types.go\""), - "types.go entry missing:\n{json}" + json.contains("\"file_path\": \"empty.go\""), + "empty.go entry missing:\n{json}" ); - // Per-file status is the ANALYZER's per-file message; a comment-free - // file reports the empty-result message. + // A file with neither comments nor declarations reports the + // empty-result message. assert!( json.contains("\"status\": \"No comments found\""), "per-file empty-result status missing:\n{json}" @@ -1117,17 +555,186 @@ mod per_file_tests { #[test] fn no_per_file_flag_omits_files() { let dir = fixture(); - let bytes = comments_report_json_flags( - dir.path().to_str().unwrap(), - &StaticFilter::default(), - false, - ) - .unwrap(); - let json = String::from_utf8(bytes).unwrap(); + let json = json(&dir, false); assert!( !json.contains("\"files\""), "files key must be omitted:\n{json}" ); assert!(!json.contains("\"file_path\"")); } + + #[test] + fn coverage_and_good_ratio_are_weighted_over_all_files() { + let dir = tempfile::tempdir().expect("tempdir"); + write(&dir, "a.go", A_GO); + write(&dir, "b.go", B_GO); + write(&dir, "README.md", README_MD); + let json = json(&dir, false); + // 2 documented of 6 functions (1/2 + 1/4), never the mean of the + // per-file ratios (0.5, 0.25, 0) and never diluted by the README. + assert_eq!(metric_value(&json, "Total Functions"), "6"); + assert_eq!(metric_value(&json, "Doc Coverage"), "33.3%"); + assert_eq!(metric_value(&json, "Good Ratio"), "100.0%"); + assert_eq!(metric_value(&json, "Total Comments"), "2"); + assert!( + json.contains("\"overall_score\": 1\n"), + "score follows the weighted good ratio:\n{json}" + ); + } + + #[test] + fn zero_comment_file_counts_its_functions() { + let dir = tempfile::tempdir().expect("tempdir"); + write( + &dir, + "a.go", + "package main\n\nfunc add() int {\n\treturn 1\n}\n\nfunc sub() int {\n\treturn 2\n}\n", + ); + let json = json(&dir, false); + assert_eq!(metric_value(&json, "Total Functions"), "2"); + assert_eq!(metric_value(&json, "Doc Coverage"), "0.0%"); + assert!(json.contains("\"name\": \"add\""), "{json}"); + assert!(json.contains("\"name\": \"sub\""), "{json}"); + } + + #[test] + fn yaml_rows_carry_real_names_and_status() { + let dir = fixture(); + let yaml = yaml(&dir); + for needle in [ + "name: add\n", + "is_documented: true", + "status: Documented", + "name: sub\n", + "is_documented: false", + "status: Undocumented", + "target_name: add", + "quality: good", + "placement: above", + "total_functions: 2", + "documented_functions: 1", + "documentation_coverage: 0.5", + ] { + assert!(yaml.contains(needle), "yaml lacks {needle:?}:\n{yaml}"); + } + let undocumented = yaml + .split("undocumented_functions:") + .nth(1) + .and_then(|rest| rest.split("aggregate:").next()) + .expect("undocumented_functions block"); + assert_eq!(undocumented.matches("- name:").count(), 1, "{undocumented}"); + assert!(undocumented.contains("name: sub"), "{undocumented}"); + assert!(undocumented.contains("risk_level: LOW"), "{undocumented}"); + } + + #[test] + fn risk_level_follows_the_declaration_length() { + assert_eq!(risk_level(RISK_MEDIUM_MIN_LINES - 1), "LOW"); + assert_eq!(risk_level(RISK_MEDIUM_MIN_LINES), "MEDIUM"); + assert_eq!(risk_level(RISK_HIGH_MIN_LINES), "HIGH"); + } + + #[test] + fn bin_payload_is_the_yaml_value_in_a_cfb1_envelope() { + let dir = fixture(); + let bytes = comments_report_bin(root(&dir), &StaticFilter::default()).expect("bin"); + assert_eq!(&bytes[..4], b"CFB1"); + let payload = String::from_utf8_lossy(&bytes[8..]); + assert!(payload.contains("\"documented_functions\":1"), "{payload}"); + assert!(payload.contains("\"name\":\"sub\""), "{payload}"); + } + + #[test] + fn file_root_stamps_the_file_name() { + let dir = fixture(); + let file = dir.path().join("a.go"); + let bytes = + comments_report_json_flags(file.to_str().unwrap(), &StaticFilter::default(), true) + .expect("json report"); + let json = String::from_utf8(bytes).expect("utf-8 json"); + assert!(json.contains("\"file_path\": \"a.go\""), "{json}"); + assert!(json.contains("\"location\": \"a.go\""), "{json}"); + assert!(!json.contains("\"file_path\": \".\""), "{json}"); + } + + #[test] + fn nested_files_are_stamped_root_relative() { + let dir = fixture(); + std::fs::create_dir_all(dir.path().join("sub")).expect("sub dir"); + std::fs::write(dir.path().join("sub").join("c.go"), A_GO).expect("sub/c.go"); + let json = json(&dir, true); + assert!(json.contains("\"file_path\": \"sub/c.go\""), "{json}"); + assert!(json.contains("\"location\": \"sub/c.go\""), "{json}"); + assert!(!json.contains("\"../"), "{json}"); + } + + #[test] + fn empty_directory_yields_the_empty_report() { + let dir = tempfile::tempdir().expect("tempdir"); + let json = json(&dir, false); + assert!(json.contains("\"status\": \"No comments found\""), "{json}"); + assert_eq!(metric_value(&json, "Total Functions"), "0"); + assert!(!json.contains("\"distribution\""), "{json}"); + let raw = comments_raw_report_value(root(&dir), &StaticFilter::default()).expect("raw"); + assert_eq!( + raw.as_map().and_then(|m| m.get("total_functions")), + Some(&GoValue::Int(0)) + ); + assert!(raw.as_map().and_then(|m| m.get("functions")).is_none()); + } + + #[test] + fn unparsable_supported_file_is_an_empty_file() { + let dir = tempfile::tempdir().expect("tempdir"); + write(&dir, "README.md", README_MD); + let json = json(&dir, true); + assert!(json.contains("\"file_path\": \"README.md\""), "{json}"); + assert!(json.contains("\"status\": \"No comments found\""), "{json}"); + assert_eq!(metric_value(&json, "Total Functions"), "0"); + } + + #[test] + fn text_summary_shares_the_weighted_metrics() { + let dir = tempfile::tempdir().expect("tempdir"); + write(&dir, "a.go", A_GO); + write(&dir, "b.go", B_GO); + let summary = comments_report_value_summary(root(&dir), &StaticFilter::default()) + .expect("summary value"); + let encoded = cf_gojson::Encoder::indented(" ").encode(&summary); + let text = String::from_utf8(encoded).expect("utf-8"); + assert_eq!(metric_value(&text, "Doc Coverage"), "33.3%"); + assert!( + !text.contains("\"name\""), + "summary carries no issues:\n{text}" + ); + } + + #[test] + fn raw_report_rows_are_stamped() { + let dir = fixture(); + let raw = comments_raw_report_value(root(&dir), &StaticFilter::default()).expect("raw"); + let functions = match raw.as_map().and_then(|m| m.get("functions")) { + Some(GoValue::Array(rows)) => rows.clone(), + other => panic!("functions missing: {other:?}"), + }; + assert_eq!(functions.len(), 2); + let first = functions[0].as_map().expect("row map"); + assert_eq!( + first.get("function"), + Some(&GoValue::Str("add".to_string())) + ); + assert_eq!( + first.get("_source_file"), + Some(&GoValue::Str("a.go".to_string())) + ); + assert_eq!( + first.get("_language"), + Some(&GoValue::Str("go".to_string())) + ); + assert_eq!( + first.get("_directory"), + Some(&GoValue::Str(".".to_string())) + ); + assert_eq!(first.get("doc_line"), Some(&GoValue::Int(3))); + } } diff --git a/crates/cf-commands/src/handlers/static_complexity.rs b/crates/cf-commands/src/handlers/static_complexity.rs index 0944dcc6..f282eda5 100644 --- a/crates/cf-commands/src/handlers/static_complexity.rs +++ b/crates/cf-commands/src/handlers/static_complexity.rs @@ -1,1003 +1,362 @@ -//! Static-analysis JSON report path for the UAST `static/complexity` analyzer. +//! The `static/complexity` handlers: one directory walk shared by every +//! output format, plus the JSON sections report (`--format json`, also the +//! value behind `text` / `compact`) and the raw aggregated report +//! (`--format plot` / `report.json`). The yaml and bin siblings +//! ([`super::static_complexity_yaml`], [`super::static_complexity_bin`]) +//! serialize the same [`Collected`] walk products through +//! [`cf_complexity::report::computed_metrics`]. //! -//! Reproduces the reference static pipeline for the single-analyzer -//! `codefang run --analyzers static/complexity --format json` capture: -//! -//! 1. the reference `StaticService.uastPhase` -//! walks `rootPath` with `filepath.WalkDir` in lexical order — directories -//! are recursed (except `.git`), and every regular file that (a) is -//! UAST-supported (`parser.IsSupported`), (b) matches the `--languages` -//! globs (none here → all), and (c) is **not** excluded by -//! `pathpolicy.Exclude(path, nil, opts)` is streamed for analysis. -//! 2. Each surviving file is parsed into a UAST and the complexity analyzer -//! computes per-function metrics (`(*Analyzer).Analyze` → -//! `calculateAllFunctionMetrics`): the per-file `functions` table is sorted -//! by cyclomatic desc, cognitive desc, name asc (`sort.Slice`, pdqsort). -//! Each item is stamped with `_source_file` (path made relative to the -//! analyzed root, here the file basename). -//! 3. The complexity `Aggregator` -//! sums the count/numeric totals across files, tracks the true max -//! complexity, concatenates the per-file `functions` tables in file-walk -//! order (`DetailedDataCollector`, append, no dedup), and computes the -//! derived `average_complexity` / `max_complexity` / `message`. -//! 4. The aggregated report becomes one `complexity.ReportSection` -//! (`renderer.SectionsToJSON`): the section status is the aggregator -//! message, the 6 key metrics, the 4-bucket distribution, and the issues -//! list = **all** functions sorted by cyclomatic desc, cognitive desc, -//! nesting desc, name asc (`mapx.SortAndLimit`, `sort.Slice`/pdqsort), each -//! rendered `name`/`location`/`value`/`severity`. -//! 5. Serialized via `json.NewEncoder(w).SetIndent("", " ").Encode(report)` — -//! two-space indent, one trailing newline — routed through cf-gojson. -//! -//! The two `sort.Slice` calls are the reference implementation's **unstable** pdqsort; we reproduce its -//! exact element movement with [`go_pdqsort`] so the tie order (functions equal -//! on every sort key) matches the reference output byte-for-byte. - -use std::fs; -use std::path::Path; - -use cf_complexity::node::{Node as CxNode, Positions as CxPositions}; -use cf_complexity::{Analyzer, FunctionMetrics}; +//! Division of labour: [`cf_complexity`] owns every number (counting rules, +//! thresholds, aggregation, ordering, the UAST bridge); the shared static +//! walk ([`super::walk_source_files`]) owns file selection (parser support, +//! `--languages` globs, path policy, the generated-content marker, the read +//! cap); this module owns the per-file stamping and the section/value +//! shapes. + +use cf_complexity::node::Node as CxNode; +use cf_complexity::report::{aggregate, function_order, Aggregate, FunctionInput}; +use cf_complexity::thresholds::{ + cognitive_assessment, complexity_assessment, complexity_message, nesting_assessment, + section_score, severity, terminal_bucket, TERMINAL_BUCKETS, +}; +use cf_complexity::{AnalysisOptions, Analyzer}; use cf_gojson::{Encoder, GoMap, GoValue, MapOrigin}; use cf_uast::Parser; -use cf_uast_node::Node as UastNode; - -use super::StaticFilter; -// --- Section rendering constants --- +use super::{SkipReason, SourceFile, StaticFilter}; const SECTION_TITLE: &str = "COMPLEXITY"; - -const SCORE_EXCELLENT_THRESHOLD: f64 = 1.0; -const SCORE_GOOD_THRESHOLD: f64 = 3.0; -const SCORE_FAIR_THRESHOLD: f64 = 5.0; -const SCORE_MODERATE_THRESHOLD: f64 = 7.0; -const SCORE_POOR_THRESHOLD: f64 = 10.0; - -const SCORE_EXCELLENT: f64 = 1.0; -const SCORE_GOOD: f64 = 0.8; -const SCORE_FAIR: f64 = 0.6; -const SCORE_MODERATE: f64 = 0.4; -const SCORE_POOR: f64 = 0.2; -const SCORE_CRITICAL: f64 = 0.1; - -const DIST_SIMPLE_MAX: i64 = 5; -const DIST_MODERATE_MAX: i64 = 10; -const DIST_COMPLEX_MAX: i64 = 20; -const DIST_LABEL_SIMPLE: &str = "Simple (1-5)"; -const DIST_LABEL_MOD: &str = "Moderate (6-10)"; -const DIST_LABEL_COMPLEX: &str = "Complex (11-20)"; -const DIST_LABEL_VERYC: &str = "Very Complex (>20)"; - -const ISSUE_SEVERITY_FAIR_MIN: i64 = 6; -const ISSUE_SEVERITY_POOR_MIN: i64 = 11; - -const SEVERITY_GOOD: &str = "good"; -const SEVERITY_FAIR: &str = "fair"; -const SEVERITY_POOR: &str = "poor"; +const ANALYZER_NAME: &str = "complexity"; const METRIC_TOTAL_FUNCTIONS: &str = "Total Functions"; const METRIC_AVG_COMPLEXITY: &str = "Avg Complexity"; const METRIC_MAX_COMPLEXITY: &str = "Max Complexity"; const METRIC_TOTAL_COMPLEXITY: &str = "Total Complexity"; const METRIC_COGNITIVE_TOTAL: &str = "Cognitive Total"; +const METRIC_MAX_NESTING: &str = "Max Nesting"; const METRIC_DECISION_POINTS: &str = "Decision Points"; +/// Section status when the walk analyzed no file at all. const DEFAULT_STATUS_MESSAGE: &str = "No complexity data available"; +/// Per-file status / raw-report message for a file without functions. +const MSG_NO_FUNCTIONS: &str = "No functions found"; -// Aggregator messages (reference buildComplexityMessage). -const MSG_EXCELLENT: &str = "Excellent complexity - functions are simple and maintainable"; -const MSG_GOOD: &str = "Good complexity - functions have reasonable complexity"; -const MSG_FAIR: &str = "Fair complexity - some functions could be simplified"; -const MSG_HIGH: &str = "High complexity - functions are complex and should be refactored"; +/// Score label denominator: scores are rendered as `N/10`. +const SCORE_LABEL_SCALE: f64 = 10.0; -/// One aggregated function record (a per-file metric stamped with its source -/// file), carrying the numeric sort keys plus the rendered location and the -/// per-file stamps the raw aggregated report serializes (`_language`; the -/// `_directory` stamp derives from `location`). -pub(crate) struct FnRecord { - pub(crate) name: String, - pub(crate) cyclomatic: i64, - pub(crate) cognitive: i64, - pub(crate) nesting: i64, - pub(crate) lines_of_code: i64, - pub(crate) location: String, - pub(crate) language: String, +/// One analyzed (parsed) file and its functions, in crate order. +pub(crate) struct AnalyzedFile { + /// Path relative to the analyzed root (the file name for a FILE root). + location: String, + /// Functions stamped with `location`, language and directory. + functions: Vec<FunctionInput>, } -/// Builds the `static/complexity --format json` report bytes for `root_path`, -/// or `None` when the path cannot be read (the reference implementation would surface a walk error; the -/// caller then falls through to the blocked-dependency sentinel). -#[must_use] -pub fn complexity_report(root_path: &str) -> Option<Vec<u8>> { - complexity_report_flags(root_path, &StaticFilter::default(), false) +/// The walk products: every parsed file in walk order, function-free files +/// included. +pub(crate) struct Collected { + files: Vec<AnalyzedFile>, +} + +impl Collected { + /// Every function across all files, in the report order + /// ([`function_order`]: cyclomatic desc, cognitive desc, nesting desc, + /// name, file, line). + pub(crate) fn functions(&self) -> Vec<FunctionInput> { + let mut all: Vec<FunctionInput> = self + .files + .iter() + .flat_map(|f| f.functions.iter().cloned()) + .collect(); + all.sort_by(function_order); + all + } +} + +/// Walks `root_path` through the shared static walk and analyzes every +/// admitted file, or `None` when the path does not exist. +pub(crate) fn collect(root_path: &str, filter: &StaticFilter) -> Option<Collected> { + let parser = Parser::new(); + let files = super::walk_source_files(root_path, &parser, filter) + .ok()? + .filter_map(|file| analyze_file(&file, &parser)) + .collect(); + Some(Collected { files }) +} + +/// Parses and measures one admitted file; `None` when it fails to parse +/// (counted as skipped). +fn analyze_file(file: &SourceFile, parser: &Parser) -> Option<AnalyzedFile> { + let Ok(tree) = parser.parse(&file.rel_path, &file.bytes) else { + super::note_skipped(&file.path, SkipReason::Unparseable); + return None; + }; + let root = CxNode::from_uast(&tree); + let metrics = Analyzer.function_metrics_with( + Some(&root), + &AnalysisOptions { + source: Some(&file.bytes), + find_depth: None, + }, + ); + let directory = file.directory(); + let functions = metrics + .iter() + .map(|m| FunctionInput::stamped(m, &file.rel_path, &file.language, &directory)) + .collect(); + Some(AnalyzedFile { + location: file.rel_path.clone(), + functions, + }) } -/// `static/complexity --format json` with the run-level static flags applied: -/// `filter` carries `--languages` + `--include-vendored` / `--include-generated` -/// (the shared walk filter the reference implementation builds in `pathPolicyFromFlags` / -/// `applyStaticLanguageFilter`), and -/// `per_file` enables the reference implementation's `--per-file` section enrichment -/// (`StaticService.enrichWithPerFileData` → `JSONReport.EnrichWithPerFileData`: -/// one `JSONFileEntry` per ANALYZED file — function-free files included — keyed -/// into the section's `files` array). +/// `static/complexity --format json` bytes: the sections report, with the +/// `--per-file` enrichment when `per_file` is set. `None` when the path does +/// not exist. #[must_use] pub fn complexity_report_flags( root_path: &str, filter: &StaticFilter, per_file: bool, ) -> Option<Vec<u8>> { - let report = complexity_report_value_opts(root_path, false, filter, per_file)?; - let bytes = Encoder::indented(" ") - .with_trailing_newline(true) - .encode_to_vec(&report); - Some(bytes) + let report = complexity_report_value_flags(root_path, filter, per_file)?; + Some( + Encoder::indented(" ") + .with_trailing_newline(true) + .encode_to_vec(&report), + ) } -/// Builds the `static/complexity` `renderer.JSONReport` GoValue (single scored -/// section), shared by the single-analyzer byte path and the multi-analyzer -/// static-JSON merge. `None` when the path cannot be walked. -#[must_use] -pub fn complexity_report_value(root_path: &str) -> Option<GoValue> { - complexity_report_value_opts(root_path, false, &StaticFilter::default(), false) -} - -/// [`complexity_report_value`] with the run-level walk filter and the -/// `--per-file` section enrichment (the multi-analyzer static-JSON merge -/// threads the run flags through this). +/// The sections report value (one scored `COMPLEXITY` section) with the +/// optional `--per-file` `files` array. Shared by the single-analyzer JSON +/// path and the multi-analyzer static-JSON merge. #[must_use] pub fn complexity_report_value_flags( root_path: &str, filter: &StaticFilter, per_file: bool, ) -> Option<GoValue> { - complexity_report_value_opts(root_path, false, filter, per_file) + let collected = collect(root_path, filter)?; + Some(build_json_report(&collected, per_file)) } -/// Builds the `static/complexity` section tree in the reference implementation's `AggregationModeSummaryOnly` -/// shape used for the `text` / `compact` formats: the per-item `functions` -/// detailed collection is a no-op, so the distribution and top-issues sections -/// (both derived from `functions`) are absent while the scalar Key Metrics -/// (computed by the always-on `MetricsProcessor`) are unchanged. mirrors the reference implementation -/// `ResolveAggregationMode(FormatText|FormatCompact) -> SummaryOnly`. +/// The section value the `text` / `compact` renderers consume: the same +/// section as `--format json` (key metrics, distribution and the ranked +/// function rows the text renderer prints as "Top Issues"), never per-file. #[must_use] pub fn complexity_report_value_summary(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - complexity_report_value_opts(root_path, true, filter, false) + complexity_report_value_flags(root_path, filter, false) } -fn complexity_report_value_opts( - root_path: &str, - summary_only: bool, - filter: &StaticFilter, - per_file: bool, -) -> Option<GoValue> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - - let parser = Parser::new(); - let analyzer = Analyzer; - - // Aggregated totals (aggregator count/numeric keys). - let mut total_functions: i64 = 0; - let mut total_complexity: i64 = 0; // sum of cyclomatic - let mut cognitive_total: i64 = 0; - let mut nesting_total: i64 = 0; - let mut decision_points: i64 = 0; - let mut max_complexity: i64 = 0; - let mut report_count: usize = 0; - - // Concatenated per-file function tables (file-walk order). - let mut records: Vec<FnRecord> = Vec::new(); - // Per-analyzed-file boundaries into `records` (reference per-file aggregator - // snapshots; one entry per parsed file, function-free included). - let mut files: Vec<FileBoundary> = Vec::new(); - - walk( - root, - root_path, - &parser, - filter, - &analyzer, - &mut total_functions, - &mut total_complexity, - &mut cognitive_total, - &mut nesting_total, - &mut decision_points, - &mut max_complexity, - &mut report_count, - &mut records, - &mut files, - ); - - // average_complexity = total_complexity / total_functions (aggregator). - let avg_complexity = if total_functions > 0 { - total_complexity as f64 / total_functions as f64 - } else { - 0.0 - }; - - // cognitive_complexity / nesting_depth are NUMERIC (averaged) keys in the - // base aggregator: the per-file sums are accumulated, then divided by the - // number of aggregated reports (reportCount = every parsed file, including - // empty-result files) in CalculateAverages. The section's "Cognitive Total" - // metric reads this averaged value back through GetInt → safeconv.ToInt, - // which truncates the float toward zero. - let cognitive_metric: i64 = if report_count > 0 { - (cognitive_total as f64 / report_count as f64) as i64 - } else { - 0 - }; - let _ = nesting_total; // numeric key, averaged but not surfaced in the section. - - let message = if report_count > 0 { - build_complexity_message(avg_complexity) - } else { - // Empty result: aggregator never adds a message; the section falls back - // to DefaultStatusMessage. (Not reachable for the sets fixture.) - DEFAULT_STATUS_MESSAGE.to_string() - }; - - // SummaryOnly: the detailed `functions` collection is a no-op, so the - // section's distribution + issues (both read `functions`) are empty while the - // scalar metrics above are untouched. - let records_ref: &[FnRecord] = if summary_only { &[] } else { &records }; - - // --per-file: one JSONFileEntry per analyzed file, in walk order (the reference implementation ranges - // a map here — run-to-run random; the oracle's measured-variance - // canonicalization compares the set). - let file_entries: Option<Vec<GoValue>> = if per_file { - Some( - files - .iter() - .map(|f| build_file_entry(f, &records)) - .collect(), - ) - } else { - None - }; - - let report = build_json_report( - total_functions, - avg_complexity, - max_complexity, - total_complexity, - cognitive_metric, - decision_points, - &message, - records_ref, - file_entries, - ); - - Some(report) -} - -/// Builds the AGGREGATED RAW `analyze.Report` GoValue for `static/complexity` -/// — the value the reference implementation's complexity `Aggregator.GetResult()` returns after the -/// folder walk, which is what `--format plot` consumes -/// (`PlotSectionsFor("static/complexity")` over the raw report) and what -/// `writeReportJSON` serializes into `report.json`. -/// -/// Shape (a reference-style `map[string]any`, keys byte-sorted at encode time): -/// -/// * `analyzer_name`, `message` (derived via `buildComplexityMessage(avg)`), -/// * counts: `total_functions`, `total_complexity`, `decision_points` -/// (summed ints — `MetricsProcessor.GetCounts`), -/// * averages: `cognitive_complexity`, `nesting_depth` (per-file sums divided -/// by the parsed-file count — `MetricsProcessor.CalculateAverages`), -/// * derived: `average_complexity` (float), `max_complexity` (int), -/// * `functions`: the per-file tables concatenated in walk order -/// (`DetailedDataCollector`), each item the reference `convertFunctionReportItems` -/// map + the `_source_file`/`_language`/`_directory` stamps -/// (`StampSourceFile`/`StampLanguage` + `stampCollectionMetadata`). -/// -/// With no parsed files the reference base aggregator returns -/// `buildEmptyComplexityResult` instead (5 keys, no `analyzer_name`). +/// The raw aggregated report (`--format plot` input and `report.json`): +/// a map-origin object with `analyzer_name`, the count/sum/max scalars, +/// every function (`functions`, with assessments and the `_source_file` / +/// `_language` / `_directory` stamps), `average_complexity`, +/// `max_complexity` and `message`. With no parsed file the empty shape +/// (`total_functions`, `average_complexity`, `max_complexity`, +/// `total_complexity`, `message`) is returned. #[must_use] pub fn complexity_raw_report_value(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - - let parser = Parser::new(); - let analyzer = Analyzer; - - let mut total_functions: i64 = 0; - let mut total_complexity: i64 = 0; - let mut cognitive_total: i64 = 0; - let mut nesting_total: i64 = 0; - let mut decision_points: i64 = 0; - let mut max_complexity: i64 = 0; - let mut report_count: usize = 0; - let mut records: Vec<FnRecord> = Vec::new(); - let mut files: Vec<FileBoundary> = Vec::new(); - - walk( - root, - root_path, - &parser, - filter, - &analyzer, - &mut total_functions, - &mut total_complexity, - &mut cognitive_total, - &mut nesting_total, - &mut decision_points, - &mut max_complexity, - &mut report_count, - &mut records, - &mut files, - ); - - if report_count == 0 { - // The reference base `Aggregator.GetResult` with reportCount==0 returns the - // analyzer's emptyResultBuilder (buildEmptyComplexityResult). - let mut m = GoMap::new(MapOrigin::Map); + let collected = collect(root_path, filter)?; + let functions = collected.functions(); + let agg = aggregate(&functions); + let mut m = GoMap::new(MapOrigin::Map); + if collected.files.is_empty() { m.push("total_functions", GoValue::Int(0)); m.push("average_complexity", GoValue::Float(0.0)); m.push("max_complexity", GoValue::Int(0)); m.push("total_complexity", GoValue::Int(0)); - m.push("message", GoValue::Str("No functions found".to_string())); + m.push("message", GoValue::Str(MSG_NO_FUNCTIONS.to_string())); return Some(GoValue::Map(m)); } - - let avg_complexity = if total_functions > 0 { - total_complexity as f64 / total_functions as f64 - } else { - 0.0 - }; - - let mut m = GoMap::new(MapOrigin::Map); - m.push("analyzer_name", GoValue::Str("complexity".to_string())); - // counts (MetricsProcessor.GetCounts): present whenever any parsed file - // carried the key — total_functions/total_complexity always do (the empty - // per-file result sets them to 0); decision_points only when some file had - // functions (the empty result omits it). - m.push("total_functions", GoValue::Int(total_functions)); - m.push("total_complexity", GoValue::Int(total_complexity)); - if !records.is_empty() { - m.push("decision_points", GoValue::Int(decision_points)); - // averages (CalculateAverages): per-file sums / parsed-file count; - // the keys appear only when some per-file report carried them. - m.push( - "cognitive_complexity", - GoValue::Float(cognitive_total as f64 / report_count as f64), - ); - m.push( - "nesting_depth", - GoValue::Float(nesting_total as f64 / report_count as f64), - ); - } - // functions: the detailed collection in walk order; with zero functions - // the base collector's empty (non-nil) slice survives — `[]`. + m.push("analyzer_name", GoValue::Str(ANALYZER_NAME.to_string())); + m.push("total_functions", GoValue::Int(agg.total_functions)); + m.push("total_complexity", GoValue::Int(agg.total_complexity)); + m.push("decision_points", GoValue::Int(agg.decision_points)); m.push( - "functions", - GoValue::Array(records.iter().map(raw_function_item).collect()), + "cognitive_complexity", + GoValue::Int(agg.cognitive_complexity), ); - // derived metrics (addDerivedMetrics). - m.push("average_complexity", GoValue::Float(avg_complexity)); - m.push("max_complexity", GoValue::Int(max_complexity)); + m.push("nesting_depth", GoValue::Int(agg.nesting_depth)); m.push( - "message", - GoValue::Str(build_complexity_message(avg_complexity)), + "functions", + GoValue::Array(functions.iter().map(raw_function_item).collect()), ); + m.push("average_complexity", GoValue::Float(agg.average_complexity)); + m.push("max_complexity", GoValue::Int(agg.max_complexity)); + m.push("message", GoValue::Str(agg.message.to_string())); Some(GoValue::Map(m)) } -/// One raw `functions` item: the reference `convertFunctionReportItems` map -/// plus the `_source_file`/`_language`/`_directory` stamps. -/// Map-origin, so JSON keys byte-sort at encode time. -fn raw_function_item(r: &FnRecord) -> GoValue { +/// One raw `functions` item: metrics, assessments and the file stamps +/// (map-origin, so keys byte-sort on encode). +fn raw_function_item(f: &FunctionInput) -> GoValue { let mut m = GoMap::new(MapOrigin::Map); - m.push("name", GoValue::Str(r.name.clone())); - m.push("cyclomatic_complexity", GoValue::Int(r.cyclomatic)); - m.push("cognitive_complexity", GoValue::Int(r.cognitive)); - m.push("nesting_depth", GoValue::Int(r.nesting)); - m.push("lines_of_code", GoValue::Int(r.lines_of_code)); + m.push("name", GoValue::Str(f.name.clone())); + m.push( + "cyclomatic_complexity", + GoValue::Int(f.cyclomatic_complexity), + ); + m.push("cognitive_complexity", GoValue::Int(f.cognitive_complexity)); + m.push("nesting_depth", GoValue::Int(f.nesting_depth)); + m.push("lines_of_code", GoValue::Int(f.lines_of_code)); m.push( "complexity_assessment", - GoValue::Str(cf_complexity::get_complexity_assessment(r.cyclomatic).to_string()), + GoValue::Str(complexity_assessment(f.cyclomatic_complexity).to_string()), ); m.push( "cognitive_assessment", - GoValue::Str(cf_complexity::get_cognitive_assessment(r.cognitive).to_string()), + GoValue::Str(cognitive_assessment(f.cognitive_complexity).to_string()), ); m.push( "nesting_assessment", - GoValue::Str(cf_complexity::get_nesting_assessment(r.nesting).to_string()), + GoValue::Str(nesting_assessment(f.nesting_depth).to_string()), ); - m.push("_source_file", GoValue::Str(r.location.clone())); - if !r.language.is_empty() { - m.push("_language", GoValue::Str(r.language.clone())); - } - let dir = go_filepath_dir(&r.location); - if !dir.is_empty() { - m.push("_directory", GoValue::Str(dir)); + m.push("_source_file", GoValue::Str(f.source_file.clone())); + if !f.language.is_empty() { + m.push("_language", GoValue::Str(f.language.clone())); } + m.push("_directory", GoValue::Str(f.directory.clone())); GoValue::Map(m) } -/// The reference `filepath.Dir` over the clean relative paths `MakeRelativePath` yields: -/// everything before the final separator, or `"."` when there is none. -fn go_filepath_dir(path: &str) -> String { - match path.rfind('/') { - Some(0) => "/".to_string(), - Some(pos) => path[..pos].to_string(), - None => ".".to_string(), - } -} - -/// One analyzed file's slice of the walk products (the Rust shape of the reference implementation's -/// per-file aggregator snapshot). -struct FileBoundary { - /// Path relative to the analyzed root. - location: String, - /// Range into the concatenated `records` (empty for function-free files). - start: usize, - end: usize, - /// Per-file totals (the analyzer's single-file report values). - total_complexity: i64, - cognitive_total: i64, - decision_points: i64, - max_complexity: i64, -} - -/// Builds one `renderer.JSONFileEntry` (the reference `SectionToJSONFileEntry` over the -/// per-file report's section): `file_path`, `score_label`, `status`, `metrics`, -/// `distribution` (omitempty — absent for function-free files), `issues`, -/// `score`. -fn build_file_entry(f: &FileBoundary, records: &[FnRecord]) -> GoValue { - let frecs = &records[f.start..f.end]; - let n_fns = frecs.len() as i64; - let avg = if n_fns > 0 { - f.total_complexity as f64 / n_fns as f64 - } else { - 0.0 - }; - let score = calculate_score(avg); - // Per-file status: the analyzer's single-file report message — the empty - // result carries "No functions found" (reference buildEmptyResult). - let status = if n_fns > 0 { - build_complexity_message(avg) +/// The sections report: `{overall_score_label, sections: [section], +/// overall_score}`. +fn build_json_report(collected: &Collected, per_file: bool) -> GoValue { + let functions = collected.functions(); + let agg = aggregate(&functions); + let score = section_score(agg.average_complexity); + let status = if collected.files.is_empty() { + DEFAULT_STATUS_MESSAGE.to_string() } else { - "No functions found".to_string() + agg.message.to_string() }; - let metrics = GoValue::Array(vec![ - metric(METRIC_TOTAL_FUNCTIONS, &n_fns.to_string()), - metric(METRIC_AVG_COMPLEXITY, &format!("{avg:.1}")), - metric(METRIC_MAX_COMPLEXITY, &f.max_complexity.to_string()), - metric(METRIC_TOTAL_COMPLEXITY, &f.total_complexity.to_string()), - metric(METRIC_COGNITIVE_TOTAL, &f.cognitive_total.to_string()), - metric(METRIC_DECISION_POINTS, &f.decision_points.to_string()), - ]); - - // Distribution over THIS file's functions (4 buckets; absent when empty). - let mut simple = 0i64; - let mut moderate = 0i64; - let mut complex = 0i64; - let mut very_complex = 0i64; - for r in frecs { - if r.cyclomatic <= DIST_SIMPLE_MAX { - simple += 1; - } else if r.cyclomatic <= DIST_MODERATE_MAX { - moderate += 1; - } else if r.cyclomatic <= DIST_COMPLEX_MAX { - complex += 1; - } else { - very_complex += 1; - } - } - let mut dist_items = Vec::new(); - if n_fns != 0 { - dist_items.push(dist_item(DIST_LABEL_SIMPLE, pct(simple, n_fns), simple)); - dist_items.push(dist_item(DIST_LABEL_MOD, pct(moderate, n_fns), moderate)); - dist_items.push(dist_item(DIST_LABEL_COMPLEX, pct(complex, n_fns), complex)); - dist_items.push(dist_item( - DIST_LABEL_VERYC, - pct(very_complex, n_fns), - very_complex, - )); - } - - // Issues: this file's functions in the section comparator order. - let mut order: Vec<usize> = (0..frecs.len()).collect(); - go_pdqsort(&mut order, &|&ia, &ib| { - let a = &frecs[ia]; - let b = &frecs[ib]; - if a.cyclomatic != b.cyclomatic { - return a.cyclomatic > b.cyclomatic; - } - if a.cognitive != b.cognitive { - return a.cognitive > b.cognitive; - } - if a.nesting != b.nesting { - return a.nesting > b.nesting; - } - a.name < b.name - }); - let issue_items: Vec<GoValue> = order - .iter() - .map(|&i| { - let r = &frecs[i]; - let mut iss = GoMap::new(MapOrigin::Struct); - iss.push("name", GoValue::Str(r.name.clone())); - iss.push("location", GoValue::Str(r.location.clone())); - iss.push( - "value", - GoValue::Str(format!( - "CC={} | Cog={} | Nest={}", - r.cyclomatic, r.cognitive, r.nesting - )), - ); - iss.push( - "severity", - GoValue::Str(severity_for_complexity(r.cyclomatic).to_string()), - ); - GoValue::Map(iss) - }) - .collect(); - - let mut entry = GoMap::new(MapOrigin::Struct); - entry.push("file_path", GoValue::Str(f.location.clone())); - entry.push("score_label", GoValue::Str(score_label(score))); - entry.push("status", GoValue::Str(status)); - entry.push("metrics", metrics); - if !dist_items.is_empty() { - entry.push("distribution", GoValue::Array(dist_items)); - } - entry.push("issues", GoValue::Array(issue_items)); - entry.push("score", GoValue::Float(score)); - GoValue::Map(entry) -} - -/// Recursively walks `dir` in lexical order, mirroring `filepath.WalkDir`: -/// directories are recursed (except `.git`), files are filtered through -/// parser support + path policy, parsed, and analyzed. -#[allow(clippy::too_many_arguments)] -fn walk( - dir: &Path, - root_path: &str, - parser: &Parser, - filter: &StaticFilter, - analyzer: &Analyzer, - total_functions: &mut i64, - total_complexity: &mut i64, - cognitive_total: &mut i64, - nesting_total: &mut i64, - decision_points: &mut i64, - max_complexity: &mut i64, - report_count: &mut usize, - records: &mut Vec<FnRecord>, - files: &mut Vec<FileBoundary>, -) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file( - dir, - root_path, - parser, - filter, - analyzer, - total_functions, - total_complexity, - cognitive_total, - nesting_total, - decision_points, - max_complexity, - report_count, - records, - files, - ); - return; - } - let Ok(read) = fs::read_dir(dir) else { - return; - }; - - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let Ok(file_type) = entry.file_type() else { - continue; - }; - - if file_type.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; - } - walk( - &path, - root_path, - parser, - filter, - analyzer, - total_functions, - total_complexity, - cognitive_total, - nesting_total, - decision_points, - max_complexity, - report_count, - records, - files, - ); - continue; - } - - visit_file( - &path, - root_path, - parser, - filter, - analyzer, - total_functions, - total_complexity, - cognitive_total, - nesting_total, - decision_points, - max_complexity, - report_count, - records, - files, - ); - } -} - -/// Analyzes one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): parser support + path policy filters, parse, -/// per-file metrics, and fold into the accumulators. -#[allow(clippy::too_many_arguments)] -fn visit_file( - path: &Path, - root_path: &str, - parser: &Parser, - filter: &StaticFilter, - analyzer: &Analyzer, - total_functions: &mut i64, - total_complexity: &mut i64, - cognitive_total: &mut i64, - nesting_total: &mut i64, - decision_points: &mut i64, - max_complexity: &mut i64, - report_count: &mut usize, - records: &mut Vec<FnRecord>, - files: &mut Vec<FileBoundary>, -) { - { - let path_str = path.to_string_lossy(); - - // ShouldSkipFolderNode: must be UAST-supported. - if !parser.is_supported(&path_str) { - return; - } - // matchesLanguageGlobs (empty → all match), then pathpolicy.Exclude. - if filter.skips(&path_str) { - return; - } - - let Some(content) = super::read_source_capped(path) else { - return; - }; - let Ok(uast_root) = parser.parse(&path_str, &content) else { - super::note_skipped_file(); - return; - }; - - // Every successfully parsed & analyzed file produces a report, even one - // with no functions (`buildEmptyResult`). The base aggregator counts - // ALL such reports (`reportCount`), which is the divisor for the - // averaged numeric keys (`cognitive_complexity`, `nesting_depth`). - *report_count += 1; - - let cx_root = convert_node(&uast_root); - // Per-file metrics (the crate sorts by cc desc, cog desc, name asc; - // the reference implementation's per-file sort.Slice over the same comparator yields the same - // order for the sets fixture — no within-file all-key ties). - let fns = analyzer.function_metrics(Some(&cx_root)); - - // _source_file stamp = path relative to the analyzed root. - let location = make_relative_path(&path_str, root_path); - // _language stamp (reference: StampLanguage(parser.GetLanguage(filePath))). - let language = parser.get_language(&path_str); - - if fns.is_empty() { - // Empty-result report: no functions, no cognitive/nesting/count - // contribution; only reportCount (above) and the per-file snapshot - // (an empty "No functions found" entry) are affected. - files.push(FileBoundary { - location, - start: records.len(), - end: records.len(), - total_complexity: 0, - cognitive_total: 0, - decision_points: 0, - max_complexity: 0, - }); - return; - } - - // Aggregate the per-file totals (matches the analyzer's own result - // computed in buildResult: totals over all functions). - let start = records.len(); - let mut file_max: i64 = 0; - let mut file_cx: i64 = 0; - let mut file_cog: i64 = 0; - let mut file_dec: i64 = 0; - for m in &fns { - *total_functions += 1; - *total_complexity += m.cyclomatic_complexity; - *cognitive_total += m.cognitive_complexity; - *nesting_total += m.nesting_depth; - *decision_points += m.decision_points; - file_cx += m.cyclomatic_complexity; - file_cog += m.cognitive_complexity; - file_dec += m.decision_points; - if m.cyclomatic_complexity > file_max { - file_max = m.cyclomatic_complexity; - } - records.push(record_for(m, &location, &language)); - } - if file_max > *max_complexity { - *max_complexity = file_max; - } - files.push(FileBoundary { - location, - start, - end: records.len(), - total_complexity: file_cx, - cognitive_total: file_cog, - decision_points: file_dec, - max_complexity: file_max, - }); - } -} - -fn record_for(m: &FunctionMetrics, location: &str, language: &str) -> FnRecord { - FnRecord { - name: m.name.clone(), - cyclomatic: m.cyclomatic_complexity, - cognitive: m.cognitive_complexity, - nesting: m.nesting_depth, - lines_of_code: m.lines_of_code, - location: location.to_string(), - language: language.to_string(), - } -} - -/// The reference `MakeRelativePath`: `filepath.Rel(rootPath, filePath)`. -/// For the flat sets fixture this yields the basename. -fn make_relative_path(file_path: &str, root_path: &str) -> String { - if root_path.is_empty() { - return file_path.to_string(); - } - let root = Path::new(root_path); - let file = Path::new(file_path); - match file.strip_prefix(root) { - // filepath.Rel(root, root) == "." (a FILE root stamps "."). - Ok(rel) if rel.as_os_str().is_empty() => ".".to_string(), - Ok(rel) => rel.to_string_lossy().into_owned(), - Err(_) => file_path.to_string(), - } -} - -/// Converts a `cf_uast_node::Node` into the `cf_complexity::node::Node` subset -/// the complexity analyzer reads (type, token, roles, props, children, pos). -/// -/// Wrapped in `stacker::maybe_grow` like the cf-uast lowering walk that built -/// this tree: real sources exceed 10k nesting levels, and an unguarded -/// recursive deep-copy of a tree that needed a segmented stack to construct -/// would SIGSEGV on the fixed 8 MiB main stack. -fn convert_node(n: &UastNode) -> CxNode { - stacker::maybe_grow(64 * 1024, 16 * 1024 * 1024, || convert_node_inner(n)) -} - -fn convert_node_inner(n: &UastNode) -> CxNode { - let mut out = CxNode::new(n.node_type.clone()); - out.token = n.token.clone(); - out.roles = n.roles.clone(); - out.props = n - .props - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - out.pos = n.pos.as_ref().map(|p| CxPositions { - start_line: p.start_line as u32, - start_col: p.start_col as u32, - start_offset: p.start_offset as u32, - end_line: p.end_line as u32, - end_col: p.end_col as u32, - end_offset: p.end_offset as u32, - }); - out.children = n.children.iter().map(convert_node).collect(); - out -} - -// --- Score / message / severity / distribution helpers --- - -fn calculate_score(avg: f64) -> f64 { - if avg <= SCORE_EXCELLENT_THRESHOLD { - SCORE_EXCELLENT - } else if avg <= SCORE_GOOD_THRESHOLD { - SCORE_GOOD - } else if avg <= SCORE_FAIR_THRESHOLD { - SCORE_FAIR - } else if avg <= SCORE_MODERATE_THRESHOLD { - SCORE_MODERATE - } else if avg <= SCORE_POOR_THRESHOLD { - SCORE_POOR - } else { - SCORE_CRITICAL - } -} - -fn build_complexity_message(score: f64) -> String { - if score <= 1.0 { - MSG_EXCELLENT - } else if score <= SCORE_GOOD_THRESHOLD { - MSG_GOOD - } else if score <= SCORE_MODERATE_THRESHOLD { - MSG_FAIR - } else { - MSG_HIGH - } - .to_string() -} - -fn severity_for_complexity(cc: i64) -> &'static str { - if cc >= ISSUE_SEVERITY_POOR_MIN { - SEVERITY_POOR - } else if cc >= ISSUE_SEVERITY_FAIR_MIN { - SEVERITY_FAIR - } else { - SEVERITY_GOOD - } -} - -fn pct(count: i64, total: i64) -> f64 { - if total == 0 { - 0.0 - } else { - count as f64 / total as f64 - } -} - -/// Builds the `renderer.JSONReport` GoValue for a single complexity section. -#[allow(clippy::too_many_arguments)] -fn build_json_report( - total_functions: i64, - avg_complexity: f64, - max_complexity: i64, - total_complexity: i64, - cognitive_metric: i64, - decision_points: i64, - message: &str, - records: &[FnRecord], - file_entries: Option<Vec<GoValue>>, -) -> GoValue { - let score = calculate_score(avg_complexity); - - // ---- metrics (reference KeyMetrics) ---- - let metrics = GoValue::Array(vec![ - metric(METRIC_TOTAL_FUNCTIONS, &total_functions.to_string()), - metric(METRIC_AVG_COMPLEXITY, &format!("{avg_complexity:.1}")), - metric(METRIC_MAX_COMPLEXITY, &max_complexity.to_string()), - metric(METRIC_TOTAL_COMPLEXITY, &total_complexity.to_string()), - metric(METRIC_COGNITIVE_TOTAL, &cognitive_metric.to_string()), - metric(METRIC_DECISION_POINTS, &decision_points.to_string()), - ]); - - // ---- distribution (categorize over all functions) ---- - let mut simple = 0i64; - let mut moderate = 0i64; - let mut complex = 0i64; - let mut very_complex = 0i64; - for r in records { - if r.cyclomatic <= DIST_SIMPLE_MAX { - simple += 1; - } else if r.cyclomatic <= DIST_MODERATE_MAX { - moderate += 1; - } else if r.cyclomatic <= DIST_COMPLEX_MAX { - complex += 1; - } else { - very_complex += 1; - } - } - let total = records.len() as i64; - let mut dist_items = Vec::new(); - if total != 0 { - dist_items.push(dist_item(DIST_LABEL_SIMPLE, pct(simple, total), simple)); - dist_items.push(dist_item(DIST_LABEL_MOD, pct(moderate, total), moderate)); - dist_items.push(dist_item(DIST_LABEL_COMPLEX, pct(complex, total), complex)); - dist_items.push(dist_item( - DIST_LABEL_VERYC, - pct(very_complex, total), - very_complex, - )); - } - - // ---- issues: ALL functions sorted by cc desc, cog desc, nest desc, name asc ---- - let mut order: Vec<usize> = (0..records.len()).collect(); - go_pdqsort(&mut order, &|&ia, &ib| { - let a = &records[ia]; - let b = &records[ib]; - if a.cyclomatic != b.cyclomatic { - return a.cyclomatic > b.cyclomatic; - } - if a.cognitive != b.cognitive { - return a.cognitive > b.cognitive; - } - if a.nesting != b.nesting { - return a.nesting > b.nesting; - } - a.name < b.name - }); - - let issue_items: Vec<GoValue> = order - .iter() - .map(|&i| { - let r = &records[i]; - let mut iss = GoMap::new(MapOrigin::Struct); - iss.push("name", GoValue::Str(r.name.clone())); - iss.push("location", GoValue::Str(r.location.clone())); - iss.push( - "value", - GoValue::Str(format!( - "CC={} | Cog={} | Nest={}", - r.cyclomatic, r.cognitive, r.nesting - )), - ); - iss.push( - "severity", - GoValue::Str(severity_for_complexity(r.cyclomatic).to_string()), - ); - GoValue::Map(iss) - }) - .collect(); - - // ---- section (renderer.SectionToJSON) ---- let mut section = GoMap::new(MapOrigin::Struct); section.push("title", GoValue::Str(SECTION_TITLE.to_string())); section.push("score_label", GoValue::Str(score_label(score))); - section.push("status", GoValue::Str(message.to_string())); - section.push("metrics", metrics); - if !dist_items.is_empty() { - section.push("distribution", GoValue::Array(dist_items)); - } - section.push("issues", GoValue::Array(issue_items)); - // --per-file enrichment: `files` sits between `issues` and `score` - // (renderer.JSONSection field order, `files` is `omitempty` without the - // flag and an explicit — possibly empty — array with it). - if let Some(entries) = file_entries { - section.push("files", GoValue::Array(entries)); + section.push("status", GoValue::Str(status)); + section.push("metrics", key_metrics(&agg)); + if let Some(dist) = distribution(&functions) { + section.push("distribution", dist); + } + section.push("issues", issues(&functions)); + if per_file { + section.push( + "files", + GoValue::Array(collected.files.iter().map(file_entry).collect()), + ); } section.push("score", GoValue::Float(score)); - // ---- report (renderer.SectionsToJSON over one scored section) ---- let mut report = GoMap::new(MapOrigin::Struct); report.push("overall_score_label", GoValue::Str(score_label(score))); report.push("sections", GoValue::Array(vec![GoValue::Map(section)])); report.push("overall_score", GoValue::Float(score)); - GoValue::Map(report) } -/// Renders a numeric score as the `N/10` label (renderer score formatting). +/// One `--per-file` entry: the file's own section-shaped summary +/// (`file_path`, `score_label`, `status`, `metrics`, `distribution` when it +/// has functions, `issues`, `score`). +fn file_entry(file: &AnalyzedFile) -> GoValue { + let mut functions = file.functions.clone(); + functions.sort_by(function_order); + let agg = aggregate(&functions); + let score = section_score(agg.average_complexity); + let status = if functions.is_empty() { + MSG_NO_FUNCTIONS.to_string() + } else { + complexity_message(agg.average_complexity).to_string() + }; + let mut entry = GoMap::new(MapOrigin::Struct); + entry.push("file_path", GoValue::Str(file.location.clone())); + entry.push("score_label", GoValue::Str(score_label(score))); + entry.push("status", GoValue::Str(status)); + entry.push("metrics", key_metrics(&agg)); + if let Some(dist) = distribution(&functions) { + entry.push("distribution", dist); + } + entry.push("issues", issues(&functions)); + entry.push("score", GoValue::Float(score)); + GoValue::Map(entry) +} + +/// The key metrics rows, as the terminal and JSON renderers print them. +fn key_metrics(agg: &Aggregate) -> GoValue { + GoValue::Array(vec![ + metric(METRIC_TOTAL_FUNCTIONS, &agg.total_functions.to_string()), + metric( + METRIC_AVG_COMPLEXITY, + &format!("{:.1}", agg.average_complexity), + ), + metric(METRIC_MAX_COMPLEXITY, &agg.max_complexity.to_string()), + metric(METRIC_TOTAL_COMPLEXITY, &agg.total_complexity.to_string()), + metric( + METRIC_COGNITIVE_TOTAL, + &agg.cognitive_complexity.to_string(), + ), + metric(METRIC_MAX_NESTING, &agg.nesting_depth.to_string()), + metric(METRIC_DECISION_POINTS, &agg.decision_points.to_string()), + ]) +} + +/// The four terminal buckets with their share of all functions; `None` +/// without functions (the key is omitted). +fn distribution(functions: &[FunctionInput]) -> Option<GoValue> { + if functions.is_empty() { + return None; + } + let mut counts = [0i64; TERMINAL_BUCKETS.len()]; + for f in functions { + counts[terminal_bucket(f.cyclomatic_complexity)] += 1; + } + let total = functions.len() as f64; + Some(GoValue::Array( + TERMINAL_BUCKETS + .iter() + .zip(counts) + .map(|(label, count)| { + let mut d = GoMap::new(MapOrigin::Struct); + d.push("label", GoValue::Str((*label).to_string())); + d.push("percent", GoValue::Float(count as f64 / total)); + d.push("count", GoValue::Int(count)); + GoValue::Map(d) + }) + .collect(), + )) +} + +/// Every function as an issue row (`name`, `location`, `CC=.. | Cog=.. | +/// Nest=..`, `severity`), in the given (already ranked) order. +fn issues(functions: &[FunctionInput]) -> GoValue { + GoValue::Array( + functions + .iter() + .map(|f| { + let mut iss = GoMap::new(MapOrigin::Struct); + iss.push("name", GoValue::Str(f.name.clone())); + iss.push("location", GoValue::Str(f.source_file.clone())); + iss.push( + "value", + GoValue::Str(format!( + "CC={} | Cog={} | Nest={}", + f.cyclomatic_complexity, f.cognitive_complexity, f.nesting_depth + )), + ); + iss.push( + "severity", + GoValue::Str(severity(f.cyclomatic_complexity).to_string()), + ); + GoValue::Map(iss) + }) + .collect(), + ) +} + +/// A 0..=1 score rendered as `N/10`. fn score_label(score: f64) -> String { - // BaseReportSection.ScoreLabel: fmt.Sprintf("%d/10", int(score*10)) for a - // score in [0,1]; -1 (info) renders "Info" but complexity is never info. - let n = (score * 10.0).round() as i64; - format!("{n}/10") + format!("{}/10", (score * SCORE_LABEL_SCALE).round() as i64) } fn metric(label: &str, value: &str) -> GoValue { @@ -1007,386 +366,55 @@ fn metric(label: &str, value: &str) -> GoValue { GoValue::Map(m) } -fn dist_item(label: &str, percent: f64, count: i64) -> GoValue { - let mut d = GoMap::new(MapOrigin::Struct); - d.push("label", GoValue::Str(label.to_string())); - d.push("percent", GoValue::Float(percent)); - d.push("count", GoValue::Int(count)); - GoValue::Map(d) -} - -// =========================================================================== -// the reference `sort.Slice` (pdqsort) port — exact element-movement parity. -// Operates on an index permutation; `less(&a, &b)` returns true when a < b. -// Mirrors the reference sort sources (limit = bits.Len(len)). -// =========================================================================== - -const MAX_INSERTION: usize = 12; - -fn bits_len(n: usize) -> u32 { - usize::BITS - n.leading_zeros() -} - -/// Sorts `data` with the reference `sort.Slice` semantics using `less`. -pub(crate) fn go_pdqsort<T, F: Fn(&T, &T) -> bool>(data: &mut [T], less: &F) { - let n = data.len(); - let limit = bits_len(n); - pdqsort(data, 0, n, limit, less); -} - -fn pdqsort<T, F: Fn(&T, &T) -> bool>( - data: &mut [T], - mut a: usize, - mut b: usize, - mut limit: u32, - less: &F, -) { - let mut was_balanced = true; - let mut was_partitioned = true; - - loop { - let length = b - a; - - if length <= MAX_INSERTION { - insertion_sort(data, a, b, less); - return; - } - - if limit == 0 { - heap_sort(data, a, b, less); - return; - } - - if !was_balanced { - break_patterns(data, a, b); - limit -= 1; - } - - let (mut pivot, mut hint) = choose_pivot(data, a, b, less); - if hint == HINT_DECREASING { - reverse_range(data, a, b); - pivot = (b - 1) - (pivot - a); - hint = HINT_INCREASING; - } - - if was_balanced - && was_partitioned - && hint == HINT_INCREASING - && partial_insertion_sort(data, a, b, less) - { - return; - } - - // a > 0 && !less(a-1, pivot) - if a > 0 && !less(&data[a - 1], &data[pivot]) { - let mid = partition_equal(data, a, b, pivot, less); - a = mid; - continue; - } - - let (mid, already_partitioned) = partition(data, a, b, pivot, less); - was_partitioned = already_partitioned; - - let left_len = mid - a; - let right_len = b - mid; - let balance_threshold = length / 8; - if left_len < right_len { - was_balanced = left_len >= balance_threshold; - pdqsort(data, a, mid, limit, less); - a = mid + 1; - } else { - was_balanced = right_len >= balance_threshold; - pdqsort(data, mid + 1, b, limit, less); - b = mid; - } - } -} - -fn insertion_sort<T, F: Fn(&T, &T) -> bool>(data: &mut [T], a: usize, b: usize, less: &F) { - for i in (a + 1)..b { - let mut j = i; - while j > a && less(&data[j], &data[j - 1]) { - data.swap(j, j - 1); - j -= 1; - } - } -} - -fn sift_down<T, F: Fn(&T, &T) -> bool>( - data: &mut [T], - lo: usize, - hi: usize, - first: usize, - less: &F, -) { - let mut root = lo; - loop { - let mut child = 2 * root + 1; - if child >= hi { - break; - } - if child + 1 < hi && less(&data[first + child], &data[first + child + 1]) { - child += 1; - } - if !less(&data[first + root], &data[first + child]) { - return; - } - data.swap(first + root, first + child); - root = child; - } -} - -fn heap_sort<T, F: Fn(&T, &T) -> bool>(data: &mut [T], a: usize, b: usize, less: &F) { - let first = a; - let lo = 0; - let hi = b - a; - - let mut i = (hi as isize - 1) / 2; - while i >= 0 { - sift_down(data, i as usize, hi, first, less); - i -= 1; - } - - let mut i = hi as isize - 1; - while i >= 0 { - data.swap(first, first + i as usize); - sift_down(data, lo, i as usize, first, less); - i -= 1; - } -} - -fn partition<T, F: Fn(&T, &T) -> bool>( - data: &mut [T], - a: usize, - b: usize, - pivot: usize, - less: &F, -) -> (usize, bool) { - data.swap(a, pivot); - let mut i = a + 1; - let mut j = b - 1; - - while i <= j && less(&data[i], &data[a]) { - i += 1; - } - while i <= j && !less(&data[j], &data[a]) { - j -= 1; - } - if i > j { - data.swap(j, a); - return (j, true); - } - data.swap(i, j); - i += 1; - j -= 1; - - loop { - while i <= j && less(&data[i], &data[a]) { - i += 1; - } - while i <= j && !less(&data[j], &data[a]) { - j -= 1; - } - if i > j { - break; - } - data.swap(i, j); - i += 1; - j -= 1; - } - data.swap(j, a); - (j, false) -} - -fn partition_equal<T, F: Fn(&T, &T) -> bool>( - data: &mut [T], - a: usize, - b: usize, - pivot: usize, - less: &F, -) -> usize { - data.swap(a, pivot); - let mut i = a + 1; - let mut j = b - 1; - - loop { - while i <= j && !less(&data[a], &data[i]) { - i += 1; - } - while i <= j && less(&data[a], &data[j]) { - j -= 1; - } - if i > j { - break; - } - data.swap(i, j); - i += 1; - j -= 1; - } - i -} - -fn partial_insertion_sort<T, F: Fn(&T, &T) -> bool>( - data: &mut [T], - a: usize, - b: usize, - less: &F, -) -> bool { - const MAX_STEPS: usize = 5; - const SHORTEST_SHIFTING: usize = 50; - let mut i = a + 1; - for _ in 0..MAX_STEPS { - while i < b && !less(&data[i], &data[i - 1]) { - i += 1; - } - - if i == b { - return true; - } - - if b - a < SHORTEST_SHIFTING { - return false; - } - - data.swap(i, i - 1); - - // Shift the smaller one to the left. - if i - a >= 2 { - let mut j = i - 1; - while j >= 1 { - if !less(&data[j], &data[j - 1]) { - break; - } - data.swap(j, j - 1); - j -= 1; - } - } - // Shift the greater one to the right. - if b - i >= 2 { - let mut j = i + 1; - while j < b { - if !less(&data[j], &data[j - 1]) { - break; - } - data.swap(j, j - 1); - j += 1; - } - } - } - false -} - -fn break_patterns<T>(data: &mut [T], a: usize, b: usize) { - let length = b - a; - if length >= 8 { - let mut random: u64 = length as u64; - let modulus = next_power_of_two(length); - - let base = a + (length / 4) * 2; - for idx in (base - 1)..=(base + 1) { - random = xorshift_next(&mut random); - let mut other = (random as usize) & (modulus - 1); - if other >= length { - other -= length; - } - data.swap(idx, a + other); - } - } -} - -fn xorshift_next(r: &mut u64) -> u64 { - *r ^= *r << 13; - *r ^= *r >> 7; - *r ^= *r << 17; - *r -} - -fn next_power_of_two(length: usize) -> usize { - let shift = bits_len(length); - 1usize << shift -} - -const HINT_UNKNOWN: u8 = 0; -const HINT_INCREASING: u8 = 1; -const HINT_DECREASING: u8 = 2; - -fn choose_pivot<T, F: Fn(&T, &T) -> bool>( - data: &mut [T], - a: usize, - b: usize, - less: &F, -) -> (usize, u8) { - const SHORTEST_NINTHER: usize = 50; - const MAX_SWAPS: i32 = 4 * 3; - - let l = b - a; - - let mut swaps: i32 = 0; - let mut i = a + l / 4; - let mut j = a + l / 4 * 2; - let mut k = a + l / 4 * 3; - - if l >= 8 { - if l >= SHORTEST_NINTHER { - i = median_adjacent(data, i, &mut swaps, less); - j = median_adjacent(data, j, &mut swaps, less); - k = median_adjacent(data, k, &mut swaps, less); - } - j = median(data, i, j, k, &mut swaps, less); - } - - match swaps { - 0 => (j, HINT_INCREASING), - MAX_SWAPS => (j, HINT_DECREASING), - _ => (j, HINT_UNKNOWN), - } -} - -fn order2<T, F: Fn(&T, &T) -> bool>( - data: &[T], - a: usize, - b: usize, - swaps: &mut i32, - less: &F, -) -> (usize, usize) { - if less(&data[b], &data[a]) { - *swaps += 1; - (b, a) - } else { - (a, b) +#[cfg(test)] +mod tests { + use super::{complexity_report_value_flags, score_label}; + use crate::handlers::StaticFilter; + use cf_gojson::GoValue; + + /// A FILE root stamps the file's name (never `.`) on the per-file entry + /// and on every function's location, and a nested file is stamped + /// root-relative with its directory. + #[test] + fn stamps_are_root_relative_and_a_file_root_stamps_the_name() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::create_dir_all(dir.path().join("pkg")).unwrap(); + let src = + "package main\n\nfunc f(a int) int {\n\tif a > 0 {\n\t\treturn a\n\t}\n\treturn 0\n}\n"; + std::fs::write(dir.path().join("pkg/a.go"), src).unwrap(); + let file = dir.path().join("pkg/a.go"); + let report = + complexity_report_value_flags(file.to_str().unwrap(), &StaticFilter::default(), true) + .expect("file root"); + let json = String::from_utf8(cf_gojson::marshal(&report)).unwrap(); + assert!(json.contains("\"file_path\":\"a.go\""), "{json}"); + assert!(json.contains("\"location\":\"a.go\""), "{json}"); + assert!(!json.contains("\"file_path\":\".\""), "{json}"); + + let raw = super::complexity_raw_report_value( + dir.path().to_str().unwrap(), + &StaticFilter::default(), + ) + .expect("dir root"); + let functions = match raw.as_map().and_then(|m| m.get("functions")) { + Some(GoValue::Array(items)) => items.clone(), + other => panic!("functions missing: {other:?}"), + }; + let first = functions[0].as_map().expect("function map"); + assert_eq!( + first.get("_source_file"), + Some(&GoValue::Str("pkg/a.go".to_string())) + ); + assert_eq!( + first.get("_directory"), + Some(&GoValue::Str("pkg".to_string())) + ); } -} - -fn median<T, F: Fn(&T, &T) -> bool>( - data: &[T], - a: usize, - b: usize, - c: usize, - swaps: &mut i32, - less: &F, -) -> usize { - let (a, b) = order2(data, a, b, swaps, less); - let (b, c) = order2(data, b, c, swaps, less); - let (_a, b) = order2(data, a, b, swaps, less); - let _ = c; - b -} - -fn median_adjacent<T, F: Fn(&T, &T) -> bool>( - data: &[T], - a: usize, - swaps: &mut i32, - less: &F, -) -> usize { - median(data, a - 1, a, a + 1, swaps, less) -} -fn reverse_range<T>(data: &mut [T], a: usize, b: usize) { - let mut i = a; - let mut j = b - 1; - while i < j { - data.swap(i, j); - i += 1; - j -= 1; + #[test] + fn score_labels_round_to_tenths() { + assert_eq!(score_label(1.0), "10/10"); + assert_eq!(score_label(0.6), "6/10"); + assert_eq!(score_label(0.1), "1/10"); } } diff --git a/crates/cf-commands/src/handlers/static_complexity_bin.rs b/crates/cf-commands/src/handlers/static_complexity_bin.rs index e051b663..3e9646a1 100644 --- a/crates/cf-commands/src/handlers/static_complexity_bin.rs +++ b/crates/cf-commands/src/handlers/static_complexity_bin.rs @@ -1,232 +1,27 @@ -//! Static-analysis `--format bin` report path for the UAST `static/complexity` -//! analyzer. -//! -//! Reproduces the `codefang run --analyzers static/complexity --format bin` -//! capture (`static/static_complexity.bin`). The reference static pipeline -//! (reference flow: `StaticService.AnalyzeFolder` → UAST phase → per-file -//! `parser.Parse` → `complexity.Analyzer.Analyze` → `StampSourceFile` / -//! `StampLanguage` → `complexity.Aggregator` → `ParseReportData` → -//! `ComputeAllMetrics` → `complexity.FormatReportBinary` = -//! `reportutil.EncodeBinaryEnvelope(json.Marshal(ComputedMetrics))`) reduces, for -//! this single UAST analyzer over a Go source tree with `--static-workers 1`, to: -//! -//! 1. a lexical directory walk (`filepath.WalkDir` / `streamFiles`), keeping the -//! files the parser supports and that survive `pathpolicy.Exclude`; -//! 2. per file: parse to UAST, run [`cf_complexity::Analyzer::function_metrics`] -//! (the analyzer's deterministic per-file function order), stamp the relative -//! `source_file` / `directory` and detected `language`; -//! 3. concatenate the per-file function lists in walk order (the detailed -//! collector's single-worker append order) — the exact input permutation -//! the reference implementation's `sort.Slice` (pdqsort) operates on; -//! 4. [`cf_complexity::report::computed_metrics`] builds the identical -//! `ComputedMetrics` value the YAML/JSON-sections siblings derive, applying -//! the reference implementation's pdqsort for the two sorted collections; -//! 5. the compact cf-gojson payload is wrapped in the CFB1 envelope -//! (cf-reportutil: `CFB1` magic + LE u32 length + compact payload), exactly -//! as `FormatReportBinary` does. - -use std::fs; -use std::path::Path; - -use cf_complexity::node::{Node as CxNode, Positions as CxPos}; -use cf_complexity::report::{computed_metrics, FunctionInput, ReportScalars}; -use cf_complexity::Analyzer; -use cf_uast::Parser; -use cf_uast_node::Node as UNode; - -/// Builds the `static/complexity --format bin` report bytes for `root_path`, or -/// `None` when the path does not exist (the caller falls through to the -/// blocked-dependency sentinel). +//! `static/complexity --format bin`: the walk products of +//! [`super::static_complexity::collect`] serialized as the +//! [`cf_complexity::report::computed_metrics`] value inside the CFB1 +//! envelope (cf-reportutil: magic + LE u32 length + compact payload). +//! `--per-file` is a JSON-only enrichment. + +use cf_complexity::report::computed_metrics; +use cf_reportutil::encode_binary_envelope; + +use super::static_complexity::collect; +use super::StaticFilter; + +/// Builds the `static/complexity --format bin` report bytes for `root_path`. +/// `None` when the path does not exist, or when the payload exceeds the +/// envelope limit (reported on stderr; the run then fails instead of +/// panicking). #[must_use] -pub fn complexity_report_bin(root_path: &str, filter: &super::StaticFilter) -> Option<Vec<u8>> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - - let parser = Parser::new(); - let analyzer = Analyzer; - - let mut files: Vec<std::path::PathBuf> = Vec::new(); - walk(root, &parser, filter, &mut files); - - let mut inputs: Vec<FunctionInput> = Vec::new(); - let mut total_functions = 0i64; - let mut total_complexity = 0i64; - let mut decision_points = 0i64; - let mut max_complexity = 0i64; - - for path in &files { - let Some(content) = super::read_source_capped(path) else { - continue; - }; - let path_str = path.to_string_lossy(); - let Ok(tree) = parser.parse(&path_str, &content) else { - super::note_skipped_file(); - continue; - }; - let cx = convert(&tree); - let metrics = analyzer.function_metrics(Some(&cx)); - if metrics.is_empty() { - continue; +pub fn complexity_report_bin(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { + let collected = collect(root_path, filter)?; + match encode_binary_envelope(&computed_metrics(&collected.functions())) { + Ok(bytes) => Some(bytes), + Err(e) => { + eprintln!("Error: static/complexity --format bin: {e}"); + None } - - let language = parser.get_language(&path_str); - let (source_file, directory) = relative_parts(path, root); - - // The aggregator tracks the max over each per-file report's max_complexity. - let file_max = metrics - .iter() - .map(|m| m.cyclomatic_complexity) - .max() - .unwrap_or(0); - if file_max > max_complexity { - max_complexity = file_max; - } - - for m in metrics { - total_functions += 1; - total_complexity += m.cyclomatic_complexity; - decision_points += m.decision_points; - inputs.push(FunctionInput { - name: m.name, - source_file: source_file.clone(), - language: language.clone(), - directory: directory.clone(), - cyclomatic_complexity: m.cyclomatic_complexity, - cognitive_complexity: m.cognitive_complexity, - nesting_depth: m.nesting_depth, - lines_of_code: m.lines_of_code, - }); - } - } - - let average_complexity = if total_functions > 0 { - total_complexity as f64 / total_functions as f64 - } else { - 0.0 - }; - let scalars = ReportScalars { - total_functions, - average_complexity, - max_complexity, - total_complexity, - decision_points, - }; - - let value = computed_metrics(&inputs, &scalars); - Some( - cf_reportutil::encode_binary_envelope(&value) - .expect("complexity payload within CFB1 limit"), - ) -} - -/// Recursively walks `dir` in lexical order (mirroring `filepath.WalkDir` + -/// `streamFiles`): directories are recursed (skipping `.git`); files are kept -/// when the parser supports them and they survive `pathpolicy.Exclude`. -fn walk( - dir: &Path, - parser: &Parser, - filter: &super::StaticFilter, - out: &mut Vec<std::path::PathBuf>, -) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file(dir, parser, filter, out); - return; - } - let Ok(read) = fs::read_dir(dir) else { - return; - }; - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let Ok(file_type) = entry.file_type() else { - continue; - }; - if file_type.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; - } - walk(&path, parser, filter, out); - continue; - } - visit_file(&path, parser, filter, out); - } -} - -/// Collects one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): parser support + path policy filters. -fn visit_file( - path: &Path, - parser: &Parser, - filter: &super::StaticFilter, - out: &mut Vec<std::path::PathBuf>, -) { - let path_str = path.to_string_lossy(); - if !parser.is_supported(&path_str) { - return; - } - if filter.skips(&path_str) { - return; - } - out.push(path.to_path_buf()); -} - -/// Computes the stamped `source_file` (relative to `root`) and its `directory` -/// (`filepath.Dir(rel)`), mirroring `MakeRelativePath` + `filepath.Dir`. -fn relative_parts(path: &Path, root: &Path) -> (String, String) { - let rel = path.strip_prefix(root).unwrap_or(path); - let source_file = if rel.as_os_str().is_empty() { - // filepath.Rel(root, root) == "." (a FILE root stamps "."). - ".".to_string() - } else { - rel.to_string_lossy().to_string() - }; - let directory = rel.parent().map_or_else( - || ".".to_string(), - |p| { - let s = p.to_string_lossy(); - if s.is_empty() { - ".".to_string() - } else { - s.to_string() - } - }, - ); - (source_file, directory) -} - -/// Converts a parsed [`cf_uast_node::Node`] into the complexity analyzer's -/// minimal [`cf_complexity::node::Node`] (the two models are field-compatible; -/// types and roles are plain strings). -fn convert(u: &UNode) -> CxNode { - let mut n = CxNode::new(u.node_type.clone()); - n.token = u.token.clone(); - n.roles = u.roles.clone(); - for (k, v) in &u.props { - n.props.insert(k.clone(), v.clone()); - } - if let Some(p) = &u.pos { - n.pos = Some(CxPos { - start_line: p.start_line as u32, - start_col: p.start_col as u32, - start_offset: p.start_offset as u32, - end_line: p.end_line as u32, - end_col: p.end_col as u32, - end_offset: p.end_offset as u32, - }); - } - for c in &u.children { - n.add_child(convert(c)); } - n } diff --git a/crates/cf-commands/src/handlers/static_complexity_yaml.rs b/crates/cf-commands/src/handlers/static_complexity_yaml.rs index 3c2258b5..b0b61282 100644 --- a/crates/cf-commands/src/handlers/static_complexity_yaml.rs +++ b/crates/cf-commands/src/handlers/static_complexity_yaml.rs @@ -1,568 +1,20 @@ -//! Static-analysis YAML report path for the UAST `static/complexity` analyzer. -//! -//! Reproduces `codefang run --analyzers static/complexity --format yaml` over a -//! Go source tree (`static/static_complexity.yaml`). The reference static pipeline -//! (`StaticService.AnalyzeFolder` → per-file UAST parse → -//! `complexity.Analyzer.Analyze` → `StampSourceFile`/`StampLanguage` → -//! `complexity.Aggregator` (detailed `functions` collection in file-walk order + -//! summed totals) → `StaticService.FormatPerAnalyzer` → -//! `complexity.FormatReportYAML` = `yaml.Marshal(ComputeAllMetrics(report))`) -//! reduces, for this single UAST analyzer with `--static-workers 1`, to: -//! -//! 1. a lexical directory walk (mirroring `filepath.WalkDir`), parsing every -//! parser-supported file with [`cf_uast::Parser`]; -//! 2. per file: run the ported [`cf_complexity::Analyzer`] (bridged from the -//! canonical `cf_uast_node::Node` to the analyzer's minimal node model) to -//! obtain the per-function metrics, stamped with the file's relative path, -//! language, and directory; -//! 3. concatenating those per-file function lists **in walk order** (the -//! `DetailedDataCollector` appends without re-sorting) plus summing the -//! aggregate totals; -//! 4. `ComputeAllMetrics` over the assembled report — `function_complexity` -//! (the reference `sort.Slice` by cyclomatic desc, reproduced via [`crate::handlers::go_sort`]), -//! `distribution`, `high_risk_functions` (the reference `sort.Slice` by risk priority), -//! `aggregate`; -//! 5. marshaling the `ComputedMetrics` struct through cf-goyaml -//! (`gopkg.in/yaml.v3` parity; the static YAML path writes the marshaled -//! metrics directly, with no version header). -//! -//! The aggregate `cognitive_complexity`/`nesting_depth` are emitted as `0` -//! because the aggregator stores them as float64 *averages*; `ParseReportData`'s -//! `int` type assertion fails on a float, leaving the zero value (a faithful the reference implementation -//! quirk — see the reference `parseReportScalars`). - -use std::fs; -use std::path::{Path, PathBuf}; - -use cf_complexity::node::{Node as CNode, Positions as CPos}; -use cf_complexity::{Analyzer, FunctionMetrics}; -use cf_gojson::{GoMap, GoValue, MapOrigin}; +//! `static/complexity --format yaml`: the walk products of +//! [`super::static_complexity::collect`] serialized as the +//! [`cf_complexity::report::computed_metrics`] value +//! (`function_complexity`, `distribution`, `high_risk_functions`, +//! `aggregate`) through cf-goyaml. `--per-file` is a JSON-only enrichment +//! (the run dispatcher forwards the flag to the JSON handler alone). + +use cf_complexity::report::computed_metrics; use cf_goyaml::marshal; -use cf_uast::Parser; -use cf_uast_node::Node as UNode; - -use crate::handlers::go_sort; - -// Risk thresholds. -const CYCLOMATIC_THRESHOLD_HIGH: i64 = 10; -const CYCLOMATIC_THRESHOLD_MODERATE: i64 = 5; -const COGNITIVE_THRESHOLD_HIGH: i64 = 15; -const COGNITIVE_THRESHOLD_MODERATE: i64 = 7; -const NESTING_THRESHOLD_HIGH: i64 = 5; -const NESTING_THRESHOLD_MODERATE: i64 = 3; -const RISK_SCORE_CRITICAL: i64 = 5; -const RISK_SCORE_HIGH: i64 = 3; +use super::static_complexity::collect; +use super::StaticFilter; -/// One function's data as carried in the merged `functions` collection, in walk -/// order (mirrors `complexity.FunctionData` after `Stamp*`). -struct FunctionData { - name: String, - source_file: String, - language: String, - directory: String, - cyclomatic: i64, - cognitive: i64, - nesting: i64, - loc: i64, -} - -/// Builds the `static/complexity --format yaml` report bytes for `root_path`. -/// Returns `None` when the path does not exist (caller falls through to the -/// blocked-dependency sentinel). +/// Builds the `static/complexity --format yaml` report bytes for +/// `root_path`, or `None` when the path does not exist. #[must_use] -pub fn complexity_report_yaml(root_path: &str, filter: &super::StaticFilter) -> Option<Vec<u8>> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - - let parser = Parser::new(); - let analyzer = Analyzer; - - let mut functions: Vec<FunctionData> = Vec::new(); - let mut total_functions: i64 = 0; - let mut total_complexity: i64 = 0; - let mut decision_points: i64 = 0; - let mut max_complexity: i64 = 0; - - walk( - root, - root_path, - &parser, - filter, - &analyzer, - &mut functions, - &mut total_functions, - &mut total_complexity, - &mut decision_points, - &mut max_complexity, - ); - - let average_complexity = if total_functions > 0 { - total_complexity as f64 / total_functions as f64 - } else { - 0.0 - }; - - let metrics = build_computed_metrics( - &functions, - total_functions, - average_complexity, - max_complexity, - total_complexity, - decision_points, - ); - - Some(marshal(&metrics)) -} - -/// Recursively walks `dir` in lexical order (mirroring `filepath.WalkDir`). -/// `.git` is skipped; every parser-supported file is parsed and analyzed. -#[allow(clippy::too_many_arguments)] -fn walk( - dir: &Path, - root_path: &str, - parser: &Parser, - filter: &super::StaticFilter, - analyzer: &Analyzer, - functions: &mut Vec<FunctionData>, - total_functions: &mut i64, - total_complexity: &mut i64, - decision_points: &mut i64, - max_complexity: &mut i64, -) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file( - dir, - root_path, - parser, - filter, - analyzer, - functions, - total_functions, - total_complexity, - decision_points, - max_complexity, - ); - return; - } - let Ok(read) = fs::read_dir(dir) else { - return; - }; - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let Ok(ft) = entry.file_type() else { continue }; - if ft.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; - } - walk( - &path, - root_path, - parser, - filter, - analyzer, - functions, - total_functions, - total_complexity, - decision_points, - max_complexity, - ); - continue; - } - - visit_file( - &path, - root_path, - parser, - filter, - analyzer, - functions, - total_functions, - total_complexity, - decision_points, - max_complexity, - ); - } -} - -/// Analyzes one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): parser support + path policy filters, parse, -/// per-file metrics, and fold into the accumulators. -#[allow(clippy::too_many_arguments)] -fn visit_file( - path: &Path, - root_path: &str, - parser: &Parser, - filter: &super::StaticFilter, - analyzer: &Analyzer, - functions: &mut Vec<FunctionData>, - total_functions: &mut i64, - total_complexity: &mut i64, - decision_points: &mut i64, - max_complexity: &mut i64, -) { - let path_str = path.to_string_lossy(); - if !parser.is_supported(&path_str) { - return; - } - // pathpolicy.Exclude(path, nil, opts): skip vendored / generated paths, - // exactly as the reference static `streamFiles` filter does (without this, the - // walk over a repo with a `vendor/` tree pulls in thousands of extra - // functions and the report diverges from the reference output). - if filter.skips(&path_str) { - return; - } - let Some(content) = super::read_source_capped(path) else { - return; - }; - let Ok(uroot) = parser.parse(&path_str, &content) else { - super::note_skipped_file(); - return; - }; - - let croot = bridge(&uroot); - let metrics: Vec<FunctionMetrics> = analyzer.function_metrics(Some(&croot)); - if metrics.is_empty() { - // A per-file report with no functions ("No functions found") - // contributes nothing to the aggregator's count keys. - return; - } - - let stamped = make_relative_path(path, root_path); - let dir_rel = parent_dir(&stamped); - let language = parser.get_language(&path_str); - - let mut file_max: i64 = 0; - *total_functions += metrics.len() as i64; - for m in &metrics { - *total_complexity += m.cyclomatic_complexity; - *decision_points += m.decision_points; - if m.cyclomatic_complexity > file_max { - file_max = m.cyclomatic_complexity; - } - } - if file_max > *max_complexity { - *max_complexity = file_max; - } - - for m in metrics { - functions.push(FunctionData { - name: m.name, - source_file: stamped.clone(), - language: language.clone(), - directory: dir_rel.clone(), - cyclomatic: m.cyclomatic_complexity, - cognitive: m.cognitive_complexity, - nesting: m.nesting_depth, - loc: m.lines_of_code, - }); - } -} - -/// Converts the canonical `cf_uast_node::Node` tree into the complexity -/// analyzer's minimal node model (`cf_complexity::node::Node`). -fn bridge(u: &UNode) -> CNode { - let mut c = CNode::new(u.node_type.clone()); - c.token = u.token.clone(); - c.roles = u.roles.clone(); - for (k, v) in &u.props { - c.props.insert(k.clone(), v.clone()); - } - if let Some(p) = &u.pos { - c.pos = Some(CPos { - start_line: p.start_line as u32, - start_col: p.start_col as u32, - start_offset: p.start_offset as u32, - end_line: p.end_line as u32, - end_col: p.end_col as u32, - end_offset: p.end_offset as u32, - }); - } - c.children = u.children.iter().map(bridge).collect(); - c -} - -/// The reference `analyze.MakeRelativePath(filePath, rootPath)`: path relative to root. -/// When the file is directly under the analyzed dir, this is the bare filename. -fn make_relative_path(path: &Path, root_path: &str) -> String { - let root = Path::new(root_path); - match path.strip_prefix(root) { - // filepath.Rel(root, root) == "." (a FILE root stamps "."). - Ok(rel) if rel.as_os_str().is_empty() => ".".to_string(), - Ok(rel) => rel.to_string_lossy().into_owned(), - Err(_) => path.to_string_lossy().into_owned(), - } -} - -/// The reference `filepath.Dir(stamped)`: directory portion (`.` for a bare filename). -fn parent_dir(stamped: &str) -> String { - let p = PathBuf::from(stamped); - match p.parent() { - Some(d) if !d.as_os_str().is_empty() => d.to_string_lossy().into_owned(), - _ => ".".to_string(), - } -} - -/// Builds the `ComputedMetrics` GoValue (struct-origin map, declaration order: -/// `function_complexity`, `distribution`, `high_risk_functions`, `aggregate`). -fn build_computed_metrics( - functions: &[FunctionData], - total_functions: i64, - average_complexity: f64, - max_complexity: i64, - total_complexity: i64, - decision_points: i64, -) -> GoValue { - let function_complexity = build_function_complexity(functions); - let distribution = build_distribution(functions); - let high_risk = build_high_risk(functions); - let aggregate = build_aggregate( - total_functions, - average_complexity, - max_complexity, - total_complexity, - decision_points, - ); - - let mut m = GoMap::new(MapOrigin::Struct); - m.push("function_complexity", function_complexity); - m.push("distribution", distribution); - m.push("high_risk_functions", high_risk); - m.push("aggregate", aggregate); - GoValue::Map(m) -} - -/// `FunctionComplexityMetric.Compute`: per-function density + risk, then -/// `sort.Slice` by cyclomatic complexity descending (unstable; pdqsort). -fn build_function_complexity(functions: &[FunctionData]) -> GoValue { - let mut idx: Vec<usize> = (0..functions.len()).collect(); - go_sort::slice(&mut idx, |&a, &b| { - functions[a].cyclomatic > functions[b].cyclomatic - }); - - let mut arr = Vec::with_capacity(functions.len()); - for &i in &idx { - let f = &functions[i]; - let density = if f.loc > 0 { - f.cyclomatic as f64 / f.loc as f64 - } else { - 0.0 - }; - let risk = classify_function_risk(f.cyclomatic, f.cognitive, f.nesting); - - let mut o = GoMap::new(MapOrigin::Struct); - o.push("name", GoValue::Str(f.name.clone())); - push_omitempty(&mut o, "source_file", &f.source_file); - push_omitempty(&mut o, "language", &f.language); - push_omitempty(&mut o, "directory", &f.directory); - o.push("cyclomatic_complexity", GoValue::Int(f.cyclomatic)); - o.push("cognitive_complexity", GoValue::Int(f.cognitive)); - o.push("nesting_depth", GoValue::Int(f.nesting)); - o.push("lines_of_code", GoValue::Int(f.loc)); - o.push("complexity_density", GoValue::Float(density)); - o.push("risk_level", GoValue::Str(risk.to_string())); - arr.push(GoValue::Map(o)); - } - GoValue::Array(arr) -} - -fn push_omitempty(m: &mut GoMap, key: &str, value: &str) { - if !value.is_empty() { - m.push(key, GoValue::Str(value.to_string())); - } -} - -/// `DistributionMetric.Compute` = `stats.Distribution`: counts per complexity -/// level. The resulting `map[string]int` is byte-sorted by yaml.v3. -fn build_distribution(functions: &[FunctionData]) -> GoValue { - let mut simple = 0i64; - let mut moderate = 0i64; - let mut complex = 0i64; - for f in functions { - if f.cyclomatic <= CYCLOMATIC_THRESHOLD_MODERATE { - simple += 1; - } else if f.cyclomatic <= CYCLOMATIC_THRESHOLD_HIGH { - moderate += 1; - } else { - complex += 1; - } - } - let mut m = GoMap::new(MapOrigin::Map); - if complex > 0 { - m.push("complex", GoValue::Int(complex)); - } - if moderate > 0 { - m.push("moderate", GoValue::Int(moderate)); - } - if simple > 0 { - m.push("simple", GoValue::Int(simple)); - } - GoValue::Map(m) -} - -/// `HighRiskFunctionMetric.Compute`: functions with at least one issue, then -/// `sort.Slice` by risk priority ascending (unstable; pdqsort). -fn build_high_risk(functions: &[FunctionData]) -> GoValue { - struct HighRisk { - idx: usize, - issues: Vec<&'static str>, - risk: &'static str, - } - - let mut items: Vec<HighRisk> = Vec::new(); - for (i, f) in functions.iter().enumerate() { - let mut issues: Vec<&'static str> = Vec::new(); - if f.cyclomatic >= CYCLOMATIC_THRESHOLD_HIGH { - issues.push("High cyclomatic complexity"); - } - if f.cognitive >= COGNITIVE_THRESHOLD_HIGH { - issues.push("High cognitive complexity"); - } - if f.nesting >= NESTING_THRESHOLD_HIGH { - issues.push("Deep nesting"); - } - if issues.is_empty() { - continue; - } - let risk = classify_function_risk(f.cyclomatic, f.cognitive, f.nesting); - items.push(HighRisk { - idx: i, - issues, - risk, - }); - } - - go_sort::slice(&mut items, |a, b| { - risk_priority(a.risk) < risk_priority(b.risk) - }); - - let mut arr = Vec::with_capacity(items.len()); - for it in items { - let f = &functions[it.idx]; - let mut o = GoMap::new(MapOrigin::Struct); - o.push("name", GoValue::Str(f.name.clone())); - push_omitempty(&mut o, "source_file", &f.source_file); - push_omitempty(&mut o, "language", &f.language); - push_omitempty(&mut o, "directory", &f.directory); - o.push("cyclomatic_complexity", GoValue::Int(f.cyclomatic)); - o.push("cognitive_complexity", GoValue::Int(f.cognitive)); - o.push("risk_level", GoValue::Str(it.risk.to_string())); - let issues: Vec<GoValue> = it - .issues - .iter() - .map(|s| GoValue::Str((*s).to_string())) - .collect(); - o.push("issues", GoValue::Array(issues)); - arr.push(GoValue::Map(o)); - } - GoValue::Array(arr) -} - -/// `AggregateMetric.Compute`: summary stats. `cognitive_complexity` and -/// `nesting_depth` are 0 (float averages fail the int assertion in -/// `ParseReportData`). -fn build_aggregate( - total_functions: i64, - average_complexity: f64, - max_complexity: i64, - total_complexity: i64, - decision_points: i64, -) -> GoValue { - let health = calculate_health_score(average_complexity); - let message = build_complexity_message(average_complexity); - - let mut m = GoMap::new(MapOrigin::Struct); - m.push("total_functions", GoValue::Int(total_functions)); - m.push("average_complexity", GoValue::Float(average_complexity)); - m.push("max_complexity", GoValue::Int(max_complexity)); - m.push("total_complexity", GoValue::Int(total_complexity)); - m.push("cognitive_complexity", GoValue::Int(0)); - m.push("nesting_depth", GoValue::Int(0)); - m.push("decision_points", GoValue::Int(decision_points)); - m.push("health_score", GoValue::Float(health)); - m.push("message", GoValue::Str(message.to_string())); - GoValue::Map(m) -} - -/// `classifyFunctionRisk`. -fn classify_function_risk(cyclomatic: i64, cognitive: i64, nesting: i64) -> &'static str { - let mut score = 0i64; - if cyclomatic >= CYCLOMATIC_THRESHOLD_HIGH { - score += 2; - } else if cyclomatic >= CYCLOMATIC_THRESHOLD_MODERATE { - score += 1; - } - if cognitive >= COGNITIVE_THRESHOLD_HIGH { - score += 2; - } else if cognitive >= COGNITIVE_THRESHOLD_MODERATE { - score += 1; - } - if nesting >= NESTING_THRESHOLD_HIGH { - score += 2; - } else if nesting >= NESTING_THRESHOLD_MODERATE { - score += 1; - } - if score >= RISK_SCORE_CRITICAL { - "CRITICAL" - } else if score >= RISK_SCORE_HIGH { - "HIGH" - } else if score >= 1 { - "MEDIUM" - } else { - "LOW" - } -} - -/// `metrics.RiskPriority`. -fn risk_priority(level: &str) -> i64 { - match level { - "CRITICAL" => 0, - "HIGH" => 1, - "MEDIUM" => 2, - _ => 3, - } -} - -/// `calculateHealthScore`. -fn calculate_health_score(avg: f64) -> f64 { - const PERFECT: f64 = 100.0; - const GOOD_BASE: f64 = 80.0; - const MODERATE_BASE: f64 = 50.0; - const AVG_LOW: f64 = 1.0; - const AVG_GOOD: f64 = 3.0; - const AVG_HIGH: f64 = 7.0; - if avg <= AVG_LOW { - PERFECT - } else if avg <= AVG_GOOD { - GOOD_BASE + (AVG_GOOD - avg) * 10.0 - } else if avg <= AVG_HIGH { - MODERATE_BASE + (AVG_HIGH - avg) * 7.5 - } else { - (MODERATE_BASE - (avg - AVG_HIGH) * 5.0).max(0.0) - } -} - -/// `buildComplexityMessage`. -fn build_complexity_message(score: f64) -> &'static str { - if score <= 1.0 { - "Excellent complexity - functions are simple and maintainable" - } else if score <= 3.0 { - "Good complexity - functions have reasonable complexity" - } else if score <= 7.0 { - "Fair complexity - some functions could be simplified" - } else { - "High complexity - functions are complex and should be refactored" - } +pub fn complexity_report_yaml(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { + let collected = collect(root_path, filter)?; + Some(marshal(&computed_metrics(&collected.functions()))) } diff --git a/crates/cf-commands/src/handlers/static_halstead.rs b/crates/cf-commands/src/handlers/static_halstead.rs index 4389d593..a0dead9d 100644 --- a/crates/cf-commands/src/handlers/static_halstead.rs +++ b/crates/cf-commands/src/handlers/static_halstead.rs @@ -1,1328 +1,365 @@ -//! Static-analysis report path for the UAST `static/halstead` analyzer. +//! Report path for the `static/halstead` analyzer. //! -//! Reproduces the reference static pipeline for the single-analyzer -//! `codefang run --analyzers static/halstead` capture across its machine formats. +//! The handler owns serialization only: the shared static walk +//! ([`super::walk_source_files`]) selects and reads the files, this module +//! parses each one with `cf_uast::Parser`, hands each tree **and its source +//! bytes** to `cf_halstead`, and serializes the results. All measurement — the operator +//! / operand classification, the per-function and union aggregates, every +//! threshold, label, health score and message — lives in the `cf-halstead` +//! crate (`cf_halstead::classify`, `::aggregate`, `::thresholds`), so each +//! format below reports the same numbers. //! -//! Pipeline (the reference `StaticService.uastPhase` → per-file `halstead.Analyze` → -//! `common.Aggregator` → format-specific serialization): +//! ## Aggregation //! -//! 1. `streamFiles` walks `rootPath` with `filepath.WalkDir` (lexical order, -//! `.git` skipped), keeping every UAST-supported, non-vendor/-generated file -//! (`pathpolicy.Exclude(path, nil, opts)`, content `nil`). -//! 2. Each file is parsed by `cf_uast::Parser` and run through the Halstead -//! analyzer's VISITOR path (`CreateVisitor` + `MultiAnalyzerTraverser`, the -//! path `analyze.Factory` actually routes `VisitorProvider` analyzers -//! through): a function-context-stack DFS attributes each operator/operand -//! token to the innermost enclosing function (no depth limit), derives the -//! Halstead measures, and produces a per-file report whose `functions` -//! collection is stamped with `_source_file` (path relative to root), -//! `_language` (`"go"`), and `_directory`. -//! 3. The base `common.Aggregator` sums each numeric key across the per-file -//! reports and divides by the report count (file count), sums the count key -//! `total_functions`, and collects every detailed function (dedup key -//! `[_source_file, name]`). +//! The codebase aggregate (and each `--per-file` entry) is the UNION of the +//! operator / operand multisets of its functions: `n1` / `n2` are distinct +//! across every function, `N1` / `N2` are the summed occurrences, and +//! V / D / E / T / B derive from those four numbers. Per-function statistics +//! (mean / max volume, difficulty, effort; high-effort count and share) sit +//! alongside; the status message, health score and section score come from +//! the share of high-effort functions (`cf_halstead::thresholds`). //! //! ## Formats //! -//! * **bin** (`FormatReportBinary`): `ComputeAllMetrics(report)` → -//! `reportutil.EncodeBinaryEnvelope(metrics)`. The metric computer parses the -//! aggregated report back into scalars; the integer-typed aggregate fields -//! (`distinct_operators`, …) are *averaged* and therefore stored as `float64`, -//! so the `.(int)` type assertions in `ParseReportData` fail and those fields -//! come out **0** — a faithful reproduction of the reference quirk. The bin payload is -//! the `ComputedMetrics` struct marshaled with compact `encoding/json` -//! (cf-gojson) inside the CFB1 envelope. +//! * **json** — the structured `run` report: one `HALSTEAD` section with key +//! metrics, the per-function volume distribution, every function as an +//! issue (effort descending), and — with `--per-file` — one entry per +//! analyzed file. `--per-file` is json-only (the dispatch passes the flag +//! to the json builder alone). +//! * **yaml** / **bin** — the `ComputedMetrics` value: `function_halstead`, +//! `distribution`, `high_effort_functions` (effort ≥ the high-effort bound) +//! and `aggregate`; bin wraps it in the CFB1 envelope. +//! * **text** / **compact** — the summary-only section (metrics + status). +//! * **plot** — the raw report (`halstead_raw_report_value`): every function +//! with its operator / operand maps. //! -//! ## A note on the nondeterministic `message` -//! -//! The aggregate `message` is built from "the first numeric metric" in a -//! reference-side `map[string]float64` iterated in randomized order (`common.Aggregator.GetResult` -//! → `buildHalsteadMessage(firstAverage)`), so the reference binary emits a -//! *different* message label on different runs (the field is unstable in the reference binary; the -//! canonicalizer measured and normalizes it). We compute the label -//! deterministically from the real aggregated volume via -//! [`build_aggregate_message`] — a genuine computation, never a captured -//! constant. Every other byte of the report is deterministic. +//! Function lists are ordered by one total key everywhere: effort desc, +//! volume desc, name asc, file asc. A folder with no analyzable files, or +//! none containing functions, yields the empty report (zeros, status +//! `No functions found`) with exit 0; only a non-existent path yields `None`. -use std::collections::HashMap; -use std::path::Path; +use std::cmp::Ordering; use super::StaticFilter; use cf_gojson::{GoMap, GoValue, MapOrigin}; +use cf_halstead::aggregate::{analyze_functions, summarize, FunctionMetrics, Summary}; +use cf_halstead::thresholds::{ + difficulty_assessment, effort_assessment, is_high_effort, risk_level, severity, + volume_assessment, VolumeLevel, +}; +use cf_halstead::ANALYZER_NAME; use cf_uast::{Node, Parser}; -/// Threshold above which CMS sketches are populated (`cmsTokenThreshold`). The -/// CMS total count is exact, so the estimated totals equal the exact sums; we -/// therefore set them directly without a sketch. -const CMS_TOKEN_THRESHOLD: i64 = 1000; - -// --- detector classification tables --- - -const OPERATOR_TYPES: &[&str] = &[ - "BinaryOp", - "UnaryOp", - "Assignment", - "Call", - "Index", - "Slice", - "Return", -]; -const OPERATOR_ROLES: &[&str] = &["Operator", "Assignment", "Call", "Return"]; -const OPERAND_TYPES: &[&str] = &["Identifier", "Literal", "Field"]; -const OPERAND_ROLES: &[&str] = &["Name", "Literal", "Variable", "Argument"]; -const DECLARATION_TYPES: &[&str] = &[ - "Function", - "FunctionDecl", - "Method", - "Parameter", - "Variable", - "Field", - "Import", - "Package", - "Struct", - "Class", - "Interface", - "Enum", -]; -const DECLARATION_PARENT_ROLES: &[&str] = &["Declaration", "Parameter", "Import", "Type"]; - -const TOKEN_OPERATOR_SET: &[&str] = &[ - "===", "!==", "==", "!=", "<=", ">=", "&&", "||", "<<=", ">>=", "<<", ">>", "**", ":=", "+=", - "-=", "*=", "/=", "%=", "&=", "|=", "^=", "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", - "^", "!", -]; -const TOKEN_OPERATORS_BY_LENGTH: &[&str] = &[ - "===", "!==", "<<=", ">>=", "==", "!=", "<=", ">=", "&&", "||", "<<", ">>", "**", ":=", "+=", - "-=", "*=", "/=", "%=", "&=", "|=", "^=", "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", - "^", "!", -]; - -// --- Halstead formula constants --- -const TIME_CONSTANT: f64 = 18.0; -const BUG_CONSTANT: f64 = 3000.0; -const DIFFICULTY_DIVISOR: f64 = 2.0; - -/// Bit-exact reproduction of the reference math library's `Log2` so the -/// Halstead floats match the report contract byte-for-byte (pinned by -/// tests/compat). Rust's `f64::log2` differs in the last ULP for some inputs; -/// the reference computes `Log2(x) = log(frac)*(1/Ln2) + exp` (via `Frexp`) -/// with its own polynomial `log`, and that path is reproduced exactly here. -// The float literals reproduce the reference constants verbatim (they round to -// the same f64 values the lints would suggest); keep them textually identical -// to the pinned implementation for auditability. -#[allow(clippy::approx_constant, clippy::excessive_precision)] -mod goflt { - const LN2: f64 = 0.693147180559945309417232121458176568075500134360255254120680009; - const SQRT2: f64 = 1.41421356237309504880168872420969807856967187537694807317667974; - - /// The reference `math.Frexp`: returns `(frac, exp)` with `frac ∈ [0.5, 1)` and - /// `x == frac · 2^exp`. Only the normal-positive path is needed here. - fn frexp(f: f64) -> (f64, i32) { - if f == 0.0 || !f.is_finite() { - return (f, 0); - } - // normalize: scale subnormals into the normal range. - const SMALLEST_NORMAL: f64 = 2.2250738585072014e-308; // 2**-1022 - let (x, mut exp) = if f.abs() < SMALLEST_NORMAL { - (f * (1u64 << 52) as f64, -52) - } else { - (f, 0) - }; - let bits = x.to_bits(); - const SHIFT: u64 = 52; - const MASK: u64 = 0x7FF; - const BIAS: i64 = 1023; - exp += (((bits >> SHIFT) & MASK) as i64 - BIAS + 1) as i32; - let mut nb = bits; - nb &= !(MASK << SHIFT); - nb |= ((-1 + BIAS) as u64) << SHIFT; - (f64::from_bits(nb), exp) - } - - /// The reference `math.log` (portable polynomial), matching the amd64 `archLog` results. - fn log(x: f64) -> f64 { - const LN2HI: f64 = 6.93147180369123816490e-01; - const LN2LO: f64 = 1.90821492927058770002e-10; - const L1: f64 = 6.666666666666735130e-01; - const L2: f64 = 3.999999999940941908e-01; - const L3: f64 = 2.857142874366239149e-01; - const L4: f64 = 2.222219843214978396e-01; - const L5: f64 = 1.818357216161805012e-01; - const L6: f64 = 1.531383769920937332e-01; - const L7: f64 = 1.479819860511658591e-01; - - if x.is_nan() || x == f64::INFINITY { - return x; - } - if x < 0.0 { - return f64::NAN; - } - if x == 0.0 { - return f64::NEG_INFINITY; - } - - let (mut f1, mut ki) = frexp(x); - if f1 < SQRT2 / 2.0 { - f1 *= 2.0; - ki -= 1; - } - let f = f1 - 1.0; - let k = f64::from(ki); - - let s = f / (2.0 + f); - let s2 = s * s; - let s4 = s2 * s2; - let t1 = s2 * (L1 + s4 * (L3 + s4 * (L5 + s4 * L7))); - let t2 = s4 * (L2 + s4 * (L4 + s4 * L6)); - let r = t1 + t2; - let hfsq = 0.5 * f * f; - k * LN2HI - ((hfsq - (s * (hfsq + r) + k * LN2LO)) - f) - } - - /// The reference `math.Log2` (amd64: `log2` with `Frexp` + `archLog`). - #[must_use] - pub fn log2(x: f64) -> f64 { - let (frac, exp) = frexp(x); - if frac == 0.5 { - return f64::from(exp - 1); - } - log(frac) * (1.0 / LN2) + f64::from(exp) - } -} - -// --- aggregate numeric keys (reference getNumericKeys) --- -const NUMERIC_KEYS: &[&str] = &[ - "volume", - "difficulty", - "effort", - "time_to_program", - "delivered_bugs", - "distinct_operators", - "distinct_operands", - "total_operators", - "total_operands", - "vocabulary", - "length", - "estimated_length", -]; - -/// Per-function Halstead metrics (`FunctionHalsteadMetrics`). -/// -/// The raw-count fields (`distinct_*`, `total_*`, `vocabulary`, `length`, -/// `estimated_length`) are part of the per-function record but are not emitted in -/// the bin `function_halstead` entry (the reference `FunctionHalsteadData` carries only the -/// derived measures + complexity level); they are retained for the report value's -/// completeness and the sibling JSON/YAML encodings. -#[derive(Default, Clone)] -#[allow(dead_code)] -struct FunctionMetrics { - name: String, +/// Section title in the structured report. +const SECTION_TITLE: &str = "HALSTEAD"; +/// Score scale of the `N/10` labels. +const SCORE_MAX: f64 = 10.0; +/// Health score scale (the section score is `health / 100`). +const HEALTH_SCALE: f64 = 100.0; + +/// One function stamped with its file location. +struct FunctionRecord { + metrics: FunctionMetrics, + /// Path relative to the analyzed root. source_file: String, language: String, directory: String, - distinct_operators: i64, - distinct_operands: i64, - total_operators: i64, - total_operands: i64, - vocabulary: i64, - length: i64, - estimated_length: f64, - volume: f64, - difficulty: f64, - effort: f64, - time_to_program: f64, - delivered_bugs: f64, - /// CMS-estimated totals: the exact totals when the - /// function's token count reaches `cmsTokenThreshold`, 0 otherwise. - estimated_total_operators: i64, - estimated_total_operands: i64, - /// Per-function operator/operand counts (`FunctionReportItem.Operators` / - /// `.Operands`) — serialized into the raw aggregated report. - operators: std::collections::BTreeMap<String, i64>, - operands: std::collections::BTreeMap<String, i64>, } -/// Per-file aggregate of the Halstead `report` map's scalar metrics, plus the -/// detailed function list (the reference per-file `analyze.Report`). +/// One analyzed file: its union summary and the range of its functions +/// inside [`Aggregate::functions`] (empty for function-free files). struct FileReport { - /// Scalar metrics keyed exactly as the reference report map (numeric keys). - scalars: HashMap<&'static str, f64>, - total_functions: i64, - functions: Vec<FunctionMetrics>, -} - -/// Eight derived Halstead measures (`CalculateHalsteadMetrics`). -fn derive(n1: i64, n2: i64, big_n1: i64, big_n2: i64) -> (i64, i64, f64, f64, f64, f64, f64, f64) { - let vocabulary = n1 + n2; - let length = big_n1 + big_n2; - - let estimated_length = if n1 > 0 && n2 > 0 { - (n1 as f64) * goflt::log2(n1 as f64) + (n2 as f64) * goflt::log2(n2 as f64) - } else { - 0.0 - }; - let volume = if vocabulary > 0 { - (length as f64) * goflt::log2(vocabulary as f64) - } else { - 0.0 - }; - let difficulty = if n2 > 0 { - (n1 as f64 / DIFFICULTY_DIVISOR) * (big_n2 as f64 / n2 as f64) - } else { - 0.0 - }; - let effort = volume * difficulty; - let time_to_program = effort / TIME_CONSTANT; - let delivered_bugs = volume / BUG_CONSTANT; - ( - vocabulary, - length, - estimated_length, - volume, - difficulty, - effort, - time_to_program, - delivered_bugs, - ) -} - -/// `node.HasAnyRole` over a string slice. -fn has_any_role(node: &Node, roles: &[&str]) -> bool { - node.roles.iter().any(|nr| roles.iter().any(|r| nr == r)) -} - -fn is_operator(node: &Node) -> bool { - OPERATOR_TYPES.contains(&node.node_type.as_str()) || has_any_role(node, OPERATOR_ROLES) -} - -fn is_operand(node: &Node) -> bool { - OPERAND_TYPES.contains(&node.node_type.as_str()) || has_any_role(node, OPERAND_ROLES) -} - -fn operator_name(node: &Node) -> String { - if let Some(op) = node.props.get("operator") { - return op.clone(); - } - if let Some(op) = extract_operator_from_token(&node.token) { - return op.to_string(); - } - if !node.token.is_empty() { - return node.token.clone(); - } - node.node_type.clone() -} - -fn operand_name(node: &Node) -> String { - if !node.token.is_empty() { - return node.token.clone(); - } - if let Some(name) = node.props.get("name") { - return name.clone(); - } - if let Some(value) = node.props.get("value") { - return value.clone(); - } - String::new() -} - -fn extract_operator_from_token(token: &str) -> Option<&'static str> { - if token.trim().is_empty() { - return None; - } - if let Some(&op) = TOKEN_OPERATOR_SET.iter().find(|&&op| op == token) { - return Some(op); - } - for &op in TOKEN_OPERATORS_BY_LENGTH { - let needle = format!(" {op} "); - if token.contains(&needle) { - return Some(op); - } - } - None -} - -fn is_declaration_identifier(node: &Node, parent: Option<&Node>) -> bool { - let Some(parent) = parent else { return false }; - if node.node_type != "Identifier" || !has_any_role(node, &["Name"]) { - return false; - } - if has_any_role(parent, DECLARATION_PARENT_ROLES) { - return true; - } - DECLARATION_TYPES.contains(&parent.node_type.as_str()) -} - -/// `node.HasAnyType` over the given type list. -fn has_any_type(node: &Node, types: &[&str]) -> bool { - types.iter().any(|t| node.node_type == *t) -} - -/// `node.HasAllRoles`: node must carry every role in `roles`. -fn has_all_roles(node: &Node, roles: &[&str]) -> bool { - roles.iter().all(|r| node.roles.iter().any(|nr| nr == r)) -} - -/// Mirrors `Visitor.isFunction`: a `Function`/`Method` type OR a node carrying -/// BOTH the `Function` and `Declaration` roles. -fn is_function_node(node: &Node) -> bool { - has_any_type(node, &["Function", "Method"]) || has_all_roles(node, &["Function", "Declaration"]) -} - -/// Faithful port of `common.ExtractEntityName`: props["name"] -> own token -> -/// first child's token -> first child's props["name"]. Returns `None` only when -/// none of those sources is present (matching the reference implementation's `(string, bool)` where the -/// caller treats `!ok` and `""` identically via the `name == "" -> "anonymous"` -/// fallback in `pushContext`). -fn extract_entity_name(n: &Node) -> Option<String> { - if let Some(v) = n.props.get("name") { - return Some(v.clone()); - } - if !n.token.is_empty() { - return Some(n.token.clone()); - } - if let Some(child) = n.children.first() { - if !child.token.is_empty() { - return Some(child.token.clone()); - } - if let Some(v) = child.props.get("name") { - return Some(v.clone()); - } - } - None -} - -type FuncCounts = (String, HashMap<String, i64>, HashMap<String, i64>); - -/// Reproduces the reference `MultiAnalyzerTraverser.Traverse` driving the halstead -/// `Visitor`: a pre/post-order DFS with a function-context STACK. Each -/// operator/operand token is attributed to the innermost open function context -/// (so nested closures do not double-count into their parents). Functions are -/// returned in OnExit (post-order completion) order — the order the reference implementation appends to -/// `functionMetrics`. -fn collect_function_metrics(root: &Node) -> Vec<FuncCounts> { - // Each open context: (name, operators, operands). - struct Ctx { - name: String, - operators: HashMap<String, i64>, - operands: HashMap<String, i64>, - } - let mut contexts: Vec<Ctx> = Vec::new(); - let mut out: Vec<FuncCounts> = Vec::new(); - - // Manual explicit stack mirroring traverseFrame (childIdx == -1 => pre-enter). - struct Frame<'a> { - node: &'a Node, - parent: Option<&'a Node>, - child_idx: i64, - is_fn: bool, - } - let mut stack: Vec<Frame> = vec![Frame { - node: root, - parent: None, - child_idx: -1, - is_fn: false, - }]; - - while let Some(top) = stack.last_mut() { - if top.child_idx == -1 { - // OnEnter. - let n = top.node; - let parent = top.parent; - let is_fn = is_function_node(n); - top.is_fn = is_fn; - if is_fn { - let name = match extract_entity_name(n) { - Some(s) if !s.is_empty() => s, - _ => "anonymous".to_string(), - }; - contexts.push(Ctx { - name, - operators: HashMap::new(), - operands: HashMap::new(), - }); - } - // processNode against the current (innermost) context. - if let Some(ctx) = contexts.last_mut() { - if is_operator(n) { - let op = operator_name(n); - if !op.is_empty() { - *ctx.operators.entry(op).or_insert(0) += 1; - } - // operator matched: do not also try operand (recordOperator - // returns true even when the name is empty). - } else if is_operand(n) && !is_declaration_identifier(n, parent) { - let opnd = operand_name(n); - if !opnd.is_empty() { - *ctx.operands.entry(opnd).or_insert(0) += 1; - } - } - } - top.child_idx = 0; - // fallthrough to push children below. - } - - let top = stack.last_mut().unwrap(); - let nchildren = top.node.children.len() as i64; - if top.child_idx < nchildren { - let idx = top.child_idx as usize; - top.child_idx += 1; - // SAFETY of borrows: re-borrow via raw indices to satisfy the - // borrow checker by cloning the needed references first. - let parent_node: &Node = top.node; - let child: &Node = &top.node.children[idx]; - stack.push(Frame { - node: child, - parent: Some(parent_node), - child_idx: -1, - is_fn: false, - }); - continue; - } - - // OnExit: pop frame, finalize context if this node opened one. - let finished = stack.pop().unwrap(); - if finished.is_fn { - if let Some(ctx) = contexts.pop() { - out.push((ctx.name, ctx.operators, ctx.operands)); - } - } - } - out + rel: String, + summary: Summary, + start: usize, + end: usize, } -/// Computes the per-file Halstead report (`Analyzer.Analyze` + file-level -/// aggregation). Returns the empty report (all-zero scalars, no functions) when -/// the file has no functions. -fn analyze_file(root: &Node, source_file: &str, language: &str, directory: &str) -> FileReport { - // Mirror the reference VISITOR path (the static pipeline uses `CreateVisitor` + - // MultiAnalyzerTraverser, NOT the standalone `findFunctions`/Analyze path). - // The visitor maintains a context STACK: every operator/operand token is - // recorded into the INNERMOST enclosing function only, so tokens inside a - // nested closure do not leak into the outer function's counts. Functions are - // emitted in OnExit (post-order) order. There is no depth limit. - let functions = collect_function_metrics(root); - - let mut scalars: HashMap<&'static str, f64> = NUMERIC_KEYS.iter().map(|k| (*k, 0.0)).collect(); - - if functions.is_empty() { - return FileReport { - scalars, - total_functions: 0, - functions: Vec::new(), - }; - } - - // Per-function metrics. - let mut fn_metrics: Vec<FunctionMetrics> = Vec::with_capacity(functions.len()); - // File-level operator/operand maps for the file aggregate. - let mut file_operators: HashMap<String, i64> = HashMap::new(); - let mut file_operands: HashMap<String, i64> = HashMap::new(); - let mut est_total_ops: i64 = 0; - let mut est_total_opnds: i64 = 0; - - for (fname, operators, operands) in &functions { - let total_ops: i64 = operators.values().copied().sum(); - let total_opnds: i64 = operands.values().copied().sum(); - let n1 = operators.len() as i64; - let n2 = operands.len() as i64; - - let (vocab, length, est_len, vol, diff, eff, ttp, bugs) = - derive(n1, n2, total_ops, total_opnds); - - // CMS path: the exact total count equals the exact sum, so estimated - // totals equal the exact totals when the threshold is reached - // (reference behavior; below the threshold the sketches are nil and the - // per-function estimated totals stay 0). - let (est_fn_ops, est_fn_opnds) = if total_ops + total_opnds >= CMS_TOKEN_THRESHOLD { - est_total_ops += total_ops; - est_total_opnds += total_opnds; - (total_ops, total_opnds) - } else { - (0, 0) - }; - - for (k, v) in operators { - *file_operators.entry(k.clone()).or_insert(0) += *v; - } - for (k, v) in operands { - *file_operands.entry(k.clone()).or_insert(0) += *v; - } - - fn_metrics.push(FunctionMetrics { - name: fname.clone(), - source_file: source_file.to_string(), +/// The whole analyzed tree. +struct Aggregate { + /// Union summary over every function of every file. + summary: Summary, + /// Every function in walk order (files lexical, functions post-order). + functions: Vec<FunctionRecord>, + /// Per-file reports in walk order. + files: Vec<FileReport>, +} + +/// Measures one parsed file. +fn analyze_file( + root: &Node, + content: &[u8], + rel: &str, + language: &str, + directory: &str, +) -> Vec<FunctionRecord> { + analyze_functions(root, Some(content)) + .into_iter() + .map(|metrics| FunctionRecord { + metrics, + source_file: rel.to_string(), language: language.to_string(), directory: directory.to_string(), - distinct_operators: n1, - distinct_operands: n2, - total_operators: total_ops, - total_operands: total_opnds, - vocabulary: vocab, - length, - estimated_length: est_len, - volume: vol, - difficulty: diff, - effort: eff, - time_to_program: ttp, - delivered_bugs: bugs, - estimated_total_operators: est_fn_ops, - estimated_total_operands: est_fn_opnds, - operators: operators.iter().map(|(k, v)| (k.clone(), *v)).collect(), - operands: operands.iter().map(|(k, v)| (k.clone(), *v)).collect(), - }); - } - - // File-level aggregate (calculateFileLevelMetrics). - let f_n1 = file_operators.len() as i64; - let f_n2 = file_operands.len() as i64; - let f_big_n1: i64 = file_operators.values().sum(); - let f_big_n2: i64 = file_operands.values().sum(); - let (f_vocab, f_length, f_est_len, f_vol, f_diff, f_eff, f_ttp, f_bugs) = - derive(f_n1, f_n2, f_big_n1, f_big_n2); - - scalars.insert("distinct_operators", f_n1 as f64); - scalars.insert("distinct_operands", f_n2 as f64); - scalars.insert("total_operators", f_big_n1 as f64); - scalars.insert("total_operands", f_big_n2 as f64); - scalars.insert("vocabulary", f_vocab as f64); - scalars.insert("length", f_length as f64); - scalars.insert("estimated_length", f_est_len); - scalars.insert("volume", f_vol); - scalars.insert("difficulty", f_diff); - scalars.insert("effort", f_eff); - scalars.insert("time_to_program", f_ttp); - scalars.insert("delivered_bugs", f_bugs); - let _ = (est_total_ops, est_total_opnds); - - FileReport { - scalars, - total_functions: fn_metrics.len() as i64, - functions: fn_metrics, - } -} - -/// Aggregated cross-file Halstead result (`common.Aggregator.GetResult`). -struct Aggregate { - /// Averaged numeric metrics (sum / report_count). - averages: HashMap<&'static str, f64>, - total_functions: i64, - functions: Vec<FunctionMetrics>, - /// Per-file snapshots (the reference `PerFileRetainer` clones) consumed by - /// the `--per-file` section enrichment; walk order. - file_reports: Vec<PerFileHalstead>, -} - -/// One retained per-file report: the FILE-level scalars plus the range of this -/// file's functions inside the concatenated [`Aggregate::functions`] list -/// (empty range for function-free files). -struct PerFileHalstead { - /// Path relative to the analyzed root (the `StampSourceFile` stamp). - rel: String, - /// FILE-level scalar metrics (exact per-file values, not averages). - scalars: HashMap<&'static str, f64>, - total_functions: i64, - start: usize, - end: usize, + }) + .collect() } -/// Walks `root` (lexical order, `.git` skipped, vendor/generated excluded), -/// parses each supported file, and aggregates the Halstead reports. +/// Walks `root_path` through the shared static walk, parses each admitted +/// file, and aggregates. `None` only when the path does not exist. fn aggregate_opts(root_path: &str, filter: &StaticFilter) -> Option<Aggregate> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - let parser = Parser::new(); - - // Numeric-key sums + report count (every analyzed file contributes a report, - // including empty-result files whose scalars are all 0). - let mut sums: HashMap<&'static str, f64> = NUMERIC_KEYS.iter().map(|k| (*k, 0.0)).collect(); - let mut report_count: i64 = 0; - let mut total_functions: i64 = 0; - // Detailed functions: the halstead `DetailedDataCollector` appends every - // per-file function WITHOUT deduplication (so two same-named functions in one - // file are both kept), in file-processing (lexical walk) order. - let mut functions: Vec<FunctionMetrics> = Vec::new(); - // Per-file snapshots for --per-file (rel path + file-level scalars + the - // function range just appended); retained unconditionally — one small map - // and two indices per file. - let mut file_reports: Vec<PerFileHalstead> = Vec::new(); - - let mut files: Vec<String> = Vec::new(); - collect_files(root, &parser, filter, &mut files); - - for path in &files { - let Some(content) = super::read_source_capped(std::path::Path::new(path)) else { - continue; - }; - - let rel = make_relative(path, root_path); - let directory = dir_of(&rel); - // `_language`: the reference implementation stamps `parser.GetLanguage(filePath)` (which - // StampLanguage) — the detected language name, NOT a fixed "go". - let language = parser.get_language(path); - - // The reference implementation's static pipeline parses EVERY UAST-supported file (`IsSupported` - // true) and runs the analyzer on the resulting tree. For files whose - // tree carries no functions (e.g. markdown READMEs), the analyzer - // returns the all-zero empty report, which still counts as one file in - // the cross-file average denominator (`report_count`). Rust may lack a - // wired grammar for some supported extensions (markdown), so a parse - // failure on a supported file is treated as that same empty report — - // matching the reference implementation's denominator byte-for-byte. - let report = match parser.parse(path, &content) { - Ok(node) => analyze_file(&node, &rel, &language, &directory), - Err(_) => FileReport { - scalars: NUMERIC_KEYS.iter().map(|k| (*k, 0.0)).collect(), - total_functions: 0, - functions: Vec::new(), - }, + let walk = super::walk_source_files(root_path, &parser, filter).ok()?; + + let mut functions: Vec<FunctionRecord> = Vec::new(); + let mut file_reports: Vec<FileReport> = Vec::new(); + for file in walk { + let directory = file.directory(); + // A supported file whose grammar is not wired (or that fails to + // parse) still counts as an analyzed, function-free file. + let records = match parser.parse(&file.rel_path, &file.bytes) { + Ok(node) => analyze_file( + &node, + &file.bytes, + &file.rel_path, + &file.language, + &directory, + ), + Err(_) => Vec::new(), }; - - report_count += 1; - for (k, v) in &report.scalars { - *sums.get_mut(k).unwrap() += *v; - } - total_functions += report.total_functions; - let start = functions.len(); - functions.extend(report.functions); - file_reports.push(PerFileHalstead { - rel, - scalars: report.scalars, - total_functions: report.total_functions, + let summary = summarize(records.iter().map(|r| &r.metrics)); + functions.extend(records); + file_reports.push(FileReport { + rel: file.rel_path, + summary, start, end: functions.len(), }); } - if report_count == 0 { - return None; - } - - let averages: HashMap<&'static str, f64> = sums - .iter() - .map(|(k, v)| (*k, *v / report_count as f64)) - .collect(); - Some(Aggregate { - averages, - total_functions, + summary: summarize(functions.iter().map(|r| &r.metrics)), functions, - file_reports, + files: file_reports, }) } -/// Recursively gathers UAST-supported, non-excluded files in lexical order -/// (`streamFiles` walk order; `filepath.WalkDir` visits entries name-sorted). -fn collect_files(dir: &Path, parser: &Parser, filter: &StaticFilter, out: &mut Vec<String>) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file(dir, parser, filter, out); - return; - } - let Ok(read) = std::fs::read_dir(dir) else { - return; - }; - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let Ok(ft) = entry.file_type() else { continue }; - if ft.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; - } - collect_files(&path, parser, filter, out); - continue; - } - visit_file(&path, parser, filter, out); - } +/// The one total order of function lists: effort desc, volume desc, name +/// asc, file asc. +fn function_order(a: &FunctionRecord, b: &FunctionRecord) -> Ordering { + let (da, db) = (&a.metrics.derived, &b.metrics.derived); + db.effort + .total_cmp(&da.effort) + .then_with(|| db.volume.total_cmp(&da.volume)) + .then_with(|| a.metrics.name.cmp(&b.metrics.name)) + .then_with(|| a.source_file.cmp(&b.source_file)) } -/// Collects one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): parser support + path policy filters. -fn visit_file(path: &Path, parser: &Parser, filter: &StaticFilter, out: &mut Vec<String>) { - let path_str = path.to_string_lossy().to_string(); - if !parser.is_supported(&path_str) { - return; - } - if filter.skips(&path_str) { - return; - } - out.push(path_str); -} - -/// `filepath.Rel(root, path)` for the simple in-tree case. -fn make_relative(file_path: &str, root_path: &str) -> String { - if root_path.is_empty() { - return file_path.to_string(); - } - let root = root_path.trim_end_matches('/'); - if let Some(stripped) = file_path.strip_prefix(root) { - let stripped = stripped.trim_start_matches('/'); - if stripped.is_empty() { - // filepath.Rel(root, root) == "." (a FILE root stamps "."). - return ".".to_string(); - } - return stripped.to_string(); - } - file_path.to_string() -} - -/// `filepath.Dir` of a relative path. -fn dir_of(rel: &str) -> String { - match rel.rfind('/') { - Some(idx) => rel[..idx].to_string(), - None => ".".to_string(), - } -} - -/// Bit-exact reproduction of the reference implementation's `sort.Slice` (`pdqsort_func`, as of reference release 1.26) for the -/// volume-descending ordering of functions. the reference implementation's `sort.Slice` is an UNSTABLE -/// pattern-defeating quicksort, so the relative order of equal-volume functions -/// depends on the exact algorithm, not just the comparator — we therefore port -/// the algorithm verbatim (including `breakPatterns`' xorshift) so ties land in -/// the same positions as the reference. -mod gosort { - /// `data.Less(i, j)`: volume descending (`result[i].Volume > result[j].Volume`). - #[inline] - fn less(v: &[f64], i: usize, j: usize) -> bool { - v[i] > v[j] - } - - /// Sorts `volumes` (and the parallel `payload`) by volume descending using - /// the reference implementation's `sort.Slice`. Swaps are applied to both slices. - pub fn slice_by_volume_desc<T>(volumes: &mut [f64], payload: &mut [T]) { - let n = volumes.len(); - let limit = usize_bits_len(n); - pdqsort(volumes, payload, 0, n, limit); - } - - /// The reference `bits.Len(uint(length))`. - fn usize_bits_len(x: usize) -> u32 { - usize::BITS - x.leading_zeros() - } - - #[inline] - fn swap<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize) { - v.swap(a, b); - p.swap(a, b); - } - - fn insertion_sort<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize) { - for i in a + 1..b { - let mut j = i; - while j > a && less(v, j, j - 1) { - swap(v, p, j, j - 1); - j -= 1; - } - } - } - - fn sift_down<T>(v: &mut [f64], p: &mut [T], lo: usize, hi: usize, first: usize) { - let mut root = lo; - loop { - let mut child = 2 * root + 1; - if child >= hi { - break; - } - if child + 1 < hi && less(v, first + child, first + child + 1) { - child += 1; - } - if !less(v, first + root, first + child) { - return; - } - swap(v, p, first + root, first + child); - root = child; - } - } - - fn heap_sort<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize) { - let first = a; - let lo = 0; - let hi = b - a; - let mut i = (hi as isize - 1) / 2; - while i >= 0 { - sift_down(v, p, i as usize, hi, first); - i -= 1; - } - let mut i = hi as isize - 1; - while i >= 0 { - swap(v, p, first, first + i as usize); - sift_down(v, p, lo, i as usize, first); - i -= 1; - } - } - - // hints - const UNKNOWN: i32 = 0; - const INCREASING: i32 = 1; - const DECREASING: i32 = 2; - - fn order2(v: &[f64], a: usize, b: usize, swaps: &mut i32) -> (usize, usize) { - if less(v, b, a) { - *swaps += 1; - (b, a) - } else { - (a, b) - } - } - - fn median<T>( - v: &mut [f64], - _p: &mut [T], - a: usize, - b: usize, - c: usize, - swaps: &mut i32, - ) -> usize { - let (a, b) = order2(v, a, b, swaps); - let (b, c) = order2(v, b, c, swaps); - let (_a, b) = order2(v, a, b, swaps); - let _ = c; - b - } - - fn median_adjacent<T>(v: &mut [f64], p: &mut [T], a: usize, swaps: &mut i32) -> usize { - median(v, p, a - 1, a, a + 1, swaps) - } - - fn choose_pivot<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize) -> (usize, i32) { - const SHORTEST_NINTHER: usize = 50; - const MAX_SWAPS: i32 = 4 * 3; - let l = b - a; - let mut swaps = 0i32; - let mut i = a + l / 4; - let mut j = a + l / 4 * 2; - let mut k = a + l / 4 * 3; - if l >= 8 { - if l >= SHORTEST_NINTHER { - i = median_adjacent(v, p, i, &mut swaps); - j = median_adjacent(v, p, j, &mut swaps); - k = median_adjacent(v, p, k, &mut swaps); - } - j = median(v, p, i, j, k, &mut swaps); - } - match swaps { - 0 => (j, INCREASING), - MAX_SWAPS => (j, DECREASING), - _ => (j, UNKNOWN), - } - } - - fn reverse_range<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize) { - let mut i = a; - let mut j = b - 1; - while i < j { - swap(v, p, i, j); - i += 1; - j -= 1; - } - } - - fn partition<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize, pivot: usize) -> (usize, bool) { - swap(v, p, a, pivot); - let mut i = a + 1; - let mut j = b - 1; - while i <= j && less(v, i, a) { - i += 1; - } - while i <= j && !less(v, j, a) { - j -= 1; - } - if i > j { - swap(v, p, j, a); - return (j, true); - } - swap(v, p, i, j); - i += 1; - j = j.wrapping_sub(1); - loop { - while i <= j && less(v, i, a) { - i += 1; - } - while i <= j && !less(v, j, a) { - j = j.wrapping_sub(1); - } - if i > j { - break; - } - swap(v, p, i, j); - i += 1; - j = j.wrapping_sub(1); - } - swap(v, p, j, a); - (j, false) - } - - fn partition_equal<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize, pivot: usize) -> usize { - swap(v, p, a, pivot); - let mut i = a + 1; - let mut j = b - 1; - loop { - while i <= j && !less(v, a, i) { - i += 1; - } - while i <= j && less(v, a, j) { - j = j.wrapping_sub(1); - } - if i > j { - break; - } - swap(v, p, i, j); - i += 1; - j = j.wrapping_sub(1); - } - i - } - - fn partial_insertion_sort<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize) -> bool { - const MAX_STEPS: usize = 5; - const SHORTEST_SHIFTING: usize = 50; - let mut i = a + 1; - for _ in 0..MAX_STEPS { - while i < b && !less(v, i, i - 1) { - i += 1; - } - if i == b { - return true; - } - if b - a < SHORTEST_SHIFTING { - return false; - } - swap(v, p, i, i - 1); - if i - a >= 2 { - let mut j = i - 1; - while j >= 1 { - if !less(v, j, j - 1) { - break; - } - swap(v, p, j, j - 1); - j -= 1; - } - } - if b - i >= 2 { - let mut j = i + 1; - while j < b { - if !less(v, j, j - 1) { - break; - } - swap(v, p, j, j - 1); - j += 1; - } - } - } - false - } - - fn next_power_of_two(length: usize) -> u64 { - let shift = usize_bits_len(length); - 1u64 << shift - } - - fn xorshift_next(state: &mut u64) -> u64 { - *state ^= *state << 13; - *state ^= *state >> 7; - *state ^= *state << 17; - *state - } - - fn break_patterns<T>(v: &mut [f64], p: &mut [T], a: usize, b: usize) { - let length = b - a; - if length >= 8 { - let mut random = length as u64; - let modulus = next_power_of_two(length); - let start = a + (length / 4) * 2 - 1; - for idx in start..=a + (length / 4) * 2 + 1 { - let mut other = (xorshift_next(&mut random) & (modulus - 1)) as usize; - if other >= length { - other -= length; - } - swap(v, p, idx, a + other); - } - } - } - - #[allow(clippy::too_many_lines)] - fn pdqsort<T>(v: &mut [f64], p: &mut [T], mut a: usize, mut b: usize, mut limit: u32) { - const MAX_INSERTION: usize = 12; - let mut was_balanced = true; - let mut was_partitioned = true; - loop { - let length = b - a; - if length <= MAX_INSERTION { - insertion_sort(v, p, a, b); - return; - } - if limit == 0 { - heap_sort(v, p, a, b); - return; - } - if !was_balanced { - break_patterns(v, p, a, b); - limit -= 1; - } - let (mut pivot, mut hint) = choose_pivot(v, p, a, b); - if hint == DECREASING { - reverse_range(v, p, a, b); - pivot = (b - 1) - (pivot - a); - hint = INCREASING; - } - if was_balanced - && was_partitioned - && hint == INCREASING - && partial_insertion_sort(v, p, a, b) - { - return; - } - if a > 0 && !less(v, a - 1, pivot) { - let mid = partition_equal(v, p, a, b, pivot); - a = mid; - continue; - } - let (mid, already) = partition(v, p, a, b, pivot); - was_partitioned = already; - let left_len = mid - a; - let right_len = b - mid; - let balance_threshold = length / 8; - if left_len < right_len { - was_balanced = left_len >= balance_threshold; - pdqsort(v, p, a, mid, limit); - a = mid + 1; - } else { - was_balanced = right_len >= balance_threshold; - pdqsort(v, p, mid + 1, b, limit); - b = mid; - } - } - } +fn sorted(records: &[FunctionRecord]) -> Vec<&FunctionRecord> { + let mut out: Vec<&FunctionRecord> = records.iter().collect(); + out.sort_by(|a, b| function_order(a, b)); + out } // --------------------------------------------------------------------------- -// bin format +// shared serialization pieces // --------------------------------------------------------------------------- -/// Volume thresholds. -const VOL_LOW: f64 = 100.0; -const VOL_MED: f64 = 1000.0; -const VOL_HIGH: f64 = 5000.0; +/// Section score (0..=1) and its `N/10` label from the health score. +fn score_of(summary: &Summary) -> (f64, String) { + let score = summary.health_score / HEALTH_SCALE; + let scaled = (score * SCORE_MAX).round(); + (score, format!("{scaled}/{SCORE_MAX}")) +} + +/// Pushes the union counts and derived measures under their machine keys. +fn push_counts(m: &mut GoMap, summary: &Summary) { + let (c, d) = (&summary.counts, &summary.derived); + m.push("distinct_operators", GoValue::Int(c.distinct_operators)); + m.push("distinct_operands", GoValue::Int(c.distinct_operands)); + m.push("total_operators", GoValue::Int(c.total_operators)); + m.push("total_operands", GoValue::Int(c.total_operands)); + m.push("vocabulary", GoValue::Int(d.vocabulary)); + m.push("length", GoValue::Int(d.length)); + m.push("estimated_length", GoValue::Float(d.estimated_length)); +} + +/// Pushes the per-function statistics under their machine keys. +fn push_stats(m: &mut GoMap, summary: &Summary) { + let s = &summary.stats; + m.push("mean_volume", GoValue::Float(s.mean_volume)); + m.push("max_volume", GoValue::Float(s.max_volume)); + m.push("mean_difficulty", GoValue::Float(s.mean_difficulty)); + m.push("max_difficulty", GoValue::Float(s.max_difficulty)); + m.push("mean_effort", GoValue::Float(s.mean_effort)); + m.push("max_effort", GoValue::Float(s.max_effort)); + m.push("high_effort_functions", GoValue::Int(s.high_effort_count)); + m.push( + "high_effort_share", + GoValue::Float(summary.high_effort_share), + ); +} -fn classify_volume_level(volume: f64) -> &'static str { - if volume >= VOL_HIGH { - "Very High" - } else if volume >= VOL_MED { - "High" - } else if volume >= VOL_LOW { - "Medium" - } else { - "Low" - } +/// Pushes the derived scalars (volume … bugs) under their machine keys. +fn push_derived(m: &mut GoMap, summary: &Summary) { + let d = &summary.derived; + m.push("volume", GoValue::Float(d.volume)); + m.push("difficulty", GoValue::Float(d.difficulty)); + m.push("effort", GoValue::Float(d.effort)); + m.push("time_to_program", GoValue::Float(d.time_to_program)); + m.push("delivered_bugs", GoValue::Float(d.delivered_bugs)); } -/// Builds the aggregate message (the reference `buildHalsteadMessage`) for ONE -/// numeric average — the reference `common.ThresholdLabeler` over the 5000 / -/// 1000 / 100 / 0 volume limits. -fn build_aggregate_message(metric: f64) -> &'static str { - if metric >= VOL_HIGH { - "Very high Halstead complexity - significant refactoring recommended" - } else if metric >= VOL_MED { - "High Halstead complexity - consider refactoring" - } else if metric >= VOL_LOW { - "Moderate Halstead complexity - acceptable" - } else { - "Low Halstead complexity - well-structured code" - } +/// The key-metrics list of a section or file entry. +fn metrics_list(summary: &Summary) -> Vec<GoValue> { + let fmt_f = cf_reportutil::format_float; + let (c, d, s) = (&summary.counts, &summary.derived, &summary.stats); + let metric = |label: &str, value: String| { + let mut m = GoMap::new(MapOrigin::Struct); + m.push("label", GoValue::Str(label.to_string())); + m.push("value", GoValue::Str(value)); + GoValue::Map(m) + }; + vec![ + metric("Total Functions", summary.total_functions.to_string()), + metric("Distinct Operators (n1)", c.distinct_operators.to_string()), + metric("Distinct Operands (n2)", c.distinct_operands.to_string()), + metric("Total Operators (N1)", c.total_operators.to_string()), + metric("Total Operands (N2)", c.total_operands.to_string()), + metric("Vocabulary", d.vocabulary.to_string()), + metric("Length", d.length.to_string()), + metric("Volume", fmt_f(d.volume)), + metric("Difficulty", fmt_f(d.difficulty)), + metric("Effort", fmt_f(d.effort)), + metric("Est. Bugs", fmt_f(d.delivered_bugs)), + metric("Mean Effort", fmt_f(s.mean_effort)), + metric("Max Effort", fmt_f(s.max_effort)), + metric("High-Effort Functions", s.high_effort_count.to_string()), + ] +} + +/// The volume-band distribution items (empty for a set without functions). +#[allow(clippy::cast_precision_loss)] // function counts are far below 2^52 +fn distribution_items(summary: &Summary) -> Vec<GoValue> { + if summary.total_functions == 0 { + return Vec::new(); + } + let total = summary.total_functions as f64; + VolumeLevel::ALL + .iter() + .map(|level| { + let count = summary.distribution[level.index()]; + let mut m = GoMap::new(MapOrigin::Struct); + m.push("label", GoValue::Str(level.bucket_label().to_string())); + m.push("percent", GoValue::Float(count as f64 / total)); + m.push("count", GoValue::Int(count)); + GoValue::Map(m) + }) + .collect() } -/// Builds the aggregate message the way the enforceable Go contract behaves. -/// -/// The reference implementation's `common.Aggregator.GetResult` feeds the -/// threshold labeler the *first* numeric average of a randomized Go map -/// iteration (`for _, value := range averages { message = ...; break }`), i.e. -/// an (approximately) uniformly random pick among the 12 `NUMERIC_KEYS` -/// averages — so the reference binary emits a different label per process. -/// Measured with the live oracle (N Go runs per cell): the differential only -/// ever enforces this field when all N reference runs collide on one label, -/// and a collision lands on the *modal* bucket of the 12 per-key labels with -/// overwhelming probability (p_max^N dominates). The deterministic -/// max-likelihood port is therefore the modal label over the labels of ALL 12 -/// aggregated averages — a genuine computation over real aggregated data that -/// varies with the input (verified live: hercules/ioq3/kubernetes -> Moderate, -/// fallout2-ce -> Moderate, agent-client-protocol -> Low, each matching the -/// measured Go plurality). Ties are broken toward the more severe label -/// (the reference labeler's check order). -fn modal_aggregate_message(averages: &HashMap<&'static str, f64>) -> &'static str { - // Bucket counts indexed by severity: [VeryHigh, High, Moderate, Low] — - // the reference ThresholdLabeler's descending-limit check order. - let labels = [ - "Very high Halstead complexity - significant refactoring recommended", - "High Halstead complexity - consider refactoring", - "Moderate Halstead complexity - acceptable", - "Low Halstead complexity - well-structured code", - ]; - let mut counts = [0usize; 4]; - for key in NUMERIC_KEYS { - let v = averages.get(key).copied().unwrap_or(0.0); - let label = build_aggregate_message(v); - let bucket = labels - .iter() - .position(|l| *l == label) - .unwrap_or(labels.len() - 1); - counts[bucket] += 1; - } - let mut best = 0; - for i in 1..4 { - if counts[i] > counts[best] { - best = i; - } - } - labels[best] +/// `effort=… | vol=… | bugs=…` with one decimal. +fn format_issue_value(f: &FunctionMetrics) -> String { + let d = &f.derived; + format!( + "effort={} | vol={} | bugs={}", + cf_reportutil::format_float(d.effort), + cf_reportutil::format_float(d.volume), + cf_reportutil::format_float(d.delivered_bugs) + ) } -fn calculate_health_score(avg_volume: f64) -> f64 { - if avg_volume < VOL_LOW { - 100.0 - } else if avg_volume < VOL_MED { - let range = VOL_MED - VOL_LOW; - 70.0 + (VOL_MED - avg_volume) / range * 30.0 - } else if avg_volume < VOL_HIGH { - let range = VOL_HIGH - VOL_MED; - 30.0 + (VOL_HIGH - avg_volume) / range * 40.0 - } else { - let excess = (avg_volume - VOL_HIGH) / 1000.0 * 10.0; - (30.0 - excess).max(0.0) - } +/// Every function as an issue, in the total order. +fn issue_items(records: &[FunctionRecord]) -> Vec<GoValue> { + sorted(records) + .into_iter() + .map(|r| { + let d = &r.metrics.derived; + let mut m = GoMap::new(MapOrigin::Struct); + m.push("name", GoValue::Str(r.metrics.name.clone())); + m.push("location", GoValue::Str(r.source_file.clone())); + m.push("value", GoValue::Str(format_issue_value(&r.metrics))); + m.push( + "severity", + GoValue::Str(severity(d.effort, d.delivered_bugs).to_string()), + ); + GoValue::Map(m) + }) + .collect() } -/// A function entry omitting empty string fields. -fn function_halstead_entry(f: &FunctionMetrics) -> GoValue { +// --------------------------------------------------------------------------- +// yaml / bin: ComputedMetrics +// --------------------------------------------------------------------------- + +/// One `function_halstead` entry (empty location fields omitted). +fn function_halstead_entry(r: &FunctionRecord) -> GoValue { + let d = &r.metrics.derived; let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(f.name.clone())); - if !f.source_file.is_empty() { - m.push("source_file", GoValue::Str(f.source_file.clone())); + m.push("name", GoValue::Str(r.metrics.name.clone())); + if !r.source_file.is_empty() { + m.push("source_file", GoValue::Str(r.source_file.clone())); } - if !f.language.is_empty() { - m.push("language", GoValue::Str(f.language.clone())); + if !r.language.is_empty() { + m.push("language", GoValue::Str(r.language.clone())); } - if !f.directory.is_empty() { - m.push("directory", GoValue::Str(f.directory.clone())); + if !r.directory.is_empty() { + m.push("directory", GoValue::Str(r.directory.clone())); } - m.push("volume", GoValue::Float(f.volume)); - m.push("difficulty", GoValue::Float(f.difficulty)); - m.push("effort", GoValue::Float(f.effort)); - m.push("time_to_program", GoValue::Float(f.time_to_program)); - m.push("delivered_bugs", GoValue::Float(f.delivered_bugs)); + m.push("volume", GoValue::Float(d.volume)); + m.push("difficulty", GoValue::Float(d.difficulty)); + m.push("effort", GoValue::Float(d.effort)); + m.push("time_to_program", GoValue::Float(d.time_to_program)); + m.push("delivered_bugs", GoValue::Float(d.delivered_bugs)); m.push( "complexity_level", - GoValue::Str(classify_volume_level(f.volume).to_string()), + GoValue::Str(VolumeLevel::of(d.volume).name().to_string()), ); GoValue::Map(m) } -/// Builds the `ComputedMetrics` GoValue (`ComputeAllMetrics`) for the aggregated -/// report. The integer-typed aggregate scalars are averaged (float) in the -/// report, so `ParseReportData`'s `.(int)` assertions fail and they read 0; only -/// the float-typed scalars survive. `total_functions` is a count (int) and -/// survives. The `message` is computed from the real aggregated volume via -/// [`build_aggregate_message`] (the field is unstable in the reference binary; see module docs). +/// One `high_effort_functions` entry. +fn high_effort_entry(r: &FunctionRecord) -> GoValue { + let d = &r.metrics.derived; + let mut m = GoMap::new(MapOrigin::Struct); + m.push("name", GoValue::Str(r.metrics.name.clone())); + if !r.source_file.is_empty() { + m.push("source_file", GoValue::Str(r.source_file.clone())); + } + m.push("volume", GoValue::Float(d.volume)); + m.push("effort", GoValue::Float(d.effort)); + m.push("time_to_program", GoValue::Float(d.time_to_program)); + m.push("delivered_bugs", GoValue::Float(d.delivered_bugs)); + m.push("risk_level", GoValue::Str(risk_level(d.effort).to_string())); + GoValue::Map(m) +} + +/// The `ComputedMetrics` value shared by the yaml and bin formats. fn computed_metrics(agg: &Aggregate) -> GoValue { - // function_halstead: per-function data sorted by volume descending using - // the reference implementation's UNSTABLE sort.Slice (gosort), so equal-volume ties land in the same - // positions as the reference. Input order is the aggregated (walk-order) - // function list. - let mut funcs: Vec<&FunctionMetrics> = agg.functions.iter().collect(); - let mut vols: Vec<f64> = funcs.iter().map(|f| f.volume).collect(); - gosort::slice_by_volume_desc(&mut vols, &mut funcs); + let ordered = sorted(&agg.functions); let function_halstead: Vec<GoValue> = - funcs.iter().map(|f| function_halstead_entry(f)).collect(); - - // distribution. - let (mut low, mut medium, mut high, mut very_high) = (0i64, 0i64, 0i64, 0i64); - for f in &agg.functions { - if f.volume >= VOL_HIGH { - very_high += 1; - } else if f.volume >= VOL_MED { - high += 1; - } else if f.volume >= VOL_LOW { - medium += 1; - } else { - low += 1; - } - } - let mut dist = GoMap::new(MapOrigin::Struct); - dist.push("low", GoValue::Int(low)); - dist.push("medium", GoValue::Int(medium)); - dist.push("high", GoValue::Int(high)); - dist.push("very_high", GoValue::Int(very_high)); - - // high_effort_functions: volume >= 1000, sorted by volume desc. - let mut high_eff: Vec<&FunctionMetrics> = agg - .functions + ordered.iter().map(|r| function_halstead_entry(r)).collect(); + let high_effort_functions: Vec<GoValue> = ordered .iter() - .filter(|f| f.volume >= VOL_MED) - .collect(); - let mut he_vols: Vec<f64> = high_eff.iter().map(|f| f.volume).collect(); - gosort::slice_by_volume_desc(&mut he_vols, &mut high_eff); - let high_effort_functions: Vec<GoValue> = high_eff - .iter() - .map(|f| { - let risk = if f.volume >= VOL_HIGH { - "HIGH" - } else { - "MEDIUM" - }; - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(f.name.clone())); - if !f.source_file.is_empty() { - m.push("source_file", GoValue::Str(f.source_file.clone())); - } - // language/directory are not stamped on the high-effort struct. - m.push("volume", GoValue::Float(f.volume)); - m.push("effort", GoValue::Float(f.effort)); - m.push("time_to_program", GoValue::Float(f.time_to_program)); - m.push("delivered_bugs", GoValue::Float(f.delivered_bugs)); - m.push("risk_level", GoValue::Str(risk.to_string())); - GoValue::Map(m) - }) + .filter(|r| is_high_effort(r.metrics.derived.effort)) + .map(|r| high_effort_entry(r)) .collect(); - // aggregate: float scalars from averages; int scalars read 0; total_functions - // is a count and survives; health_score from avg volume per function. - let avg = |k: &str| agg.averages.get(k).copied().unwrap_or(0.0); - let volume = avg("volume"); + let mut dist = GoMap::new(MapOrigin::Struct); + for level in VolumeLevel::ALL { + dist.push( + level.key(), + GoValue::Int(agg.summary.distribution[level.index()]), + ); + } + let mut aggregate = GoMap::new(MapOrigin::Struct); - aggregate.push("total_functions", GoValue::Int(agg.total_functions)); - aggregate.push("volume", GoValue::Float(volume)); - aggregate.push("difficulty", GoValue::Float(avg("difficulty"))); - aggregate.push("effort", GoValue::Float(avg("effort"))); - aggregate.push("time_to_program", GoValue::Float(avg("time_to_program"))); - aggregate.push("delivered_bugs", GoValue::Float(avg("delivered_bugs"))); - aggregate.push("distinct_operators", GoValue::Int(0)); - aggregate.push("distinct_operands", GoValue::Int(0)); - aggregate.push("total_operators", GoValue::Int(0)); - aggregate.push("total_operands", GoValue::Int(0)); - aggregate.push("vocabulary", GoValue::Int(0)); - aggregate.push("length", GoValue::Int(0)); - aggregate.push("estimated_length", GoValue::Float(avg("estimated_length"))); - let health = if agg.total_functions > 0 { - calculate_health_score(volume / agg.total_functions as f64) - } else { - 0.0 - }; - aggregate.push("health_score", GoValue::Float(health)); - aggregate.push( - "message", - GoValue::Str(modal_aggregate_message(&agg.averages).to_string()), - ); + aggregate.push("total_functions", GoValue::Int(agg.summary.total_functions)); + push_derived(&mut aggregate, &agg.summary); + push_counts(&mut aggregate, &agg.summary); + push_stats(&mut aggregate, &agg.summary); + aggregate.push("health_score", GoValue::Float(agg.summary.health_score)); + aggregate.push("message", GoValue::Str(agg.summary.message.to_string())); let mut root = GoMap::new(MapOrigin::Struct); root.push("function_halstead", GoValue::Array(function_halstead)); @@ -1335,78 +372,57 @@ fn computed_metrics(agg: &Aggregate) -> GoValue { GoValue::Map(root) } -/// The reference `ReportFormatter.GetVolumeAssessment`. The first -/// branch compares against `volumeThresholdHigh` (5000), so the `🟡 Medium` -/// branch (`<= 1000`) is unreachable — a faithful reference-implementation quirk. -fn volume_assessment(volume: f64) -> &'static str { - if volume <= VOL_HIGH { - "🟢 Low" - } else if volume <= VOL_MED { - "🟡 Medium" - } else { - "🔴 High" - } +/// Builds the `static/halstead --format bin` CFB1 envelope, or `None` when +/// the path does not exist. +#[must_use] +pub fn halstead_bin_report(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { + let agg = aggregate_opts(root_path, filter)?; + cf_reportutil::encode_binary_envelope(&computed_metrics(&agg)).ok() } -/// The reference `ReportFormatter.GetDifficultyAssessment`. -fn difficulty_assessment(difficulty: f64) -> &'static str { - if difficulty <= 5.0 { - "🟢 Simple" - } else if difficulty <= 15.0 { - "🟡 Moderate" - } else { - "🔴 Complex" - } +/// Builds the `static/halstead --format yaml` bytes (the same +/// `ComputedMetrics` value as bin, marshaled as YAML), or `None` when the +/// path does not exist. +#[must_use] +pub fn halstead_yaml_report(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { + let agg = aggregate_opts(root_path, filter)?; + Some(cf_goyaml::marshal(&computed_metrics(&agg))) } -/// The reference `ReportFormatter.GetEffortAssessment`. -fn effort_assessment(effort: f64) -> &'static str { - if effort <= 1000.0 { - "🟢 Low" - } else if effort <= 10000.0 { - "🟡 Medium" - } else { - "🔴 High" - } -} +// --------------------------------------------------------------------------- +// plot: the raw aggregated report +// --------------------------------------------------------------------------- -/// One raw `functions` item: the reference `convertHalsteadFunctionItems` map -/// plus the `_source_file`/`_language`/`_directory` stamps -/// (`stampCollectionMetadata`). Map-origin, so JSON keys byte-sort at encode. -fn raw_function_item(f: &FunctionMetrics) -> GoValue { +/// One raw `functions` item: every per-function number, the three +/// assessments, the operator / operand maps, and the location stamps. +fn raw_function_item(r: &FunctionRecord) -> GoValue { + let f = &r.metrics; + let (c, d) = (&f.counts, &f.derived); let mut m = GoMap::new(MapOrigin::Map); m.push("name", GoValue::Str(f.name.clone())); - m.push("volume", GoValue::Float(f.volume)); - m.push("difficulty", GoValue::Float(f.difficulty)); - m.push("effort", GoValue::Float(f.effort)); - m.push("time_to_program", GoValue::Float(f.time_to_program)); - m.push("delivered_bugs", GoValue::Float(f.delivered_bugs)); - m.push("distinct_operators", GoValue::Int(f.distinct_operators)); - m.push("distinct_operands", GoValue::Int(f.distinct_operands)); - m.push("total_operators", GoValue::Int(f.total_operators)); - m.push("total_operands", GoValue::Int(f.total_operands)); - m.push("vocabulary", GoValue::Int(f.vocabulary)); - m.push("length", GoValue::Int(f.length)); - m.push("estimated_length", GoValue::Float(f.estimated_length)); - m.push( - "estimated_total_operators", - GoValue::Int(f.estimated_total_operators), - ); - m.push( - "estimated_total_operands", - GoValue::Int(f.estimated_total_operands), - ); + m.push("volume", GoValue::Float(d.volume)); + m.push("difficulty", GoValue::Float(d.difficulty)); + m.push("effort", GoValue::Float(d.effort)); + m.push("time_to_program", GoValue::Float(d.time_to_program)); + m.push("delivered_bugs", GoValue::Float(d.delivered_bugs)); + m.push("distinct_operators", GoValue::Int(c.distinct_operators)); + m.push("distinct_operands", GoValue::Int(c.distinct_operands)); + m.push("total_operators", GoValue::Int(c.total_operators)); + m.push("total_operands", GoValue::Int(c.total_operands)); + m.push("vocabulary", GoValue::Int(d.vocabulary)); + m.push("length", GoValue::Int(d.length)); + m.push("estimated_length", GoValue::Float(d.estimated_length)); m.push( "volume_assessment", - GoValue::Str(volume_assessment(f.volume).to_string()), + GoValue::Str(volume_assessment(d.volume).to_string()), ); m.push( "difficulty_assessment", - GoValue::Str(difficulty_assessment(f.difficulty).to_string()), + GoValue::Str(difficulty_assessment(d.difficulty).to_string()), ); m.push( "effort_assessment", - GoValue::Str(effort_assessment(f.effort).to_string()), + GoValue::Str(effort_assessment(d.effort).to_string()), ); let mut operators = GoMap::new(MapOrigin::Map); for (k, v) in &f.operators { @@ -1418,338 +434,73 @@ fn raw_function_item(f: &FunctionMetrics) -> GoValue { operands.push(k, GoValue::Int(*v)); } m.push("operands", GoValue::Map(operands)); - m.push("_source_file", GoValue::Str(f.source_file.clone())); - if !f.language.is_empty() { - m.push("_language", GoValue::Str(f.language.clone())); + m.push("_source_file", GoValue::Str(r.source_file.clone())); + if !r.language.is_empty() { + m.push("_language", GoValue::Str(r.language.clone())); } - if !f.directory.is_empty() { - m.push("_directory", GoValue::Str(f.directory.clone())); + if !r.directory.is_empty() { + m.push("_directory", GoValue::Str(r.directory.clone())); } GoValue::Map(m) } -/// Builds the AGGREGATED RAW `analyze.Report` GoValue for `static/halstead` — -/// the value the reference implementation's `halstead.Aggregator.GetResult()` returns (base -/// `BuildCollectionResult` + the `DetailedDataCollector` `functions` -/// overwrite), which is what `--format plot` consumes and what -/// `writeReportJSON` serializes into `report.json`: -/// -/// * `analyzer_name`, `message` (reference keys it off a random numeric average — the -/// measured modal bucket is reproduced from the averaged difficulty, see -/// [`build_aggregate_message`]), -/// * count: `total_functions` (summed), -/// * the 12 numeric keys averaged over the parsed-file count, -/// * `functions`: the per-file convert maps concatenated in walk order, each -/// stamped `_source_file`/`_language`/`_directory`. -/// -/// With no parsed files the reference implementation returns `buildEmptyHalsteadResult` instead (14 -/// keys, no `analyzer_name`/`functions`). +/// Builds the raw aggregated report consumed by `--format plot` +/// (`report.json`): `analyzer_name`, `total_functions`, `functions` (walk +/// order, each with its operator / operand maps), `message`, the union +/// counts and measures, the per-function statistics and `health_score`. +/// `None` when the path does not exist. #[must_use] pub fn halstead_raw_report_value(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - if !Path::new(root_path).exists() { - return None; - } - let Some(agg) = aggregate_opts(root_path, filter) else { - // Folder exists but no parsed files: the aggregator's empty result. - let mut m = GoMap::new(MapOrigin::Map); - m.push("total_functions", GoValue::Int(0)); - m.push("volume", GoValue::Float(0.0)); - m.push("difficulty", GoValue::Float(0.0)); - m.push("effort", GoValue::Float(0.0)); - m.push("time_to_program", GoValue::Float(0.0)); - m.push("delivered_bugs", GoValue::Float(0.0)); - m.push("distinct_operators", GoValue::Int(0)); - m.push("distinct_operands", GoValue::Int(0)); - m.push("total_operators", GoValue::Int(0)); - m.push("total_operands", GoValue::Int(0)); - m.push("vocabulary", GoValue::Int(0)); - m.push("length", GoValue::Int(0)); - m.push("estimated_length", GoValue::Float(0.0)); - m.push("message", GoValue::Str("No functions found".to_string())); - return Some(GoValue::Map(m)); - }; - - let avg = |k: &str| agg.averages.get(k).copied().unwrap_or(0.0); + let agg = aggregate_opts(root_path, filter)?; let mut m = GoMap::new(MapOrigin::Map); - m.push("analyzer_name", GoValue::Str("halstead".to_string())); - m.push("total_functions", GoValue::Int(agg.total_functions)); + m.push("analyzer_name", GoValue::Str(ANALYZER_NAME.to_string())); + m.push("total_functions", GoValue::Int(agg.summary.total_functions)); m.push( "functions", GoValue::Array(agg.functions.iter().map(raw_function_item).collect()), ); - m.push( - "message", - GoValue::Str(modal_aggregate_message(&agg.averages).to_string()), - ); - for key in NUMERIC_KEYS { - m.push(*key, GoValue::Float(avg(key))); - } + m.push("message", GoValue::Str(agg.summary.message.to_string())); + push_derived(&mut m, &agg.summary); + push_counts(&mut m, &agg.summary); + push_stats(&mut m, &agg.summary); + m.push("health_score", GoValue::Float(agg.summary.health_score)); Some(GoValue::Map(m)) } -/// Builds the `static/halstead --format bin` report bytes for `root_path`, or -/// `None` when the folder cannot be walked / no file produces a report. -#[must_use] -pub fn halstead_bin_report(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let agg = aggregate_opts(root_path, filter)?; - let metrics = computed_metrics(&agg); - cf_reportutil::encode_binary_envelope(&metrics).ok() -} - -/// Builds the `static/halstead --format yaml` report bytes for `root_path`, or -/// `None` when the folder cannot be walked / no file produces a report. -/// -/// The reference implementation's static YAML path marshals the same `ComputedMetrics` value the bin path -/// builds (`ComputeAllMetrics(report)`) directly through `gopkg.in/yaml.v3` -/// (no CFB1 envelope), so this reuses [`computed_metrics`] and serializes it via -/// cf-goyaml. -#[must_use] -pub fn halstead_yaml_report(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let agg = aggregate_opts(root_path, filter)?; - let metrics = computed_metrics(&agg); - Some(cf_goyaml::marshal(&metrics)) -} - // --------------------------------------------------------------------------- -// json format (the structured `run` report, NOT the per-analyzer ComputeAllMetrics) +// json: the structured `run` report // --------------------------------------------------------------------------- -// -// The reference `run --format json` path does NOT call `halstead.FormatReportJSON`. -// Instead `StaticService.FormatJSON` builds a structured "report" via -// `BuildSections` → `halstead.CreateReportSection` (a `ReportSection` over the -// aggregated report map) → `renderer.SectionsToJSON` → `json.NewEncoder` with -// `SetIndent("", " ")` (trailing newline). The shape is `JSONReport` -// (`overall_score_label`, `sections`, `overall_score`) with one `JSONSection` -// per analyzer (`title`, `score_label`, `status`, `metrics`, `distribution`, -// `issues`, `score`). -// -// All numbers are derived from the aggregated report: -// * metrics: `reportutil.GetInt` (truncate-to-int of the averaged float) for the -// count-like fields, `reportutil.FormatFloat` (`%.1f`) for the float fields. -// * distribution: per-function volume buckets over the detailed function list. -// * issues: every detailed function (no limit) sorted by **effort descending** -// via the reference implementation's UNSTABLE `sort.Slice` (gosort over a copy, walk-order input). -// * score / score_label / overall_score(_label): `calculateScore(difficulty)` -// of the averaged difficulty → `terminal.FormatScore` ("N/10"). -// -// `status` is the aggregate `message`, which the reference implementation builds from "the first numeric -// metric" of a randomized-order `map[string]float64` (`common.Aggregator -// .GetResult`) and is therefore nondeterministic (the canonicalizer measured and -// normalizes it). We compute it deterministically from the real aggregated -// volume via `build_aggregate_message` — a genuine computation, not a constant. - -// Halstead section score thresholds (reference calculateScore). -const SCORE_EXCELLENT_MAX: f64 = 5.0; -const SCORE_GOOD_MAX: f64 = 15.0; -const SCORE_FAIR_MAX: f64 = 30.0; -const SCORE_EXCELLENT: f64 = 1.0; -const SCORE_GOOD: f64 = 0.8; -const SCORE_FAIR: f64 = 0.6; -const SCORE_POOR: f64 = 0.3; - -// Distribution bucket bounds (vol <= bound). -const DIST_LOW_MAX: f64 = 100.0; -const DIST_MED_MAX: f64 = 1000.0; -const DIST_HIGH_MAX: f64 = 5000.0; - -// Issue severity thresholds (reference severityForFunction). -const ISSUE_FAIR_MIN: f64 = 10000.0; -const ISSUE_POOR_MIN: f64 = 50000.0; - -const SCORE_MAX: i64 = 10; - -/// The reference `calculateScore` over the averaged difficulty. -fn section_score(difficulty: f64) -> f64 { - if difficulty <= SCORE_EXCELLENT_MAX { - SCORE_EXCELLENT - } else if difficulty <= SCORE_GOOD_MAX { - SCORE_GOOD - } else if difficulty <= SCORE_FAIR_MAX { - SCORE_FAIR - } else { - SCORE_POOR - } -} - -/// `terminal.FormatScore`: `round(score*10)` then `"N/10"`. the reference implementation uses -/// `math.Round` (round-half-away-from-zero), matched by `f64::round`. -fn format_score(score: f64) -> String { - let scaled = (score * SCORE_MAX as f64).round() as i64; - format!("{scaled}/{SCORE_MAX}") -} - -/// `reportutil.GetInt` over an averaged (float) report value: `safeconv.ToInt` -/// reflect-converts float64→int, which truncates toward zero. -fn get_int(v: f64) -> i64 { - v as i64 -} - -/// Severity for an issue function (`severityForFunction`). -fn severity_for_function(effort: f64, bugs: f64) -> &'static str { - if effort >= ISSUE_POOR_MIN || bugs >= 1.0 { - "poor" - } else if effort >= ISSUE_FAIR_MIN || bugs >= 0.3 { - "fair" - } else { - "good" - } -} - -/// `formatIssueValue`: `effort=… | vol=… | bugs=…` with `%.1f` floats. -fn format_issue_value(effort: f64, volume: f64, bugs: f64) -> String { - format!( - "effort={} | vol={} | bugs={}", - cf_reportutil::format_float(effort), - cf_reportutil::format_float(volume), - cf_reportutil::format_float(bugs) - ) -} - -/// The reference `ReportFormatter.GetHalsteadMessage` — the PER-FILE report -/// `message` the analyzer builds from the file-level volume/difficulty/effort -/// (an all-zero function-free file lands in the "Excellent" bucket, matching -/// the visitor-path reports the reference retainer snapshots). -fn per_file_message(volume: f64, difficulty: f64, effort: f64) -> &'static str { - if volume <= 100.0 && difficulty <= 5.0 && effort <= 1000.0 { - "Excellent complexity - code is simple and maintainable" - } else if volume <= 1000.0 && difficulty <= 15.0 && effort <= 10000.0 { - "Good complexity - code is reasonably complex" - } else if volume <= 5000.0 && difficulty <= 30.0 && effort <= 50000.0 { - "Fair complexity - consider simplifying some functions" - } else { - "High complexity - code should be refactored for better maintainability" - } -} - -/// Builds one `renderer.JSONFileEntry` for `--per-file` (the reference -/// `SectionToJSONFileEntry` over `halstead.NewReportSection(perFileReport)`): -/// the per-file report is the analyzer's single-file result, so the ten key -/// metrics are the FILE-level scalars (exact ints, not cross-file averages), -/// the status is the per-file `GetHalsteadMessage` label, the distribution and -/// effort-descending issues cover this file's functions only, and the score -/// derives from the FILE difficulty. -fn build_file_entry(f: &PerFileHalstead, functions: &[FunctionMetrics]) -> GoValue { - let s = |k: &str| f.scalars.get(k).copied().unwrap_or(0.0); - let difficulty = s("difficulty"); - let score = section_score(difficulty); - - let fmt_f = cf_reportutil::format_float; - let metric = |label: &str, value: String| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("value", GoValue::Str(value)); - GoValue::Map(m) - }; - let metrics = vec![ - metric("Total Functions", f.total_functions.to_string()), - metric( - "Distinct Operators (n1)", - get_int(s("distinct_operators")).to_string(), - ), - metric( - "Distinct Operands (n2)", - get_int(s("distinct_operands")).to_string(), - ), - metric( - "Total Operators (N1)", - get_int(s("total_operators")).to_string(), - ), - metric( - "Total Operands (N2)", - get_int(s("total_operands")).to_string(), - ), - metric("Vocabulary", get_int(s("vocabulary")).to_string()), - metric("Volume", fmt_f(s("volume"))), - metric("Difficulty", fmt_f(difficulty)), - metric("Effort", fmt_f(s("effort"))), - metric("Est. Bugs", fmt_f(s("delivered_bugs"))), - ]; - - // Distribution over THIS file's functions (4 volume buckets; absent for - // function-free files — Distribution() returns nil ⇒ omitempty). - let frecs = &functions[f.start..f.end]; - let total = frecs.len(); - let (mut low, mut medium, mut high, mut very_high) = (0i64, 0i64, 0i64, 0i64); - for r in frecs { - if r.volume <= DIST_LOW_MAX { - low += 1; - } else if r.volume <= DIST_MED_MAX { - medium += 1; - } else if r.volume <= DIST_HIGH_MAX { - high += 1; - } else { - very_high += 1; - } - } - let dist_item = |label: &str, count: i64| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("percent", GoValue::Float(count as f64 / total as f64)); - m.push("count", GoValue::Int(count)); - GoValue::Map(m) - }; - let distribution: Vec<GoValue> = if total == 0 { - Vec::new() - } else { - vec![ - dist_item("Low (<=100)", low), - dist_item("Medium (101-1000)", medium), - dist_item("High (1001-5000)", high), - dist_item("Very High (>5000)", very_high), - ] - }; - - // Issues: this file's functions sorted by effort descending (the same - // unstable-sort reproduction the aggregate section uses). - let mut issue_funcs: Vec<&FunctionMetrics> = frecs.iter().collect(); - let mut efforts: Vec<f64> = issue_funcs.iter().map(|f| f.effort).collect(); - gosort::slice_by_volume_desc(&mut efforts, &mut issue_funcs); - let issues: Vec<GoValue> = issue_funcs - .iter() - .map(|r| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(r.name.clone())); - m.push("location", GoValue::Str(r.source_file.clone())); - m.push( - "value", - GoValue::Str(format_issue_value(r.effort, r.volume, r.delivered_bugs)), - ); - m.push( - "severity", - GoValue::Str(severity_for_function(r.effort, r.delivered_bugs).to_string()), - ); - GoValue::Map(m) - }) - .collect(); +/// One `--per-file` entry: the file's union metrics, its own distribution, +/// its functions as issues, and a score from its own high-effort share. +fn build_file_entry(file: &FileReport, functions: &[FunctionRecord]) -> GoValue { + let (score, score_label) = score_of(&file.summary); + let distribution = distribution_items(&file.summary); let mut entry = GoMap::new(MapOrigin::Struct); - entry.push("file_path", GoValue::Str(f.rel.clone())); - entry.push("score_label", GoValue::Str(format_score(score))); - entry.push( - "status", - GoValue::Str(per_file_message(s("volume"), difficulty, s("effort")).to_string()), - ); - entry.push("metrics", GoValue::Array(metrics)); + entry.push("file_path", GoValue::Str(file.rel.clone())); + entry.push("score_label", GoValue::Str(score_label)); + entry.push("status", GoValue::Str(file.summary.message.to_string())); + entry.push("metrics", GoValue::Array(metrics_list(&file.summary))); if !distribution.is_empty() { entry.push("distribution", GoValue::Array(distribution)); } - entry.push("issues", GoValue::Array(issues)); + entry.push( + "issues", + GoValue::Array(issue_items(&functions[file.start..file.end])), + ); entry.push("score", GoValue::Float(score)); GoValue::Map(entry) } -/// Builds the `static/halstead --format json` structured report bytes for -/// `root_path`, or `None` when the folder cannot be walked. +/// Builds the `static/halstead --format json` bytes for `root_path`, or +/// `None` when the path does not exist. #[must_use] pub fn halstead_json_report(root_path: &str) -> Option<Vec<u8>> { halstead_json_report_flags(root_path, &StaticFilter::default(), false) } -/// [`halstead_json_report`] with the `--per-file` flag applied: `per_file` -/// enables the reference implementation's section enrichment -/// (`StaticService.enrichWithPerFileData` → `JSONReport.EnrichWithPerFileData` -/// over the `PerFileRetainer` snapshots — one `JSONFileEntry` per ANALYZED -/// file, function-free files included, keyed into the section's `files` array). +/// [`halstead_json_report`] with the policy filter and the `--per-file` +/// enrichment (one entry per analyzed file, function-free files included). #[must_use] pub fn halstead_json_report_flags( root_path: &str, @@ -1764,15 +515,14 @@ pub fn halstead_json_report_flags( ) } -/// Builds the `static/halstead` `renderer.JSONReport` GoValue (single section), -/// shared by the single-analyzer byte path and the multi-analyzer static-JSON -/// merge. `None` when the path cannot be walked. +/// Builds the single-section structured report value, shared by the json +/// byte path and the multi-analyzer static-JSON merge. #[must_use] pub fn halstead_report_value(root_path: &str) -> Option<GoValue> { - halstead_report_value_mode(root_path, &StaticFilter::default(), false) + halstead_report_value_mode_flags(root_path, &StaticFilter::default(), false, false) } -/// [`halstead_report_value`] with the `--per-file` section enrichment. +/// [`halstead_report_value`] with the policy filter and `--per-file`. #[must_use] pub fn halstead_report_value_flags( root_path: &str, @@ -1782,21 +532,11 @@ pub fn halstead_report_value_flags( halstead_report_value_mode_flags(root_path, filter, false, per_file) } -/// Builds the `static/halstead` section tree in the reference implementation's `AggregationModeSummaryOnly` -/// shape (`text` / `compact`): the detailed `functions` collection is a no-op, so -/// the per-function volume distribution and the top-issues list are absent while -/// the averaged scalar Key Metrics are unchanged. +/// The summary-only section (`text` / `compact`): key metrics and status, +/// without the per-function distribution and issues. #[must_use] pub fn halstead_report_value_summary(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - halstead_report_value_mode(root_path, filter, true) -} - -fn halstead_report_value_mode( - root_path: &str, - filter: &StaticFilter, - summary_only: bool, -) -> Option<GoValue> { - halstead_report_value_mode_flags(root_path, filter, summary_only, false) + halstead_report_value_mode_flags(root_path, filter, true, false) } fn halstead_report_value_mode_flags( @@ -1805,172 +545,49 @@ fn halstead_report_value_mode_flags( summary_only: bool, per_file: bool, ) -> Option<GoValue> { - let mut agg = aggregate_opts(root_path, filter)?; - - // --per-file: one JSONFileEntry per ANALYZED file, in walk order (the - // reference implementation ranges the retainer map here — run-to-run - // random; the oracle's measured-variance canonicalization compares the - // set). Built BEFORE the summary-only clear (the modes never combine, but - // the ranges index into `functions`). - let file_entries: Option<Vec<GoValue>> = if per_file { - Some( - agg.file_reports - .iter() - .map(|f| build_file_entry(f, &agg.functions)) - .collect(), - ) - } else { - None - }; - - if summary_only { - agg.functions.clear(); - } - - let avg = |k: &str| agg.averages.get(k).copied().unwrap_or(0.0); - - // --- section score / labels (deterministic from averaged difficulty) --- - let difficulty = avg("difficulty"); - let score = section_score(difficulty); - let score_label = format_score(score); - // Single section ⇒ overall == section score. - let overall_score = score; - let overall_score_label = format_score(overall_score); - - // --- key metrics (reference KeyMetrics order) --- - let fmt_f = cf_reportutil::format_float; - let metric = |label: &str, value: String| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("value", GoValue::Str(value)); - GoValue::Map(m) - }; - let metrics = vec![ - metric("Total Functions", agg.total_functions.to_string()), - metric( - "Distinct Operators (n1)", - get_int(avg("distinct_operators")).to_string(), - ), - metric( - "Distinct Operands (n2)", - get_int(avg("distinct_operands")).to_string(), - ), - metric( - "Total Operators (N1)", - get_int(avg("total_operators")).to_string(), - ), - metric( - "Total Operands (N2)", - get_int(avg("total_operands")).to_string(), - ), - metric("Vocabulary", get_int(avg("vocabulary")).to_string()), - metric("Volume", fmt_f(avg("volume"))), - metric("Difficulty", fmt_f(difficulty)), - metric("Effort", fmt_f(avg("effort"))), - metric("Est. Bugs", fmt_f(avg("delivered_bugs"))), - ]; - - // --- distribution (per-function volume buckets; bound-inclusive lower edge) --- - let funcs = &agg.functions; - let total = funcs.len(); - let (mut low, mut medium, mut high, mut very_high) = (0i64, 0i64, 0i64, 0i64); - for f in funcs { - if f.volume <= DIST_LOW_MAX { - low += 1; - } else if f.volume <= DIST_MED_MAX { - medium += 1; - } else if f.volume <= DIST_HIGH_MAX { - high += 1; - } else { - very_high += 1; - } - } - let dist_item = |label: &str, count: i64| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - let percent = if total == 0 { - 0.0 - } else { - count as f64 / total as f64 - }; - m.push("percent", GoValue::Float(percent)); - m.push("count", GoValue::Int(count)); - GoValue::Map(m) - }; - // `Distribution()` returns nil for an empty function list ⇒ omitempty omits it. - let distribution: Vec<GoValue> = if total == 0 { - Vec::new() - } else { - vec![ - dist_item("Low (<=100)", low), - dist_item("Medium (101-1000)", medium), - dist_item("High (1001-5000)", high), - dist_item("Very High (>5000)", very_high), - ] - }; - - // --- issues: all functions sorted by effort descending (unstable sort.Slice) --- - let mut issue_funcs: Vec<&FunctionMetrics> = funcs.iter().collect(); - let mut efforts: Vec<f64> = issue_funcs.iter().map(|f| f.effort).collect(); - gosort::slice_by_volume_desc(&mut efforts, &mut issue_funcs); - let issues: Vec<GoValue> = issue_funcs - .iter() - .map(|f| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(f.name.clone())); - m.push("location", GoValue::Str(f.source_file.clone())); - m.push( - "value", - GoValue::Str(format_issue_value(f.effort, f.volume, f.delivered_bugs)), - ); - m.push( - "severity", - GoValue::Str(severity_for_function(f.effort, f.delivered_bugs).to_string()), - ); - GoValue::Map(m) - }) - .collect(); + let agg = aggregate_opts(root_path, filter)?; + let (score, score_label) = score_of(&agg.summary); - // --- section (JSONSection field order) --- let mut section = GoMap::new(MapOrigin::Struct); - section.push("title", GoValue::Str("HALSTEAD".to_string())); - section.push("score_label", GoValue::Str(score_label)); - section.push( - "status", - GoValue::Str(modal_aggregate_message(&agg.averages).to_string()), - ); - section.push("metrics", GoValue::Array(metrics)); - if !distribution.is_empty() { - section.push("distribution", GoValue::Array(distribution)); - } - section.push("issues", GoValue::Array(issues)); - // --per-file enrichment: `files` sits between `issues` and `score` - // (renderer.JSONSection field order; omitempty on a nil pointer without - // the flag). - if let Some(entries) = file_entries { + section.push("title", GoValue::Str(SECTION_TITLE.to_string())); + section.push("score_label", GoValue::Str(score_label.clone())); + section.push("status", GoValue::Str(agg.summary.message.to_string())); + section.push("metrics", GoValue::Array(metrics_list(&agg.summary))); + if !summary_only { + let distribution = distribution_items(&agg.summary); + if !distribution.is_empty() { + section.push("distribution", GoValue::Array(distribution)); + } + section.push("issues", GoValue::Array(issue_items(&agg.functions))); + } + if per_file { + let entries: Vec<GoValue> = agg + .files + .iter() + .map(|f| build_file_entry(f, &agg.functions)) + .collect(); section.push("files", GoValue::Array(entries)); } section.push("score", GoValue::Float(score)); - // --- top-level JSONReport (field order: label, sections, score) --- let mut root = GoMap::new(MapOrigin::Struct); - root.push("overall_score_label", GoValue::Str(overall_score_label)); + root.push("overall_score_label", GoValue::Str(score_label)); root.push("sections", GoValue::Array(vec![GoValue::Map(section)])); - root.push("overall_score", GoValue::Float(overall_score)); - + root.push("overall_score", GoValue::Float(score)); Some(GoValue::Map(root)) } #[cfg(test)] -mod per_file_tests { +mod tests { use super::*; + use cf_halstead::thresholds::{MSG_LOW, MSG_NO_FUNCTIONS}; - /// Fixture: one file with a function, one function-free file. + /// `hal.go` (`c := a + b; return c * 2`) and a function-free file. fn fixture() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("tempdir"); std::fs::write( - dir.path().join("a.go"), - "package main\n\nfunc add(a, b int) int {\n\treturn a + b\n}\n", + dir.path().join("hal.go"), + "package main\n\nfunc hal(a int, b int) int {\n\tc := a + b\n\treturn c * 2\n}\n", ) .unwrap(); std::fs::write( @@ -1981,47 +598,174 @@ mod per_file_tests { dir } - #[test] - fn per_file_flag_emits_files_entries() { - let dir = fixture(); + fn json_of(dir: &tempfile::TempDir, per_file: bool) -> String { let bytes = halstead_json_report_flags( dir.path().to_str().unwrap(), &StaticFilter::default(), - true, + per_file, ) - .unwrap(); - let json = String::from_utf8(bytes).unwrap(); + .expect("report"); + String::from_utf8(bytes).unwrap() + } + + #[test] + fn per_file_flag_emits_one_entry_per_analyzed_file() { + let dir = fixture(); + let json = json_of(&dir, true); assert!(json.contains("\"files\""), "files key missing:\n{json}"); + assert!(json.contains("\"file_path\": \"hal.go\""), "{json}"); + assert!(json.contains("\"file_path\": \"types.go\""), "{json}"); + // The function-free file carries the empty status. + assert!( + json.contains(&format!("\"status\": \"{MSG_NO_FUNCTIONS}\"")), + "{json}" + ); + // The function file carries the low-complexity status. + assert!( + json.contains(&format!("\"status\": \"{MSG_LOW}\"")), + "{json}" + ); + } + + #[test] + fn no_per_file_flag_omits_files() { + let dir = fixture(); + let json = json_of(&dir, false); + assert!(!json.contains("\"files\""), "{json}"); + assert!(!json.contains("\"file_path\"")); + } + + /// `hal`: n1 = 4, n2 = 4, N1 = 4, N2 = 5 → V = 27, D = 2.5, E = 67.5. + #[test] + fn json_metrics_are_the_union_counts_of_the_functions() { + let dir = fixture(); + let json = json_of(&dir, false); + for needle in [ + "\"label\": \"Total Functions\",\n \"value\": \"1\"", + "\"label\": \"Distinct Operators (n1)\",\n \"value\": \"4\"", + "\"label\": \"Distinct Operands (n2)\",\n \"value\": \"4\"", + "\"label\": \"Total Operators (N1)\",\n \"value\": \"4\"", + "\"label\": \"Total Operands (N2)\",\n \"value\": \"5\"", + "\"label\": \"Vocabulary\",\n \"value\": \"8\"", + "\"label\": \"Length\",\n \"value\": \"9\"", + "\"label\": \"Volume\",\n \"value\": \"27.0\"", + "\"label\": \"Difficulty\",\n \"value\": \"2.5\"", + "\"label\": \"Effort\",\n \"value\": \"67.5\"", + "\"value\": \"effort=67.5 | vol=27.0 | bugs=0.0\"", + ] { + assert!(json.contains(needle), "missing {needle:?} in:\n{json}"); + } + } + + #[test] + fn empty_and_unsupported_folders_yield_the_empty_report_not_none() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("notes.txt"), "this is not source\n").unwrap(); + let path = dir.path().to_str().unwrap(); + let filter = StaticFilter::default(); + + let json = + String::from_utf8(halstead_json_report_flags(path, &filter, true).unwrap()).unwrap(); assert!( - json.contains("\"file_path\": \"a.go\""), - "a.go entry missing:\n{json}" + json.contains(&format!("\"status\": \"{MSG_NO_FUNCTIONS}\"")), + "{json}" ); + assert!(json.contains("\"label\": \"Total Functions\",\n \"value\": \"0\"")); assert!( - json.contains("\"file_path\": \"types.go\""), - "types.go entry missing:\n{json}" + !json.contains("\"distribution\""), + "no buckets without functions" ); - // Per-file status is the reference GetHalsteadMessage over the - // FILE-level metrics (all-zero for the function-free file). + assert!(json.contains("\"files\": []"), "{json}"); + + let yaml = String::from_utf8(halstead_yaml_report(path, &filter).unwrap()).unwrap(); + assert!(yaml.contains("total_functions: 0"), "{yaml}"); assert!( - json.contains("\"status\": \"Excellent complexity - code is simple and maintainable\""), - "per-file halstead message missing:\n{json}" + yaml.contains(&format!("message: {MSG_NO_FUNCTIONS}")), + "{yaml}" ); + assert!(yaml.contains("function_halstead: []"), "{yaml}"); + + assert!(halstead_bin_report(path, &filter).is_some()); + let raw = halstead_raw_report_value(path, &filter).expect("raw"); + let raw = raw.as_map().expect("map"); + assert!(matches!(raw.get("total_functions"), Some(GoValue::Int(0)))); + assert!(matches!(raw.get("functions"), Some(GoValue::Array(v)) if v.is_empty())); + + let text = halstead_report_value_summary(path, &filter).expect("summary"); + assert!(text.as_map().is_some()); } #[test] - fn no_per_file_flag_omits_files() { + fn missing_path_is_none() { + assert!(halstead_json_report("/nonexistent/halstead/path").is_none()); + assert!( + halstead_yaml_report("/nonexistent/halstead/path", &StaticFilter::default()).is_none() + ); + } + + /// yaml and json classify functions into the same volume buckets, and + /// the yaml aggregate carries the real union ints (never zeros). + #[test] + fn yaml_aggregate_carries_union_ints_and_the_same_distribution() { let dir = fixture(); - let bytes = halstead_json_report_flags( - dir.path().to_str().unwrap(), - &StaticFilter::default(), - false, + let yaml = String::from_utf8( + halstead_yaml_report(dir.path().to_str().unwrap(), &StaticFilter::default()).unwrap(), ) .unwrap(); - let json = String::from_utf8(bytes).unwrap(); - assert!( - !json.contains("\"files\""), - "files key must be omitted:\n{json}" - ); - assert!(!json.contains("\"file_path\"")); + for needle in [ + "distinct_operators: 4", + "distinct_operands: 4", + "total_operators: 4", + "total_operands: 5", + "vocabulary: 8", + "length: 9", + "volume: 27", + "difficulty: 2.5", + "effort: 67.5", + "high_effort_functions: 0", + "health_score: 100", + &format!("message: {MSG_LOW}"), + "low: 1", + "medium: 0", + "complexity_level: Low", + ] { + assert!(yaml.contains(needle), "missing {needle:?} in:\n{yaml}"); + } + let json = json_of(&dir, false); + assert!(json.contains("\"label\": \"Low (\\u003c100)\",\n \"percent\": 1,\n \"count\": 1"), "{json}"); + } + + /// Issues and `function_halstead` share the total order: effort desc, + /// then volume desc, then name, then file. + #[test] + fn function_lists_follow_the_total_order() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write( + dir.path().join("a.go"), + "package main\n\nfunc small() int { return 1 }\n\nfunc big(a, b, c int) int {\n\td := a + b*c - a/b\n\treturn d * d\n}\n", + ) + .unwrap(); + std::fs::write( + dir.path().join("b.go"), + "package main\n\nfunc small() int { return 2 }\n", + ) + .unwrap(); + let path = dir.path().to_str().unwrap(); + let json = String::from_utf8( + halstead_json_report_flags(path, &StaticFilter::default(), false).unwrap(), + ) + .unwrap(); + let big = json.find("\"name\": \"big\"").expect("big"); + let small_a = json.find("\"location\": \"a.go\"").expect("a.go issue"); + let small_b = json.find("\"location\": \"b.go\"").expect("b.go issue"); + assert!(big < small_a, "highest effort first:\n{json}"); + assert!(small_a < small_b, "ties break by name then file:\n{json}"); + + let yaml = String::from_utf8(halstead_yaml_report(path, &StaticFilter::default()).unwrap()) + .unwrap(); + let big_y = yaml.find("name: big").expect("big"); + let a_y = yaml.find("source_file: a.go").expect("a.go"); + let b_y = yaml.find("source_file: b.go").expect("b.go"); + assert!(big_y < a_y && a_y < b_y, "{yaml}"); } } diff --git a/crates/cf-commands/src/handlers/static_imports.rs b/crates/cf-commands/src/handlers/static_imports.rs index 311323e2..c74fb688 100644 --- a/crates/cf-commands/src/handlers/static_imports.rs +++ b/crates/cf-commands/src/handlers/static_imports.rs @@ -1,117 +1,76 @@ //! Static-analysis report path for the UAST `static/imports` analyzer. //! -//! Reproduces the reference static pipeline for `codefang run --analyzers -//! static/imports` across its machine formats (yaml, bin, json). +//! Builds `codefang run --analyzers static/imports` across its machine +//! formats (yaml, bin, json) and the section value the text/compact +//! renderers and the multi-analyzer static JSON merge consume. //! -//! Pipeline (the reference `StaticService.streamFiles` → per-file `uast.Parser.Parse` → -//! `imports.Analyzer.Analyze` (`extractImportsFromUAST`) → `imports.Aggregator` -//! → format-specific serialization): +//! Pipeline: //! -//! 1. `streamFiles` walks `rootPath` with `filepath.WalkDir` (lexical order, -//! `.git` skipped), keeping every UAST-supported, non-vendor/-generated file -//! (`ShouldSkipFolderNode` → `parser.IsSupported`, `pathpolicy.Exclude`). -//! 2. Each file is parsed to a real UAST and run through -//! [`cf_imports::extract_imports_from_uast`] — the import path is taken from -//! each `Import` node's token (or first literal/identifier child), cleaned -//! (quotes/semicolons trimmed, statement forms parsed), and **deduplicated -//! within the file in first-seen order**. Crucially the import token retains -//! any alias prefix (e.g. `fwk "k8s.io/kube-scheduler/framework`), exactly -//! as the reference implementation emits it — the previous text-based extractor incorrectly stripped -//! aliases. -//! 3. The cross-file [`cf_imports::Aggregator`] increments `total_files` for -//! EVERY analyzed file (including markdown READMEs that contribute no -//! imports — the reference implementation parses every supported file and folds an empty report) and -//! sums per-import occurrence counts. -//! 4. `ComputeAllMetrics(report)` derives the structured metrics; the machine -//! formats serialize that value through cf-goyaml / cf-reportutil / cf-gojson -//! byte-identically. +//! 1. The shared static walk ([`super::walk_source_files`]) admits every +//! UAST-supported file that passes the run filter (`--languages`, the +//! vendor/generated path policy, the generated-content marker, the read +//! cap). A FILE root is analyzed as that single file. +//! 2. Parse each file and run [`cf_imports::extract_imports`] with the +//! parser's language name and the file bytes: one entry per imported +//! module/package/file, deduplicated within the file in first-seen order +//! (aliases, specifier lists and keywords never count — see the crate's +//! `extract` module for the per-language rules). +//! 3. Fold into [`cf_imports::Aggregate`]: `total_files` counts EVERY +//! analyzed file (import-free files included; a supported file that +//! fails to parse folds as import-free), and each import's value is the +//! number of files importing it. +//! 4. Serialize: yaml/bin carry [`cf_imports::ComputedMetrics`] (classified +//! per the importing file's language); json carries the info-only IMPORTS +//! section with issues ordered count-desc, then name. //! -//! ## Markdown / ungrammared-but-supported files -//! -//! The reference implementation's `IsSupported` is true for markdown (a wired tree-sitter grammar), so a -//! `README.md` is parsed (to a tree with no import nodes) and still counts as -//! one file in `total_files`. Rust may lack a wired grammar for some supported -//! extensions; a parse failure on a supported file is therefore folded as that -//! same empty report (`total_files += 1`, no imports) — keeping `total_files` -//! (and thus every derived metric and the JSON `Total Files` value) identical. - -use std::path::Path; +//! A root with no analyzable files is the EMPTY report (status +//! `No import data available`, zero metrics), never an error; only a missing +//! path yields `None`. use super::StaticFilter; use cf_gojson::{GoMap, GoValue, MapOrigin}; -use cf_uast::{Node as UastNode, Parser}; +use cf_imports::section::{ + status_message, KEY_COUNT, KEY_IMPORTS, KEY_IMPORT_COUNTS, KEY_TOTAL_FILES, METRIC_TOTAL_FILES, + METRIC_UNIQUE_IMPORTS, SCORE_INFO, SCORE_LABEL_INFO, SECTION_TITLE, SEVERITY_INFO, +}; +use cf_imports::{extract_imports, Aggregate}; +use cf_uast::Parser; + +/// Number of files a `--per-file` entry stands for. +const PER_FILE_TOTAL_FILES: i64 = 1; +/// Issue value of an import inside a `--per-file` entry (one file). +const PER_FILE_ISSUE_VALUE: &str = "1"; +/// Location of an aggregate issue (imports are not located in one file). +const AGGREGATE_LOCATION: &str = ""; /// Builds the `static/imports --format yaml` report bytes for `root_path`. -/// Returns `None` when the path does not exist (the caller falls through to the -/// blocked-dependency sentinel). +/// Returns `None` only when the path does not exist. pub fn imports_report_yaml(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let report = aggregate_report_value(root_path, filter)?; - let metrics = - cf_imports::compute_all_metrics(&report).expect("compute_all_metrics is infallible"); + let scan = scan(root_path, filter, false)?; + let metrics = cf_imports::compute_metrics(&scan.aggregate.report_data()); Some(cf_goyaml::marshal(&metrics.to_go_value_yaml())) } -/// Builds the `static/imports --format bin` report bytes for `root_path`. -/// -/// Same aggregate report + [`cf_imports::compute_all_metrics`] as the yaml -/// sibling, but wrapped in a CFB1 envelope (`reportutil.EncodeBinaryEnvelope`). +/// Builds the `static/imports --format bin` report bytes for `root_path`: +/// the same metrics as the yaml sibling in a CFB1 envelope. pub fn imports_report_bin(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let report = aggregate_report_value(root_path, filter)?; - let metrics = - cf_imports::compute_all_metrics(&report).expect("compute_all_metrics is infallible"); + let scan = scan(root_path, filter, false)?; + let metrics = cf_imports::compute_metrics(&scan.aggregate.report_data()); Some( cf_reportutil::encode_binary_envelope(&metrics.to_go_value()) .expect("imports metrics never exceed the CFB1 length cap"), ) } -/// Aggregates into the `cf_imports::ReportValue` aggregator report (the -/// `imports.Aggregator.GetResult` shape) consumed by `compute_all_metrics`. -fn aggregate_report_value( - root_path: &str, - filter: &StaticFilter, -) -> Option<cf_imports::ReportValue> { - let (all_imports, total_files) = walk_and_count_opts(root_path, filter, None)?; - let imports: Vec<cf_imports::ReportValue> = all_imports - .keys() - .map(|k| cf_imports::ReportValue::Str(k.clone())) - .collect(); - let mut import_counts = cf_imports::ReportValue::map(); - for (imp, c) in &all_imports { - import_counts.insert(imp.clone(), cf_imports::ReportValue::Int(*c)); - } - let mut report = cf_imports::ReportValue::map(); - report.insert("imports", cf_imports::ReportValue::List(imports)); - report.insert("import_counts", import_counts); - report.insert( - "count", - cf_imports::ReportValue::Int(all_imports.len() as i64), - ); - report.insert("total_files", cf_imports::ReportValue::Int(total_files)); - Some(report) -} - -/// Builds the `static/imports --format json` structured-report bytes. -/// -/// The reference JSON path is `StaticService.FormatJSON` → -/// `imports.CreateReportSection(aggregatedReport)` → `renderer.SectionToJSON` → -/// `json.NewEncoder(SetIndent(""," ")).Encode`. Imports is an INFO-only section -/// (`score = -1` ⇒ `score_label = "Info"`); the single-section overall is also -/// info-only. The `issues` list is every unique import, ordered by occurrence -/// count descending via the reference implementation's `sort.Slice` over a map iterated in random order — -/// so the tie order is intrinsically nondeterministic in the reference binary (the project MANIFEST -/// marks `static_imports.json` nonBinding). We emit a DETERMINISTIC, correct -/// ordering: count descending, ties broken by the aggregator's sorted import -/// keys (the same set the reference implementation emits, just stably ordered). +/// Builds the `static/imports --format json` structured-report bytes with +/// the default filter and no per-file enrichment. pub fn imports_report_json(root_path: &str) -> Option<Vec<u8>> { imports_report_json_flags(root_path, &StaticFilter::default(), false) } -/// [`imports_report_json`] with the `--per-file` flag applied: `per_file` -/// enables the reference implementation's section enrichment -/// (`StaticService.enrichWithPerFileData` → `JSONReport.EnrichWithPerFileData` -/// over the `PerFileRetainer` snapshots — one `JSONFileEntry` per ANALYZED -/// file, import-free files included, keyed into the section's `files` array). +/// [`imports_report_json`] with the run filter and the `--per-file` flag: +/// `per_file` adds one entry per ANALYZED file (import-free files included) +/// to the section's `files` array. #[must_use] pub fn imports_report_json_flags( root_path: &str, @@ -126,405 +85,192 @@ pub fn imports_report_json_flags( ) } -/// Builds the `static/imports` `renderer.JSONReport` GoValue (single info-only -/// section), shared by the single-analyzer byte path and the multi-analyzer -/// static-JSON merge. `None` when the path cannot be walked. +/// Builds the `static/imports` JSON report value (single info-only section) +/// with the default filter, shared by the single-analyzer path and the +/// multi-analyzer static-JSON merge. #[must_use] pub fn imports_report_value(root_path: &str) -> Option<GoValue> { imports_report_value_flags(root_path, &StaticFilter::default(), false) } -/// [`imports_report_value`] with the `--per-file` section enrichment. +/// [`imports_report_value`] with the run filter and `--per-file` enrichment. #[must_use] pub fn imports_report_value_flags( root_path: &str, filter: &StaticFilter, per_file: bool, ) -> Option<GoValue> { - let mut per_file_data: Vec<(String, Vec<String>)> = Vec::new(); - let (all_imports, total_files) = - walk_and_count_opts(root_path, filter, per_file.then_some(&mut per_file_data))?; - let count = all_imports.len() as i64; - - // --- status / score (info-only) --- - let status = build_status_message(count); - - // --- key metrics (reference KeyMetrics order) --- - let metric = |label: &str, value: String| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("value", GoValue::Str(value)); - GoValue::Map(m) - }; - let metrics = vec![ - metric("Unique Imports", cf_reportutil::format_int(count)), - metric("Total Files", cf_reportutil::format_int(total_files)), - ]; - - // --- issues: imports sorted by count desc (deterministic tie-break by key) --- - // the reference implementation sorts `import_counts` (a map iterated in RANDOM order) by count desc via - // an unstable sort.Slice, so its tie order is nondeterministic (the project - // MANIFEST marks static_imports.json nonBinding). We emit a DETERMINISTIC, - // correct ordering: count descending, ties broken by ascending import key. - let mut entries: Vec<(String, i64)> = - all_imports.iter().map(|(k, c)| (k.clone(), *c)).collect(); - // all_imports is a BTreeMap (keys already ascending); stable sort by count - // desc keeps that ascending tie order. - entries.sort_by(|a, b| b.1.cmp(&a.1)); - let issues: Vec<GoValue> = entries - .iter() - .map(|(name, c)| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str(name.clone())); - m.push("location", GoValue::Str(String::new())); - m.push("value", GoValue::Str(cf_reportutil::format_int(*c))); - m.push("severity", GoValue::Str("info".to_string())); - GoValue::Map(m) - }) + let scan = scan(root_path, filter, per_file)?; + let count = scan.aggregate.unique_count(); + + let issues: Vec<GoValue> = scan + .aggregate + .ranked() + .into_iter() + .map(|(name, c)| issue(name, AGGREGATE_LOCATION, &cf_reportutil::format_int(c))) .collect(); - // --- --per-file: one JSONFileEntry per ANALYZED file, in walk order (the - // reference implementation ranges the retainer map here — run-to-run - // random; the oracle's measured-variance canonicalization compares the set) --- - let file_entries: Option<Vec<GoValue>> = if per_file { - Some( - per_file_data - .iter() - .map(|(rel, imports)| build_file_entry(rel, imports)) - .collect(), - ) - } else { - None - }; - - // --- section (JSONSection field order) --- let mut section = GoMap::new(MapOrigin::Struct); - section.push("title", GoValue::Str("IMPORTS".to_string())); - section.push("score_label", GoValue::Str("Info".to_string())); - section.push("status", GoValue::Str(status)); - section.push("metrics", GoValue::Array(metrics)); - // Distribution() returns nil ⇒ omitempty omits it. + section.push("title", GoValue::Str(SECTION_TITLE.to_string())); + section.push("score_label", GoValue::Str(SCORE_LABEL_INFO.to_string())); + section.push("status", GoValue::Str(status_message(count))); + section.push( + "metrics", + GoValue::Array(metrics(count, scan.aggregate.total_files())), + ); section.push("issues", GoValue::Array(issues)); - // --per-file enrichment: `files` sits between `issues` and `score` - // (renderer.JSONSection field order; omitempty without the flag). - if let Some(entries) = file_entries { - section.push("files", GoValue::Array(entries)); + if per_file { + let files = scan + .per_file + .iter() + .map(|f| build_file_entry(&f.rel_path, &f.imports)) + .collect(); + section.push("files", GoValue::Array(files)); } - section.push("score", GoValue::Float(-1.0)); + section.push("score", GoValue::Float(SCORE_INFO)); - // --- top-level JSONReport (overall info-only) --- let mut root = GoMap::new(MapOrigin::Struct); - root.push("overall_score_label", GoValue::Str("Info".to_string())); + root.push( + "overall_score_label", + GoValue::Str(SCORE_LABEL_INFO.to_string()), + ); root.push("sections", GoValue::Array(vec![GoValue::Map(section)])); - root.push("overall_score", GoValue::Float(-1.0)); - + root.push("overall_score", GoValue::Float(SCORE_INFO)); Some(GoValue::Map(root)) } -/// Builds one `renderer.JSONFileEntry` for `--per-file` (the reference -/// `SectionToJSONFileEntry` over `imports.CreateReportSection(perFileReport)`): -/// the per-file report is the analyzer's `{imports, count}` map — no -/// `import_counts` — so the issues take the alphabetical-list fallback, each -/// valued `"1"` and located at the stamped relative path, and `Total Files` -/// reads the absent key as 0. Imports stays info-only per file (`score = -1`). -fn build_file_entry(rel_path: &str, imports: &[String]) -> GoValue { - let count = imports.len() as i64; - - let metric = |label: &str, value: String| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("value", GoValue::Str(value)); - GoValue::Map(m) - }; - let metrics = vec![ - metric("Unique Imports", cf_reportutil::format_int(count)), - metric("Total Files", cf_reportutil::format_int(0)), - ]; - - let mut sorted: Vec<&String> = imports.iter().collect(); - sorted.sort(); - let issues: Vec<GoValue> = sorted - .iter() - .map(|name| { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("name", GoValue::Str((*name).clone())); - m.push("location", GoValue::Str(rel_path.to_string())); - m.push("value", GoValue::Str("1".to_string())); - m.push("severity", GoValue::Str("info".to_string())); - GoValue::Map(m) - }) - .collect(); - - let mut entry = GoMap::new(MapOrigin::Struct); - entry.push("file_path", GoValue::Str(rel_path.to_string())); - entry.push("score_label", GoValue::Str("Info".to_string())); - entry.push("status", GoValue::Str(build_status_message(count))); - entry.push("metrics", GoValue::Array(metrics)); - // Distribution() returns nil for imports ⇒ omitempty omits it. - entry.push("issues", GoValue::Array(issues)); - entry.push("score", GoValue::Float(-1.0)); - GoValue::Map(entry) -} - -/// Builds the AGGREGATED RAW `analyze.Report` GoValue for `static/imports` — -/// the value the reference implementation's `imports.Aggregator.GetResult()` returns, -/// which is what `--format plot` consumes and what `writeReportJSON` -/// serializes into `report.json`: -/// -/// * `imports`: the unique import paths. the reference implementation materializes them from a -/// `map[string]int` in RANDOM iteration order (measured-nondeterministic; -/// the harness compares the multiset) — we emit the byte-sorted order. -/// * `import_counts`: import path → file-occurrence count. -/// * `count`: unique import count; `total_files`: every analyzed file. +/// Builds the AGGREGATED RAW report value for `static/imports` — what +/// `--format plot` consumes and what `report.json` stores: `imports` (unique +/// paths, byte-sorted), `import_counts` (path -> number of importing files), +/// `count` (unique paths) and `total_files`. #[must_use] pub fn imports_raw_report_value(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - let (all_imports, total_files) = walk_and_count_opts(root_path, filter, None)?; + let scan = scan(root_path, filter, false)?; - let imports: Vec<GoValue> = all_imports - .keys() - .map(|k| GoValue::Str(k.clone())) + let imports: Vec<GoValue> = scan + .aggregate + .entries() + .map(|(path, _)| GoValue::Str(path.to_string())) .collect(); let mut import_counts = GoMap::new(MapOrigin::Map); - for (imp, c) in &all_imports { - import_counts.push(imp, GoValue::Int(*c)); + for (path, entry) in scan.aggregate.entries() { + import_counts.push(path, GoValue::Int(entry.count)); } let mut m = GoMap::new(MapOrigin::Map); - m.push("imports", GoValue::Array(imports)); - m.push("import_counts", GoValue::Map(import_counts)); - m.push("count", GoValue::Int(all_imports.len() as i64)); - m.push("total_files", GoValue::Int(total_files)); + m.push(KEY_IMPORTS, GoValue::Array(imports)); + m.push(KEY_IMPORT_COUNTS, GoValue::Map(import_counts)); + m.push(KEY_COUNT, GoValue::Int(scan.aggregate.unique_count())); + m.push(KEY_TOTAL_FILES, GoValue::Int(scan.aggregate.total_files())); Some(GoValue::Map(m)) } -/// `imports.buildStatusMessage`. -fn build_status_message(count: i64) -> String { - if count == 0 { - "No import data available".to_string() - } else { - format!("Found {} unique imports", cf_reportutil::format_int(count)) - } -} - -/// Walks `root_path`, parses each supported file to a real UAST, extracts its -/// imports, and folds them into a cross-file `import path -> file count` map -/// alongside the total analyzed-file count. Returns `None` when the path is -/// missing. Mirrors the per-file analyze + `imports.Aggregator` accumulation. -/// `filter` carries the run-level walk filter (`--languages` + the path -/// policy) applied to every walked file. When `per_file_out` -/// is set, each analyzed file's deduplicated import list is also pushed as -/// `(relative path, imports)` in walk order — the reference `PerFileRetainer` -/// snapshot the `--per-file` section enrichment consumes. -fn walk_and_count_opts( - root_path: &str, - filter: &StaticFilter, - mut per_file_out: Option<&mut Vec<(String, Vec<String>)>>, -) -> Option<(std::collections::BTreeMap<String, i64>, i64)> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - - let parser = Parser::new(); - - let mut files: Vec<String> = Vec::new(); - collect_files(root, &parser, filter, &mut files); - files.sort(); - - // The reference aggregator increments total_files for every analyzed file and sums - // per-import occurrence counts across files. - let mut all_imports: std::collections::BTreeMap<String, i64> = - std::collections::BTreeMap::new(); - let mut total_files: i64 = 0; - - for path in &files { - let Ok(content) = std::fs::read(path) else { - continue; - }; - total_files += 1; - // Parse failures on supported files (markdown without a wired grammar) - // fold as an empty report: +1 file, no imports (see module docs). - // extract_imports_from_uast deduplicates within the file in first-seen - // order; the cross-file aggregator then counts file occurrences. - let file_imports: Vec<String> = match parser.parse(path, &content) { - Ok(node) => extract_imports_from_uast(&node), - Err(_) => Vec::new(), - }; - for imp in &file_imports { - *all_imports.entry(imp.clone()).or_insert(0) += 1; - } - if let Some(out) = per_file_out.as_deref_mut() { - out.push((make_relative_path(path, root_path), file_imports)); - } - } - - if total_files == 0 { - return None; - } - Some((all_imports, total_files)) +/// One analyzed file's deduplicated import paths, for `--per-file`. +struct FileImports { + /// Path relative to the walk root (a file root stamps the file name). + rel_path: String, + /// Import paths in first-seen order. + imports: Vec<String>, } -/// The reference `filepath.Rel(rootPath, filePath)` (flat repos → path under -/// the root) — the `StampSourceFile` path stamped onto per-file reports. -fn make_relative_path(file_path: &str, root_path: &str) -> String { - if root_path.is_empty() { - return file_path.to_string(); - } - match Path::new(file_path).strip_prefix(Path::new(root_path)) { - Ok(rel) => rel.to_string_lossy().into_owned(), - Err(_) => file_path.to_string(), - } +/// The result of walking a root. +#[derive(Default)] +struct Scan { + aggregate: Aggregate, + per_file: Vec<FileImports>, } -/// Recursively collects UAST-supported, non-excluded regular files under `dir` -/// (lexical order; `.git` skipped). Mirrors `streamFiles` / -/// `ShouldSkipFolderNode`. -fn collect_files(dir: &Path, parser: &Parser, filter: &StaticFilter, out: &mut Vec<String>) { - let Ok(read) = std::fs::read_dir(dir) else { - return; - }; - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - let path = entry.path(); - let Ok(ft) = entry.file_type() else { continue }; - if ft.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; +/// Walks `root_path` (a directory or a single file) through the shared +/// static walk, parses each admitted file, extracts its imports and folds +/// them into the aggregate. Returns `None` only when the path does not +/// exist; a root with no analyzable files yields an empty [`Scan`]. +fn scan(root_path: &str, filter: &StaticFilter, keep_per_file: bool) -> Option<Scan> { + let parser = Parser::new(); + let walk = super::walk_source_files(root_path, &parser, filter).ok()?; + + let mut scan = Scan::default(); + for file in walk { + let imports: Vec<String> = match parser.parse(&file.rel_path, &file.bytes) { + Ok(node) => { + let found = extract_imports(&node, &file.language, &file.bytes); + scan.aggregate.add_file(&file.language, &found); + found.into_iter().map(|i| i.path).collect() } - collect_files(&path, parser, filter, out); - continue; - } - let path_str = path.to_string_lossy().to_string(); - if !parser.is_supported(&path_str) { - continue; - } - if filter.skips(&path_str) { - continue; - } - out.push(path_str); - } -} - -/// Extracts deduplicated import strings from a real cf-uast tree. -/// -/// Faithful port of the reference `extractImportsFromUAST` (mirrored in -/// `cf_imports::extract_imports_from_uast`, here applied to `cf_uast::Node`): -/// pre-order traversal; a node contributes when its type is `Import` or it -/// carries the `Import` role. Duplicates (by extracted path) are dropped, -/// preserving first-seen order. -pub fn extract_imports_from_uast(root: &UastNode) -> Vec<String> { - let mut imports: Vec<String> = Vec::new(); - let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new(); - visit_pre_order(root, &mut |n: &UastNode| { - if n.node_type == "Import" || has_role(n, "Import") { - let p = extract_import_path(n); - if !p.is_empty() && seen.insert(p.clone()) { - imports.push(p); + Err(_) => { + scan.aggregate.add_file(&file.language, &[]); + Vec::new() } + }; + if keep_per_file { + scan.per_file.push(FileImports { + rel_path: file.rel_path, + imports, + }); } - }); - imports -} - -fn visit_pre_order<F: FnMut(&UastNode)>(n: &UastNode, f: &mut F) { - f(n); - for c in &n.children { - visit_pre_order(c, f); } + Some(scan) } -fn has_role(n: &UastNode, role: &str) -> bool { - n.roles.iter().any(|r| r == role) +/// The section's key metrics: unique imports and analyzed files. +fn metrics(unique_imports: i64, total_files: i64) -> Vec<GoValue> { + vec![ + metric( + METRIC_UNIQUE_IMPORTS, + cf_reportutil::format_int(unique_imports), + ), + metric(METRIC_TOTAL_FILES, cf_reportutil::format_int(total_files)), + ] } -/// `extractImportPath`: node token (cleaned), else search children. -fn extract_import_path(n: &UastNode) -> String { - if !n.token.is_empty() { - return clean_import_path(&n.token); - } - if n.children.is_empty() { - return String::new(); - } - extract_import_path_from_children(&n.children) +/// One `{label, value}` metric. +fn metric(label: &str, value: String) -> GoValue { + let mut m = GoMap::new(MapOrigin::Struct); + m.push("label", GoValue::Str(label.to_string())); + m.push("value", GoValue::Str(value)); + GoValue::Map(m) } -/// `extractImportPathFromChildren`: first literal-with-token, then -/// identifier-with-token, then recurse. -fn extract_import_path_from_children(children: &[UastNode]) -> String { - for c in children { - if c.node_type == "Literal" && !c.token.is_empty() { - return clean_import_path(&c.token); - } - } - for c in children { - if c.node_type == "Identifier" && !c.token.is_empty() { - return clean_import_path(&c.token); - } - } - for c in children { - let p = extract_import_path(c); - if !p.is_empty() { - return p; - } - } - String::new() +/// One info issue `{name, location, value, severity}`. +fn issue(name: &str, location: &str, value: &str) -> GoValue { + let mut m = GoMap::new(MapOrigin::Struct); + m.push("name", GoValue::Str(name.to_string())); + m.push("location", GoValue::Str(location.to_string())); + m.push("value", GoValue::Str(value.to_string())); + m.push("severity", GoValue::Str(SEVERITY_INFO.to_string())); + GoValue::Map(m) } -/// `cleanImportPath`: trim `"`/`'`/`;` from both ends, skip empty/`{`/`}`, apply -/// the statement-form parser, else return the trimmed value. -fn clean_import_path(path: &str) -> String { - let path = path.trim_matches(|c| c == '"' || c == '\'' || c == ';'); - if path.is_empty() || path == "{" || path == "}" { - return String::new(); - } - let parsed = parse_import_format(path); - if !parsed.is_empty() { - return parsed; - } - path.to_string() -} +/// One `--per-file` entry: the file's imports alphabetically, each located +/// at the file and valued `1`, with `Total Files` `1`. +fn build_file_entry(rel_path: &str, imports: &[String]) -> GoValue { + let count = imports.len() as i64; + let mut sorted: Vec<&String> = imports.iter().collect(); + sorted.sort(); + let issues: Vec<GoValue> = sorted + .iter() + .map(|name| issue(name, rel_path, PER_FILE_ISSUE_VALUE)) + .collect(); -/// `parseImportFormat`: Python/JS statement forms; empty for Go-style specs (so -/// `cleanImportPath` falls back to the bare token, retaining any alias prefix). -fn parse_import_format(path: &str) -> String { - if path.starts_with("from ") { - let parts: Vec<&str> = path.split_whitespace().collect(); - if parts.len() >= 2 { - return parts[1].to_string(); - } - return String::new(); - } - if path.contains(" from ") { - let parts: Vec<&str> = path.splitn(2, " from ").collect(); - if parts.len() >= 2 { - return parts[1].trim_matches(|c| c == '"' || c == '\'').to_string(); - } - return String::new(); - } - if path.starts_with("import ") { - let parts: Vec<&str> = path.split_whitespace().collect(); - if parts.len() >= 2 { - return parts[1].trim_matches(|c| c == '"' || c == '\'').to_string(); - } - return String::new(); - } - if path.contains("import ") { - let parts: Vec<&str> = path.splitn(2, "import ").collect(); - if parts.len() >= 2 { - return parts[1].trim_matches(|c| c == '"' || c == '\'').to_string(); - } - return String::new(); - } - String::new() + let mut entry = GoMap::new(MapOrigin::Struct); + entry.push("file_path", GoValue::Str(rel_path.to_string())); + entry.push("score_label", GoValue::Str(SCORE_LABEL_INFO.to_string())); + entry.push("status", GoValue::Str(status_message(count))); + entry.push( + "metrics", + GoValue::Array(metrics(count, PER_FILE_TOTAL_FILES)), + ); + entry.push("issues", GoValue::Array(issues)); + entry.push("score", GoValue::Float(SCORE_INFO)); + GoValue::Map(entry) } #[cfg(test)] -mod per_file_tests { +mod tests { use super::*; + use std::path::Path; - /// Fixture: one file with two imports, one import-free file. + /// Fixture: one file with two imports, one import-free file, one + /// unsupported file. fn fixture() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("tempdir"); std::fs::write( @@ -537,53 +283,148 @@ mod per_file_tests { "package main\n\ntype Config struct {\n\tName string\n}\n", ) .unwrap(); + std::fs::write(dir.path().join("notes.txt"), "import nothing\n").unwrap(); dir } + fn json(dir: &Path, per_file: bool) -> String { + let bytes = + imports_report_json_flags(dir.to_str().unwrap(), &StaticFilter::default(), per_file) + .expect("existing path"); + String::from_utf8(bytes).unwrap() + } + #[test] - fn per_file_flag_emits_files_entries() { + fn per_file_flag_emits_one_entry_per_analyzed_file() { let dir = fixture(); - let bytes = - imports_report_json_flags(dir.path().to_str().unwrap(), &StaticFilter::default(), true) - .unwrap(); - let json = String::from_utf8(bytes).unwrap(); - // The section gains a `files` array with one JSONFileEntry per - // ANALYZED file (import-free files included). + let json = json(dir.path(), true); assert!(json.contains("\"files\""), "files key missing:\n{json}"); + assert!(json.contains("\"file_path\": \"a.go\""), "{json}"); + assert!(json.contains("\"file_path\": \"types.go\""), "{json}"); assert!( - json.contains("\"file_path\": \"a.go\""), - "a.go entry missing:\n{json}" + !json.contains("notes.txt"), + "unsupported file listed:\n{json}" ); assert!( - json.contains("\"file_path\": \"types.go\""), - "types.go entry missing:\n{json}" + json.contains("\"status\": \"No import data available\""), + "import-free per-file status missing:\n{json}" ); - // Per-file status uses the per-file unique-import count. + assert!(json.contains("\"location\": \"a.go\""), "{json}"); + // Every per-file entry stands for exactly one file. + let value = imports_report_value_flags( + dir.path().to_str().unwrap(), + &StaticFilter::default(), + true, + ) + .expect("existing path"); + let files = section_files(&value); + assert_eq!(files.len(), 2); + for entry in files { + assert_eq!( + metric_value(entry, METRIC_TOTAL_FILES), + Some("1"), + "{entry:?}" + ); + } + } + + /// The `files` array of the single IMPORTS section. + fn section_files(report: &GoValue) -> &[GoValue] { + let Some(GoValue::Array(sections)) = report.as_map().and_then(|m| m.get("sections")) else { + panic!("sections array missing: {report:?}"); + }; + match sections[0].as_map().and_then(|m| m.get("files")) { + Some(GoValue::Array(files)) => files, + other => panic!("files array missing: {other:?}"), + } + } + + /// The value of the metric labelled `label` in a section or file entry. + fn metric_value<'a>(entry: &'a GoValue, label: &str) -> Option<&'a str> { + let GoValue::Array(metrics) = entry.as_map()?.get("metrics")? else { + return None; + }; + metrics + .iter() + .filter_map(GoValue::as_map) + .find(|m| m.get("label") == Some(&GoValue::Str(label.to_string()))) + .and_then(|m| match m.get("value") { + Some(GoValue::Str(v)) => Some(v.as_str()), + _ => None, + }) + } + + #[test] + fn no_per_file_flag_omits_files() { + let dir = fixture(); + let json = json(dir.path(), false); + assert!(!json.contains("\"files\""), "{json}"); + assert!(!json.contains("\"file_path\"")); + } + + #[test] + fn empty_root_is_the_empty_report() { + let dir = tempfile::tempdir().expect("tempdir"); + let json = json(dir.path(), false); assert!( json.contains("\"status\": \"No import data available\""), - "import-free per-file status missing:\n{json}" + "{json}" ); - // Per-file issues carry the stamped relative path as location. + assert!(json.contains("\"issues\": []"), "{json}"); + let yaml = imports_report_yaml(dir.path().to_str().unwrap(), &StaticFilter::default()) + .expect("existing path"); + assert!(String::from_utf8(yaml) + .unwrap() + .contains("total_imports: 0")); + } + + #[test] + fn missing_root_is_none() { + let dir = tempfile::tempdir().expect("tempdir"); + let missing = dir.path().join("nope"); + assert!(imports_report_value(missing.to_str().unwrap()).is_none()); + } + + #[test] + fn file_root_stamps_the_file_name() { + let dir = fixture(); + let file = dir.path().join("a.go"); + let json = json(&file, true); + assert!(json.contains("\"file_path\": \"a.go\""), "{json}"); assert!( - json.contains("\"location\": \"a.go\""), - "per-file issue location missing:\n{json}" + json.contains("\"label\": \"Total Files\",\n \"value\": \"1\""), + "{json}" ); + assert!(!json.contains("\"file_path\": \".\""), "{json}"); } + /// A nested file is stamped root-relative (slash-joined). #[test] - fn no_per_file_flag_omits_files() { + fn nested_files_are_stamped_root_relative() { let dir = fixture(); - let bytes = imports_report_json_flags( - dir.path().to_str().unwrap(), - &StaticFilter::default(), - false, + std::fs::create_dir_all(dir.path().join("sub")).unwrap(); + std::fs::write( + dir.path().join("sub/c.go"), + "package sub\n\nimport \"io\"\n\nvar _ = io.EOF\n", ) .unwrap(); - let json = String::from_utf8(bytes).unwrap(); - assert!( - !json.contains("\"files\""), - "files key must be omitted:\n{json}" - ); - assert!(!json.contains("\"file_path\"")); + let json = json(dir.path(), true); + assert!(json.contains("\"file_path\": \"sub/c.go\""), "{json}"); + assert!(json.contains("\"location\": \"sub/c.go\""), "{json}"); + } + + #[test] + fn raw_report_counts_files_per_import() { + let dir = fixture(); + let raw = imports_raw_report_value(dir.path().to_str().unwrap(), &StaticFilter::default()) + .expect("existing path"); + let top = raw.as_map().expect("map"); + assert_eq!(top.get(KEY_COUNT), Some(&GoValue::Int(2))); + assert_eq!(top.get(KEY_TOTAL_FILES), Some(&GoValue::Int(2))); + let counts = top + .get(KEY_IMPORT_COUNTS) + .and_then(GoValue::as_map) + .expect("counts"); + assert_eq!(counts.get("fmt"), Some(&GoValue::Int(1))); } } diff --git a/crates/cf-commands/src/handlers/static_json.rs b/crates/cf-commands/src/handlers/static_json.rs index 6d7d029a..e503e25f 100644 --- a/crates/cf-commands/src/handlers/static_json.rs +++ b/crates/cf-commands/src/handlers/static_json.rs @@ -1,133 +1,57 @@ -//! Static-analysis JSON report path for raw-file analyzers (`static/composition`). +//! Static-analysis report path for the raw-file `static/composition` +//! analyzer: `codefang run --analyzers static/composition --format +//! {json,yaml,bin,compact,text}`. //! -//! Reproduces the reference static pipeline for the single-analyzer -//! `codefang run --analyzers static/composition --format json` capture: +//! 1. The shared all-files walk ([`super::walk_all_files`]) offers EVERY +//! regular file under `root_path` (lexical order, `.git` and cache +//! directories skipped) to the analyzer. Composition exists to report +//! vendored / generated shares, so the vendor/generated path policy +//! (`--include-vendored` / `--include-generated`) does not apply to it; +//! only `--languages` and `--extra-excluded-prefixes` narrow the walk. +//! 2. Each file is classified from its root-relative path and its first +//! [`super::ALL_FILES_HEADER_BYTES`] bytes by +//! [`cf_composition::Classifier`] and counted by the +//! [`cf_composition::Aggregator`]; with `--per-file` the file's class +//! and language are retained as a [`cf_composition::FileRecord`]. +//! 3. The [`cf_composition::CompositionReport`] drives every format through +//! the crate's builders: `json` / `text` / `compact` ← +//! [`cf_composition::report_section::json_value`], `yaml` / `bin` and the +//! plot `report.json` ← [`cf_composition::CompositionReport::to_raw_report`]. //! -//! 1. the reference `StaticService.rawFilePhase` -//! walks `rootPath` with `filepath.WalkDir` — directories are recursed -//! (except `.git`), every regular file is offered to the requested -//! `RawFileAnalyzer`s. A file survives when it (a) matches the `--languages` -//! globs (none here → all match) and (b) is **not** excluded by -//! `pathpolicy.Exclude(path, nil, opts)` (default opts: drop vendor + -//! generated paths). -//! 2. Each surviving file is classified by reading its first -//! `contentHeaderSize` (8192) bytes and running the composition classifier -//! (`composition.Analyzer.AnalyzeFileContent`), then aggregated -//! (`composition.Aggregator`): `total_files` and a per-category breakdown. -//! 3. The aggregated report becomes one `renderer.JSONSection` -//! (`SectionToJSON`) wrapped in a `renderer.JSONReport` -//! (`SectionsToJSON`), and serialized via -//! `json.NewEncoder(w).SetIndent("", " ").Encode(report)` — two-space -//! indent, one trailing newline. -//! -//! `composition` is informational (`ScoreInfoOnly = -1`), so the overall score -//! and the section score are both `-1` and the labels are `"Info"` -//! (`BaseReportSection.ScoreLabel` / `ExecutiveSummary.OverallScoreLabel`). -//! -//! WalkDir visits directory entries in lexical (byte-sorted) order; we reproduce -//! that with a sorted recursive `read_dir`. The order is load-bearing for -//! analyzers whose section preserves discovery order, and harmless here. - -use std::fs; -use std::path::Path; +//! The analyzer math lives in `cf-composition`; this module owns only the +//! per-file classification call and the serializer routing. use super::StaticFilter; -use cf_composition::{Category, Classifier, ALL_CATEGORIES}; -use cf_gojson::{Encoder, GoMap, GoValue, MapOrigin}; - -/// Max bytes read per file in the raw-file pre-pass (the reference `contentHeaderSize`, -/// the reference static service). -const CONTENT_HEADER_SIZE: usize = 8192; - -/// Section title / status / score constants (composition report section). -const SECTION_TITLE: &str = "COMPOSITION"; -const STATUS_DEFAULT: &str = "File composition analysis completed"; -const STATUS_EMPTY: &str = "No files analyzed"; -const METRIC_TOTAL_FILES: &str = "Total Files"; -const METRIC_SOURCE: &str = "Source Files"; -const METRIC_SOURCE_PCT: &str = "Source %"; -const SCORE_INFO_ONLY: f64 = -1.0; -const SCORE_LABEL_INFO: &str = "Info"; -const SEVERITY_INFO: &str = "info"; -const SEVERITY_POOR: &str = "poor"; +use cf_composition::{Aggregator, Classifier, CompositionReport, FileRecord}; +use cf_gojson::{Encoder, GoValue}; -/// Aggregated composition counts. -#[derive(Default)] -struct Counts { - total_files: i64, - /// Per-category counts, indexed by [`ALL_CATEGORIES`] position. - by_category: std::collections::HashMap<&'static str, i64>, - /// Walked file paths relative to the analyzed root (walk order) — the - /// reference `PerFileRetainer` key set consumed by the `--per-file` - /// section enrichment. Composition is a raw-file analyzer, so EVERY - /// classified file (non-UAST files included) gets an entry. - files: Vec<String>, -} - -impl Counts { - fn get(&self, cat: Category) -> i64 { - self.by_category.get(cat.as_str()).copied().unwrap_or(0) - } -} - -/// Walks `root_path` with explicit path-policy options and returns the -/// aggregated composition [`Counts`], or `None` when the path cannot be read. -fn composition_counts_opts(root_path: &str, filter: &StaticFilter) -> Option<Counts> { - let root = Path::new(root_path); - if !root.exists() { - return None; - } - - let classifier = Classifier::new(); - let mut counts = Counts::default(); - - walk(root, root_path, &classifier, filter, &mut counts); - Some(counts) +/// What the walk collected: the aggregate plus, under `--per-file`, one +/// record per classified file in walk order. +struct Walked { + report: CompositionReport, + files: Option<Vec<FileRecord>>, } /// Builds the AGGREGATED RAW `analyze.Report` GoValue for `static/composition` -/// (`composition.Aggregator.GetResult`) — composition registers NO plot -/// section renderer in the reference implementation, so `--format plot` renders no page for it; this raw -/// value is what `writeReportJSON` serializes into `report.json`. +/// (`breakdown` / `percentages` / `total_files`) — composition renders no +/// plot page, so this is what `writeReportJSON` serializes into +/// `report.json`. #[must_use] pub fn composition_raw_report_value(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - let counts = composition_counts_opts(root_path, filter)?; - Some(build_raw_report(&counts)) -} - -/// Builds the `static/composition --format json` report bytes for `root_path`, -/// or `None` when the path cannot be read (the reference implementation would surface a walk error; the -/// caller then falls through to the blocked-dependency sentinel). -#[must_use] -pub fn composition_report(root_path: &str) -> Option<Vec<u8>> { - composition_report_flags(root_path, false) + let walked = walk_root(root_path, filter, false)?; + Some(GoValue::Map(walked.report.to_raw_report())) } -/// [`composition_report_flags`] with explicit path-policy options: the -/// reference static walk applies `pathpolicy.Exclude` with the RUN flags -/// (`--include-vendored` / `--include-generated`), so the aggregated counts — -/// and every encoding derived from them — must honor them too. +/// Builds the `static/composition --format json` report bytes for +/// `root_path` (two-space indent, trailing newline), or `None` when the path +/// cannot be read. `per_file` adds one `files` entry per classified file. #[must_use] pub fn composition_report_opts_flags( root_path: &str, filter: &StaticFilter, per_file: bool, ) -> Option<Vec<u8>> { - let report = composition_report_value_opts_flags(root_path, filter, per_file)?; - let bytes = Encoder::indented(" ") - .with_trailing_newline(true) - .encode_to_vec(&report); - Some(bytes) -} - -/// [`composition_report`] with the `--per-file` flag applied: `per_file` -/// enables the reference implementation's section enrichment -/// (`StaticService.enrichWithPerFileData` → `JSONReport.EnrichWithPerFileData` -/// over the `PerFileRetainer` snapshots — one `JSONFileEntry` per WALKED file, -/// non-UAST files included since composition is a raw-file analyzer). -#[must_use] -pub fn composition_report_flags(root_path: &str, per_file: bool) -> Option<Vec<u8>> { - let report = composition_report_value_flags(root_path, &StaticFilter::default(), per_file)?; + let report = composition_report_value_flags(root_path, filter, per_file)?; let bytes = Encoder::indented(" ") .with_trailing_newline(true) .encode_to_vec(&report); @@ -135,440 +59,183 @@ pub fn composition_report_flags(root_path: &str, per_file: bool) -> Option<Vec<u } /// Builds the `static/composition` `renderer.JSONReport` GoValue (single -/// info-only section), shared by the single-analyzer byte path and the -/// multi-analyzer static-JSON merge. `None` when the path cannot be walked. -#[must_use] -pub fn composition_report_value(root_path: &str) -> Option<GoValue> { - composition_report_value_flags(root_path, &StaticFilter::default(), false) -} - -/// [`composition_report_value`] with explicit path-policy options (the text / -/// compact renders consume the same aggregated report the walk produced under -/// the run's include flags). +/// info-only section) without per-file entries — the `text` / `compact` +/// render source. #[must_use] pub fn composition_report_value_opts(root_path: &str, filter: &StaticFilter) -> Option<GoValue> { - composition_report_value_opts_flags(root_path, filter, false) + composition_report_value_flags(root_path, filter, false) } -/// [`composition_report_value`] with the `--per-file` section enrichment. -/// -/// The reference per-file composition entry is DEGENERATE: the retained -/// per-file report is the raw-file classify result (`file_category`, no -/// `total_files`/`breakdown`), so `NewReportSection` reads every aggregate key -/// as zero — each entry carries the empty-report section values -/// (`No files analyzed`, all-zero metrics, info-only score) and only -/// `file_path` varies. Reproduced byte-for-byte. +/// Builds the `static/composition` `renderer.JSONReport` GoValue, shared by +/// the single-analyzer JSON path and the multi-analyzer static-JSON merge. +/// With `per_file` the section carries one entry per classified file: its +/// class (`Classified as vendor`), a one-file breakdown and its language. #[must_use] pub fn composition_report_value_flags( root_path: &str, filter: &StaticFilter, per_file: bool, ) -> Option<GoValue> { - composition_report_value_opts_flags(root_path, filter, per_file) + let walked = walk_root(root_path, filter, per_file)?; + Some(cf_composition::report_section::json_value( + walked.report, + walked.files.as_deref(), + )) } -/// [`composition_report_value_flags`] with explicit path-policy options. -#[must_use] -pub fn composition_report_value_opts_flags( - root_path: &str, - filter: &StaticFilter, - per_file: bool, -) -> Option<GoValue> { - let counts = composition_counts_opts(root_path, filter)?; - let file_entries: Option<Vec<GoValue>> = if per_file { - Some( - counts - .files - .iter() - .map(|rel| build_file_entry(rel)) - .collect(), - ) - } else { - None - }; - Some(build_json_report_files(&counts, file_entries)) -} - -/// Builds one degenerate composition `renderer.JSONFileEntry` (see -/// [`composition_report_value_flags`]). -fn build_file_entry(rel_path: &str) -> GoValue { - let metrics = vec![ - metric(METRIC_TOTAL_FILES, "0"), - metric(METRIC_SOURCE, "0"), - metric(METRIC_SOURCE_PCT, &format_percent(0.0)), - ]; - - let mut entry = GoMap::new(MapOrigin::Struct); - entry.push("file_path", GoValue::Str(rel_path.to_string())); - entry.push("score_label", GoValue::Str(SCORE_LABEL_INFO.to_string())); - entry.push("status", GoValue::Str(STATUS_EMPTY.to_string())); - entry.push("metrics", GoValue::Array(metrics)); - // Distribution/issues are empty for the zero-value report. - entry.push("issues", GoValue::Array(Vec::new())); - entry.push("score", GoValue::Float(SCORE_INFO_ONLY)); - GoValue::Map(entry) -} - -/// Builds the `static/composition --format bin` report bytes for `root_path`, -/// or `None` when the path cannot be read. -/// -/// The reference per-analyzer binary path (`StaticService.FormatPerAnalyzer` → -/// `composition.Analyzer.FormatReportBinary` → -/// `reportutil.EncodeBinaryEnvelope(report)`) wraps the analyzer's **raw** -/// aggregated `analyze.Report` — NOT the renderer JSON section structure used by -/// the JSON capture. That report is `composition.Aggregator.GetResult`: -/// `{breakdown: map[string]int, percentages: map[string]float64, total_files: -/// int}`, where `breakdown` lists every category (zero counts included) and -/// `percentages` is `count/total*100` (omitted entirely when `total_files == 0`). -/// The payload is the compact `encoding/json` marshal of this `map[string]any` -/// (top-level + nested map keys byte-sorted) inside the CFB1 envelope. -#[must_use] -pub fn composition_bin(root_path: &str) -> Option<Vec<u8>> { - composition_bin_opts(root_path, &StaticFilter::default()) -} - -/// [`composition_bin`] with explicit path-policy options. +/// Builds the `static/composition --format bin` report bytes for +/// `root_path` (the CFB1 envelope of the raw aggregated report), or `None` +/// when the path cannot be read. #[must_use] pub fn composition_bin_opts(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let counts = composition_counts_opts(root_path, filter)?; - let report = build_raw_report(&counts); - let bytes = cf_reportutil::encode_binary_envelope(&report) - .expect("composition payload within CFB1 limit"); - Some(bytes) -} - -/// Builds the `static/composition --format yaml` report bytes for `root_path`, -/// or `None` when the path cannot be read. -/// -/// The reference per-analyzer YAML path (`StaticService.FormatPerAnalyzer` → -/// `composition.Analyzer.FormatReportYAML` → `yaml.NewEncoder(w).Encode(report)`) -/// marshals the analyzer's **raw** aggregated `analyze.Report` — -/// `composition.Aggregator.GetResult` (`breakdown` / `percentages` / -/// `total_files`) — exactly the same report value the `bin` capture wraps, only -/// encoded as gopkg.in/yaml.v3 block YAML (4-space indent, byte-sorted map keys) -/// via `cf-goyaml::marshal` instead of the CFB1 JSON envelope. -#[must_use] -pub fn composition_yaml(root_path: &str) -> Option<Vec<u8>> { - composition_yaml_opts(root_path, &StaticFilter::default()) + let walked = walk_root(root_path, filter, false)?; + cf_reportutil::encode_binary_envelope(&GoValue::Map(walked.report.to_raw_report())).ok() } -/// [`composition_yaml`] with explicit path-policy options. +/// Builds the `static/composition --format yaml` report bytes for +/// `root_path` (the raw aggregated report as block YAML), or `None` when the +/// path cannot be read. #[must_use] pub fn composition_yaml_opts(root_path: &str, filter: &StaticFilter) -> Option<Vec<u8>> { - let counts = composition_counts_opts(root_path, filter)?; - let report = build_raw_report(&counts); - Some(cf_goyaml::marshal(&report)) -} - -/// Builds the raw aggregated `analyze.Report` GoValue -/// (`composition.Aggregator.GetResult`) as a map-origin value: top-level keys -/// `breakdown`, `percentages`, `total_files` and every-category nested maps, -/// all byte-sorted by the encoder. -fn build_raw_report(counts: &Counts) -> GoValue { - let total = counts.total_files; - - // breakdown: map[string]int over every category (zero counts included). - let mut breakdown = GoMap::new(MapOrigin::Map); - for cat in ALL_CATEGORIES { - breakdown.push(cat.as_str(), GoValue::Int(counts.get(cat))); - } - - // percentages: map[string]float64 over every category, count/total*100; - // omitted entirely when total_files == 0 (reference: leaves the map empty). - let mut percentages = GoMap::new(MapOrigin::Map); - if total > 0 { - for cat in ALL_CATEGORIES { - let value = (counts.get(cat) as f64) / (total as f64) * 100.0; - percentages.push(cat.as_str(), GoValue::Float(value)); - } - } - - let mut report = GoMap::new(MapOrigin::Map); - report.push("breakdown", GoValue::Map(breakdown)); - report.push("percentages", GoValue::Map(percentages)); - report.push("total_files", GoValue::Int(total)); - GoValue::Map(report) -} - -/// Recursively walks `dir` in lexical order, mirroring `filepath.WalkDir`: -/// directories are recursed (except `.git`), files are filtered through the -/// path policy and classified. -fn walk( - dir: &Path, - root_path: &str, - classifier: &Classifier, - filter: &StaticFilter, - counts: &mut Counts, -) { - // Go parity: filepath.WalkDir visits a FILE root as a single entry, so - // `codefang run <analyzer> path/to/file.c` analyzes that one file. - if dir.is_file() { - visit_file(dir, root_path, classifier, filter, counts); - return; - } - let Ok(read) = fs::read_dir(dir) else { - // Permission / not-exist errors are skipped. - return; - }; - - let mut entries: Vec<_> = read.filter_map(Result::ok).collect(); - // filepath.WalkDir sorts entries by name (os.ReadDir guarantees sorted). - entries.sort_by_key(std::fs::DirEntry::file_name); - - for entry in entries { - // SIGINT/SIGTERM: bail out of the walk promptly (durability, not - // output-affecting: the run exits 130 before any report is written). - if super::run_cancelled() { - return; - } - let path = entry.path(); - let file_type = match entry.file_type() { - Ok(ft) => ft, - Err(_) => continue, - }; - - if file_type.is_dir() { - if super::should_skip_walk_dir(&entry.path(), &entry.file_name()) { - continue; // filepath.SkipDir on .git - } - walk(&path, root_path, classifier, filter, counts); - continue; - } - - visit_file(&path, root_path, classifier, filter, counts); - } + let walked = walk_root(root_path, filter, false)?; + Some(cf_goyaml::marshal(&GoValue::Map( + walked.report.to_raw_report(), + ))) } -/// Classifies one file entry of the walk (the non-directory branch of the -/// `filepath.WalkDir` callback): path policy filter, header read, classify. -fn visit_file( - path: &Path, - root_path: &str, - classifier: &Classifier, - filter: &StaticFilter, - counts: &mut Counts, -) { - // Only regular files participate. `--languages` is empty → all match. - let path_str = path.to_string_lossy(); - - // pathpolicy.Exclude(path, nil, opts): the raw-file phase passes nil - // content, so only path-based vendor/generated heuristics apply. - if filter.skips(&path_str) { - return; - } - - let header = read_header(path); - let category = classifier.classify(&path_str, &header); - counts.total_files += 1; - *counts.by_category.entry(category.as_str()).or_insert(0) += 1; - counts.files.push(make_relative_path(&path_str, root_path)); -} - -/// The reference `filepath.Rel(rootPath, filePath)` (flat repos → path under -/// the root) — the `StampSourceFile` path stamped onto per-file reports. -fn make_relative_path(file_path: &str, root_path: &str) -> String { - if root_path.is_empty() { - return file_path.to_string(); - } - match Path::new(file_path).strip_prefix(Path::new(root_path)) { - Ok(rel) => rel.to_string_lossy().into_owned(), - Err(_) => file_path.to_string(), - } -} - -/// Reads up to [`CONTENT_HEADER_SIZE`] bytes from `path`; returns empty on error -/// (the reference `readFileHeader` returns nil, which classifies as empty content). -fn read_header(path: &Path) -> Vec<u8> { - use std::io::Read; - let Ok(mut f) = fs::File::open(path) else { - return Vec::new(); - }; - let mut buf = vec![0u8; CONTENT_HEADER_SIZE]; - match f.read(&mut buf) { - Ok(n) => { - buf.truncate(n); - buf - } - Err(_) => Vec::new(), - } -} - -/// Builds the `renderer.JSONReport` GoValue for a single composition section. -/// -/// Field order mirrors the reference structs exactly (struct-origin maps keep -/// declaration order): `overall_score_label`, `sections`, `overall_score`; and -/// per section `title`, `score_label`, `status`, `metrics`, `distribution` -/// (omitted when empty), `issues`, `score`. `file_entries` is the `--per-file` -/// enrichment (`None` without the flag). -fn build_json_report_files(counts: &Counts, file_entries: Option<Vec<GoValue>>) -> GoValue { - // Single info-only section → overall score is ScoreInfoOnly (-1), label Info. - let mut report = GoMap::new(MapOrigin::Struct); - report.push( - "overall_score_label", - GoValue::Str(SCORE_LABEL_INFO.to_string()), - ); - report.push( - "sections", - GoValue::Array(vec![build_section_files(counts, file_entries)]), - ); - report.push("overall_score", GoValue::Float(SCORE_INFO_ONLY)); - GoValue::Map(report) -} - -/// Builds the single composition `renderer.JSONSection` GoValue (reference: -/// `SectionToJSON`). Field order mirrors the reference struct: `title`, `score_label`, -/// `status`, `metrics`, `distribution` (omitted when empty), `issues`, `score`. -fn build_section_files(counts: &Counts, file_entries: Option<Vec<GoValue>>) -> GoValue { - let total = counts.total_files; - let source_count = counts.get(Category::Source); - - // ---- metrics ---- - let metrics = GoValue::Array(vec![ - metric(METRIC_TOTAL_FILES, &total.to_string()), - metric(METRIC_SOURCE, &source_count.to_string()), - metric(METRIC_SOURCE_PCT, &format_percent(pct(source_count, total))), - ]); - - // ---- distribution (ALL_CATEGORIES order, non-zero counts) ---- - let mut dist_items = Vec::new(); - if total != 0 { - for cat in ALL_CATEGORIES { - let count = counts.get(cat); - if count == 0 { - continue; - } - let mut d = GoMap::new(MapOrigin::Struct); - d.push("label", GoValue::Str(cat.as_str().to_string())); - d.push("percent", GoValue::Float(pct(count, total))); - d.push("count", GoValue::Int(count)); - dist_items.push(GoValue::Map(d)); - } - } - - // ---- issues (non-source categories; `[]` when none) ---- - let mut issue_items = Vec::new(); - if total != 0 { - for cat in ALL_CATEGORIES { - if cat == Category::Source { - continue; - } - let count = counts.get(cat); - if count == 0 { - continue; - } - let percent = (count as f64) / (total as f64) * 100.0; - let mut iss = GoMap::new(MapOrigin::Struct); - iss.push("name", GoValue::Str(cat.as_str().to_string())); - iss.push("location", GoValue::Str(String::new())); - iss.push( - "value", - GoValue::Str(format!("{count} files ({percent:.1}%)")), - ); - iss.push("severity", GoValue::Str(severity_for(cat).to_string())); - issue_items.push(GoValue::Map(iss)); +/// Walks `root_path` through the shared all-files walk and classifies every +/// admitted file against its root-relative path and header bytes, or `None` +/// when the path does not exist. +fn walk_root(root_path: &str, filter: &StaticFilter, per_file: bool) -> Option<Walked> { + let classifier = Classifier::new(); + let mut agg = Aggregator::new(); + let mut files = per_file.then(Vec::new); + for file in super::walk_all_files(root_path, filter).ok()? { + let class = classifier.classify_file(&file.rel_path, &file.bytes); + agg.add(class); + if let Some(records) = files.as_mut() { + records.push(FileRecord { + language: classifier.language(&file.rel_path, &file.bytes), + path: file.rel_path, + class, + }); } } - - let status = if total == 0 { - STATUS_EMPTY - } else { - STATUS_DEFAULT - }; - - // ---- section ---- - let mut section = GoMap::new(MapOrigin::Struct); - section.push("title", GoValue::Str(SECTION_TITLE.to_string())); - section.push("score_label", GoValue::Str(SCORE_LABEL_INFO.to_string())); - section.push("status", GoValue::Str(status.to_string())); - section.push("metrics", metrics); - // `distribution` carries `omitempty`: omit the key entirely when empty. - if !dist_items.is_empty() { - section.push("distribution", GoValue::Array(dist_items)); - } - section.push("issues", GoValue::Array(issue_items)); - // --per-file enrichment: `files` sits between `issues` and `score` - // (renderer.JSONSection field order; a nil `*[]JSONFileEntry` — omitempty — - // without the flag). - if let Some(entries) = file_entries { - section.push("files", GoValue::Array(entries)); - } - section.push("score", GoValue::Float(SCORE_INFO_ONLY)); - - GoValue::Map(section) -} - -fn metric(label: &str, value: &str) -> GoValue { - let mut m = GoMap::new(MapOrigin::Struct); - m.push("label", GoValue::Str(label.to_string())); - m.push("value", GoValue::Str(value.to_string())); - GoValue::Map(m) -} - -/// The reference `reportutil.Pct`: fraction in [0,1] (NOT a percentage). -fn pct(count: i64, total: i64) -> f64 { - if total == 0 { - return 0.0; - } - (count as f64) / (total as f64) -} - -/// The reference `reportutil.FormatPercent`: `%.1f%%` over `v*100`. -fn format_percent(v: f64) -> String { - format!("{:.1}%", v * 100.0) -} - -fn severity_for(cat: Category) -> &'static str { - match cat { - Category::Binary => SEVERITY_POOR, - _ => SEVERITY_INFO, - } + Some(Walked { + report: agg.report(), + files, + }) } #[cfg(test)] mod per_file_tests { use super::*; - /// Fixture: one UAST-supported file and one plain-text file — composition - /// is a raw-file analyzer, so BOTH get per-file entries. + /// Fixture: a source file, a vendored file, a generated file, a doc + /// under `docs/` and an unknown extension. fn fixture() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("tempdir"); - std::fs::write(dir.path().join("a.go"), "package main\n\nfunc main() {}\n").unwrap(); - std::fs::write(dir.path().join("notes.txt"), "just text\n").unwrap(); + let files: &[(&str, &[u8])] = &[ + ("a.go", b"package main\n\nfunc main() {}\n"), + ("vendor/x/y.go", b"package x\n"), + ( + "gen/t.pb.go", + b"// Code generated. DO NOT EDIT.\npackage gen\n", + ), + ("docs/guide.md", b"# Guide\n"), + ("data.xyz", b"hello\n"), + ]; + for (rel, content) in files { + let path = dir.path().join(rel); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, content).unwrap(); + } dir } + fn json(dir: &tempfile::TempDir, filter: &StaticFilter, per_file: bool) -> String { + let bytes = + composition_report_opts_flags(dir.path().to_str().unwrap(), filter, per_file).unwrap(); + String::from_utf8(bytes).unwrap() + } + + #[test] + fn vendor_and_generated_are_counted_under_the_default_policy() { + let dir = fixture(); + let json = json(&dir, &StaticFilter::default(), false); + assert!(json.contains("\"label\": \"vendor\""), "{json}"); + assert!(json.contains("\"label\": \"generated\""), "{json}"); + assert!(json.contains("\"label\": \"other\""), "{json}"); + // docs/guide.md is classified against its RELATIVE path. + assert!(json.contains("\"label\": \"documentation\""), "{json}"); + assert!( + json.contains("\"label\": \"Total Files\",\n \"value\": \"5\""), + "{json}" + ); + } + #[test] - fn per_file_flag_emits_files_entries() { + fn per_file_entries_carry_the_class() { let dir = fixture(); - let bytes = composition_report_flags(dir.path().to_str().unwrap(), true).unwrap(); - let json = String::from_utf8(bytes).unwrap(); - assert!(json.contains("\"files\""), "files key missing:\n{json}"); + let json = json(&dir, &StaticFilter::default(), true); + assert!(json.contains("\"file_path\": \"vendor/x/y.go\""), "{json}"); + assert!( + json.contains("\"status\": \"Classified as vendor\""), + "{json}" + ); assert!( - json.contains("\"file_path\": \"a.go\""), - "a.go entry missing:\n{json}" + json.contains("\"status\": \"Classified as generated\""), + "{json}" ); - // Raw-file analyzer: non-UAST files get entries too. assert!( - json.contains("\"file_path\": \"notes.txt\""), - "notes.txt entry missing:\n{json}" + json.contains("\"status\": \"Classified as other\""), + "{json}" ); - // The per-file report lacks the aggregate keys, so every entry carries - // the reference empty-report section values. assert!( - json.contains("\"status\": \"No files analyzed\""), - "degenerate per-file status missing:\n{json}" + json.contains("\"label\": \"Language\",\n \"value\": \"Go\""), + "{json}" ); } #[test] fn no_per_file_flag_omits_files() { let dir = fixture(); - let bytes = composition_report_flags(dir.path().to_str().unwrap(), false).unwrap(); - let json = String::from_utf8(bytes).unwrap(); + let json = json(&dir, &StaticFilter::default(), false); + assert!(!json.contains("\"files\""), "{json}"); + assert!(!json.contains("\"file_path\"")); + } + + #[test] + fn languages_globs_narrow_the_walk() { + let dir = fixture(); + let filter = StaticFilter { + policy: cf_pathpolicy::Options::default(), + language_globs: Some(vec!["*.go".to_string()]), + }; + let json = json(&dir, &filter, false); assert!( - !json.contains("\"files\""), - "files key must be omitted:\n{json}" + json.contains("\"label\": \"Total Files\",\n \"value\": \"3\""), + "{json}" ); - assert!(!json.contains("\"file_path\"")); + assert!(!json.contains("\"label\": \"other\"")); + } + + #[test] + fn file_root_is_classified_by_its_name() { + let dir = fixture(); + let file = dir.path().join("docs").join("guide.md"); + let yaml = composition_yaml_opts(file.to_str().unwrap(), &StaticFilter::default()).unwrap(); + let yaml = String::from_utf8(yaml).unwrap(); + assert!(yaml.contains("total_files: 1\n"), "{yaml}"); + assert!(yaml.contains(" documentation: 1\n"), "{yaml}"); + } + + #[test] + fn missing_root_yields_none() { + assert!(composition_bin_opts( + "/nonexistent/codefang-composition", + &StaticFilter::default() + ) + .is_none()); } } diff --git a/crates/cf-commands/src/handlers/uast_walk.rs b/crates/cf-commands/src/handlers/uast_walk.rs index 14556c62..1c8fd55b 100644 --- a/crates/cf-commands/src/handlers/uast_walk.rs +++ b/crates/cf-commands/src/handlers/uast_walk.rs @@ -1,163 +1,288 @@ -//! Shared UAST history walk for the five tree-sitter-heavy history analyzers +//! The UAST history pass behind the five tree-sitter-heavy history analyzers //! (`history/{imports,quality,sentiment,shotness,typos}`). //! -//! Each of those analyzers consumes the SAME per-commit inputs: the tree diff -//! against the first parent, the common gates (spill threshold, 256 KiB blob -//! cap, path policy), and the UAST parse of the changed blobs — the dominant -//! cost of a history run. When a single `run` selects two or more of them, the -//! reference pipeline streams ONE commit walk into every leaf; running five -//! independent walks here multiplied the wall time by the selection size. +//! Each of those analyzers consumes the SAME per-commit inputs: the filtered +//! tree diff against the first parent, the 256 KiB blob cap, and the UAST +//! parse of the changed blobs — the dominant cost of a history run. ONE +//! chunked pass of the history core ([`walk_chunks`]) feeds every analyzer +//! the run selected: per commit, the tree diff is computed once, then the +//! changes are visited CHANGE-MAJOR — each change's two sides are read and +//! parsed at most once ([`ChangeParses`]), every selected analyzer adds that +//! change's contribution to its per-commit product, and the parses are +//! dropped before the next change. A worker's parse live set is therefore +//! the two sides of one change, never a whole commit's (the per-thread +//! memory peak follows the largest live set, not the window or the commit +//! size). Each commit's [`UastCommitProduct`] is folded in window order into +//! the selected analyzers' finished per-commit records ([`UastFold`]) and +//! dropped, so no whole-window product vector exists. //! -//! This module restores the single-walk shape WITHOUT touching any analyzer -//! logic: one [`parallel_prepare`] pass computes the tree diff once per commit -//! and parses each needed `(file name, blob hash)` at most once per commit -//! through [`CommitParseCache`], then calls the SAME per-commit product -//! functions the single-analyzer walks call -//! ([`history::quality_commit_product`], [`history::sentiment_commit_product`], -//! [`history::imports_commit_product`], [`history::typos_commit_product`], -//! [`shotness_run::shotness_commit_product`]). The per-analyzer sequential -//! reduces (identity/tick stamping, shotness's cumulative state machine) run -//! unchanged over the shared walk's outputs, so byte identity follows from -//! shared code, not from re-verified duplication. -//! -//! The computed walks are memoized process-wide (one CLI invocation = one walk -//! parameter set), so every consumer — the combined render, the plot -//! orchestrators, the per-id pipeline loop, the merged ndjson/timeseries/text -//! documents, and the anomaly store enrichment that re-reads quality/sentiment -//! metrics — reuses the one walk instead of re-walking. Single-analyzer -//! invocations (fewer than two of the five selected) keep their existing -//! direct walks and never touch this store. - -use std::collections::HashMap; -use std::rc::Rc; -use std::sync::Mutex; +//! The finished results ([`UastResults`]) are memoized process-wide (one CLI +//! invocation = one walk parameter set), so every consumer — the combined +//! render, the plot orchestrators, the per-id pipeline loop, the merged +//! ndjson/timeseries/text documents — reads the one pass through a shared +//! [`Arc`] instead of re-walking or copying. A single-analyzer run is the +//! same pass with one analyzer selected, so a report never depends on which +//! other UAST analyzers are co-selected. + +use std::cell::OnceCell; +use std::sync::{Arc, Mutex}; use cf_gitlib::blob::CachedBlob; +use cf_gitlib::changes::{Change, ChangeEntry}; use cf_gitlib::Hash; -use cf_pathpolicy::{exclude, Options as PathPolicyOptions}; -use crate::handlers::history::{self, ImportsCommit, QualityCommit, SentimentCommit, TyposCommit}; -use crate::handlers::shotness_run::{self, ShotnessChangeProduct, ShotnessCommit, ShotnessReducer}; -use crate::handlers::{ - effective_first_parent, expand_combined_ids, floor_tick_secs, load_history_commit_hashes, - run_repo_path, +use crate::handlers::history::imports::{ImportsCommit, ImportsWalk}; +use crate::handlers::history::quality::QualityCommit; +use crate::handlers::history::sentiment::SentimentCommit; +use crate::handlers::history::typos::{TyposCommit, TyposContext}; +use cf_units::{KIB, MIB}; + +use crate::handlers::history::{ + self, gated_product, walk_chunks, ChunkPlan, CommitStamp, Gated, HistoryRun, StampSummary, + WalkCost, +}; +use crate::handlers::shotness_run::{ + self, CompiledDsl, ShotnessChangeProduct, ShotnessCommit, ShotnessDsl, ShotnessFold, }; +use crate::handlers::spill::{SpillError, Spilled}; +use crate::handlers::{expand_combined_ids, run_repo_path, HistoryFilter}; -/// `UASTChangesAnalyzer` spill threshold, identical across the five analyzers: -/// a commit with more than this many file changes streams zero UAST changes. -pub(crate) const SPILL_THRESHOLD: usize = 32; -/// UAST blob size cap (`UASTPipeline.parseBlob`). +/// UAST blob size cap: larger blobs are not parsed. pub(crate) const MAX_BLOB_SIZE: usize = 256 * 1024; -/// Outcome of one gated `(file name, blob hash)` UAST parse, memoized per -/// commit by [`CommitParseCache`]. The three cases preserve the distinctions -/// the analyzers branch on: a gate-skipped file contributes nothing anywhere, -/// while a gates-passed-but-unparsable file still counts as an analyzed file -/// for quality and feeds sentiment's shell-comment fallback (which needs the -/// blob bytes). +/// Seconds in one `--tick-size` hour. +const SECS_PER_HOUR: i64 = 3_600; + +/// Outcome of one gated UAST parse of a change side, memoized by +/// [`ChangeParses`]. The three cases preserve the distinctions the analyzers +/// branch on: a gate-skipped file contributes nothing anywhere, while a +/// gates-passed-but-unparsable file feeds sentiment's shell-comment fallback +/// (which reads the side's blob bytes). /// -/// Every instance lives behind an `Rc` in [`CommitParseCache`], so the size gap -/// between `Skipped` and the payload variants never reaches a stack frame or a -/// hot copy; boxing them would only add an indirection to the parse path. +/// An outcome is built in place in one of the two parse cells of a +/// [`ChangeParses`] and never moved, so the size gap between `Skipped` and +/// the inline root never reaches a hot copy (boxing the root would only add +/// an allocation per parse). #[allow(clippy::large_enum_variant)] pub(crate) enum ParseOutcome { - /// A pre-parse gate rejected the file: zero hash, path policy, unsupported - /// extension, blob read failure, the 256 KiB cap, or content-aware - /// generated detection. + /// A pre-parse gate rejected the file: zero hash, unsupported extension, + /// blob read failure, or the 256 KiB cap. Skipped, - /// Every gate passed but the tree-sitter parse failed; the blob is kept - /// for content fallbacks (sentiment's `.sh` comment extraction). - Failed(Rc<CachedBlob>), + /// Every gate passed but the tree-sitter parse failed. + Failed, /// The parsed UAST root. Parsed(cf_uast::Node), } -/// Per-commit blob + UAST parse memo shared by every analyzer product function -/// run over that commit. Keyed by `(file name, blob hash)` so each blob parses -/// at most once per commit no matter how many of the five analyzers (or change -/// sides) need it. The gate sequence inside [`CommitParseCache::parse`] is the -/// exact `UASTPipeline.parseBlob` order every individual walk applied: zero -/// hash, `pathpolicy.Exclude(name, nil)`, parser language support, blob read, -/// the 256 KiB cap, `pathpolicy.Exclude(name, content)`, then the parse. All -/// gates are pure functions of `(name, blob)`, so memoizing them is -/// observation-free. -pub(crate) struct CommitParseCache<'a> { +/// One side of a change. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Side { + /// The old side (`change.from`; empty for an insert). + Before, + /// The new side (`change.to`; empty for a delete). + After, +} + +/// The blob reads and gated UAST parses of ONE change's two sides, shared by +/// every analyzer the pass computes and dropped before the next change +/// ([`for_each_change`]), so a worker holds the parses of at most one change +/// at a time. Each side reads its blob and parses at most once, however many +/// analyzers ask; a Before side identical to the After side (same blob, and +/// for the parse the same name) shares the After side's read and parse. The +/// vendor / generated / language policy was already applied to the change +/// list by the shared [`HistoryFilter`], so the gates here are purely about +/// parseability: zero hash, parser language support, blob read, the 256 KiB +/// cap, then the parse. +pub(crate) struct ChangeParses<'a> { repo: &'a cf_gitlib::Repository, parser: &'a cf_uast::Parser, - opts: &'a PathPolicyOptions, - blobs: HashMap<Hash, Option<Rc<CachedBlob>>>, - parses: HashMap<(String, Hash), Rc<ParseOutcome>>, + change: &'a Change, + before_blob: OnceCell<Option<CachedBlob>>, + after_blob: OnceCell<Option<CachedBlob>>, + before_parse: OnceCell<ParseOutcome>, + after_parse: OnceCell<ParseOutcome>, } -impl<'a> CommitParseCache<'a> { - /// Builds an empty per-commit cache over this worker thread's repository - /// handle, UAST parser, and path-policy options. - pub(crate) fn new( +impl<'a> ChangeParses<'a> { + /// An empty scope over `change`, this worker thread's repository handle + /// and UAST parser. + fn new( repo: &'a cf_gitlib::Repository, parser: &'a cf_uast::Parser, - opts: &'a PathPolicyOptions, + change: &'a Change, ) -> Self { Self { repo, parser, - opts, - blobs: HashMap::new(), - parses: HashMap::new(), + change, + before_blob: OnceCell::new(), + after_blob: OnceCell::new(), + before_parse: OnceCell::new(), + after_parse: OnceCell::new(), } } - /// Reads (and memoizes) a blob; `None` mirrors the `CachedBlob::from_repo` - /// error every walk treated as "skip this change". - pub(crate) fn blob(&mut self, hash: Hash) -> Option<Rc<CachedBlob>> { - if let Some(cached) = self.blobs.get(&hash) { - return cached.clone(); - } - let blob = CachedBlob::from_repo(self.repo, hash).ok().map(Rc::new); - self.blobs.insert(hash, blob.clone()); - blob + /// The change this scope parses. + pub(crate) const fn change(&self) -> &'a Change { + self.change } - /// Runs the gated UAST parse for `(name, hash)`, memoized per commit. - pub(crate) fn parse(&mut self, name: &str, hash: Hash) -> Rc<ParseOutcome> { - let key = (name.to_string(), hash); - if let Some(cached) = self.parses.get(&key) { - return Rc::clone(cached); - } - let outcome = Rc::new(self.parse_uncached(name, hash)); - self.parses.insert(key, Rc::clone(&outcome)); - outcome + /// This worker thread's repository handle. + pub(crate) const fn repo(&self) -> &'a cf_gitlib::Repository { + self.repo } - /// The one gated parse (`UASTPipeline.parseBlob` gate order). - fn parse_uncached(&mut self, name: &str, hash: Hash) -> ParseOutcome { - if hash.is_zero() { - return ParseOutcome::Skipped; - } - if exclude(name, None, self.opts) { - return ParseOutcome::Skipped; + /// This worker thread's UAST parser. + pub(crate) const fn parser(&self) -> &'a cf_uast::Parser { + self.parser + } + + /// The change entry of `side`. + pub(crate) const fn entry(&self, side: Side) -> &'a ChangeEntry { + match side { + Side::Before => &self.change.from, + Side::After => &self.change.to, } - if !self.parser.is_supported(name) { + } + + /// Reads (once) the blob of `side`; `None` mirrors the + /// `CachedBlob::from_repo` error every analyzer treats as "skip this + /// change". + pub(crate) fn blob(&self, side: Side) -> Option<&CachedBlob> { + let same_blob = self.change.from.hash == self.change.to.hash; + let cell = match side { + Side::Before if !same_blob => &self.before_blob, + _ => &self.after_blob, + }; + cell.get_or_init(|| CachedBlob::from_repo(self.repo, self.entry(side).hash).ok()) + .as_ref() + } + + /// Runs (once) the gated UAST parse of `side`. + pub(crate) fn parse(&self, side: Side) -> &ParseOutcome { + let (from, to) = (&self.change.from, &self.change.to); + let same_file = from.hash == to.hash && from.name == to.name; + let cell = match side { + Side::Before if !same_file => &self.before_parse, + _ => &self.after_parse, + }; + cell.get_or_init(|| { + #[cfg(test)] + live_parses::born(); + self.gated_parse(side) + }) + } + + /// The one gated parse of `side`. + fn gated_parse(&self, side: Side) -> ParseOutcome { + let entry = self.entry(side); + if entry.hash.is_zero() || !self.parser.is_supported(&entry.name) { return ParseOutcome::Skipped; } - let Some(blob) = self.blob(hash) else { + let Some(blob) = self.blob(side) else { return ParseOutcome::Skipped; }; if blob.data.len() > MAX_BLOB_SIZE { return ParseOutcome::Skipped; } - if exclude(name, Some(&blob.data), self.opts) { - return ParseOutcome::Skipped; - } - match self.parser.parse(name, &blob.data) { + match self.parser.parse(&entry.name, &blob.data) { Ok(root) => ParseOutcome::Parsed(root), - Err(_) => ParseOutcome::Failed(blob), + Err(_) => ParseOutcome::Failed, + } + } +} + +/// Visits `changes` in change order, each through a FRESH [`ChangeParses`] +/// that is dropped before the next change: the parse live set of a worker is +/// the two sides of one change, never a whole commit's. +fn for_each_change( + repo: &cf_gitlib::Repository, + parser: &cf_uast::Parser, + changes: &[Change], + mut visit: impl FnMut(&ChangeParses<'_>), +) { + for change in changes { + visit(&ChangeParses::new(repo, parser, change)); + } +} + +/// Test-only accounting of the [`ParseOutcome`]s alive on the current +/// thread: the parse live set of a worker is what its memory peak follows. +#[cfg(test)] +mod live_parses { + use std::cell::Cell; + + thread_local! { + /// Outcomes alive now. + static LIVE: Cell<usize> = const { Cell::new(0) }; + /// The most outcomes alive at once since the last [`reset`]. + static PEAK: Cell<usize> = const { Cell::new(0) }; + /// Outcomes created since the last [`reset`]. + static BORN: Cell<usize> = const { Cell::new(0) }; + } + + /// Counts a new outcome. + pub(super) fn born() { + let live = LIVE.get() + 1; + LIVE.set(live); + PEAK.set(PEAK.get().max(live)); + BORN.set(BORN.get() + 1); + } + + /// Counts a dropped outcome. + pub(super) fn died() { + LIVE.set(LIVE.get() - 1); + } + + /// Restarts the peak and birth counts from the outcomes alive now. + pub(super) fn reset() { + PEAK.set(LIVE.get()); + BORN.set(0); + } + + /// `(alive now, peak, created)` since the last [`reset`]. + pub(super) fn snapshot() -> (usize, usize, usize) { + (LIVE.get(), PEAK.get(), BORN.get()) + } +} + +#[cfg(test)] +impl Drop for ParseOutcome { + fn drop(&mut self) { + live_parses::died(); + } +} + +/// One of the five UAST-heavy history analyzers. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum UastAnalyzer { + /// `history/quality`. + Quality, + /// `history/sentiment`. + Sentiment, + /// `history/imports`. + Imports, + /// `history/typos`. + Typos, + /// `history/shotness`. + Shotness, +} + +impl UastAnalyzer { + /// The analyzer id. + const fn id(self) -> &'static str { + match self { + Self::Quality => "history/quality", + Self::Sentiment => "history/sentiment", + Self::Imports => "history/imports", + Self::Typos => "history/typos", + Self::Shotness => "history/shotness", } } } -/// Which of the five UAST-heavy history analyzers the current `run` selected -/// (resolved over the registry exactly like the dispatch paths do). -#[derive(Clone, Copy, PartialEq, Eq, Default)] +/// Which of the five UAST-heavy history analyzers one pass feeds. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub(crate) struct UastSelection { quality: bool, sentiment: bool, @@ -186,6 +311,37 @@ impl UastSelection { } } + /// This selection plus `analyzer`. + const fn with(mut self, analyzer: UastAnalyzer) -> Self { + match analyzer { + UastAnalyzer::Quality => self.quality = true, + UastAnalyzer::Sentiment => self.sentiment = true, + UastAnalyzer::Imports => self.imports = true, + UastAnalyzer::Typos => self.typos = true, + UastAnalyzer::Shotness => self.shotness = true, + } + self + } + + /// The selected analyzers' ids in report order + /// ([`crate::handlers::HISTORY_PHASE_EMIT_ORDER`]), comma-joined: the + /// pass's name in the `-v` plan line. + fn walk_name(self) -> String { + let selected = [ + ("history/quality", self.quality), + ("history/sentiment", self.sentiment), + ("history/shotness", self.shotness), + ("history/imports", self.imports), + ("history/typos", self.typos), + ]; + selected + .iter() + .filter(|(_, on)| *on) + .map(|(id, _)| *id) + .collect::<Vec<_>>() + .join(",") + } + /// How many of the five are selected. fn count(self) -> usize { usize::from(self.quality) @@ -194,391 +350,681 @@ impl UastSelection { + usize::from(self.typos) + usize::from(self.shotness) } - - /// The shared walk only pays off (and only changes the execution shape) - /// when at least two of the five run in one process; single-analyzer - /// invocations keep their direct walks. - fn shared_applies(self) -> bool { - self.count() >= 2 - } } -/// Everything that determines the walk outputs; a key mismatch (impossible +/// Everything that determines the pass results; a key mismatch (impossible /// within one CLI invocation, possible across library tests) recomputes. #[derive(Clone, PartialEq, Eq)] struct WalkKey { path: String, head: bool, - limit: i64, + limit: usize, first_parent: bool, since: crate::handlers::SinceSpec, + tick_size_hours: i64, max_distance: i64, + min_comment_len: i64, max_changes: usize, + exact_signatures: bool, + people_dict: Option<String>, + anonymize: bool, selection: UastSelection, - /// The TreeDiff-level change filter (policy + `--languages`): part of the - /// key because it changes the walk outputs. - filter: crate::handlers::HistoryFilter, + /// The change filter (policy + `--languages`): part of the key because it + /// changes the walk outputs. + filter: HistoryFilter, + /// The shotness entity selection (`--shotness-dsl-struct`, + /// `--shotness-dsl-name`); the default when shotness is not selected. + shotness_dsl: ShotnessDsl, } impl WalkKey { - fn from_matches(sub: &clap::ArgMatches, selection: UastSelection) -> Self { - Self { + fn from_matches(sub: &clap::ArgMatches, selection: UastSelection) -> Option<Self> { + let opts = history::HistoryOptions::from_matches(sub).ok()?; + let shotness_dsl = if selection.shotness { + ShotnessDsl::from_matches(sub).ok()? + } else { + ShotnessDsl::default() + }; + Some(Self { path: run_repo_path(sub), - head: sub.get_flag("head"), - limit: sub.get_one::<i64>("limit").copied().unwrap_or(0), - first_parent: effective_first_parent(sub), - since: crate::handlers::history_since_spec(sub), - max_distance: history::typos_max_distance(sub), - max_changes: history::max_changes_per_commit_cap(sub), + head: opts.head, + limit: opts.limit, + first_parent: opts.first_parent, + since: opts.since, + tick_size_hours: opts.tick_size_hours, + max_distance: history::typos::typos_max_distance(sub), + min_comment_len: history::sentiment::sentiment_min_comment_len(sub), + max_changes: opts.max_changes, + exact_signatures: opts.exact_signatures, + people_dict: opts.people_dict, + anonymize: opts.anonymize, selection, - filter: crate::handlers::history_filter(sub).unwrap_or_default(), - } + filter: opts.filter, + shotness_dsl, + }) } } -/// The shared walk's per-analyzer outputs — exactly what each direct walk -/// returns. `None` for an unselected analyzer. -struct SharedWalks { - quality: Option<Vec<QualityCommit>>, - sentiment: Option<Vec<SentimentCommit>>, - imports: Option<Vec<ImportsCommit>>, - typos: Option<Vec<TyposCommit>>, - shotness: Option<Vec<ShotnessCommit>>, +/// The finished per-analyzer results of one pass — the per-commit records +/// every report builder reads, shared behind an [`Arc`]. `None` for an +/// unselected analyzer. +#[derive(Clone, Debug, Default, PartialEq)] +pub(crate) struct UastResults { + /// `history/quality` records, walk order. + pub quality: Option<Arc<Vec<QualityCommit>>>, + /// `history/sentiment` records, walk order. + pub sentiment: Option<Arc<Vec<SentimentCommit>>>, + /// `history/imports` records with the identity state naming the authors. + pub imports: Option<Arc<ImportsWalk>>, + /// `history/typos` records, walk order. + pub typos: Option<Arc<Vec<TyposCommit>>>, + /// `history/shotness` records, walk order. + pub shotness: Option<Arc<Vec<ShotnessCommit>>>, } /// Process-wide memo: one CLI invocation has one walk parameter set, so the -/// five walk functions (and every format/section consumer above them) share -/// ONE computation. The inner `Option<SharedWalks>` records a failed walk -/// (repo unreadable) so it is not retried. The lock is held across the -/// computation: concurrent analyzer tasks asking for the walk block until the -/// first one finishes it — the shared walk is one task, never N. -static STORE: Mutex<Option<(WalkKey, Option<SharedWalks>)>> = Mutex::new(None); - -/// Pre-computes the shared walk (when applicable) so the multi-analyzer -/// dispatch paths can fan out their per-analyzer tasks AFTER the one heavy -/// walk completes, instead of having several tasks block on the store lock. +/// five analyzers (and every format/section consumer above them) share ONE +/// computation. The inner `None` records a failed walk (repo unreadable) so +/// it is not retried. The lock is held across the computation: concurrent +/// analyzer tasks asking for the pass block until the first one finishes +/// it — the pass is one task, never N. +static STORE: Mutex<Option<(WalkKey, Option<HeldResults>)>> = Mutex::new(None); + +/// Pre-computes the pass when two or more UAST analyzers are selected, so the +/// multi-analyzer dispatch paths can fan out their per-analyzer tasks AFTER +/// the one heavy pass completes, instead of having several tasks block on the +/// store lock. A single UAST analyzer keeps running concurrently with the +/// other selected history analyzers. pub(crate) fn prewarm(sub: &clap::ArgMatches) { let selection = UastSelection::from_matches(sub); - if !selection.shared_applies() { - return; - } - let key = WalkKey::from_matches(sub, selection); - let mut guard = STORE.lock().expect("shared walk store poisoned"); - if matches!(guard.as_ref(), Some((have, _)) if *have == key) { - return; - } - let walks = compute_shared_walks(selection, &key); - *guard = Some((key, walks)); -} - -/// Looks up (computing if needed) one analyzer's shared-walk output. Returns -/// `None` when the shared walk does not apply to this invocation — the caller -/// then runs its existing direct walk. `Some(inner)` is the walk result -/// (`inner == None` ⇔ the walk failed, exactly like the direct walk's `None`). -fn shared_component<T>( - sub: &clap::ArgMatches, - selected: impl Fn(UastSelection) -> bool, - extract: impl Fn(&SharedWalks) -> Option<T>, -) -> Option<Option<T>> { - let selection = UastSelection::from_matches(sub); - if !selection.shared_applies() || !selected(selection) { - return None; + if selection.count() >= 2 { + memoized(sub, selection); } - let key = WalkKey::from_matches(sub, selection); +} + +/// The pass results for this invocation's selection plus `want`, computed +/// once and memoized; only `want`'s field is filled. `None` when the walk +/// fails (the repository cannot be opened or walked) or spilled results +/// cannot be read back (the cause goes to stderr). +pub(crate) fn uast_results(sub: &clap::ArgMatches, want: UastAnalyzer) -> Option<UastResults> { + let held = memoized(sub, UastSelection::from_matches(sub).with(want))?; + held.view(want) + .map_err(|e| eprintln!("Error: cannot read the spilled {}: {e}", want.id())) + .ok() +} + +/// Forgets the memoized pass results (and their spill files): a +/// long-running process (`codefang mcp`) must not hold one call's results +/// into the next. +pub(crate) fn forget_results() { + *STORE.lock().expect("shared walk store poisoned") = None; +} + +/// Looks up (computing if needed) the pass results for `selection`. The +/// lock is released before a consumer reads spilled results back. +fn memoized(sub: &clap::ArgMatches, selection: UastSelection) -> Option<HeldResults> { + let key = WalkKey::from_matches(sub, selection)?; let mut guard = STORE.lock().expect("shared walk store poisoned"); - if let Some((have, walks)) = guard.as_ref() { + if let Some((have, results)) = guard.as_ref() { if *have == key { - return Some(walks.as_ref().and_then(&extract)); + return results.clone(); } } - let walks = compute_shared_walks(selection, &key); - let result = walks.as_ref().and_then(&extract); - *guard = Some((key, walks)); - Some(result) + let results = compute_results(sub, &key).map(|results| hold(sub, results)); + *guard = Some((key, results.clone())); + results } -/// The `history/quality` view of the shared walk (see [`shared_component`]). -pub(crate) fn shared_quality_walk(sub: &clap::ArgMatches) -> Option<Option<Vec<QualityCommit>>> { - shared_component(sub, |sel| sel.quality, |w| w.quality.clone()) +/// How the memo keeps finished results: on disk while the run's other +/// history walks run after the pass (they would otherwise share memory with +/// results nobody reads until rendering), in memory otherwise. A failed +/// spill keeps them in memory. +fn hold(sub: &clap::ArgMatches, results: UastResults) -> HeldResults { + if !history::other_walks_follow_the_uast_pass(sub) { + return HeldResults::in_memory(results); + } + match HeldResults::spilled(&results) { + Ok(held) => { + if sub.try_get_one::<bool>("verbose").ok().flatten() == Some(&true) { + for (id, bytes) in held.spilled_bytes() { + eprintln!( + "progress: {id} results spilled to disk: {} MiB", + bytes.div_ceil(cf_units::MIB as u64) + ); + } + } + held + } + Err(e) => { + eprintln!("warning: keeping the UAST results in memory: {e}"); + HeldResults::in_memory(results) + } + } } -/// The `history/sentiment` view of the shared walk. -pub(crate) fn shared_sentiment_walk( - sub: &clap::ArgMatches, -) -> Option<Option<Vec<SentimentCommit>>> { - shared_component(sub, |sel| sel.sentiment, |w| w.sentiment.clone()) +/// One analyzer's finished results: shared in memory, or spilled to disk. +enum Held<T> { + Memory(Arc<T>), + Disk(Arc<Spilled<T>>), } -/// The `history/imports` view of the shared walk. -pub(crate) fn shared_imports_walk(sub: &clap::ArgMatches) -> Option<Option<Vec<ImportsCommit>>> { - shared_component(sub, |sel| sel.imports, |w| w.imports.clone()) +impl<T> Clone for Held<T> { + fn clone(&self) -> Self { + match self { + Self::Memory(value) => Self::Memory(Arc::clone(value)), + Self::Disk(spilled) => Self::Disk(Arc::clone(spilled)), + } + } } -/// The `history/typos` view of the shared walk. -pub(crate) fn shared_typos_walk(sub: &clap::ArgMatches) -> Option<Option<Vec<TyposCommit>>> { - shared_component(sub, |sel| sel.typos, |w| w.typos.clone()) +impl<T: serde::Serialize + serde::de::DeserializeOwned + Default> Held<T> { + /// `value` written to disk. + fn spill(value: &T) -> Result<Self, SpillError> { + Spilled::write(value).map(|spilled| Self::Disk(Arc::new(spilled))) + } + + /// The results: the shared copy, or a fresh copy read back from disk. + fn get(&self) -> Result<Arc<T>, SpillError> { + match self { + Self::Memory(value) => Ok(Arc::clone(value)), + Self::Disk(spilled) => spilled.read().map(Arc::new), + } + } + + /// Bytes on disk, `None` when held in memory. + fn disk_bytes(&self) -> Option<u64> { + match self { + Self::Memory(_) => None, + Self::Disk(spilled) => Some(spilled.bytes()), + } + } } -/// The `history/shotness` view of the shared walk. -pub(crate) fn shared_shotness_walk(sub: &clap::ArgMatches) -> Option<Option<Vec<ShotnessCommit>>> { - shared_component(sub, |sel| sel.shotness, |w| w.shotness.clone()) +/// The pass results as the memo keeps them, one [`Held`] per selected +/// analyzer. +#[derive(Clone)] +pub(crate) struct HeldResults { + quality: Option<Held<Vec<QualityCommit>>>, + sentiment: Option<Held<Vec<SentimentCommit>>>, + imports: Option<Held<ImportsWalk>>, + typos: Option<Held<Vec<TyposCommit>>>, + shotness: Option<Held<Vec<ShotnessCommit>>>, } -/// One commit's products across the selected analyzers — each field is the -/// exact value the analyzer's direct walk computes per commit. -#[derive(Default)] -struct SharedCommitProduct { - /// RAW (pre-filter) tree-diff change count, for the oversized-commit gate. - raw_change_count: usize, - quality: Option<cf_quality::TickQuality>, - sentiment: Option<Option<Vec<String>>>, - imports: Option<Vec<cf_imports::history::ImportEntry>>, - typos: Option<Vec<cf_typos::Typo>>, - shotness: Option<Vec<ShotnessChangeProduct>>, -} - -/// Runs the ONE shared walk: the same window selection as every direct walk, -/// one parallel pure-compute stage (tree diff once, every parse through the -/// per-commit cache), then the SAME sequential ordered reduces (identity/tick -/// stamping computed once — it is identical across the five walks — plus -/// shotness's cumulative state machine). `None` mirrors the direct walks' -/// failure (`.ok()?` on a fatal git error). -fn compute_shared_walks(sel: UastSelection, key: &WalkKey) -> Option<SharedWalks> { - use cf_alg_levenshtein::Context as LevenshteinContext; - use cf_analyzers_plumbing::identity_detector::IdentityDetector; - - let path = &key.path; - let repo = cf_gitlib::Repository::open(path).ok()?; - - // Window: identical to every direct walk (`--head` ⇒ exactly the HEAD - // commit; otherwise the `limit` commits oldest-first). - let hashes = if key.head { - vec![repo.head().ok()?] - } else { - load_history_commit_hashes(&repo, key.limit, key.first_parent, key.since.clone())? - }; +impl HeldResults { + /// `results`, kept in memory. + fn in_memory(results: UastResults) -> Self { + Self { + quality: results.quality.map(Held::Memory), + sentiment: results.sentiment.map(Held::Memory), + imports: results.imports.map(Held::Memory), + typos: results.typos.map(Held::Memory), + shotness: results.shotness.map(Held::Memory), + } + } - let max_distance = key.max_distance; - // Oversized-commit gate: commits whose RAW tree diff exceeds the cap are - // silently dropped from history BEFORE any analyzer (reference framework - // behaviour; flag `--max-changes-per-commit`, 0 = default 10000). - let max_changes = key.max_changes; - let opts = PathPolicyOptions::default(); - let opts_ref = &opts; - - // ---- parallel pure-compute stage ----------------------------------------- - // Per commit: ONE tree diff, ONE gated parse per (name, hash) via the - // cache, then each selected analyzer's per-commit product function — the - // same functions the direct walks call. - let workers = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); - let filter_ref = &key.filter; - let prepared = history::parallel_prepare(path, &hashes, workers, move |repo, hash| { - history::with_uast_parser(|parser| { - let commit = repo.lookup_commit(hash).ok()?; - let raw_changes = history::commit_tree_changes(repo, &commit)?; - let mut product = SharedCommitProduct { - raw_change_count: raw_changes.len(), - ..SharedCommitProduct::default() - }; - // Oversized commits are dropped before any analyzer — no per-file - // work; the reduce below removes them from the walk entirely. - if product.raw_change_count > max_changes { - return Some(product); - } - // TreeDiffAnalyzer.filterChanges (policy + --languages); the reduce - // and every per-commit product below consume only these changes. - let mut changes = raw_changes; - filter_ref.retain_changes(repo, &mut changes); - let mut cache = CommitParseCache::new(repo, parser, opts_ref); - if sel.quality { - product.quality = Some(history::quality_commit_product(&changes, &mut cache)); - } - if sel.sentiment { - product.sentiment = Some(history::sentiment_commit_product(&changes, &mut cache)); + /// `results`, every selected analyzer written to its own spill file. + fn spilled(results: &UastResults) -> Result<Self, SpillError> { + Ok(Self { + quality: results.quality.as_deref().map(Held::spill).transpose()?, + sentiment: results.sentiment.as_deref().map(Held::spill).transpose()?, + imports: results.imports.as_deref().map(Held::spill).transpose()?, + typos: results.typos.as_deref().map(Held::spill).transpose()?, + shotness: results.shotness.as_deref().map(Held::spill).transpose()?, + }) + } + + /// `want`'s results (read back from disk when spilled); the other + /// fields stay empty. + fn view(&self, want: UastAnalyzer) -> Result<UastResults, SpillError> { + let mut out = UastResults::default(); + match want { + UastAnalyzer::Quality => { + out.quality = self.quality.as_ref().map(Held::get).transpose()? } - if sel.imports { - product.imports = Some(history::imports_commit_product(&changes, &mut cache)); + UastAnalyzer::Sentiment => { + out.sentiment = self.sentiment.as_ref().map(Held::get).transpose()?; } - if sel.shotness { - product.shotness = - Some(shotness_run::shotness_commit_product(&changes, &mut cache)); + UastAnalyzer::Imports => { + out.imports = self.imports.as_ref().map(Held::get).transpose()? } - if sel.typos { - // The Levenshtein context is pure scratch buffers (results are - // state-independent), so a per-commit context is byte-identical - // to the direct walk's long-lived one. - let mut lctx = LevenshteinContext::new(); - product.typos = Some(history::typos_commit_product( - repo, - hash, - &changes, - max_distance, - &mut lctx, - &mut cache, - )); + UastAnalyzer::Typos => out.typos = self.typos.as_ref().map(Held::get).transpose()?, + UastAnalyzer::Shotness => { + out.shotness = self.shotness.as_ref().map(Held::get).transpose()?; } - Some(product) - }) - })?; - - // Oversized-commit skip: dropped from history before identity/tick - // stamping (the reference framework never shows the commit to any - // analyzer, core or leaf), exactly like each direct walk's gate. The - // ORIGINAL walk position of each surviving commit is preserved: the - // reference runner numbers consume positions before the drop suppresses a - // record, and the forked-leaf NDJSON drain order keys on that position. - let mut kept_pos: Vec<usize> = Vec::new(); - let (hashes, prepared): (Vec<_>, Vec<_>) = hashes + } + Ok(out) + } + + /// `(analyzer id, bytes)` of every spilled analyzer, in report order. + fn spilled_bytes(&self) -> Vec<(&'static str, u64)> { + [ + ( + "history/quality", + self.quality.as_ref().and_then(Held::disk_bytes), + ), + ( + "history/sentiment", + self.sentiment.as_ref().and_then(Held::disk_bytes), + ), + ( + "history/shotness", + self.shotness.as_ref().and_then(Held::disk_bytes), + ), + ( + "history/imports", + self.imports.as_ref().and_then(Held::disk_bytes), + ), + ( + "history/typos", + self.typos.as_ref().and_then(Held::disk_bytes), + ), + ] .into_iter() - .zip(prepared) - .enumerate() - .filter(|(_, (_, p))| p.raw_change_count <= max_changes) - .map(|(i, hp)| { - kept_pos.push(i); - hp - }) - .unzip(); - - // ---- sequential ordered identity/tick stamping ---------------------------- - // Identical across the five walks, so computed once here. - struct Stamp { - pos: usize, - hash_str: String, - tick: i64, - author_id: i64, - when: i64, - offset_min: i32, - num_parents: usize, - } - let mut identity = IdentityDetector::new(); - let mut tick0: Option<i64> = None; - let mut previous_tick: i64 = 0; - let mut stamps: Vec<Stamp> = Vec::with_capacity(hashes.len()); - for (j, hash) in hashes.iter().enumerate() { - let commit = repo.lookup_commit(*hash).ok()?; - let committer_when = commit.committer().when; - let when = committer_when.seconds(); - - let gsig = commit.author(); - let author_id = identity.consume_signature(&cf_analyzers_plumbing::Signature { - name: gsig.name.clone(), - email: gsig.email.clone(), - when_unix: gsig.when.seconds(), - }); + .filter_map(|(id, bytes)| Some((id, bytes?))) + .collect() + } +} + +/// The per-worker inputs of the pass's pure per-commit stage. +pub(crate) struct UastParams<'a> { + /// The shared change filter. + pub filter: &'a HistoryFilter, + /// `--max-changes-per-commit`. + pub max_changes: usize, + /// The analyzers to compute. + pub selection: UastSelection, + /// `--min-comment-len` (sentiment). + pub min_comment_len: i64, + /// `--typos-max-distance` (typos). + pub max_distance: i64, + /// The shotness DSL (read when shotness is selected). + pub shotness_dsl: &'a ShotnessDsl, +} + +/// One commit's products across the selected analyzers — each field is the +/// analyzer's per-commit product over the commit's FILTERED changes, in +/// change order; an unselected analyzer's field stays empty. +#[derive(Default)] +pub(crate) struct UastCommitProduct { + /// Per-file quality samples. + pub quality: cf_quality::TickQuality, + /// The comments the commit introduced or changed. + pub sentiment: Vec<String>, + /// The extracted `(lang, import)` entries. + pub imports: Vec<cf_imports::history::ImportEntry>, + /// The typo fixes. + pub typos: Vec<cf_typos::Typo>, + /// The per-change shotness inputs (entity lists and line diffs). + pub shotness: Vec<ShotnessChangeProduct>, +} + +/// The per-commit inputs of the selected analyzers besides the changes: +/// the compiled shotness DSL and the typos context (`None` when that +/// analyzer is not selected). +struct CommitScratch { + shotness: Option<CompiledDsl>, + typos: Option<TyposContext>, +} - let base = *tick0.get_or_insert_with(|| floor_tick_secs(when)); - let raw_tick = (when - base).div_euclid(86_400); - let tick = raw_tick.max(previous_tick); - previous_tick = tick; - - stamps.push(Stamp { - pos: kept_pos[j], - hash_str: hash.to_string(), - tick, - author_id, - when, - offset_min: committer_when.offset_minutes(), - num_parents: commit.num_parents(), +impl UastCommitProduct { + /// Computes the selected analyzers' products over `changes`, CHANGE-MAJOR: + /// every selected analyzer reads one change's shared parses before the + /// next change is parsed, and each product accumulates in change order. + fn compute( + repo: &cf_gitlib::Repository, + parser: &cf_uast::Parser, + changes: &[Change], + params: &UastParams<'_>, + scratch: &mut CommitScratch, + ) -> Self { + let mut product = Self::default(); + for_each_change(repo, parser, changes, |parses| { + product.add_change(parses, params, scratch); }); + product } - // Zips the shared stamps with each selected analyzer's per-commit product - // into that analyzer's commit-struct vec (the direct walks' exact output). - fn assemble<T>( - selected: bool, - stamps: &[Stamp], - prepared: &[SharedCommitProduct], - build: impl Fn(&Stamp, &SharedCommitProduct) -> T, - ) -> Option<Vec<T>> { - selected.then(|| { - stamps - .iter() - .zip(prepared) - .map(|(s, p)| build(s, p)) - .collect() - }) + /// Adds one change's contribution to every selected analyzer's product. + fn add_change( + &mut self, + parses: &ChangeParses<'_>, + params: &UastParams<'_>, + scratch: &mut CommitScratch, + ) { + let sel = params.selection; + if sel.quality { + history::quality::quality_change(parses, &mut self.quality); + } + if sel.sentiment { + history::sentiment::sentiment_change( + parses, + params.min_comment_len, + &mut self.sentiment, + ); + } + if sel.imports { + history::imports::imports_change(parses, &mut self.imports); + } + if let Some(dsl) = &scratch.shotness { + self.shotness + .extend(shotness_run::shotness_change(parses, dsl)); + } + if let Some(context) = &mut scratch.typos { + history::typos::typos_change(parses, context, &mut self.typos); + } } +} - let quality = assemble(sel.quality, &stamps, &prepared, |s, p| QualityCommit { - pos: s.pos, - hash: s.hash_str.clone(), - tq: p - .quality - .clone() - .expect("quality product for selected analyzer"), - tick: s.tick, - author_id: s.author_id, - when: s.when, - offset_min: s.offset_min, - }); - let sentiment = assemble(sel.sentiment, &stamps, &prepared, |s, p| SentimentCommit { - hash: s.hash_str.clone(), - comments: p - .sentiment - .clone() - .expect("sentiment product for selected analyzer"), - tick: s.tick, - author_id: s.author_id, - when: s.when, - offset_min: s.offset_min, - }); - let imports = assemble(sel.imports, &stamps, &prepared, |s, p| ImportsCommit { - hash: s.hash_str.clone(), - entries: p - .imports - .clone() - .expect("imports product for selected analyzer"), - author_id: s.author_id, - tick: s.tick, - when: s.when, - offset_min: s.offset_min, - }); - let typos = assemble(sel.typos, &stamps, &prepared, |s, p| TyposCommit { - typos: p +/// The pure per-commit stage of the pass, run on a worker thread: the raw +/// tree diff behind the `--max-changes-per-commit` gate, the shared change +/// filter, then the selected analyzers' products. `None` on a fatal git +/// error (or a shotness DSL the preflight would have rejected). +pub(crate) fn uast_prepare( + repo: &cf_gitlib::Repository, + hash: Hash, + params: &UastParams<'_>, +) -> Option<Gated<UastCommitProduct>> { + let sel = params.selection; + let mut scratch = CommitScratch { + shotness: sel + .shotness + .then(|| params.shotness_dsl.compile()) + .transpose() + .ok()?, + typos: sel .typos + .then(|| TyposContext::new(hash, params.max_distance)), + }; + history::with_uast_parser(|parser| { + gated_product( + repo, + hash, + params.filter, + params.max_changes, + |_, changes| UastCommitProduct::compute(repo, parser, changes, params, &mut scratch), + ) + }) +} + +/// The pass's reduce state: each kept commit's product is folded, in window +/// order, into the selected analyzers' records and dropped. +struct UastFold { + quality: Option<Vec<QualityCommit>>, + sentiment: Option<Vec<SentimentCommit>>, + imports: Option<Vec<ImportsCommit>>, + typos: Option<Vec<TyposCommit>>, + shotness: Option<ShotnessFold>, +} + +impl UastFold { + /// An empty fold for `sel`; `first_parent` is the walk's first-parent + /// mode (shotness skips merges otherwise). + fn new(sel: UastSelection, first_parent: bool) -> Self { + Self { + quality: sel.quality.then(Vec::new), + sentiment: sel.sentiment.then(Vec::new), + imports: sel.imports.then(Vec::new), + typos: sel.typos.then(Vec::new), + shotness: sel.shotness.then(|| ShotnessFold::new(first_parent)), + } + } + + /// Folds one kept commit, in window order. + fn consume(&mut self, stamp: &CommitStamp, product: UastCommitProduct) { + if let Some(commits) = &mut self.quality { + commits.push(QualityCommit::new(stamp, product.quality)); + } + if let Some(commits) = &mut self.sentiment { + commits.push(SentimentCommit::new(stamp, product.sentiment)); + } + if let Some(commits) = &mut self.imports { + commits.push(ImportsCommit::new(stamp, product.imports)); + } + if let Some(commits) = &mut self.typos { + commits.push(TyposCommit::new(stamp, product.typos)); + } + if let Some(fold) = &mut self.shotness { + fold.consume(stamp, &product.shotness); + } + } + + /// The finished results; imports keeps the identity state and the tick + /// size its report names authors and ticks with. + fn finish(self, summary: StampSummary, tick_size_secs: i64) -> UastResults { + UastResults { + quality: self.quality.map(Arc::new), + sentiment: self.sentiment.map(Arc::new), + imports: self.imports.map(|commits| { + Arc::new(ImportsWalk { + commits, + summary, + tick_size_secs, + }) + }), + typos: self.typos.map(Arc::new), + shotness: self.shotness.map(|fold| Arc::new(fold.finish())), + } + } +} + +/// The shared UAST pass's measured cost (any selection: every selected +/// analyzer reads the same per-change parses). Kubernetes, 7,973 commits, +/// `history/sentiment`: one worker peaks at 166 MB, 24 workers at 839 MB +/// (28 MiB per added worker); all five co-selected peak at 664 MB and +/// 1046 MB (16 MiB per added worker). Products hold at most 24 bytes more +/// per commit over the whole window than over 50-commit chunks. +const UAST_COST: WalkCost = WalkCost { + worker_bytes: 32 * MIB, + product_bytes: 4 * KIB, +}; + +/// Runs the ONE pass over the run's chunk plan: the gated per-commit +/// products are computed in parallel one chunk at a time and folded in +/// window order ([`walk_chunks`]). `None` when the walk fails. +fn compute_results(sub: &clap::ArgMatches, key: &WalkKey) -> Option<UastResults> { + let run = HistoryRun::open(sub)?; + // Two or more UAST analyzers are pre-computed ([`prewarm`]) before the + // other history walks start, so the pass has the budget to itself. + let name = key.selection.walk_name(); + let plan = if key.selection.count() >= 2 { + ChunkPlan::for_lone_walk(sub, &name, run.hashes.len(), UAST_COST) + } else { + ChunkPlan::for_walk(sub, &name, run.hashes.len(), UAST_COST) + } + .ok()?; + let params = UastParams { + filter: &run.filter, + max_changes: run.max_changes, + selection: key.selection, + min_comment_len: key.min_comment_len, + max_distance: key.max_distance, + shotness_dsl: &key.shotness_dsl, + }; + let mut fold = UastFold::new(key.selection, run.first_parent); + let summary = walk_chunks( + &run, + plan, + |repo, hash| uast_prepare(repo, hash, ¶ms), + |gated| gated.oversized(params.max_changes), + |stamp, gated| fold.consume(stamp, gated.product), + )?; + Some(fold.finish(summary, run.tick_size_hours() * SECS_PER_HOUR)) +} + +#[cfg(test)] +mod tests { + use cf_gitlib::testutil::{init_repo, CommitBuilder, TestRepo}; + + use super::*; + + /// The `-v` plan line names the pass by its selected analyzers, in + /// report order. + #[test] + fn the_pass_is_named_by_its_selection() { + let one = UastSelection::default().with(UastAnalyzer::Sentiment); + assert_eq!(one.walk_name(), "history/sentiment"); + let two = UastSelection::default() + .with(UastAnalyzer::Typos) + .with(UastAnalyzer::Quality); + assert_eq!(two.walk_name(), "history/quality,history/typos"); + } + + /// 2021-01-01T00:00:00Z. + const T0: i64 = 1_609_459_200; + /// One day. + const DAY: i64 = 86_400; + /// The Go files every fixture commit writes. + const FILES: [&str; 3] = ["a.go", "b.go", "c.go"]; + /// The two sides of one change: the most parses a worker may hold. + const SIDES_PER_CHANGE: usize = 2; + /// Every analyzer alone, then all five together (the shared pass). + const SELECTIONS: [&[UastAnalyzer]; 6] = [ + &[UastAnalyzer::Quality], + &[UastAnalyzer::Sentiment], + &[UastAnalyzer::Imports], + &[UastAnalyzer::Typos], + &[UastAnalyzer::Shotness], + &[ + UastAnalyzer::Quality, + UastAnalyzer::Sentiment, + UastAnalyzer::Imports, + UastAnalyzer::Typos, + UastAnalyzer::Shotness, + ], + ]; + + /// A Go file with an import, a comment and one function, whose name + /// carries a typo in the first version (`recieve`) that the second + /// version fixes; the second version also adds a comment. + fn go_file(file: &str, fixed: bool) -> Vec<u8> { + let name = if fixed { "receive" } else { "recieve" }; + let extra = if fixed { + "// wonderful helper that makes every caller happy\n" + } else { + "" + }; + format!( + "package p\n\nimport \"strings\"\n\n// the {file} helper keeps the happy path tidy\n{extra}func {name}(s string) string {{\n\treturn strings.TrimSpace(s)\n}}\n" + ) + .into_bytes() + } + + /// Two commits: the first adds [`FILES`], the second modifies all of + /// them (three Modify changes whose both sides parse). + fn multi_file_history() -> (TestRepo, Hash) { + let test = init_repo().expect("init"); + let mut first = CommitBuilder::new(&test, "add", T0).author("alice", "alice@x"); + let mut second = CommitBuilder::new(&test, "fix", T0 + DAY).author("bob", "bob@x"); + for file in FILES { + first = first.write(file, &go_file(file, false)); + second = second.write(file, &go_file(file, true)); + } + first.commit().expect("first commit"); + let modified = second.commit().expect("second commit"); + (test, modified) + } + + /// The parsed `run` matches over `test`. + fn run_matches(test: &TestRepo) -> clap::ArgMatches { + let path = test.path().to_str().expect("utf-8 path"); + crate::build_codefang_command() + .try_get_matches_from(["codefang", "run", "-a", "history/quality", path]) + .expect("run args parse") + .subcommand_matches("run") + .expect("run") .clone() - .expect("typos product for selected analyzer"), - tick: s.tick, - author_id: s.author_id, - when: s.when, - offset_min: s.offset_min, - }); - - // ---- shotness sequential cumulative reduce -------------------------------- - // The state machine consumes the precomputed parse/extract/diff products in - // walk order — the same `ShotnessReducer` the direct walk drives. - let shotness = sel.shotness.then(|| { - let mut reducer = ShotnessReducer::default(); - stamps + } + + impl UastResults { + /// These results with only `analyzer`'s field kept. + fn only(&self, analyzer: UastAnalyzer) -> Self { + let mut out = Self::default(); + match analyzer { + UastAnalyzer::Quality => out.quality.clone_from(&self.quality), + UastAnalyzer::Sentiment => out.sentiment.clone_from(&self.sentiment), + UastAnalyzer::Imports => out.imports.clone_from(&self.imports), + UastAnalyzer::Typos => out.typos.clone_from(&self.typos), + UastAnalyzer::Shotness => out.shotness.clone_from(&self.shotness), + } + out + } + } + + /// The five analyzers, in one selection. + const ALL: [UastAnalyzer; 5] = [ + UastAnalyzer::Quality, + UastAnalyzer::Sentiment, + UastAnalyzer::Imports, + UastAnalyzer::Typos, + UastAnalyzer::Shotness, + ]; + + /// Every analyzer's results, spilled to disk, read back equal to what + /// the pass computed; reading one analyzer loads only that analyzer. + #[test] + fn spilled_results_read_back_as_computed() { + let (test, _) = multi_file_history(); + let sub = run_matches(&test); + let selection = ALL .iter() - .zip(&prepared) - .zip(&hashes) - .map(|((s, p), hash)| { - let mut entry = ShotnessCommit { - hash: s.hash_str.clone(), - touched: std::collections::BTreeMap::new(), - tick: s.tick, - author_id: s.author_id, - when: s.when, - offset_min: s.offset_min, - }; - if reducer.should_consume(*hash, s.num_parents) { - let products = p - .shotness - .as_ref() - .expect("shotness product for selected analyzer"); - entry.touched = reducer.consume(products); - } - entry - }) - .collect() - }); - - Some(SharedWalks { - quality, - sentiment, - imports, - typos, - shotness, - }) + .fold(UastSelection::default(), |sel, &a| sel.with(a)); + let key = WalkKey::from_matches(&sub, selection).expect("key"); + let computed = compute_results(&sub, &key).expect("pass"); + let held = HeldResults::spilled(&computed).expect("spill"); + for analyzer in ALL { + assert_eq!( + held.view(analyzer).expect("read back"), + computed.only(analyzer), + "{analyzer:?}" + ); + } + let memory = HeldResults::in_memory(computed.clone()); + assert_eq!( + memory.view(UastAnalyzer::Typos).expect("view"), + computed.only(UastAnalyzer::Typos) + ); + } + + /// A worker computing a multi-file commit's products holds the parses of + /// at most ONE change (its two sides) at a time — for every analyzer + /// alone (a direct walk) and for all five together (the shared walk) — + /// and still parses each side of each change exactly once, however many + /// analyzers read it. + #[test] + fn a_worker_holds_the_parses_of_one_change_at_a_time() { + let (test, modified) = multi_file_history(); + let run = HistoryRun::open(&run_matches(&test)).expect("open run"); + let dsl = ShotnessDsl::default(); + let one_side = FILES.len(); + let both_sides = SIDES_PER_CHANGE * FILES.len(); + let expected_parses = [ + one_side, both_sides, one_side, both_sides, both_sides, both_sides, + ]; + let mut defects = Vec::new(); + for (analyzers, expected) in SELECTIONS.into_iter().zip(expected_parses) { + let selection = analyzers + .iter() + .fold(UastSelection::default(), |sel, &a| sel.with(a)); + let params = UastParams { + filter: &run.filter, + max_changes: run.max_changes, + selection, + min_comment_len: cf_sentiment::DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH, + max_distance: cf_typos::DEFAULT_MAXIMUM_ALLOWED_TYPO_DISTANCE as i64, + shotness_dsl: &dsl, + }; + live_parses::reset(); + let gated = uast_prepare(&run.repo, modified, ¶ms).expect("prepare"); + assert_eq!(gated.raw_change_count, FILES.len()); + let (live, peak, born) = live_parses::snapshot(); + if live != 0 || peak > SIDES_PER_CHANGE || born != expected { + defects.push(format!( + "{analyzers:?}: {peak} parses alive at once (at most {SIDES_PER_CHANGE}), \ + {born} parsed (expected {expected}), {live} outlived the commit" + )); + } + } + assert!(defects.is_empty(), "{}", defects.join("\n")); + } } diff --git a/crates/cf-commands/src/lib.rs b/crates/cf-commands/src/lib.rs index e284e488..bec8fd7d 100644 --- a/crates/cf-commands/src/lib.rs +++ b/crates/cf-commands/src/lib.rs @@ -1,50 +1,50 @@ //! `cf-commands` — command wiring and analyzer registration for the `codefang` //! binary. //! -//! This is the **tier-8 aggregation point** (specs/rust-rewrite/DESIGN.md §1 -//! tier 8, §4): it registers every analyzer, builds the `run` / `render` / -//! `version` command tree with all of `run`'s literal flags and the dynamic -//! per-analyzer flags, drives the analysis pipeline, and routes report +//! This is the aggregation point: it registers every analyzer, builds the +//! `run` / `version` / `completion` command tree with all of `run`'s literal +//! flags and the dynamic per-analyzer flags, layers the configuration file +//! under the flags, drives the analysis pipeline, and routes report //! serialization through the shared report-format crates (cf-gojson / //! cf-goyaml) — never raw `serde`. //! -//! # CLI parity (DESIGN §4) -//! -//! The command tree is built with clap's **builder API** (not derive) so command -//! and flag declaration order, help strings, defaults, and error wording can be -//! matched verbatim to the reference binary's CLI (a cobra surface). The three -//! wired subcommands are [`build_run_command`], [`build_render_command`], and -//! [`build_version_command`]. The reference `mcp` command is not shipped by -//! default; it is mirrored behind the non-default `mcp` Cargo feature. +//! The command tree is built with clap's **builder API** (not derive) so +//! command and flag declaration order, help strings and defaults are explicit. //! //! The major pieces: //! //! - [`formats`] — format constants and validation (`normalize_format`, -//! `validate_format`, `validate_universal_format`, the `resolve_formats` / -//! `resolve_input_format` conversion logic and the `--ndjson` + `--format -//! timeseries` → `timeseries+ndjson` composition). The error string is the -//! exact CLI-contract `unsupported format: <fmt>`. -//! - [`version`] — the `version` subcommand output (`codefang <v> (commit: <c>, -//! built: <d>)\n`), via [`cf_version`]. -//! - [`flags`] — the full `run`/`render` clap command tree: every literal `run` -//! flag (names, shorts, defaults, verbatim help), the tri-state -//! `--checkpoint`/`--resume`, the deprecated `--skip-blacklist` / -//! `--blacklisted-prefixes` (exact messages), and the **dynamic per-analyzer -//! flag** registration driven by [`cf_pipeline::ConfigurationOption`]. -//! - [`handlers`] / [`pipeline`] — the run/render execution path: analyzer -//! dispatch, the static folder walk and shared history revwalk, and -//! per-format report serialization. +//! `validate_format`, `validate_universal_format`, `resolve_formats` and the +//! `--ndjson` + `--format timeseries` → `timeseries+ndjson` composition). +//! - [`version`] — the `version` subcommand output. +//! - [`flags`] — the `run` / `completion` clap command trees: every literal +//! `run` flag, the dynamic per-analyzer flags driven by +//! [`cf_pipeline::ConfigurationOption`], and the configuration-key → flag +//! map ([`flags::CONFIG_KEY_FLAGS`]). +//! - [`handlers`] / [`pipeline`] — the run execution path: the shared run +//! preflight, analyzer dispatch, the static folder walk and shared history +//! revwalk, and per-format report serialization. //! -//! Compatibility: every machine-report surface this crate emits is pinned -//! byte-for-byte against the reference binary by the differential gate in -//! `tests/compat`. +//! # Configuration //! -//! # Example: resolve the run-command output format +//! `codefang run` loads `--config <file>` (else `./.codefang.yaml`, else +//! `$HOME/.codefang.yaml`) plus the `CODEFANG_*` environment through +//! `cf-config`, installs every configured key as the default of its flag and +//! parses the command line again: an explicit flag > the environment > the +//! file > the compiled default. Unsupported keys print one +//! `warning: ignoring unsupported config keys: …` line. +//! +//! # Errors //! -//! The format gate drives the `run` command: a user-supplied `--format` string -//! is normalized, validated, and split into per-phase formats. The `bin` alias -//! normalizes to `binary`, and a static-only run resolves an empty history -//! format: +//! Every user error prints `Error: <message>` to stderr and exits 1, with +//! the message naming the cause ([`pipeline::HandlerError`]): `path not +//! found: <p>`, `cannot open repository: <p>`, `unsupported format: <f>`, +//! `format <f> is not supported for <analyzer>`, `invalid --limit: …`, +//! `cannot resolve --since "<v>": …`, `unknown analyzer id: <id>`, +//! `cannot read --config <p>: …`, `invalid config <p>: …`, +//! `cannot write --output <p>: …`. +//! +//! # Example: resolve the run-command output format //! //! ``` //! use cf_commands::{normalize_format, resolve_formats}; @@ -62,48 +62,46 @@ pub mod flags; pub mod formats; pub mod handlers; -#[cfg(feature = "runtime")] -mod observability; +pub mod mcp; pub mod pipeline; pub mod registry; pub mod version; -pub use flags::{ - build_completion_command, build_render_command, build_run_command, deprecated_flag_message, -}; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; + +pub use flags::{build_completion_command, build_run_command}; // `FORMAT_JSON` is part of the public format-constant API (used by the binary -// and golden harness); rustc emits a spurious unused-import warning for the -// re-export because it is also referenced internally via `crate::formats`. +// and tests); rustc emits a spurious unused-import warning for the re-export +// because it is also referenced internally via `crate::formats`. #[allow(unused_imports)] pub use formats::{ - apply_ndjson_modifier, normalize_format, resolve_formats, resolve_input_format, - validate_format, validate_universal_format, FormatError, FORMAT_BINARY, FORMAT_BIN_ALIAS, - FORMAT_COMPACT, FORMAT_JSON, FORMAT_NDJSON, FORMAT_PLOT, FORMAT_TEXT, FORMAT_TIMESERIES, - FORMAT_TIMESERIES_NDJSON, FORMAT_YAML, INPUT_FORMAT_AUTO, INPUT_FORMAT_BINARY, - INPUT_FORMAT_JSON, + apply_ndjson_modifier, normalize_format, resolve_formats, validate_format, + validate_universal_format, FormatError, FORMAT_BINARY, FORMAT_BIN_ALIAS, FORMAT_COMPACT, + FORMAT_JSON, FORMAT_NDJSON, FORMAT_PLOT, FORMAT_TEXT, FORMAT_TIMESERIES, + FORMAT_TIMESERIES_NDJSON, FORMAT_YAML, }; +pub use pipeline::HandlerError; pub use registry::{ConfigOptionProvider, RegistrationError}; pub use version::{build_version_command, version_output}; /// Crate name, used by smoke tests to confirm the module links. pub const CRATE_NAME: &str = "cf-commands"; -/// Build the top-level `codefang` [`clap::Command`] with the `run`, `render`, -/// and `version` subcommands wired in declaration order, matching the -/// reference command tree. +/// The `--verbose` flag id. +const FLAG_VERBOSE: &str = "verbose"; +/// The `--quiet` flag id (`--silent` is its alias). +const FLAG_QUIET: &str = "quiet"; + +/// Build the top-level `codefang` [`clap::Command`] with the `run`, +/// `version` and `completion` subcommands wired in declaration order. /// -/// The root carries the persistent flags `--verbose`/`-v`, `--quiet`/`-q`, and -/// `--profile` (all default `false`), matching cobra's persistent flags. The -/// binary-level error-handling asymmetry (codefang sets `SilenceErrors` + -/// `SilenceUsage`) is configured by the `codefang` binary crate, not here. +/// The root carries the global flags `--verbose`/`-v`, `--quiet`/`-q` +/// (alias `--silent`) and `--profile` (all default `false`). #[must_use] pub fn build_codefang_command() -> clap::Command { clap::Command::new("codefang") .about("Codefang Code Analysis - Unified code analysis tool") - // Trailer DIVERGES from the reference top-level help (cobra prints no - // examples block) — an additive usability hint pointing at the most - // common entrypoint. The cli-surface gate compares the structured - // command/flag surface, not help prose, so this adds no surface row. .after_help( "Getting started:\n \ codefang run . # analyze the current repo with every analyzer\n \ @@ -113,76 +111,77 @@ pub fn build_codefang_command() -> clap::Command { Run 'codefang <command> --help' for details on any command.", ) .arg( - clap::Arg::new("verbose") - .long("verbose") + clap::Arg::new(FLAG_VERBOSE) + .long(FLAG_VERBOSE) .short('v') .global(true) .action(clap::ArgAction::SetTrue) - .help("Enable verbose output"), + .help( + "Print run details on stderr: the configuration file used and the \ + resolved analyzer ids", + ), ) .arg( - clap::Arg::new("quiet") - .long("quiet") + clap::Arg::new(FLAG_QUIET) + .long(FLAG_QUIET) .short('q') + .visible_alias("silent") .global(true) .action(clap::ArgAction::SetTrue) - .help("Suppress non-error output"), + .help("Suppress the progress lines on stderr (warnings and errors still print)"), ) .arg( clap::Arg::new("profile") .long("profile") .global(true) .action(clap::ArgAction::SetTrue) - .help("Enable profiling server and memory watchdog"), + .help( + "Log RSS and thread count to stderr at start and every 2 s; snapshot \ + /proc/self/maps into the temp directory at start and on RSS spikes", + ), ) .subcommand(build_run_command()) - .subcommand(build_render_command()) .subcommand(build_version_command()) .subcommand(build_completion_command()) + .subcommand(mcp::build_mcp_command()) } -/// Sentinel error message for run/render dispatch when no registered handler can -/// produce a report for the requested `(analyzer, format)` selection. Mirrors -/// the codefang error path (`Error: <msg>\n`, exit 1, no usage). -const DISPATCH_BLOCKED_MSG: &str = - "command dispatch is blocked on cf-commands (tier 8); see DESIGN.md \u{00A7}4.1"; +/// Parses `args` with `cmd`. clap prints help / usage / errors itself; a +/// usage error (bad flag / unknown command / missing arg) is exit code 1, a +/// displayed help or version is 0. +fn parse(cmd: clap::Command, args: &[OsString]) -> Result<clap::ArgMatches, i32> { + cmd.try_get_matches_from(args).map_err(|e| { + e.print().ok(); + i32::from(e.use_stderr()) + }) +} /// The single `codefang` entry point: build the command tree, parse `args` /// (which MUST start with the program name, like `std::env::args`), and dispatch -/// `run` / `render` / `version` through the general pipeline + registry. -/// Returns the process exit code (the reference `RunCommand.run` → cobra `Execute` exit). -/// -/// This is the thin shell the `codefang` binary calls; all dispatch flows -/// through [`pipeline::run_pipeline`] over [`handlers::default_registry`] — there -/// is no per-`(analyzer, format)` branching here. +/// `run` / `version` / `completion` through the general pipeline + registry. +/// Returns the process exit code. #[must_use] pub fn run<I, T>(args: I) -> i32 where I: IntoIterator<Item = T>, - T: Into<std::ffi::OsString> + Clone, + T: Into<OsString> + Clone, { - let matches = match build_codefang_command().try_get_matches_from(args) { + let args: Vec<OsString> = args.into_iter().map(Into::into).collect(); + let matches = match parse(build_codefang_command(), &args) { Ok(m) => m, - Err(e) => { - // clap prints help/usage/version itself. cobra exits 1 on a usage - // error (bad flag / unknown command / missing arg) and 0 when it - // merely displayed help/version; mirror that exit-code contract - // (the cli-surface error-path probes assert rc==1, not clap's 2). - e.print().ok(); - return i32::from(e.use_stderr()); - } + Err(code) => return code, }; install_signal_handlers(); match matches.subcommand() { Some(("version", _)) => { - print!("{}", cf_version::codefang_version_line()); + print!("{}", version_output()); 0 } - Some(("run", sub)) => run_subcommand(sub), - Some(("render", sub)) => render_subcommand(sub), + Some(("run", sub)) => run_command(&args, sub), Some(("completion", sub)) => completion_subcommand(sub), + Some(("mcp", _)) => mcp::mcp_subcommand(), _ => { build_codefang_command().print_help().ok(); println!(); @@ -191,19 +190,77 @@ where } } -/// Dispatches `codefang completion <shell>`; generates the shell-completion -/// script to stdout, the Rust analogue of the bytes cobra's auto-registered -/// completion command emits (`bash`/`fish`/`powershell`/`zsh`). The script bytes -/// are clap-vs-cobra cosmetic (Layer-D informational), but a real generator (not -/// a help stub) is required so the command behaves like the reference implementation's. With no shell -/// subcommand cobra prints the completion command's help (rc 0). +/// `codefang run`: `--list-analyzers` answers without a configuration; +/// otherwise the configuration is loaded, installed as the flag defaults, the +/// command line parsed again, and the run dispatched. +fn run_command(args: &[OsString], first: &clap::ArgMatches) -> i32 { + if first.get_flag("list-analyzers") { + use std::io::Write as _; + let mut out = std::io::stdout(); + for id in handlers::default_registry().ids() { + let _ = writeln!(out, "{id}"); + } + return 0; + } + if let Err(message) = check_verbosity(first) { + return fail(&message); + } + let loaded = match load_config(first) { + Ok(loaded) => loaded, + Err(message) => return fail(&message), + }; + if !loaded.unsupported_keys.is_empty() { + eprintln!( + "warning: ignoring unsupported config keys: {}", + loaded.unsupported_keys.join(", ") + ); + } + let cmd = flags::with_config_defaults(build_codefang_command(), &loaded.config.settings()); + let matches = match parse(cmd, args) { + Ok(m) => m, + Err(code) => return code, + }; + match matches.subcommand_matches("run") { + Some(sub) => run_subcommand(sub, loaded.path.as_deref()), + None => fail(&"the configured command line no longer selects `run`"), + } +} + +/// `--verbose` asks for more stderr, `--quiet` for less: both at once is a +/// usage error. (clap's `conflicts_with` misses a pair split across the root +/// and the subcommand, `codefang -v run -q`.) +fn check_verbosity(sub: &clap::ArgMatches) -> Result<(), &'static str> { + if sub.get_flag(FLAG_VERBOSE) && sub.get_flag(FLAG_QUIET) { + Err("--verbose and --quiet cannot be used together") + } else { + Ok(()) + } +} + +/// Loads the configuration `--config` names (or discovers), with the +/// process environment as the `CODEFANG_*` layer. +fn load_config(sub: &clap::ArgMatches) -> Result<cf_config::Loaded, String> { + let explicit = sub + .get_one::<String>("config") + .filter(|p| !p.is_empty()) + .map(PathBuf::from); + cf_config::load( + explicit.as_deref(), + &cf_config::default_search_dirs(), + |name| std::env::var(name).ok(), + ) + .map_err(|e| match (&e, &explicit) { + (cf_config::LoadError::Read { path, cause }, Some(_)) => { + format!("cannot read --config {}: {cause}", path.display()) + } + _ => e.to_string(), + }) +} + /// Installs SIGINT/SIGTERM handlers for cooperative cancellation: the first /// signal sets the shared cancel flag (the static folder walks poll it and /// bail out), a second signal falls through to the default disposition and -/// kills the process — the standard double-Ctrl-C escape hatch. Without this -/// the walks are uninterruptible computation and a signal simply dies -/// mid-run with the default handler, which is indistinguishable from a hang -/// when stdout is redirected. +/// kills the process — the standard double-Ctrl-C escape hatch. fn install_signal_handlers() { for sig in [signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM] { let flag = std::sync::Arc::clone(handlers::cancel_flag()); @@ -218,34 +275,82 @@ fn install_signal_handlers() { /// the shell convention for signal death. const EXIT_INTERRUPTED: i32 = 130; -/// Writes report bytes to stdout. A closed downstream pipe -/// (`codefang run ... | head`) is not a bug: the Go reference dies silently -/// of SIGPIPE (exit 128+13); Rust ignores SIGPIPE and surfaces `EPIPE` -/// instead, so mirror the reference exit rather than panicking mid-report. -fn write_stdout_or_exit(bytes: &[u8]) { - use std::io::Write as _; - // An interrupted run must not emit a truncated report that looks - // complete: surface the interruption and exit with the signal convention. - if handlers::run_cancelled() { - eprintln!("Error: interrupted"); - std::process::exit(EXIT_INTERRUPTED); - } - if let Err(e) = std::io::stdout().write_all(bytes) { - if e.kind() == std::io::ErrorKind::BrokenPipe { - std::process::exit(141); +/// Exit code for a closed downstream pipe (128 + SIGPIPE). +const EXIT_BROKEN_PIPE: i32 = 141; + +/// Where a run writes its report: stdout, or the `-o` file (every format but +/// `plot`, whose `-o` is the page directory). +enum ReportSink { + /// The process stdout. + Stdout, + /// The `-o` file, created (truncated) on the first write. + File { + /// The path as given. + path: String, + /// The open file once created. + file: Option<std::fs::File>, + }, +} + +impl ReportSink { + /// The sink `-o` selects for a run whose format is (`plot`) or is not + /// the plot directory renderer. + fn new(sub: &clap::ArgMatches, plot: bool) -> Self { + match sub.get_one::<String>("output").filter(|o| !o.is_empty()) { + Some(path) if !plot => Self::File { + path: path.clone(), + file: None, + }, + _ => Self::Stdout, + } + } + + /// Writes `bytes` and returns the exit code (0 on success). An + /// interrupted run never emits a truncated report that looks complete + /// (exit 130); a closed downstream pipe (`codefang run … | head`) exits + /// with the SIGPIPE convention (141). Writing nothing still creates the + /// `-o` file, so a successful run always leaves its report behind. + fn emit(&mut self, bytes: &[u8]) -> i32 { + use std::io::Write as _; + if handlers::run_cancelled() { + eprintln!("Error: interrupted"); + std::process::exit(EXIT_INTERRUPTED); + } + match self { + Self::Stdout => match std::io::stdout().write_all(bytes) { + Ok(()) => 0, + Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { + std::process::exit(EXIT_BROKEN_PIPE) + } + Err(e) => fail(&format!("cannot write stdout: {e}")), + }, + Self::File { path, file } => { + if file.is_none() { + match std::fs::File::create(path.as_str()) { + Ok(f) => *file = Some(f), + Err(e) => return fail(&format!("cannot write --output {path}: {e}")), + } + } + match file.as_mut().map(|f| f.write_all(bytes)) { + Some(Err(e)) => fail(&format!("cannot write --output {path}: {e}")), + _ => 0, + } + } } - panic!("write stdout: {e}"); } } +/// Dispatches `codefang completion <shell>`; generates the shell-completion +/// script to stdout (`--no-descriptions` drops every help text from it). +/// With no shell subcommand it prints the completion command's help (rc 0). fn completion_subcommand(sub: &clap::ArgMatches) -> i32 { use clap_complete::Shell; - let shell = match sub.subcommand_name() { - Some("bash") => Shell::Bash, - Some("fish") => Shell::Fish, - Some("powershell") => Shell::PowerShell, - Some("zsh") => Shell::Zsh, + let (shell, shell_matches) = match sub.subcommand() { + Some(("bash", m)) => (Shell::Bash, m), + Some(("fish", m)) => (Shell::Fish, m), + Some(("powershell", m)) => (Shell::PowerShell, m), + Some(("zsh", m)) => (Shell::Zsh, m), _ => { build_completion_command().print_help().ok(); println!(); @@ -254,38 +359,49 @@ fn completion_subcommand(sub: &clap::ArgMatches) -> i32 { }; let mut cmd = build_codefang_command(); + if shell_matches.get_flag("no-descriptions") { + cmd = without_descriptions(cmd); + } clap_complete::generate(shell, &mut cmd, "codefang", &mut std::io::stdout()); 0 } -/// Stderr progress for a `run`, mirroring the reference implementation's -/// `progress:` lines (the report bytes themselves go to stdout, so progress -/// never touches the frozen output contract). Suppressed by `--silent`. A -/// startup line announces the resolved selection immediately, and the Drop -/// impl prints the elapsed wall time on EVERY return path — so even a long -/// static walk is visibly working rather than looking hung. +/// `cmd` with every command and argument description removed, recursively. +fn without_descriptions(cmd: clap::Command) -> clap::Command { + cmd.about(None) + .long_about(None) + .mut_args(|a| a.help(None).long_help(None)) + .mut_subcommands(without_descriptions) +} + +/// Stderr progress for a `run`: a startup line announces the resolved +/// selection immediately, and the Drop impl prints the elapsed wall time on +/// EVERY return path — so even a long static walk is visibly working rather +/// than looking hung. Suppressed by `--quiet` / `--silent`. The report bytes +/// themselves go to stdout (or `-o`), so progress never touches the output +/// contract. struct RunProgress { start: std::time::Instant, - silent: bool, + quiet: bool, } impl RunProgress { - fn begin(silent: bool, static_n: usize, history_n: usize, output_format: &str) -> Self { - if !silent { + fn begin(quiet: bool, static_n: usize, history_n: usize, output_format: &str) -> Self { + if !quiet { eprintln!( "progress: resolved analyzers: static={static_n} history={history_n} output_format={output_format}" ); } Self { start: std::time::Instant::now(), - silent, + quiet, } } } impl Drop for RunProgress { fn drop(&mut self) { - if self.silent { + if self.quiet { return; } // Surface silent per-file skips (unreadable / oversized / parse @@ -305,269 +421,173 @@ impl Drop for RunProgress { } } -/// Dispatches `codefang run` through the general pipeline. Emits each phase's -/// report bytes to stdout in dispatch order; on an unsatisfiable selection it -/// surfaces the codefang error path (`Error: <msg>\n`, exit 1). -fn run_subcommand(sub: &clap::ArgMatches) -> i32 { - use std::io::Write; - - // Observability bracket around the whole run, mirroring the reference implementation:338-351 - // (`rc.initObservability()` + deferred `providers.Shutdown`). With no - // OTEL_EXPORTER_OTLP_ENDPOINT the providers are no-op: zero output, zero - // report-byte impact. The guard's Drop performs the shutdown. - #[cfg(feature = "runtime")] - let _observability = match observability::init_run_observability(sub.get_flag("debug-trace")) { - Ok(guard) => guard, - Err(e) => { - // Reference: return fmt.Errorf("init observability: %w", err) → cobra - // prints `Error: <msg>` and exits 1. - eprintln!("Error: init observability: {e}"); - return 1; - } - }; +/// The `--verbose` detail lines: the configuration file used and the +/// resolved analyzer ids (static phase first). +fn verbose_lines(config_path: Option<&Path>, static_ids: &[String], history_ids: &[String]) { + let config = config_path.map_or_else(|| "none".to_owned(), |p| p.display().to_string()); + eprintln!("progress: config file: {config}"); + let ids: Vec<&str> = static_ids + .iter() + .chain(history_ids) + .map(String::as_str) + .collect(); + eprintln!("progress: analyzers: {}", ids.join(",")); +} - let registry = handlers::default_registry(); +/// Prints `Error: <message>` and returns the failure exit code. +fn fail(message: &dyn std::fmt::Display) -> i32 { + eprintln!("Error: {message}"); + 1 +} - if sub.get_flag("list-analyzers") { - let mut out = std::io::stdout(); - for id in registry.ids() { - let _ = writeln!(out, "{id}"); - } - return 0; +/// True when `pat` carries a glob metacharacter. +fn is_glob(pat: &str) -> bool { + pat.contains(['*', '?', '[']) +} + +/// Dispatches `codefang run` through the general pipeline. Emits the report +/// bytes to stdout (or the `-o` file); on any user error it prints +/// `Error: <msg>` and exits 1. `config_path` is the configuration file the +/// flag defaults came from, for `--verbose`. +fn run_subcommand(sub: &clap::ArgMatches, config_path: Option<&Path>) -> i32 { + // `--no-color` is the flag form of the `NO_COLOR` convention every text / + // compact renderer reads. + if sub.get_flag("no-color") { + std::env::set_var("NO_COLOR", "1"); } let ctx = pipeline::RunContext::from_matches(sub); let ids = ctx.analyzer_ids(); - - // Special case preserved from the reference `FormatPerAnalyzer`: a static-only glob/id set - // with --format bin emits the concatenated registry-ordered per-analyzer CFB1 - // envelopes. This is a multi-id (glob) concern, not a single-id dispatch, so - // it is handled before the per-id pipeline (which dispatches literal ids). let raw_format = ctx.raw_format(); let analyzer_strs: Vec<&str> = ids.iter().map(String::as_str).collect(); - // Progress bracket: print the resolved selection now (so the user sees the - // run is alive and what it will compute), and the elapsed time on Drop — - // covering every dispatch branch's early return below. Counts use the same - // glob expansion the combined dispatch uses, so `*` reports the full - // static/history fan-out. Suppressed by `--silent`; stderr only. - let (_prog_static, _prog_history) = handlers::expand_combined_ids(&analyzer_strs); + // Progress bracket: print the resolved selection now, and the elapsed + // time on Drop — covering every dispatch branch's early return below. + let quiet = sub.get_flag(FLAG_QUIET); + let (prog_static, prog_history) = handlers::expand_combined_ids(&analyzer_strs); let _progress = RunProgress::begin( - sub.get_flag("silent"), - _prog_static.len(), - _prog_history.len(), + quiet, + prog_static.len(), + prog_history.len(), &formats::normalize_format(&raw_format), ); + if sub.get_flag(FLAG_VERBOSE) { + verbose_lines(config_path, &prog_static, &prog_history); + } - // The shared static-phase walk filter (--languages + --include-vendored / - // --include-generated). An unresolvable --languages token fails the run - // up front with the reference error (`applyStaticLanguageFilter`: - // `static --languages: unknown language: "<token>"`, exit 1) whenever the - // selection includes a static analyzer. - let static_filter = match handlers::static_filter(&ctx) { - Ok(filter) => filter, - Err(msg) => { - if _prog_static.is_empty() { - handlers::StaticFilter::default() - } else { - eprintln!("Error: {msg}"); - return 1; - } - } - }; - - // The shared history-phase change filter (the --languages set wired into - // the reference TreeDiff analyzer; the include-* flags are inert there). - // An unresolvable --languages token fails the run up - // front with the reference error (`failed to configure TreeDiff: tree-diff - // pathspec: unknown language: "<token>"`, exit 1) whenever the selection - // includes a history analyzer. The reference validates the static filter - // first, so this check stays BELOW the static one (mixed-selection order). - let _history_filter = match handlers::history_filter(sub) { - Ok(filter) => filter, - Err(msg) => { - if _prog_history.is_empty() { - handlers::HistoryFilter::default() - } else { - eprintln!("Error: {msg}"); - return 1; - } - } - }; - - // `--since` is validated against the repository BEFORE any walking, like the - // reference's `initHistoryPipeline` (which parsed it during pipeline init). - // Left to the handler, an unresolvable value surfaced only as the generic - // dispatch-blocked diagnostic — which blames the port for the user's typo - // and never names the offending flag. stdout stays empty and rc stays 1 - // either way; this only makes the failure diagnosable. Scoped to runs with - // history analyzers selected, because a static-only run never reads the - // flag (reference parity). The repository's own frozen error text is kept - // verbatim so existing greps on `cannot parse time:` still match. - if !_prog_history.is_empty() { - if let handlers::SinceSpec::Active(spec) = handlers::history_since_spec(sub) { - let repo_path = handlers::run_repo_path(sub); - if let Ok(repo) = cf_gitlib::Repository::open(&repo_path) { - if let Err(e) = repo.resolve_time(&spec) { - eprintln!("Error: cannot resolve --since {spec:?}: {e}"); - return 1; - } - } - } + // ONE validation of the path, the repository, the flags and the format + // before any walk starts; every failure names its cause. + if let Err(e) = handlers::preflight(&ctx, &prog_static, &prog_history) { + return fail(&e); } - // --format plot routes to the multi-page HTML renderer (the reference implementation: the - // static/history phases each call validatePlotFlags then the plot - // executor). The --output precheck fires for ANY plot selection (the exact - // reference `ErrPlotOutputRequired` wording, rc 1). A static-only selection renders - // pages + index + the static results report.json; a history-only selection - // renders <flag>.html pages + index + the {analyzer_ids, pages} - // report.json; a MIXED selection runs both phases into the - // SAME directory — the history report.json overwrites the static one and - // the final index is rebuilt title-sorted across both page sets (reference: - // rebuildPlotIndex). Any unported selection surfaces the dispatch-blocked - // diagnostic AFTER the flag validation, preserving the reference implementation's error ordering. if formats::normalize_format(&raw_format) == formats::FORMAT_PLOT { - let output = sub.get_one::<String>("output").map_or("", String::as_str); - if output.is_empty() { - eprintln!("Error: --output flag is required when --format plot"); - return 1; - } - // SELECTION-order expansion: the reference implementation renders plot pages (and index cards) - // in the resolved id order (`AnalyzerNamesByID(analyzerIDs)`). - let (plot_static, plot_history) = handlers::expand_selection_ids(&analyzer_strs); - if !plot_static.is_empty() && plot_history.is_empty() { - if let Some(code) = handlers::plot::run_static_plot(&ctx, &plot_static, output) { - return code; - } - } - if plot_static.is_empty() && !plot_history.is_empty() { - if let Some(code) = - handlers::history_plot::run_history_plot(&ctx, &plot_history, output, false) - { - return code; - } - } - if !plot_static.is_empty() && !plot_history.is_empty() { - // Mixed run: static phase first (pages + index + static - // report.json), then the history phase into the same directory - // (reference: runStaticPhase → runHistoryPhase → rebuildPlotIndex). - if let Some(code) = handlers::plot::run_static_plot(&ctx, &plot_static, output) { - if code != 0 { - return code; - } - if let Some(history_code) = - handlers::history_plot::run_history_plot(&ctx, &plot_history, output, true) - { - return history_code; - } - } - } - eprintln!("Error: {DISPATCH_BLOCKED_MSG}"); - return 1; + return run_plot(&ctx, sub, &analyzer_strs); + } + match render_report(&ctx, &analyzer_strs) { + Ok(bytes) => ReportSink::new(sub, false).emit(&bytes), + Err(e) => fail(&e), + } +} + +/// Renders a whole `codefang run …` command line in memory: the report bytes +/// the CLI would write, or the error message it would print. Validation is +/// the CLI's (the preflight); nothing is printed. `plot` is not supported +/// (its output is a page directory). +/// +/// # Errors +/// +/// The message for unparsable arguments, a failed preflight or a failed +/// render. +pub fn render_run(argv: &[&str]) -> Result<Vec<u8>, String> { + let matches = build_codefang_command() + .try_get_matches_from(std::iter::once("codefang").chain(argv.iter().copied())) + .map_err(|e| e.to_string())?; + let sub = matches + .subcommand_matches("run") + .ok_or_else(|| "not a run command".to_owned())?; + let ctx = pipeline::RunContext::from_matches(sub); + let ids = ctx.analyzer_ids(); + let analyzer_strs: Vec<&str> = ids.iter().map(String::as_str).collect(); + let (static_ids, history_ids) = handlers::expand_combined_ids(&analyzer_strs); + handlers::preflight(&ctx, &static_ids, &history_ids).map_err(|e| e.to_string())?; + if formats::normalize_format(&ctx.raw_format()) == formats::FORMAT_PLOT { + return Err("format plot is not supported here".to_owned()); } - if raw_format == "bin" - && !analyzer_strs.is_empty() - && analyzer_strs.iter().any(|a| a.contains(['*', '?', '['])) + render_report(&ctx, &analyzer_strs) +} + +/// Renders the (non-plot) report of the selection into `sink` through the +/// matching path: the multi-analyzer static bin / json / text merges, the +/// unified mixed document, the whole-selection history formats, or the +/// per-id pipeline. Returns the exit code. +fn render_report(ctx: &pipeline::RunContext, analyzer_strs: &[&str]) -> Result<Vec<u8>, String> { + let registry = handlers::default_registry(); + let raw_format = ctx.raw_format(); + let ids = ctx.analyzer_ids(); + // Validated by the preflight; a static-free selection never reads it. + let static_filter = handlers::static_filter(ctx).unwrap_or_default(); + let all_static = !analyzer_strs.is_empty() && analyzer_strs .iter() - .all(|a| handlers::is_static_id_or_glob(a)) - { - if let Some(bytes) = handlers::static_multi_bin(&analyzer_strs, &ctx.path, &static_filter) { - write_stdout_or_exit(&bytes); - return 0; + .all(|a| handlers::is_static_id_or_glob(a)); + + // A static-only glob with --format bin emits the concatenated + // registry-ordered per-analyzer CFB1 envelopes. + if raw_format == "bin" && all_static && analyzer_strs.iter().any(|a| is_glob(a)) { + if let Some(bytes) = handlers::static_multi_bin(analyzer_strs, &ctx.path, &static_filter) { + return Ok(bytes); } } - // Special case preserved from the reference `renderer.SectionsToJSON`: a STATIC-ONLY - // selection (no history analyzer) of MORE THAN ONE static analyzer (a literal - // multi-id list or a glob like `static/*`) with `--format json` renders ONE - // merged JSONReport — sections in registry order, overall_score the average - // of the scored sections. A single static analyzer keeps its own one-section - // document (the per-id pipeline below); a selection that also matches a - // history analyzer (e.g. `*`) uses the UnifiedModel path, not this merge. + // A STATIC-ONLY selection of MORE THAN ONE static analyzer with + // `--format json` renders ONE merged report (sections in registry order, + // overall_score the average of the scored sections); `text` renders all + // selected sections through ONE text render with the executive summary. if (raw_format == "json" || raw_format == "text") - && !analyzer_strs.is_empty() - && analyzer_strs - .iter() - .all(|a| handlers::is_static_id_or_glob(a)) - && handlers::static_json_selects_multiple(&analyzer_strs) + && all_static + && handlers::static_json_selects_multiple(analyzer_strs) { let bytes = if raw_format == "json" { handlers::static_multi_json( - &analyzer_strs, + analyzer_strs, &ctx.path, &static_filter, ctx.matches.get_flag("per-file"), ) } else { - // Multi-static text renders all selected sections through ONE - // FormatText call (the reference), so the ≥2-section executive - // summary header is emitted before the per-analyzer bodies. - handlers::static_multi_text(&analyzer_strs, &ctx.path, &static_filter) + handlers::static_multi_text(analyzer_strs, &ctx.path, &static_filter) }; if let Some(bytes) = bytes { - write_stdout_or_exit(&bytes); - return 0; + return Ok(bytes); } } - // Mixed static+history selection (both phases non-empty) renders the single - // `codefang.run.v1` unified-model envelope, mirroring the reference `runDirect` → - // renderCombinedDirect. This is NOT a per-analyzer concatenation: every - // selected analyzer's raw report is gathered (via its bin payload) into one - // model with run metadata and re-serialized in the requested format by the - // serializer layer (cf-analyze::write_converted_output). Plot is excluded - // (reference: isMixedPlot keeps the separate-phase path). On any unported analyzer - // the helper returns None and we fall through to the per-id pipeline. - let (combined_static, combined_history) = handlers::expand_combined_ids(&analyzer_strs); - if !combined_static.is_empty() - && !combined_history.is_empty() - && raw_format != "plot" - && raw_format != "compact" - { - if let Some(bytes) = - handlers::render_combined(&ctx, &combined_static, &combined_history, &raw_format) - { - write_stdout_or_exit(&bytes); - return 0; - } + // Mixed static+history selection (both phases non-empty): the unified + // `codefang.run.v1` document, or the per-phase text / compact / + // time-series renderings. + let (combined_static, combined_history) = handlers::expand_combined_ids(analyzer_strs); + if !combined_static.is_empty() && !combined_history.is_empty() { + return handlers::render_mixed(ctx, &combined_static, &combined_history, &raw_format) + .map_err(|e| e.to_string()); } - // Expand globs / literal ids to the concrete registry ids the per-id pipeline - // dispatches (the reference `registry.Split` resolves patterns before the phase loop). - // `run_pipeline` matches literal ids only, so an unexpanded glob like - // `history/*` would otherwise miss every handler. The static phase uses the - // registry static order; the history phase uses the reference implementation's SEPARATE-phase emit order - // (`HISTORY_PHASE_EMIT_ORDER` — `runHistoryPhase` over the glob-expanded leaf - // set), which differs from the combined-model order, so a history-only glob's - // per-analyzer reports concatenate in the same sequence the reference implementation writes them. Fall - // back to the raw selection when nothing expands (preserves the reference implementation's unknown-id - // error). A glob in `analyzer_strs` forces the expanded ordering even for a - // mixed selection; a purely literal selection keeps its request order. - let resolved_ids = { - let is_glob = |p: &&str| p.contains(['*', '?', '[']); - if analyzer_strs.iter().any(is_glob) { - let (s, _h) = handlers::expand_combined_ids(&analyzer_strs); - let mut v = s; - v.extend(handlers::expand_history_phase_ids(&analyzer_strs)); - if v.is_empty() { - ids.clone() - } else { - v - } + // Expand globs / literal ids to the concrete registry ids the per-id + // pipeline dispatches: the static phase uses the registry static order; + // the history phase uses the separate-phase emit order. A purely literal + // selection keeps its request order. + let resolved_ids = if analyzer_strs.iter().any(|a| is_glob(a)) { + let mut v = combined_static; + v.extend(handlers::expand_history_phase_ids(analyzer_strs)); + if v.is_empty() { + ids } else { - ids.clone() + v } + } else { + ids }; - // Centralized history-phase formats (reference: OutputHistoryResults / StreamingSink): - // text, ndjson, and timeseries are NOT per-analyzer encodings — the reference implementation routes the - // whole selected leaf set through one history output function (header + per- - // leaf sections for text; per-commit TC lines for ndjson; one merged document - // for timeseries). A history-only selection in one of those formats dispatches - // here; `None` (an unported leaf) falls through to the per-id pipeline and its - // existing dispatch-blocked diagnostic. let all_history = !resolved_ids.is_empty() && resolved_ids.iter().all(|id| { registry @@ -575,72 +595,101 @@ fn run_subcommand(sub: &clap::ArgMatches) -> i32 { .is_some_and(|e| matches!(e.mode, pipeline::Mode::History)) }); if all_history { - let normalized = formats::normalize_format(&raw_format); - let history_format = formats::apply_ndjson_modifier(&normalized, ctx.ndjson()); - let special = match history_format.as_str() { - formats::FORMAT_TEXT => handlers::history_formats::history_text(&ctx, &resolved_ids), - formats::FORMAT_NDJSON => { - handlers::history_formats::history_ndjson(&ctx, &resolved_ids) - } - formats::FORMAT_TIMESERIES => { - handlers::history_formats::history_timeseries(&ctx, &resolved_ids, false) - } - // `timeseries+ndjson` (the --ndjson modifier) is NOT the merged - // document as lines: the reference implementation routes it through the per-chunk - // TimeSeriesChunkFlusher (DrainCommitStats), which devs/burndown - // reproduce in their per-analyzer handlers — fall through. - _ => None, - }; - match special { - Some(Ok(bytes)) => { - write_stdout_or_exit(&bytes); - return 0; - } - Some(Err(fail)) => { - // The reference implementation streams the partial bytes to stdout BEFORE the serializer - // fails; cobra then prints `Error: <msg>` to stderr and exits 1. - write_stdout_or_exit(&fail.partial); - eprintln!("Error: {}", fail.message); - return 1; - } - None => {} + if let Some(rendered) = render_history_whole_selection(ctx, &resolved_ids, &raw_format) { + return rendered; } } - match pipeline::run_pipeline(®istry, &ctx, &resolved_ids) { - Ok(outputs) => { - for phase in &outputs { - write_stdout_or_exit(&phase.bytes); - } - 0 + pipeline::run_pipeline(®istry, ctx, &resolved_ids) + .map(|outputs| outputs.into_iter().flat_map(|phase| phase.bytes).collect()) + .map_err(|e| e.to_string()) +} + +/// `--format plot`: the multi-page HTML renderer. `--output` is required. A +/// static-only selection renders pages + index + the static results +/// report.json; a history-only selection renders the per-flag pages + index + +/// report.json; a MIXED selection runs both phases into the SAME directory +/// and the final index is rebuilt across both page sets. +fn run_plot(ctx: &pipeline::RunContext, sub: &clap::ArgMatches, analyzer_strs: &[&str]) -> i32 { + let output = sub.get_one::<String>("output").map_or("", String::as_str); + if output.is_empty() { + return fail(&"--output flag is required when --format plot"); + } + // SELECTION-order expansion: pages (and index cards) render in the + // resolved id order. + let (plot_static, plot_history) = handlers::expand_selection_ids(analyzer_strs); + let no_pages = || { + fail(&format!( + "no plot pages produced for {}", + analyzer_strs.join(",") + )) + }; + match (plot_static.is_empty(), plot_history.is_empty()) { + (false, true) => { + handlers::plot::run_static_plot(ctx, &plot_static, output).unwrap_or_else(no_pages) } - // An unknown analyzer id surfaces the specific reference diagnostic - // ("unknown analyzer id: <id>", reference `ErrUnknownAnalyzer`) so the error - // path is byte-class-identical to cobra (the cli-surface runtime probe - // requires the "analyzer" diagnostic, not a generic stub message). - Err(pipeline::PipelineError::UnknownAnalyzer(id)) => { - eprintln!("Error: unknown analyzer id: {id}"); - 1 + (true, false) => { + handlers::history_plot::run_history_plot(ctx, &plot_history, output, false) + .unwrap_or_else(no_pages) } - // Any other unsatisfiable selection (no handler for this analyzer/format) - // routes to the same codefang error path the legacy dispatch fell through to. - Err(_) => { - eprintln!("Error: {DISPATCH_BLOCKED_MSG}"); - 1 + (false, false) => { + // Mixed run: static phase first, then the history phase into the + // same directory. + match handlers::plot::run_static_plot(ctx, &plot_static, output) { + Some(0) => { + handlers::history_plot::run_history_plot(ctx, &plot_history, output, true) + .unwrap_or_else(no_pages) + } + Some(code) => code, + None => no_pages(), + } } + (true, true) => no_pages(), } } -/// Dispatches `codefang render <store-dir>`; reproduces the `--output` precheck -/// (the reference implementation `ErrNoOutputDir`) before the (still-blocked) render body. -fn render_subcommand(sub: &clap::ArgMatches) -> i32 { - let output = sub.get_one::<String>("output").map_or("", String::as_str); - if output.is_empty() { - eprintln!("Error: output directory is required (use --output)"); - return 1; +/// The whole-selection history formats (`text`, `compact`, `ndjson`, +/// `timeseries`, `timeseries --ndjson`), and the one-document rule for a +/// multi-analyzer `json` / `yaml` selection, written into `sink`. Returns +/// `None` when the selection+format is a plain per-analyzer dispatch (handled +/// by the per-id pipeline). +fn render_history_whole_selection( + ctx: &pipeline::RunContext, + resolved_ids: &[String], + raw_format: &str, +) -> Option<Result<Vec<u8>, String>> { + let normalized = formats::normalize_format(raw_format); + let history_format = formats::apply_ndjson_modifier(&normalized, ctx.ndjson()); + + // Several history analyzers in json/yaml: ONE unified-model document + // (`{version, metadata, analyzers: [{id, mode, report}]}`), never a + // concatenation of per-analyzer documents. + if resolved_ids.len() > 1 + && (history_format == formats::FORMAT_JSON || history_format == formats::FORMAT_YAML) + { + return Some( + handlers::render_combined(ctx, &[], resolved_ids, raw_format) + .map_err(|e| e.to_string()), + ); } - eprintln!("Error: {DISPATCH_BLOCKED_MSG}"); - 1 + + // Every whole-selection renderer builds its complete output first: a + // failure writes nothing to stdout. + let rendered = match history_format.as_str() { + formats::FORMAT_TEXT => handlers::history_formats::history_text(ctx, resolved_ids), + formats::FORMAT_COMPACT => handlers::history_formats::history_compact(ctx, resolved_ids), + formats::FORMAT_NDJSON => handlers::history_formats::history_ndjson(ctx, resolved_ids), + formats::FORMAT_TIMESERIES => { + handlers::history_formats::history_timeseries(ctx, resolved_ids, false) + } + // `--format timeseries --ndjson`: the merged document's commits, one + // line per commit. + formats::FORMAT_TIMESERIES_NDJSON => { + handlers::history_formats::history_timeseries(ctx, resolved_ids, true) + } + _ => return None, + }; + Some(rendered.map_err(|e| e.to_string())) } #[cfg(test)] @@ -652,25 +701,43 @@ mod tests { assert_eq!(CRATE_NAME, "cf-commands"); } + /// A whole `run` command renders in memory: the report bytes, or the + /// error the CLI would print. + #[test] + fn a_run_renders_in_memory() { + let dir = tempfile::tempdir().expect("dir"); + std::fs::write( + dir.path().join("m.go"), + "package m\n\nfunc f(x int) int {\n\tif x > 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n", + ) + .expect("write"); + let path = dir.path().to_str().expect("utf-8"); + let bytes = render_run(&["run", "-a", "static/complexity", "--format", "json", path]) + .expect("render"); + let report: serde_json::Value = serde_json::from_slice(&bytes).expect("json"); + assert!(report.get("sections").is_some(), "{report}"); + + let err = render_run(&["run", "-a", "static/nope", path]).expect_err("unknown id"); + assert!(err.contains("nope"), "{err}"); + let err = render_run(&["run", "--format"]).expect_err("bad flags"); + assert!(!err.is_empty()); + } + #[test] fn root_has_subcommands_in_order() { let cmd = build_codefang_command(); let names: Vec<&str> = cmd.get_subcommands().map(clap::Command::get_name).collect(); - // `completion` mirrors the command cobra auto-registers (cli-surface - // parity); it is wired last, after the three explicit subcommands. - assert_eq!(names, vec!["run", "render", "version", "completion"]); + assert_eq!(names, vec!["run", "version", "completion", "mcp"]); } #[test] - fn root_has_persistent_flags() { + fn root_has_global_flags() { let cmd = build_codefang_command(); let longs: Vec<_> = cmd .get_arguments() .filter_map(clap::Arg::get_long) .collect(); - assert!(longs.contains(&"verbose")); - assert!(longs.contains(&"quiet")); - assert!(longs.contains(&"profile")); + assert_eq!(longs, vec!["verbose", "quiet", "profile"]); } #[test] @@ -679,4 +746,54 @@ mod tests { let m = cmd.try_get_matches_from(["codefang", "version"]).unwrap(); assert_eq!(m.subcommand_name(), Some("version")); } + + /// `render` printed "not available" for every input: it is gone. + #[test] + fn render_is_not_a_subcommand() { + let parsed = build_codefang_command().try_get_matches_from(["codefang", "render", "x"]); + assert!(parsed.is_err()); + } + + #[test] + fn silent_is_an_alias_of_quiet_and_reaches_run() { + for flag in ["--silent", "--quiet", "-q"] { + let m = build_codefang_command() + .try_get_matches_from(["codefang", "run", flag]) + .unwrap(); + let run = m.subcommand_matches("run").unwrap(); + assert!(run.get_flag(FLAG_QUIET), "{flag}"); + } + } + + #[test] + fn verbose_and_quiet_conflict_wherever_they_are_given() { + for argv in [ + ["codefang", "-v", "run", "-q"], + ["codefang", "run", "-v", "--silent"], + ["codefang", "-q", "-v", "run"], + ] { + let m = build_codefang_command().try_get_matches_from(argv).unwrap(); + let run = m.subcommand_matches("run").unwrap(); + assert_eq!( + check_verbosity(run), + Err("--verbose and --quiet cannot be used together"), + "{argv:?}" + ); + } + let m = build_codefang_command() + .try_get_matches_from(["codefang", "run", "-v"]) + .unwrap(); + assert_eq!( + check_verbosity(m.subcommand_matches("run").unwrap()), + Ok(()) + ); + } + + #[test] + fn descriptions_can_be_stripped_from_the_completion_tree() { + let cmd = without_descriptions(build_codefang_command()); + let run = cmd.find_subcommand("run").unwrap(); + assert!(run.get_about().is_none()); + assert!(run.get_arguments().all(|a| a.get_help().is_none())); + } } diff --git a/crates/cf-commands/src/mcp.rs b/crates/cf-commands/src/mcp.rs new file mode 100644 index 00000000..844c58fa --- /dev/null +++ b/crates/cf-commands/src/mcp.rs @@ -0,0 +1,171 @@ +//! `codefang mcp`: the Model Context Protocol server over stdio. +//! +//! The server, its JSON-RPC transport and the tool contracts live in +//! `cf-mcp`; this module supplies the three analysis backends, each the +//! CLI's own pipeline, so a tool returns exactly the report `codefang run` +//! renders for the same input: +//! +//! * `uast_parse` and `codefang_analyze` parse with the embedded UAST parser; +//! * `codefang_analyze` writes the snippet to a temporary directory and +//! renders `run -a static/<name> --format json` there, per analyzer; +//! * `codefang_history` renders `run -a history/<names> --format json` with +//! the tool's limit, `--first-parent` and `--since`, then forgets the +//! process-wide UAST results, so a long-running server holds nothing +//! between calls. + +use cf_mcp::errors::ToolError; +use cf_mcp::gojson::JsonValue; +use cf_mcp::providers::{ + HistoryAnalysisProvider, HistoryRunOptions, Snippet, StaticAnalysisProvider, UastParser, +}; +use cf_mcp::server::{Server, ServerDeps}; +use cf_mcp::transport::NeverCancelled; +use cf_uast_node::Node; + +/// The `mcp` subcommand: no flags. +#[must_use] +pub fn build_mcp_command() -> clap::Command { + clap::Command::new("mcp").about( + "Serve the codefang analysis tools (codefang_analyze, uast_parse, codefang_history) \ +to an MCP client over stdio", + ) +} + +/// Runs the server until the client closes stdin; returns the exit code. +#[must_use] +pub fn mcp_subcommand() -> i32 { + let server = Server::new(ServerDeps { + parser: Some(Box::new(CodefangParser)), + static_provider: Some(Box::new(CodefangStatic)), + history_provider: Some(Box::new(CodefangHistory)), + ..ServerDeps::default() + }); + match server.run(&NeverCancelled) { + Ok(()) => 0, + Err(e) => { + eprintln!("Error: mcp server: {e}"); + 1 + } + } +} + +/// The embedded UAST parser (one per thread, as in the history walks). +struct CodefangParser; + +impl UastParser for CodefangParser { + /// `code` plus the primary extension of the Linguist language the token + /// names (`python` → `code.py`); an unknown token keeps `code.<token>`. + fn filename_for(&self, language: &str) -> String { + cf_langpath::primary_extension(language).map_or_else( + || cf_mcp::tools::synthetic_filename(language), + |ext| format!("code{ext}"), + ) + } + + fn is_supported(&self, filename: &str) -> bool { + crate::handlers::history::with_uast_parser(|parser| parser.is_supported(filename)) + } + + fn parse(&self, filename: &str, code: &[u8]) -> Result<Node, ToolError> { + crate::handlers::history::with_uast_parser(|parser| parser.parse(filename, code)) + .map_err(|e| ToolError::wrap("parse", e.to_string())) + } +} + +/// The CLI's static analyzers over the snippet. +struct CodefangStatic; + +impl StaticAnalysisProvider for CodefangStatic { + fn run(&self, snippet: &Snippet<'_>, names: &[String]) -> Result<JsonValue, ToolError> { + let dir = + tempfile::tempdir().map_err(|e| ToolError::wrap("create a work dir", e.to_string()))?; + std::fs::write(dir.path().join(snippet.filename), snippet.code) + .map_err(|e| ToolError::wrap("write the code", e.to_string()))?; + let path = dir + .path() + .to_str() + .ok_or_else(|| ToolError::wrap("work dir", "not UTF-8"))?; + let mut entries = Vec::with_capacity(names.len()); + for name in names { + let id = format!("static/{name}"); + let report = render_json(&["run", "-a", &id, "--format", "json", path])?; + entries.push((name.clone(), report)); + } + Ok(JsonValue::sorted_object(entries)) + } +} + +/// The CLI's history analyzers over the repository. +struct CodefangHistory; + +impl HistoryAnalysisProvider for CodefangHistory { + fn run(&self, opts: &HistoryRunOptions) -> Result<JsonValue, ToolError> { + let ids = opts + .analyzers + .iter() + .map(|key| format!("history/{key}")) + .collect::<Vec<_>>() + .join(","); + let limit = opts.limit.to_string(); + let mut argv = vec!["run", "-a", &ids, "--format", "json", "--limit", &limit]; + if opts.first_parent { + argv.push("--first-parent"); + } + if !opts.since.is_empty() { + argv.extend(["--since", &opts.since]); + } + argv.push(&opts.repo_path); + let document = render_json(&argv); + crate::handlers::uast_walk::forget_results(); + let document = document?; + match opts.analyzers.as_slice() { + [only] => Ok(JsonValue::sorted_object(vec![(only.clone(), document)])), + _ => per_analyzer_reports(document), + } + } +} + +/// Renders a `run` command line and returns its JSON report. +fn render_json(argv: &[&str]) -> Result<JsonValue, ToolError> { + let bytes = crate::render_run(argv).map_err(|e| ToolError::wrap("run analyzers", e))?; + let value: serde_json::Value = serde_json::from_slice(&bytes) + .map_err(|e| ToolError::wrap("read the report", e.to_string()))?; + Ok(JsonValue::from(&value)) +} + +/// The `{analyzer key: report}` map of a multi-analyzer document +/// (`{version, metadata, analyzers: [{id, mode, report}]}`). +fn per_analyzer_reports(document: JsonValue) -> Result<JsonValue, ToolError> { + let missing = || ToolError::wrap("read the report", "no analyzers array"); + let JsonValue::Object { entries, .. } = document else { + return Err(missing()); + }; + let analyzers = entries + .into_iter() + .find_map(|(key, value)| (key == "analyzers").then_some(value)) + .ok_or_else(missing)?; + let JsonValue::Array(items) = analyzers else { + return Err(missing()); + }; + let mut reports = Vec::with_capacity(items.len()); + for item in items { + let JsonValue::Object { entries, .. } = item else { + return Err(missing()); + }; + let mut id = None; + let mut report = None; + for (key, value) in entries { + match (key.as_str(), value) { + ("id", JsonValue::Str(s)) => id = Some(s), + ("report", value) => report = Some(value), + _ => {} + } + } + let (Some(id), Some(report)) = (id, report) else { + return Err(missing()); + }; + let key = id.strip_prefix("history/").unwrap_or(&id).to_owned(); + reports.push((key, report)); + } + Ok(JsonValue::sorted_object(reports)) +} diff --git a/crates/cf-commands/src/observability.rs b/crates/cf-commands/src/observability.rs deleted file mode 100644 index 44b78a40..00000000 --- a/crates/cf-commands/src/observability.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Run-command observability bootstrap. -//! -//! The init/shutdown bracket around the run pipeline: build the default -//! observability config, overlay the version, the standard -//! `OTEL_EXPORTER_OTLP_*` environment, and `--debug-trace`, then hand the -//! config to `cf_observability::init_otel::init`. -//! -//! With no `OTEL_EXPORTER_OTLP_ENDPOINT` set (the default CLI case) the -//! providers are no-op with zero export overhead and zero output, exactly like -//! the reference binary — nothing here can touch report bytes. - -use cf_observability::config::Config; -use cf_observability::init_otel::{self, parse_otlp_headers, Providers}; - -/// RAII bracket for the run command's observability lifetime. -/// -/// Holding it mirrors the reference implementation's `defer providers.Shutdown(ctx)`: -/// dropping the guard flushes/shuts down the providers. -pub(crate) struct ObservabilityGuard { - providers: Providers, -} - -impl Drop for ObservabilityGuard { - fn drop(&mut self) { - // The reference implementation logs "observability shutdown failed" as a warning and proceeds; - // shutdown failure must never affect the exit code or report bytes. - if let Err(e) = self.providers.shutdown() { - eprintln!("observability shutdown failed: {e}"); - } - } -} - -/// Initializes observability for one `codefang run` invocation. -/// -/// On failure the caller surfaces the reference implementation's wrapped `init observability: <err>` -/// error path (CLI `Error: <msg>`, rc 1). The default no-op -/// path cannot fail. -pub(crate) fn init_run_observability( - debug_trace: bool, -) -> Result<ObservabilityGuard, init_otel::ShutdownError> { - let cfg = Config { - service_version: cf_version::VERSION.to_string(), - otlp_endpoint: std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").unwrap_or_default(), - otlp_headers: parse_otlp_headers( - &std::env::var("OTEL_EXPORTER_OTLP_HEADERS").unwrap_or_default(), - ), - otlp_insecure: std::env::var("OTEL_EXPORTER_OTLP_INSECURE").as_deref() == Ok("true"), - // Mode is already Cli in the default config; set it explicitly anyway, - // matching the reference implementation. - mode: cf_observability::config::AppMode::Cli, - debug_trace, - ..Config::default() - }; - - init_otel::init(&cfg).map(|providers| ObservabilityGuard { providers }) -} diff --git a/crates/cf-commands/src/pipeline.rs b/crates/cf-commands/src/pipeline.rs index e4753a32..f08ba8cd 100644 --- a/crates/cf-commands/src/pipeline.rs +++ b/crates/cf-commands/src/pipeline.rs @@ -1,23 +1,17 @@ //! The single general `run` pipeline and its analyzer registry. //! -//! This module is the Rust analogue of the reference implementation -//! (`RunCommand.run` → `runDirect` → `runStaticPhase` / `runHistoryPhase`) plus -//! `internal/framework`'s streaming runner. It replaces the per-`(analyzer, -//! format)` dispatch ladder that the `codefang` binary historically carried -//! (the 31 `if analyzers == [..] && format == ".."` blocks) with **one** -//! general flow: +//! This module replaces the per-`(analyzer, format)` dispatch ladder that the +//! `codefang` binary historically carried with **one** general flow: //! //! 1. [`RunContext::from_matches`] resolves the requested analyzer set + output -//! format from the parsed clap args (the reference `RunCommand` field binding + -//! `registry.SelectedIDs`). -//! 2. [`run_pipeline`] splits the selection into static and history analyzers, -//! resolves the per-phase format (the reference `analyze.ResolveFormats`), -//! and dispatches **by analyzer id** through the -//! [`Registry`] — for static analyzers the file set is parsed to UAST once -//! and each requested static analyzer runs from its crate; for history -//! analyzers a single git revwalk feeds per-commit data to each requested -//! history analyzer from its crate (the reference `runStaticPhase` / -//! `runHistoryPhase`). +//! format from the parsed clap args. +//! 2. [`run_pipeline`] validates the run (path, repository, flags, format) +//! through the shared preflight, splits the selection into static and +//! history analyzers, resolves the per-phase format, and dispatches **by +//! analyzer id** through the [`Registry`] — for static analyzers the file +//! set is parsed to UAST once and each requested static analyzer runs from +//! its crate; for history analyzers a single git revwalk feeds per-commit +//! data to each requested history analyzer from its crate. //! 3. The resulting report bytes are produced by the analyzer crate's own //! serializer (cf-gojson / cf-goyaml / cf-reportutil per format) — this //! module never owns analyzer math or hardcodes report bytes. @@ -27,11 +21,9 @@ //! The dispatch is a single keyed lookup: `registry.lookup(id)` returns the //! [`AnalyzerEntry`] whose [`AnalyzerEntry::run`] handler is the crate-owned //! report builder. Adding an analyzer is one registry insertion; there is no -//! per-format `if` chain. The handler closure receives the [`RunContext`] -//! (resolved path + parsed options) and the already-resolved format string, and -//! returns the report bytes the crate produces. The dispatch loop in -//! [`run_pipeline`] is format-agnostic: format selection is the handler's -//! concern, exactly as each analyzer crate's `FormatReport*` family is in the reference implementation. +//! per-format `if` chain. The handler receives the [`RunContext`] (resolved +//! path + parsed options) and the already-resolved format string, and returns +//! the report bytes or a [`HandlerError`] naming the cause. use std::collections::BTreeMap; @@ -40,9 +32,8 @@ use clap::ArgMatches; use crate::formats::{apply_ndjson_modifier, resolve_formats}; /// Whether an analyzer is driven by the static (folder → UAST) phase or the -/// history (git revwalk → per-commit) phase. Mirrors the reference `analyze.AnalyzerMode` -/// (`ModeStatic` / `ModeHistory`); the analyzer id prefix (`static/` vs -/// `history/`) selects the phase, exactly as the reference `registry.Split` does. +/// history (git revwalk → per-commit) phase. The analyzer id prefix +/// (`static/` vs `history/`) selects the phase. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Mode { /// Folder-walk + per-file UAST analyzer. @@ -51,26 +42,23 @@ pub enum Mode { History, } -/// Resolved run inputs shared by every analyzer handler — the Rust analogue of -/// the per-run state the reference implementation threads through `runStaticPhase` / `runHistoryPhase` -/// (path, format, the `--head`/`--limit`/`--ndjson` toggles, path policy). +/// Resolved run inputs shared by every analyzer handler (path, format, the +/// `--head`/`--limit`/`--ndjson` toggles, path policy). /// /// Handlers read only what they need; the context is constructed once per `run` /// from the parsed clap [`ArgMatches`] so the dispatch loop stays general. pub struct RunContext<'a> { - /// Folder / repository path to analyze (the reference `RunCommand.resolvePath`: the - /// positional `[path]` arg, falling back to `--path`/`-p`, default `.`). + /// Folder / repository path to analyze: the positional `[path]` arg, + /// falling back to `--path`/`-p`, default `.`. pub path: String, /// The parsed clap matches for the `run` subcommand, so history handlers can - /// read the streaming flags (`--head`, `--limit`, `--ndjson`, `--workers`, - /// …) without this struct enumerating every one (the reference implementation reads them off - /// `RunCommand` / `HistoryRunOptions`). + /// read the walk flags (`--head`, `--limit`, `--ndjson`, …) without this + /// struct enumerating every one. pub matches: &'a ArgMatches, } impl<'a> RunContext<'a> { - /// Builds the run context from the parsed `run` subcommand matches, - /// resolving the analyze path the way the reference `RunCommand.resolvePath` does: the + /// Builds the run context from the parsed `run` subcommand matches: the /// positional `[path]` argument wins, else `--path`/`-p` (default `.`). #[must_use] pub fn from_matches(matches: &'a ArgMatches) -> Self { @@ -82,14 +70,12 @@ impl<'a> RunContext<'a> { RunContext { path, matches } } - /// The requested analyzer ids, in user-supplied order (the reference `-a/--analyzers`, + /// The requested analyzer ids, in user-supplied order (`-a/--analyzers`, /// comma-split by clap's value delimiter). /// - /// An absent (or empty) `--analyzers` resolves to the full set (`*`), - /// matching the reference command's empty-selection behavior: `codefang run` - /// with no `-a` runs every static + history analyzer (the reference logs - /// `resolved analyzers: static=7 history=10`) and renders the combined - /// output, rather than erroring. The glob is expanded downstream by the + /// An absent (or empty) `--analyzers` resolves to the full set (`*`): + /// `codefang run` with no `-a` runs every static + history analyzer and + /// renders the combined output. The glob is expanded downstream by the /// same `expand_combined_ids`/`expand_history_phase_ids` path an explicit /// `-a '*'` takes, so the resolved selection is identical. #[must_use] @@ -115,7 +101,7 @@ impl<'a> RunContext<'a> { .unwrap_or_else(|| "json".to_owned()) } - /// Whether `--head` is set (history single-HEAD-commit mode). + /// Whether `--head` is set (history single-HEAD-commit window). #[must_use] pub fn head(&self) -> bool { self.matches.get_flag("head") @@ -128,21 +114,56 @@ impl<'a> RunContext<'a> { } } +/// Why a handler (or the dispatch around it) could not produce a report. +/// +/// The `Display` form is the text after the CLI's `Error: ` prefix — the +/// user-facing contract (`site/guide/cli-reference.md`, "Errors"). +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum HandlerError { + /// The analyze path does not exist. + #[error("path not found: {0}")] + PathNotFound(String), + /// The path exists but is not a git repository (history selection). + #[error("cannot open repository: {0}")] + CannotOpenRepository(String), + /// The `--format` value is not a format codefang knows at all. + #[error("unsupported format: {0}")] + UnsupportedFormat(String), + /// A known format that this analyzer (or phase) cannot render. + #[error("format {format} is not supported for {analyzer}")] + FormatNotSupported { + /// The `--format` value as supplied. + format: String, + /// The analyzer id (or selection description) that rejects it. + analyzer: String, + }, + /// A flag value is invalid (`--limit=-5`, `--tick-size 0`, an + /// unresolvable `--since`, an unknown `--languages` token, an unreadable + /// `--people-dict`); carries the full message. + #[error("{0}")] + InvalidFlag(String), + /// The handler ran but produced no report for this input — the generic + /// fallback for a `None` from a report builder after preflight passed. + #[error("no report produced for {analyzer} --format {format}")] + NoReport { + /// The analyzer id. + analyzer: String, + /// The resolved format. + format: String, + }, +} + /// The crate-owned report builder for one analyzer. Receives the resolved /// [`RunContext`] and the already-resolved, ndjson-modified format string, and -/// returns the serialized report bytes (`None` when the analyzer cannot run for -/// this input — e.g. the repo cannot be walked — matching the reference path that -/// surfaces an error). The bytes MUST come from the analyzer crate's own +/// returns the serialized report bytes or the [`HandlerError`] naming why none +/// could be produced. The bytes MUST come from the analyzer crate's own /// serializer (cf-gojson / cf-goyaml / cf-reportutil); this signature never -/// returns a model this module would re-encode, keeping all byte-shaping in the -/// owning crate. -pub type RunHandler = fn(ctx: &RunContext, format: &str) -> Option<Vec<u8>>; +/// returns a model this module would re-encode. +pub type RunHandler = fn(ctx: &RunContext, format: &str) -> Result<Vec<u8>, HandlerError>; /// One registry entry: an analyzer's id, its phase, and its crate-owned report -/// builder. The `formats` set is advisory documentation of which output formats -/// the handler supports; dispatch does not branch on it (the handler owns format -/// selection, returning `None` for an unsupported combination so the caller can -/// report the same "unsupported format" error the reference implementation does). +/// builder. The handler owns format selection, returning +/// [`HandlerError::FormatNotSupported`] for a combination it cannot render. pub struct AnalyzerEntry { /// Canonical analyzer id, e.g. `static/complexity`, `history/burndown`. pub id: &'static str, @@ -153,10 +174,8 @@ pub struct AnalyzerEntry { } /// The analyzer registry: the single source of truth mapping analyzer id → -/// [`AnalyzerEntry`]. This is the Rust analogue of the reference `analyze.Registry` -/// (`defaultUASTAnalyzers ++ defaultRawFileAnalyzers ++ defaultHistoryLeaves`), -/// built once and queried by id. Dispatch is a keyed lookup, NOT a per-format -/// match ladder. +/// [`AnalyzerEntry`], built once and queried by id. Dispatch is a keyed +/// lookup, NOT a per-format match ladder. pub struct Registry { entries: BTreeMap<&'static str, AnalyzerEntry>, } @@ -181,17 +200,16 @@ impl Registry { self.entries.get(id) } - /// Every registered id, sorted (the reference `Registry.All` / `IDsByMode` feed - /// `--list-analyzers`). Used by the binary's `--list-analyzers` path. + /// Every registered id, sorted (feeds `--list-analyzers`). #[must_use] pub fn ids(&self) -> Vec<&'static str> { self.entries.keys().copied().collect() } /// Splits a requested id selection into (static, history) id lists, - /// preserving the requested order within each phase. mirrors the reference implementation - /// `registry.Split`: the phase is taken from the registered entry's - /// [`Mode`]. Unknown ids are dropped here (the caller reports them). + /// preserving the requested order within each phase. The phase is taken + /// from the registered entry's [`Mode`]. Unknown ids are dropped here (the + /// caller reports them). #[must_use] pub fn split<'s>(&self, ids: &'s [String]) -> (Vec<&'s String>, Vec<&'s String>) { let mut statics = Vec::new(); @@ -224,8 +242,7 @@ pub struct PhaseOutput { pub bytes: Vec<u8>, } -/// Errors the general pipeline can surface, mirroring the reference implementation error set -/// (`ErrNoAnalyzersSelected`, `ErrUnknownAnalyzer`, the format errors). +/// Errors the general pipeline can surface. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum PipelineError { /// No analyzer ids were selected. @@ -234,29 +251,24 @@ pub enum PipelineError { /// A requested id is not in the registry. #[error("unknown analyzer id: {0}")] UnknownAnalyzer(String), - /// The requested format is not supported (the reference implementation errors). - #[error("unsupported format: {0}")] - UnsupportedFormat(String), - /// An analyzer ran but could not produce a report for this input (the repo - /// or folder could not be walked); carries the analyzer id. - #[error("analyzer {0} produced no report")] - AnalyzerFailed(String), + /// A handler (or the shared preflight) rejected the run. + #[error(transparent)] + Handler(#[from] HandlerError), } -/// The single general run pipeline. Resolves the analyzer set + per-phase -/// format, then dispatches every requested analyzer through `registry` by id, -/// returning each phase's serialized report bytes in selection order. +/// The single general run pipeline. Validates the run through the shared +/// preflight, resolves the analyzer set + per-phase format, then dispatches +/// every requested analyzer through `registry` by id, returning each phase's +/// serialized report bytes in selection order. /// -/// This is the one place dispatch happens; there is no per-`(analyzer, format)` -/// branching. Static analyzers are dispatched first, then -/// history analyzers; within each phase the handlers run -/// in the user-requested order. Format resolution mirrors the reference implementation -/// `analyze.ResolveFormats` + the `--ndjson`/`timeseries` modifier. +/// Static analyzers are dispatched first, then history analyzers; within each +/// phase the handlers run in the user-requested order. /// /// # Errors /// /// Returns [`PipelineError`] when the selection is empty, references an unknown -/// id, the format is unsupported, or an analyzer cannot produce a report. +/// id, the preflight rejects the path / repository / flags / format, or a +/// handler cannot produce its report. pub fn run_pipeline( registry: &Registry, ctx: &RunContext, @@ -273,77 +285,80 @@ pub fn run_pipeline( } let (static_ids, history_ids) = registry.split(ids); + let owned = |v: &[&String]| -> Vec<String> { v.iter().map(|s| (*s).clone()).collect() }; + crate::handlers::preflight(ctx, &owned(&static_ids), &owned(&history_ids))?; let (static_format, history_format) = resolve_formats( &ctx.raw_format(), !static_ids.is_empty(), !history_ids.is_empty(), ) - .map_err(|_| PipelineError::UnsupportedFormat(ctx.raw_format()))?; + .map_err(|_| HandlerError::UnsupportedFormat(ctx.raw_format()))?; let mut outputs = Vec::with_capacity(ids.len()); - // Static phase: parse the folder once (each handler walks the same root); - // dispatch each static analyzer by id. The per-analyzer reports are - // independent (every id was validated against the registry above), so they - // are COMPUTED concurrently and emitted in the selection order; the first - // failed analyzer in that order surfaces its error exactly as the - // sequential dispatch did. - let static_reports: Vec<Option<Vec<u8>>> = crate::handlers::run_concurrent( + // Static phase: the per-analyzer reports are independent (every id was + // validated against the registry above), so they are COMPUTED concurrently + // and emitted in the selection order; the first failed analyzer in that + // order surfaces its error. + let static_reports: Vec<Result<Vec<u8>, HandlerError>> = crate::handlers::run_concurrent( static_ids.len(), crate::handlers::ANALYZER_CONCURRENCY, - |i| { - registry - .lookup(static_ids[i]) - .and_then(|entry| (entry.run)(ctx, &static_format)) - }, + |i| dispatch(registry, static_ids[i], ctx, &static_format), ); for (id, bytes) in static_ids.iter().zip(static_reports) { - let bytes = bytes.ok_or_else(|| PipelineError::AnalyzerFailed((*id).clone()))?; outputs.push(PhaseOutput { id: (*id).clone(), - bytes, + bytes: bytes?, }); } - // History phase: one revwalk feeds each history analyzer (reference: - // runHistoryPhase). The `--ndjson` modifier turns `timeseries` into - // `timeseries+ndjson` exactly as the reference `applyNDJSONModifier` does. - // The shared UAST walk for the co-selected heavy analyzers is pre-computed - // as one task, then the per-analyzer reports are computed concurrently and - // emitted in the selection order. + // History phase: one revwalk feeds each history analyzer. The `--ndjson` + // modifier turns `timeseries` into `timeseries+ndjson`. The shared UAST + // walk for the co-selected heavy analyzers is pre-computed as one task, + // then the per-analyzer reports are computed concurrently and emitted in + // the selection order. let history_format = apply_ndjson_modifier(&history_format, ctx.ndjson()); crate::handlers::uast_walk::prewarm(ctx.matches); - let history_reports: Vec<Option<Vec<u8>>> = crate::handlers::run_concurrent( + let history_reports: Vec<Result<Vec<u8>, HandlerError>> = crate::handlers::run_concurrent( history_ids.len(), crate::handlers::ANALYZER_CONCURRENCY, - |i| { - registry - .lookup(history_ids[i]) - .and_then(|entry| (entry.run)(ctx, &history_format)) - }, + |i| dispatch(registry, history_ids[i], ctx, &history_format), ); for (id, bytes) in history_ids.iter().zip(history_reports) { - let bytes = bytes.ok_or_else(|| PipelineError::AnalyzerFailed((*id).clone()))?; outputs.push(PhaseOutput { id: (*id).clone(), - bytes, + bytes: bytes?, }); } Ok(outputs) } +/// Runs one registered handler (the id was validated by the caller). +fn dispatch( + registry: &Registry, + id: &str, + ctx: &RunContext, + format: &str, +) -> Result<Vec<u8>, HandlerError> { + let entry = registry.lookup(id).ok_or_else(|| HandlerError::NoReport { + analyzer: id.to_string(), + format: format.to_string(), + })?; + (entry.run)(ctx, format) +} + #[cfg(test)] mod tests { use super::*; - fn stub_static(_ctx: &RunContext, _fmt: &str) -> Option<Vec<u8>> { - Some(b"static-report".to_vec()) + fn stub_static(_ctx: &RunContext, _fmt: &str) -> Result<Vec<u8>, HandlerError> { + Ok(b"static-report".to_vec()) } - fn stub_history(_ctx: &RunContext, fmt: &str) -> Option<Vec<u8>> { - Some(format!("history:{fmt}").into_bytes()) + fn stub_history(_ctx: &RunContext, fmt: &str) -> Result<Vec<u8>, HandlerError> { + Ok(format!("history:{fmt}").into_bytes()) } fn test_registry() -> Registry { @@ -361,6 +376,14 @@ mod tests { r } + /// A real (empty) git repository so the shared preflight passes for the + /// stubbed history analyzer. + fn repo_dir() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + git2::Repository::init(dir.path()).expect("git init"); + dir + } + fn run_matches(args: &[&str]) -> ArgMatches { crate::flags::build_run_command() .no_binary_name(true) @@ -382,9 +405,6 @@ mod tests { #[test] fn absent_analyzers_flag_defaults_to_all() { - // `codefang run` with no -a resolves to the full set (`*`), matching the - // reference command's empty-selection behavior (run every analyzer + - // combined output) instead of erroring with the dispatch-blocked path. let m = run_matches(&["."]); let ctx = RunContext::from_matches(&m); assert_eq!(ctx.analyzer_ids(), vec!["*".to_string()]); @@ -425,11 +445,13 @@ mod tests { #[test] fn dispatches_static_then_history_in_order() { let r = test_registry(); + let dir = repo_dir(); let m = run_matches(&[ "-a", "history/burndown,static/complexity", "--format", "json", + dir.path().to_str().unwrap(), ]); let ctx = RunContext::from_matches(&m); let ids = vec![ @@ -446,12 +468,14 @@ mod tests { #[test] fn ndjson_modifier_applies_to_history_format() { let r = test_registry(); + let dir = repo_dir(); let m = run_matches(&[ "-a", "history/burndown", "--format", "timeseries", "--ndjson", + dir.path().to_str().unwrap(), ]); let ctx = RunContext::from_matches(&m); let ids = vec!["history/burndown".to_string()]; @@ -465,4 +489,24 @@ mod tests { let ctx = RunContext::from_matches(&m); assert_eq!(ctx.path, "/some/repo"); } + + /// The preflight runs inside the pipeline too, so library callers get the + /// same named errors the CLI prints. + #[test] + fn pipeline_surfaces_preflight_errors() { + let r = test_registry(); + let m = run_matches(&["-a", "static/complexity", "/definitely/not/here"]); + let ctx = RunContext::from_matches(&m); + let err = run_pipeline(&r, &ctx, &["static/complexity".to_string()]).unwrap_err(); + assert_eq!(err.to_string(), "path not found: /definitely/not/here"); + + let dir = tempfile::tempdir().expect("tempdir"); + let m = run_matches(&["-a", "history/burndown", dir.path().to_str().unwrap()]); + let ctx = RunContext::from_matches(&m); + let err = run_pipeline(&r, &ctx, &["history/burndown".to_string()]).unwrap_err(); + assert_eq!( + err.to_string(), + format!("cannot open repository: {}", dir.path().display()) + ); + } } diff --git a/crates/cf-commands/src/version.rs b/crates/cf-commands/src/version.rs index b9031e3c..f28f758f 100644 --- a/crates/cf-commands/src/version.rs +++ b/crates/cf-commands/src/version.rs @@ -3,26 +3,23 @@ //! Prints, on STDOUT, with exit code 0 and no flags: //! //! ```text -//! codefang %s (commit: %s, built: %s)\n +//! codefang <version> (commit: <commit>, built: <date>)\n //! ``` //! -//! (`version.Version`, `version.Commit`, `version.Date`). The exact bytes — with -//! the trailing newline — come from [`cf_version::codefang_version_line`], which -//! reproduces the reference implementation defaults `dev` / `none` / `unknown` when nothing is -//! injected at build time. This subcommand is a Layer-D CLI golden target -//! (DESIGN §6), so the output must be byte-identical to the reference binary. +//! * `<version>` — `CF_VERSION` / `GIT_VERSION` injected at build time +//! (`cf-version`), else the crate version. +//! * `<commit>` — `CF_COMMIT` / `GIT_COMMIT` / `GIT_HASH` injected at build +//! time, else the commit `HEAD` of the checkout the build ran in (found by +//! `cf-version`'s build script), else `unknown`. +//! * `<date>` — `CF_DATE` / `SOURCE_DATE_EPOCH` injected at build time, else +//! `unknown`; the wall clock is never read, so builds stay reproducible. /// Returns the exact bytes the `codefang version` subcommand writes to STDOUT, -/// including the trailing newline: `codefang <version> (commit: <commit>, -/// built: <date>)\n`. mirrors the reference implementation `fmt.Fprintf(os.Stdout, "codefang %s (commit: -/// %s, built: %s)\n", ...)`. +/// including the trailing newline. /// /// ``` /// let line = cf_commands::version::version_output(); -/// // Always: the `codefang ` prefix, the `(commit: ..., built: ...)` shape, -/// // and a single trailing newline. -/// assert!(line.starts_with("codefang ")); -/// assert!(line.contains(" (commit: ")); +/// assert!(line.starts_with(&format!("codefang {} (commit: ", env!("CARGO_PKG_VERSION")))); /// assert!(line.contains(", built: ")); /// assert!(line.ends_with(")\n")); /// ``` @@ -31,12 +28,8 @@ pub fn version_output() -> String { cf_version::codefang_version_line() } -/// Build the `version` [`clap::Command`]. -/// -/// The reference implementation's `versionCmd` is `Use: "version"`, `Short: "Show codefang version"`, with -/// a `Run` that prints [`version_output`] and exits 0. It declares no flags. -/// The printing happens in the binary's dispatch (so this builder only models -/// the command surface for parity with cobra's `--help`/usage output). +/// Build the `version` [`clap::Command`] (no flags; the dispatch prints +/// [`version_output`]). #[must_use] pub fn build_version_command() -> clap::Command { clap::Command::new("version").about("Show codefang version") @@ -47,23 +40,21 @@ mod tests { use super::*; #[test] - fn version_output_matches_go_format_with_defaults() { - // With no build-time injection cf-version uses the reference implementation's dev/none/unknown. + fn version_names_the_crate_version_and_the_build_commit() { assert_eq!( version_output(), - "codefang dev (commit: none, built: unknown)\n" + format!( + "codefang {} (commit: {}, built: {})\n", + env!("CARGO_PKG_VERSION"), + cf_version::COMMIT, + cf_version::DATE + ) ); } - #[test] - fn version_output_has_trailing_newline() { - assert!(version_output().ends_with('\n')); - } - #[test] fn version_command_has_no_flags() { let cmd = build_version_command(); - // Only the implicit help arg may be present; no user-facing flags. let user_flags: Vec<_> = cmd .get_arguments() .filter_map(clap::Arg::get_long) diff --git a/crates/cf-commands/tests/flag_parity.rs b/crates/cf-commands/tests/flag_parity.rs index b65e9197..83ae0ad0 100644 --- a/crates/cf-commands/tests/flag_parity.rs +++ b/crates/cf-commands/tests/flag_parity.rs @@ -1,12 +1,14 @@ -//! Cross-format flag parity for the static analyzers. +//! Cross-format and cross-selection parity for the static analyzers. //! //! A shipped regression honored `--include-generated` / `--include-vendored` //! only on the JSON path (text/compact/yaml/bin silently ignored them) and made -//! `--languages` a complete no-op. The fix threads the ONE shared -//! [`StaticFilter`] built from the run flags through every static analyzer's -//! folder walk in every output format; these tests lock that in by reading the -//! same total-functions figure back out of several encodings and asserting they -//! move together when the filter changes. +//! `--languages` a complete no-op; a later one applied the generated-content +//! marker nowhere in the static phase. The fix threads the ONE shared +//! [`StaticFilter`] through the ONE shared static walk every analyzer and +//! every output format consume; these tests lock that in by reading the same +//! total figure back out of several encodings and asserting they move +//! together when the filter changes, and by asserting that a `static/*` +//! selection reports each analyzer exactly as its single run does. //! //! Fixture: a multi-language tree with one function per file spanning the //! path-policy classes (normal `.go`, suffix-generated `.pb.go`, content-marker @@ -15,9 +17,23 @@ use cf_commands::handlers::section_render::render_text_report; use cf_commands::handlers::{ - static_complexity, static_complexity_yaml, static_halstead, static_multi_json, - static_multi_text, StaticFilter, + static_clones, static_cohesion, static_comments, static_complexity, static_complexity_yaml, + static_halstead, static_imports, static_json, static_multi_bin, static_multi_json, + static_multi_text, static_single_bin, StaticFilter, }; +use cf_gojson::GoValue; +use serde_json::Value; + +/// Every static analyzer in registry (merged-section) order. +const STATIC_ANALYZERS: [&str; 7] = [ + "static/clones", + "static/complexity", + "static/comments", + "static/halstead", + "static/cohesion", + "static/imports", + "static/composition", +]; /// Builds the multi-language fixture tree. Every file holds exactly ONE /// function, so each analyzed file contributes 1 to "Total Functions". @@ -35,9 +51,9 @@ fn fixture() -> tempfile::TempDir { "package main\n\nfunc genOne() int {\n\treturn 1\n}\n", ) .unwrap(); - // Generated by CONTENT MARKER only: the walk filter classifies by PATH - // (`pathpolicy.Exclude(path, nil, opts)` — no content is read before the - // skip decision), so this file is analyzed under either policy. + // Generated by CONTENT MARKER only: the shared walk reads the file and + // applies the marker over its first 512 bytes, so this file is excluded + // under the default policy exactly like the suffix-generated one. std::fs::write( dir.path().join("marked.go"), "// Code generated by protoc-gen-go. DO NOT EDIT.\npackage main\n\nfunc markedOne() int {\n\treturn 2\n}\n", @@ -165,14 +181,14 @@ fn complexity_totals_agree_across_formats_and_observe_policy_flags() { assert_eq!(json, yaml, "json vs yaml disagree under permissive policy"); assert_eq!(json, 5, "all 5 one-function files must be analyzed"); - // Default policy: the generated + vendored files drop out of EVERY format. - // A format that ignores the flags would keep reporting the same total. + // Default policy: the generated (suffix AND marker) + vendored files drop + // out of EVERY format, leaving normal.go and helper.py. let (djson, dtext, dyaml) = complexity_totals(path, &StaticFilter::default()); assert_eq!(djson, dtext, "json vs text disagree under default policy"); assert_eq!(djson, dyaml, "json vs yaml disagree under default policy"); - assert!( - json > djson, - "include-generated/include-vendored must reach the walk (permissive {json} vs default {djson})" + assert_eq!( + djson, 2, + "default policy must exclude gen.pb.go, marked.go and vendor/dep.go" ); } @@ -189,9 +205,9 @@ fn halstead_totals_agree_across_formats_and_observe_policy_flags() { let (djson, dtext, dyaml) = halstead_totals(path, &StaticFilter::default()); assert_eq!(djson, dtext, "json vs text disagree under default policy"); assert_eq!(djson, dyaml, "json vs yaml disagree under default policy"); - assert!( - json > djson, - "include-generated/include-vendored must reach the walk (permissive {json} vs default {djson})" + assert_eq!( + djson, 2, + "default policy must exclude gen.pb.go, marked.go and vendor/dep.go" ); } @@ -273,3 +289,186 @@ fn multi_analyzer_merge_paths_observe_the_walk_filter() { "static_multi_json ignored the language globs" ); } + +// --------------------------------------------------------------------------- +// Every analyzer, every format: the single run and the `static/*` selection +// report the same numbers. +// --------------------------------------------------------------------------- + +/// The single-run json report value of `id` through its crate-owned builder +/// (the exact value `--format json` encodes). +fn single_json_value(id: &str, path: &str, filter: &StaticFilter, per_file: bool) -> GoValue { + let value = match id { + "static/clones" => static_clones::clones_report_value_flags(path, filter, per_file), + "static/complexity" => { + static_complexity::complexity_report_value_flags(path, filter, per_file) + } + "static/comments" => static_comments::comments_report_value_flags(path, filter, per_file), + "static/halstead" => static_halstead::halstead_report_value_flags(path, filter, per_file), + "static/cohesion" => static_cohesion::cohesion_report_value_flags(path, filter, per_file), + "static/imports" => static_imports::imports_report_value_flags(path, filter, per_file), + "static/composition" => static_json::composition_report_value_flags(path, filter, per_file), + other => panic!("unknown static analyzer {other}"), + }; + value.unwrap_or_else(|| panic!("{id}: no report")) +} + +/// The single-run text render of `id` (the summary value the text handler +/// feeds the renderer). +fn single_text(id: &str, path: &str, filter: &StaticFilter) -> String { + let value = match id { + "static/clones" => static_clones::clones_report_value_flags(path, filter, false), + "static/complexity" => static_complexity::complexity_report_value_summary(path, filter), + "static/comments" => static_comments::comments_report_value_summary(path, filter), + "static/halstead" => static_halstead::halstead_report_value_summary(path, filter), + "static/cohesion" => static_cohesion::cohesion_report_value_summary(path, filter), + "static/imports" => static_imports::imports_report_value_flags(path, filter, false), + "static/composition" => static_json::composition_report_value_opts(path, filter), + other => panic!("unknown static analyzer {other}"), + }; + let text = render_text_report(&value.unwrap_or_else(|| panic!("{id}: no report"))); + strip_ansi(std::str::from_utf8(&text).expect("utf-8 text")) +} + +/// Parses a report GoValue (or merged json bytes) into a serde tree. +fn parsed(bytes: &[u8]) -> Value { + serde_json::from_slice(bytes).expect("report json parses") +} + +/// The first (only) section of a single-run report. +fn single_section(id: &str, path: &str, filter: &StaticFilter, per_file: bool) -> Value { + let value = single_json_value(id, path, filter, per_file); + parsed(&cf_gojson::marshal(&value))["sections"][0].clone() +} + +/// The merged `static/*` json carries each analyzer's section EXACTLY as the +/// single run builds it — with and without `--per-file`, under the default +/// and the permissive filter. +#[test] +fn merged_json_sections_equal_the_single_runs_for_every_analyzer() { + let dir = fixture(); + let path = dir.path().to_str().unwrap(); + for filter in [StaticFilter::default(), permissive()] { + for per_file in [false, true] { + let merged = parsed( + &static_multi_json(&["static/*"], path, &filter, per_file).expect("merged json"), + ); + let sections = merged["sections"].as_array().expect("sections array"); + assert_eq!(sections.len(), STATIC_ANALYZERS.len()); + for (section, id) in sections.iter().zip(STATIC_ANALYZERS) { + assert_eq!( + section, + &single_section(id, path, &filter, per_file), + "{id} (per_file={per_file}): merged section differs from the single run" + ); + } + } + } +} + +/// The merged `static/*` bin output is the concatenation of the single-run +/// CFB1 envelopes in registry order. +#[test] +fn merged_bin_is_the_concatenation_of_the_single_runs() { + let dir = fixture(); + let path = dir.path().to_str().unwrap(); + for filter in [StaticFilter::default(), permissive()] { + let merged = static_multi_bin(&["static/*"], path, &filter).expect("merged bin"); + let mut expected = Vec::new(); + for id in STATIC_ANALYZERS { + expected.extend(static_single_bin(id, path, &filter).expect("single bin")); + } + assert!( + merged == expected, + "merged bin differs from the single runs" + ); + } +} + +/// The merged `static/*` text render contains each analyzer's single text +/// body verbatim (the executive summary is the only addition). +#[test] +fn merged_text_contains_every_single_text_body() { + let dir = fixture(); + let path = dir.path().to_str().unwrap(); + let filter = StaticFilter::default(); + let merged = static_multi_text(&["static/*"], path, &filter).expect("merged text"); + let merged = strip_ansi(std::str::from_utf8(&merged).expect("utf-8")); + for id in STATIC_ANALYZERS { + let single = single_text(id, path, &filter); + let body = single.trim(); + assert!( + merged.contains(body), + "{id}: single text body missing from the merged render:\n--- single ---\n{body}\n--- merged ---\n{merged}" + ); + } +} + +/// The headline total of every analyzer agrees across json, text and yaml +/// (the formats that print it), under both filters: ONE walk feeds them all. +#[test] +fn headline_totals_agree_across_formats_for_every_analyzer() { + let dir = fixture(); + let path = dir.path().to_str().unwrap(); + // (id, json/text metric label, yaml key — `None` when the yaml value + // carries no comparable total). + const TOTALS: [(&str, &str, Option<&str>); 7] = [ + ("static/clones", "Total Functions", Some("total_functions:")), + ( + "static/complexity", + "Total Functions", + Some("total_functions:"), + ), + ( + "static/comments", + "Total Functions", + Some("total_functions:"), + ), + ( + "static/halstead", + "Total Functions", + Some("total_functions:"), + ), + ( + "static/cohesion", + "Total Functions", + Some("total_functions:"), + ), + ("static/imports", "Total Files", None), + ("static/composition", "Total Files", Some("total_files:")), + ]; + for filter in [StaticFilter::default(), permissive()] { + for (id, label, yaml_key) in TOTALS { + let json = cf_gojson::marshal(&single_json_value(id, path, &filter, false)); + let json_total = + digits_after(std::str::from_utf8(&json).unwrap(), &format!("\"{label}\"")); + let text_total = digits_after(&single_text(id, path, &filter), label); + assert_eq!( + json_total, text_total, + "{id}: json vs text {label} disagree" + ); + if let Some(key) = yaml_key { + let yaml = single_yaml(id, path, &filter); + let yaml_total = digits_after(std::str::from_utf8(&yaml).unwrap(), key); + assert_eq!( + json_total, yaml_total, + "{id}: json vs yaml {label} disagree" + ); + } + } + } +} + +/// The single-run yaml bytes of `id`. +fn single_yaml(id: &str, path: &str, filter: &StaticFilter) -> Vec<u8> { + let bytes = match id { + "static/clones" => static_clones::clones_report_yaml(path, filter), + "static/complexity" => static_complexity_yaml::complexity_report_yaml(path, filter), + "static/comments" => static_comments::comments_report_yaml(path, filter), + "static/halstead" => static_halstead::halstead_yaml_report(path, filter), + "static/cohesion" => static_cohesion::cohesion_report_yaml(path, filter), + "static/composition" => static_json::composition_yaml_opts(path, filter), + other => panic!("no yaml total for {other}"), + }; + bytes.unwrap_or_else(|| panic!("{id}: no yaml")) +} diff --git a/crates/cf-comments/Cargo.toml b/crates/cf-comments/Cargo.toml index 7043e745..6f22e804 100644 --- a/crates/cf-comments/Cargo.toml +++ b/crates/cf-comments/Cargo.toml @@ -15,3 +15,9 @@ publish.workspace = true [dependencies] cf-gojson = { workspace = true } cf-uast-node = { path = "../cf-uast-node" } + +[dev-dependencies] +# Unit tests analyze REAL parsed trees (tree-sitter grammars through the UAST +# parser) so the placement rules are proven against the language mappings, +# not against hand-built nodes alone. +cf-uast = { path = "../cf-uast" } diff --git a/crates/cf-comments/src/aggregator.rs b/crates/cf-comments/src/aggregator.rs index ce3a3fab..d1237e06 100644 --- a/crates/cf-comments/src/aggregator.rs +++ b/crates/cf-comments/src/aggregator.rs @@ -1,158 +1,174 @@ -//! Result aggregation across multiple comment analyses: sum of count keys, -//! average of numeric keys. +//! Cross-file aggregation of per-file comment metrics. //! -//! Detailed-table merging (the `comments`/`functions` tables) is provided by -//! `cf-analyzers-common` and is wired in by the framework layer, not here. - -use std::collections::BTreeMap; - -use cf_gojson::{GoMap, GoValue, MapOrigin}; - -/// Numeric keys that are averaged across files. -const NUMERIC_KEYS: &[&str] = &[ - "overall_score", - "good_comments_ratio", - "documentation_coverage", -]; - -/// Count keys that are summed across files. -const COUNT_KEYS: &[&str] = &[ - "total_comments", - "good_comments", - "bad_comments", - "total_functions", - "documented_functions", - "total_comment_details", -]; - -/// A single file's report as a key→numeric map (the slice of a `Report` the -/// aggregator consumes). -pub type NumericReport = BTreeMap<String, f64>; - -/// Aggregates comment results across files. +//! Counters are summed and every ratio is computed from the sums: coverage is +//! Σdocumented / Σfunctions and the good-comment ratio is Σgood / Σcomments. +//! A file without declarations (or without comments) therefore never dilutes +//! the respective ratio — there is no per-file mean anywhere. + +use crate::types::{CommentCounts, CommentDetail, CommentMetrics, FunctionInfo}; + +/// Where a file's rows come from. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileStamp { + /// Path relative to the analyzed root. + pub source_file: String, + /// Language name (empty when unknown). + pub language: String, + /// Directory part of `source_file` (`.` at the root). + pub directory: String, +} + +/// One file's metrics with its stamp. +#[derive(Debug, Clone, PartialEq)] +pub struct FileReport { + /// Where the rows come from. + pub stamp: FileStamp, + /// The file's metrics. + pub metrics: CommentMetrics, +} + +/// Accumulates per-file metrics in insertion order. #[derive(Debug, Default)] pub struct Aggregator { - sums: BTreeMap<String, f64>, - numeric_totals: BTreeMap<String, f64>, - file_count: i64, + files: Vec<FileReport>, + counts: CommentCounts, } impl Aggregator { - /// Creates a new aggregator. + /// Creates an empty aggregator. + #[must_use] pub fn new() -> Self { Aggregator::default() } - /// Aggregates a batch of per-file numeric reports. - /// - /// Count keys are summed; numeric keys are averaged over the number of - /// files. Iteration order over `results` does not affect the result - /// (sums/averages are commutative). - pub fn aggregate(&mut self, results: &BTreeMap<String, NumericReport>) { - for report in results.values() { - self.file_count += 1; - for &k in COUNT_KEYS { - if let Some(v) = report.get(k) { - *self.sums.entry(k.to_string()).or_insert(0.0) += v; - } - } - for &k in NUMERIC_KEYS { - if let Some(v) = report.get(k) { - *self.numeric_totals.entry(k.to_string()).or_insert(0.0) += v; - } - } - } + /// Adds one file's metrics. + pub fn add(&mut self, stamp: FileStamp, metrics: CommentMetrics) { + self.counts.add(&metrics.counts); + self.files.push(FileReport { stamp, metrics }); } - /// Returns the aggregated result as a [`GoValue`]. - /// - /// Empty aggregation yields zeros. - pub fn get_result(&self) -> GoValue { - let mut m = GoMap::new(MapOrigin::Map); - for &k in COUNT_KEYS { - let v = self.sums.get(k).copied().unwrap_or(0.0); - m.push(k, GoValue::Int(v as i64)); - } - for &k in NUMERIC_KEYS { - let total = self.numeric_totals.get(k).copied().unwrap_or(0.0); - let avg = if self.file_count > 0 { - total / self.file_count as f64 - } else { - 0.0 - }; - m.push(k, GoValue::Float(avg)); - } - GoValue::Map(m) + /// Adds a file that produced no metrics (an unparsable file): it is + /// listed, with all counters at zero. + pub fn add_empty(&mut self, stamp: FileStamp) { + self.add(stamp, CommentMetrics::default()); + } + + /// The files added so far, in insertion order. + #[must_use] + pub fn files(&self) -> &[FileReport] { + &self.files + } + + /// Number of files added. + #[must_use] + pub fn file_count(&self) -> usize { + self.files.len() + } + + /// The summed counters; their ratios and message are the aggregate's. + #[must_use] + pub fn counts(&self) -> CommentCounts { + self.counts + } + + /// Every comment row with its file stamp, in file then line order. + pub fn comments(&self) -> impl Iterator<Item = (&FileStamp, &CommentDetail)> { + self.files + .iter() + .flat_map(|f| f.metrics.comments.iter().map(move |c| (&f.stamp, c))) + } + + /// Every declaration row with its file stamp, in file then document + /// order. + pub fn functions(&self) -> impl Iterator<Item = (&FileStamp, &FunctionInfo)> { + self.files + .iter() + .flat_map(|f| f.metrics.functions.iter().map(move |fun| (&f.stamp, fun))) } } #[cfg(test)] mod tests { use super::*; + use crate::types::NO_COMMENTS_MESSAGE; - fn report(pairs: &[(&str, f64)]) -> NumericReport { - pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect() + fn stamp(name: &str) -> FileStamp { + FileStamp { + source_file: name.to_string(), + language: "go".to_string(), + directory: ".".to_string(), + } } - fn get<'a>(v: &'a GoValue, key: &str) -> &'a GoValue { - match v { - GoValue::Map(m) => &m.entries().iter().find(|(k, _)| k == key).unwrap().1, - other => panic!("not a map: {other:?}"), + fn metrics(counts: CommentCounts) -> CommentMetrics { + CommentMetrics { + counts, + ..CommentMetrics::default() } } #[test] - fn aggregate_sums_and_averages() { + fn ratios_are_computed_from_the_sums() { let mut agg = Aggregator::new(); - let mut results: BTreeMap<String, NumericReport> = BTreeMap::new(); - results.insert( - "file1".to_string(), - report(&[ - ("total_comments", 2.0), - ("good_comments", 1.0), - ("bad_comments", 1.0), - ("total_functions", 3.0), - ("documented_functions", 1.0), - ("overall_score", 0.5), - ]), + agg.add( + stamp("a.go"), + metrics(CommentCounts { + total_comments: 1, + good_comments: 1, + bad_comments: 0, + total_functions: 1, + documented_functions: 1, + }), ); - results.insert( - "file2".to_string(), - report(&[ - ("total_comments", 1.0), - ("good_comments", 1.0), - ("bad_comments", 0.0), - ("total_functions", 2.0), - ("documented_functions", 1.0), - ("overall_score", 1.0), - ]), + agg.add( + stamp("b.go"), + metrics(CommentCounts { + total_comments: 1, + good_comments: 1, + bad_comments: 0, + total_functions: 4, + documented_functions: 1, + }), ); - agg.aggregate(&results); - - let res = agg.get_result(); - assert_eq!(get(&res, "total_comments"), &GoValue::Int(3)); - assert_eq!(get(&res, "good_comments"), &GoValue::Int(2)); - assert_eq!(get(&res, "bad_comments"), &GoValue::Int(1)); - assert_eq!(get(&res, "total_functions"), &GoValue::Int(5)); - assert_eq!(get(&res, "documented_functions"), &GoValue::Int(2)); - match get(&res, "overall_score") { - GoValue::Float(f) => assert!((f - 0.75).abs() < 1e-9), - other => panic!("overall_score not float: {other:?}"), - } + let counts = agg.counts(); + assert_eq!(counts.total_functions, 5); + assert_eq!(counts.documented_functions, 2); + // 2/5, not the mean of 1/1 and 1/4 (0.625). + assert!((counts.documentation_coverage() - 0.4).abs() < 1e-9); + assert!((counts.overall_score() - 1.0).abs() < 1e-9); } #[test] - fn empty_result_is_zero() { + fn a_file_without_declarations_does_not_dilute_coverage() { + let mut agg = Aggregator::new(); + agg.add( + stamp("a.go"), + metrics(CommentCounts { + total_comments: 1, + good_comments: 1, + bad_comments: 0, + total_functions: 1, + documented_functions: 1, + }), + ); + agg.add_empty(stamp("README.md")); + assert_eq!(agg.file_count(), 2); + assert!((agg.counts().documentation_coverage() - 1.0).abs() < 1e-9); + assert_eq!( + agg.counts().message(), + "Excellent comment quality and placement" + ); + } + + #[test] + fn empty_aggregation_is_the_empty_report() { let agg = Aggregator::new(); - let res = agg.get_result(); - assert_eq!(get(&res, "total_comments"), &GoValue::Int(0)); - assert_eq!(get(&res, "good_comments"), &GoValue::Int(0)); - assert_eq!(get(&res, "bad_comments"), &GoValue::Int(0)); - assert_eq!(get(&res, "total_functions"), &GoValue::Int(0)); - assert_eq!(get(&res, "documented_functions"), &GoValue::Int(0)); - match get(&res, "overall_score") { - GoValue::Float(f) => assert_eq!(*f, 0.0), - other => panic!("overall_score not float: {other:?}"), - } + assert_eq!(agg.file_count(), 0); + assert_eq!(agg.counts(), CommentCounts::default()); + assert_eq!(agg.counts().overall_score(), 0.0); + assert_eq!(agg.counts().documentation_coverage(), 0.0); + assert_eq!(agg.counts().message(), NO_COMMENTS_MESSAGE); + assert_eq!(agg.comments().count(), 0); + assert_eq!(agg.functions().count(), 0); } } diff --git a/crates/cf-comments/src/analyzer.rs b/crates/cf-comments/src/analyzer.rs index 349e0506..c5733b06 100644 --- a/crates/cf-comments/src/analyzer.rs +++ b/crates/cf-comments/src/analyzer.rs @@ -1,31 +1,59 @@ //! The comments analyzer. //! -//! Finds comment and function/method/class/interface/struct nodes, groups -//! consecutive comments into blocks (sorted by start line), scores each block -//! by its placement relative to the closest target, and computes aggregate -//! metrics. +//! Finds comment nodes and declarations (function / method / class / +//! interface / struct), groups adjacent comments into blocks, places every +//! block against the declarations ([`crate::placement`]) and derives the +//! per-file metrics: a declaration is documented iff a block is placed +//! `above` it; a comment is good iff its block is placed `above` something. //! //! Report building is routed through [`cf_gojson::GoValue`] using a //! *map-origin* [`cf_gojson::GoMap`] so the encoder byte-sorts keys at encode -//! time (report-format contract, DESIGN §2.2). Nothing here uses `serde_json`. +//! time. Nothing here uses `serde_json`. use cf_gojson::{GoMap, GoValue, MapOrigin}; use cf_uast_node::Node; -use crate::traverse::{extract_entity_name, find_nodes_by_type}; +use crate::placement::{anchor_line, resolve, BlockSpan, Resolved, TargetSpan}; +use crate::traverse::{ + comment_lines, comment_text, declaration_name, find_nodes_with_role, has_role, node_lines, +}; use crate::types::{ - uast, CommentBlock, CommentConfig, CommentDetail, CommentMetrics, CommentReportItem, - FunctionInfo, FunctionReportItem, + uast, CommentConfig, CommentCounts, CommentDetail, CommentMetrics, FunctionInfo, Placement, + ROLE_ATTRIBUTE, ROLE_BODY, ROLE_DOC, ROLE_NAME, ROLE_PARAMETER, ROLE_TYPE, UNKNOWN_NAME, }; -/// Scoring constants (report contract). -const SCORE_VALUE: f64 = 0.2; -const GAP_THRESHOLD_HIGH: i64 = 2; -const LEN_ARG_50: usize = 50; -const MAGIC_3: i64 = 3; -const MAGIC_999: i64 = 999; -const MAGIC_1000: i64 = 1000; -const UNKNOWN_NAME: &str = "unknown"; +/// Score of a comment attached to no declaration. +const UNASSOCIATED_PENALTY: f64 = -0.2; +/// Inline penalty for a declaration type without a configured penalty. +const DEFAULT_INLINE_PENALTY: f64 = -0.1; +/// Comment previews longer than this many bytes are shortened. +const PREVIEW_MAX_BYTES: usize = 50; +/// Bytes kept in a shortened preview before the ellipsis. +const PREVIEW_KEEP_BYTES: usize = 47; + +/// Node types counted as declarations. +const DECLARATION_TYPES: [&str; 5] = [ + uast::FUNCTION, + uast::METHOD, + uast::CLASS, + uast::INTERFACE, + uast::STRUCT, +]; +/// Declaration types that only count when they carry a name: a nameless +/// class/struct/interface node is a type literal or a class body, not a +/// declaration. +const NAMED_ONLY_TYPES: [&str; 3] = [uast::CLASS, uast::INTERFACE, uast::STRUCT]; +/// Node types that form a declaration's header (everything before the first +/// body statement): a `Doc` comment preceded only by these is a docstring. +const HEADER_TYPES: [&str; 7] = [ + uast::IDENTIFIER, + uast::PARAMETER, + uast::PATTERN, + uast::TYPE_ANNOTATION, + uast::DECORATOR, + uast::ATTRIBUTE, + uast::COMMENT, +]; /// Error returned when analysis is given no root. The error text is part of /// the CLI contract. @@ -46,40 +74,51 @@ pub struct Analyzer; impl Analyzer { /// Creates a new analyzer. + #[must_use] pub fn new() -> Self { Analyzer } /// Returns the analyzer name (`"comments"`). + #[must_use] pub fn name(&self) -> &'static str { "comments" } /// Returns the CLI flag (`"comments-analysis"`). + #[must_use] pub fn flag(&self) -> &'static str { "comments-analysis" } /// Returns the default analysis configuration. + #[must_use] pub fn default_config(&self) -> CommentConfig { CommentConfig::default_config() } - /// Performs comment analysis. + /// Performs comment analysis and returns the per-file report map. /// - /// With no comments found, returns the empty result (all counts zero, - /// `overall_score` 0.0): + /// A file with declarations but no comments keeps its declarations (all + /// undocumented): /// /// ``` /// use cf_comments::Analyzer; /// use cf_gojson::GoValue; - /// use cf_uast_node::Builder; + /// use cf_uast_node::{Builder, Positions}; /// - /// let root = Builder::new().with_type("File").build(); + /// let func = Builder::new() + /// .with_type("Function") + /// .with_props([("name".to_string(), "f".to_string())].into()) + /// .with_position(Some(Positions { start_line: 1, end_line: 3, ..Default::default() })) + /// .build(); + /// let mut root = Builder::new().with_type("File").build(); + /// root.add_child(func); /// let report = Analyzer::new().analyze(Some(&root)).unwrap(); /// let m = report.as_map().unwrap(); /// assert_eq!(m.get("total_comments"), Some(&GoValue::Int(0))); - /// assert_eq!(m.get("overall_score"), Some(&GoValue::Float(0.0))); + /// assert_eq!(m.get("total_functions"), Some(&GoValue::Int(1))); + /// assert_eq!(m.get("documented_functions"), Some(&GoValue::Int(0))); /// ``` /// /// # Errors @@ -89,11 +128,10 @@ impl Analyzer { self.analyze_with_find_depth(root, None) } - /// [`analyze`](Self::analyze) with the reference shared traverser's - /// `maxDepth` applied to the comment / function DISCOVERY walks: with - /// `Some(cap)`, only nodes at depth `<= cap` below the root (root at + /// [`analyze`](Self::analyze) with a depth cap on the discovery walks: + /// with `Some(cap)`, only nodes at depth `<= cap` below the root (root at /// depth 0) are found. The history `quality` analyzer's comments instance - /// runs with `maxDepth = 10`; the static surfaces run uncapped. + /// runs capped; the static surfaces run uncapped. /// /// # Errors /// @@ -103,586 +141,361 @@ impl Analyzer { root: Option<&Node>, find_depth: Option<usize>, ) -> Result<GoValue, NilRootNode> { - let root = root.ok_or(NilRootNode)?; - - let comments = self.find_comments(root, find_depth); - let functions = self.find_functions(root, find_depth); - - if comments.is_empty() { - return Ok(build_empty_result()); - } - - let config = self.default_config(); - let comment_details = self.analyze_comment_placement(&comments, &functions, &config); - let metrics = self.calculate_metrics(comment_details, &functions); - - Ok(self.build_result(&metrics, &functions)) - } - - // --- node discovery ----------------------------------------------------- - - fn find_comments<'a>(&self, root: &'a Node, find_depth: Option<usize>) -> Vec<&'a Node> { - match find_depth { - Some(cap) => crate::traverse::find_nodes_by_type_capped(root, &[uast::COMMENT], cap), - None => find_nodes_by_type(root, &[uast::COMMENT]), - } - } - - fn find_functions<'a>(&self, root: &'a Node, find_depth: Option<usize>) -> Vec<&'a Node> { - const TYPES: [&str; 5] = [ - uast::FUNCTION, - uast::METHOD, - uast::CLASS, - uast::INTERFACE, - uast::STRUCT, - ]; - match find_depth { - Some(cap) => crate::traverse::find_nodes_by_type_capped(root, &TYPES, cap), - None => find_nodes_by_type(root, &TYPES), - } - } - - // --- block grouping (sorted by line) ------------------------------------ - - fn analyze_comment_placement( - &self, - comments: &[&Node], - functions: &[&Node], - config: &CommentConfig, - ) -> Vec<CommentDetail> { - let blocks = self.group_comments_into_blocks(comments); - self.analyze_comment_blocks(&blocks, functions, config) - } - - fn group_comments_into_blocks(&self, comments: &[&Node]) -> Vec<CommentBlock> { - if comments.is_empty() { - return Vec::new(); - } - let sorted = self.sort_comments_by_line(comments); - self.create_comment_blocks(&sorted) - } - - /// Sorts comments by start line. When either node lacks a position the - /// comparator returns `Equal`, preserving input order — equivalent to the - /// reference comparator's "not less than" answer for well-formed inputs - /// (every comment node here carries a position). - fn sort_comments_by_line<'a>(&self, comments: &[&'a Node]) -> Vec<&'a Node> { - let mut sorted: Vec<&Node> = comments.to_vec(); - sorted.sort_by(|a, b| match (node_pos(a), node_pos(b)) { - (Some((a_start, _)), Some((b_start, _))) => a_start.cmp(&b_start), - _ => std::cmp::Ordering::Equal, - }); - sorted - } - - fn create_comment_blocks(&self, sorted: &[&Node]) -> Vec<CommentBlock> { - let mut blocks: Vec<CommentBlock> = Vec::new(); - let mut current: Option<CommentBlock> = None; - - for &comment in sorted { - let Some((start, end)) = node_pos(comment) else { - continue; - }; - let comment_start = start as i64; - let comment_end = end as i64; - - let start_new = match ¤t { - None => true, - Some(cur) => cur.comments.is_empty() || comment_start > cur.end_line + 1, - }; - - if start_new { - if let Some(cur) = current.take() { - if !cur.comments.is_empty() { - blocks.push(cur); - } - } - current = Some(CommentBlock { - comments: vec![comment.clone()], - start_line: comment_start, - end_line: comment_end, - full_text: node_token(comment).to_string(), - }); - } else if let Some(cur) = current.as_mut() { - cur.comments.push(comment.clone()); - cur.end_line = comment_end; - cur.full_text.push('\n'); - cur.full_text.push_str(node_token(comment)); - } - } - - if let Some(cur) = current.take() { - if !cur.comments.is_empty() { - blocks.push(cur); - } - } - blocks + Ok(build_report(&self.analyze_metrics(root, find_depth)?)) } - // --- block scoring ------------------------------------------------------ - - fn analyze_comment_blocks( - &self, - blocks: &[CommentBlock], - functions: &[&Node], - config: &CommentConfig, - ) -> Vec<CommentDetail> { - let mut details = Vec::new(); - for block in blocks { - details.extend(self.analyze_comment_block(block, functions, config)); - } - details - } - - fn analyze_comment_block( - &self, - block: &CommentBlock, - functions: &[&Node], - config: &CommentConfig, - ) -> Vec<CommentDetail> { - let block_detail = self.analyze_virtual_block(block, functions, config); - self.create_comment_details(block, &block_detail) - } - - fn create_comment_details( - &self, - block: &CommentBlock, - block_detail: &CommentDetail, - ) -> Vec<CommentDetail> { - block - .comments - .iter() - .map(|comment| CommentDetail { - kind: node_type(comment).to_string(), - token: node_token(comment).to_string(), - score: block_detail.score, - is_good: block_detail.is_good, - target_type: block_detail.target_type.clone(), - target_name: block_detail.target_name.clone(), - position: block_detail.position.clone(), - line_number: node_pos(comment).map_or(0, |(s, _)| s as i64), - }) - .collect() - } - - /// Scores a block using its (`start_line`, `end_line`) span as a single - /// virtual comment. - fn analyze_virtual_block( - &self, - block: &CommentBlock, - functions: &[&Node], - config: &CommentConfig, - ) -> CommentDetail { - let mut detail = CommentDetail { - kind: uast::COMMENT.to_string(), - token: block.full_text.clone(), - score: 0.0, - is_good: false, - target_type: String::new(), - target_name: String::new(), - position: String::new(), - line_number: block.start_line, - }; - - match self.find_closest_target(block.end_line, functions) { - Some(target) => { - detail.target_type = node_type(target).to_string(); - detail.target_name = self.extract_target_name(target); - detail.position = determine_position(block.start_line, block.end_line, target); - if is_comment_properly_placed(block.start_line, block.end_line, target) { - detail.score = config.reward_score; - detail.is_good = true; - } else { - detail.score = self.penalty_score(target, config); - detail.is_good = false; - } - } - None => { - detail.score = -SCORE_VALUE; - detail.is_good = false; - detail.position = "unassociated".to_string(); - } - } - detail - } - - fn penalty_score(&self, target: &Node, config: &CommentConfig) -> f64 { - config - .penalty_scores - .get(node_type(target)) - .copied() - .unwrap_or(-0.1) - } - - /// Finds the closest function/class to a comment span (see - /// [`calculate_distance`] for the asymmetric distance). - fn find_closest_target<'a>( - &self, - comment_end: i64, - functions: &[&'a Node], - ) -> Option<&'a Node> { - let mut closest: Option<&Node> = None; - let mut min_distance: i64 = -1; - for &function in functions { - let distance = calculate_distance(comment_end, function); - if min_distance == -1 || distance < min_distance { - min_distance = distance; - closest = Some(function); - } - } - closest - } - - fn extract_target_name(&self, target: &Node) -> String { - extract_entity_name(target).unwrap_or_else(|| UNKNOWN_NAME.to_string()) - } - - // --- metrics ------------------------------------------------------------ - - fn calculate_metrics( + /// Performs comment analysis and returns the typed per-file metrics. + /// + /// # Errors + /// + /// Returns [`NilRootNode`] when `root` is `None`. + pub fn analyze_metrics( &self, - details: Vec<CommentDetail>, - functions: &[&Node], - ) -> CommentMetrics { - let total_comments = details.len() as i64; - let mut good_comments = 0i64; - let mut bad_comments = 0i64; - for d in &details { - if d.is_good { - good_comments += 1; - } else { - bad_comments += 1; - } - } - let overall_score = if total_comments > 0 { - good_comments as f64 / total_comments as f64 - } else { - 0.0 - }; - - let mut function_summary: std::collections::BTreeMap<String, FunctionInfo> = - std::collections::BTreeMap::new(); - let mut documented_functions = 0i64; - for &function in functions { - let func_name = self.extract_target_name(function); - let mut info = FunctionInfo { - name: func_name.clone(), - kind: node_type(function).to_string(), - has_comment: false, - comment_type: String::new(), - }; - if has_good_comment(&func_name, &details) { - info.has_comment = true; - info.comment_type = comment_type_for(&func_name, &details); - documented_functions += 1; - } - function_summary.insert(func_name, info); - } - - CommentMetrics { - total_comments, - good_comments, - bad_comments, - overall_score, - comment_details: details, - function_summary, - total_functions: functions.len() as i64, - documented_functions, - } - } - - // --- report building (routed through cf-gojson) ------------------------- - - fn build_result(&self, metrics: &CommentMetrics, functions: &[&Node]) -> GoValue { - let comment_details_iface = self.build_comment_details_iface(&metrics.comment_details); - let comments_table = self.build_comments_table(&metrics.comment_details); - let functions_table = self.build_functions_table(functions, metrics); - let function_summary_iface = self.build_function_summary_iface(metrics); - - // Dynamic report map => map-origin so cf-gojson byte-sorts keys. - let mut m = GoMap::new(MapOrigin::Map); - m.push("total_comments", GoValue::Int(metrics.total_comments)); - m.push("good_comments", GoValue::Int(metrics.good_comments)); - m.push("bad_comments", GoValue::Int(metrics.bad_comments)); - m.push("overall_score", GoValue::Float(metrics.overall_score)); - m.push("total_functions", GoValue::Int(metrics.total_functions)); - m.push( - "documented_functions", - GoValue::Int(metrics.documented_functions), - ); - m.push( - "good_comments_ratio", - GoValue::Float(safe_div( - metrics.good_comments as f64, - metrics.total_comments as f64, - )), - ); - m.push( - "documentation_coverage", - GoValue::Float(safe_div( - metrics.documented_functions as f64, - metrics.total_functions as f64, - )), - ); - m.push( - "total_comment_details", - GoValue::Int(metrics.comment_details.len() as i64), - ); - m.push("comment_details", comment_details_iface); - m.push("comments", comments_table); - m.push("functions", functions_table); - m.push("function_summary", function_summary_iface); - m.push( - "message", - GoValue::Str(comment_message(metrics.overall_score)), - ); - GoValue::Map(m) - } - - fn build_comment_details_iface(&self, details: &[CommentDetail]) -> GoValue { - let items = details - .iter() - .map(|d| { - let mut m = GoMap::new(MapOrigin::Map); - m.push("type", GoValue::Str(d.kind.clone())); - m.push("token", GoValue::Str(d.token.clone())); - m.push("position", GoValue::Str(d.position.clone())); - m.push("score", GoValue::Float(d.score)); - m.push("is_good", GoValue::Bool(d.is_good)); - m.push("target_type", GoValue::Str(d.target_type.clone())); - m.push("target_name", GoValue::Str(d.target_name.clone())); - m.push("line_number", GoValue::Int(d.line_number)); - GoValue::Map(m) - }) - .collect(); - GoValue::Array(items) - } - - fn build_comments_table(&self, details: &[CommentDetail]) -> GoValue { - let items = details - .iter() - .map(|d| { - let item = CommentReportItem { - line: d.line_number, - comment: truncate_comment_body(&d.token), - placement: d.position.clone(), - target: d.target_name.clone(), - assessment: comment_assessment(d.is_good), - }; - let mut m = GoMap::new(MapOrigin::Map); - m.push("line", GoValue::Int(item.line)); - m.push("comment", GoValue::Str(item.comment)); - m.push("placement", GoValue::Str(item.placement)); - m.push("target", GoValue::Str(item.target)); - m.push("assessment", GoValue::Str(item.assessment)); - GoValue::Map(m) - }) - .collect(); - GoValue::Array(items) - } - - fn build_functions_table(&self, functions: &[&Node], metrics: &CommentMetrics) -> GoValue { - let items = functions - .iter() - .map(|&function| { - let func_name = self.extract_target_name(function); - let info = metrics.function_summary.get(&func_name); - let (assessment, comment_type) = function_assessment(info); - let func_type = function_type(function); - let lines = function_line_count(function); - let item = FunctionReportItem { - function: func_name, - kind: func_type, - lines, - comment: comment_type, - assessment, - }; - let mut m = GoMap::new(MapOrigin::Map); - m.push("function", GoValue::Str(item.function)); - m.push("type", GoValue::Str(item.kind)); - m.push("lines", GoValue::Int(item.lines)); - m.push("comment", GoValue::Str(item.comment)); - m.push("assessment", GoValue::Str(item.assessment)); - GoValue::Map(m) - }) - .collect(); - GoValue::Array(items) - } - - fn build_function_summary_iface(&self, metrics: &CommentMetrics) -> GoValue { - let mut outer = GoMap::new(MapOrigin::Map); - for (name, info) in &metrics.function_summary { - let mut m = GoMap::new(MapOrigin::Map); - m.push("name", GoValue::Str(info.name.clone())); - m.push("type", GoValue::Str(info.kind.clone())); - m.push("has_comment", GoValue::Bool(info.has_comment)); - m.push("comment_type", GoValue::Str(info.comment_type.clone())); - outer.push(name.clone(), GoValue::Map(m)); - } - GoValue::Map(outer) + root: Option<&Node>, + find_depth: Option<usize>, + ) -> Result<CommentMetrics, NilRootNode> { + let root = root.ok_or(NilRootNode)?; + let declarations = collect_declarations(root, find_depth); + let blocks = collect_comment_blocks(root, find_depth); + Ok(compute_metrics( + &blocks, + declarations, + &self.default_config(), + )) } } -// --- free helpers ------------------------------------------------------------ +// --- declarations ------------------------------------------------------------- -fn has_good_comment(func_name: &str, details: &[CommentDetail]) -> bool { - details - .iter() - .any(|d| d.target_name == func_name && d.is_good) +/// A declaration found in the tree. +struct Declaration { + name: String, + kind: String, + start: i64, + end: i64, + anchor: i64, } -fn comment_type_for(func_name: &str, details: &[CommentDetail]) -> String { - for d in details { - if d.target_name == func_name && d.is_good { - return d.kind.clone(); +fn collect_declarations(root: &Node, find_depth: Option<usize>) -> Vec<Declaration> { + let attributes: Vec<(i64, i64)> = find_nodes_with_role(root, ROLE_ATTRIBUTE, find_depth) + .into_iter() + .filter_map(node_lines) + .collect(); + let mut out = Vec::new(); + walk_declarations(root, find_depth, None, &mut out); + for d in &mut out { + d.anchor = anchor_line(d.start, &attributes); + if d.name.is_empty() { + d.name = UNKNOWN_NAME.to_string(); } } - String::new() + out } -fn calculate_distance(comment_end_line: i64, target: &Node) -> i64 { - let Some((target_start, _)) = node_pos(target) else { - return MAGIC_999; +fn walk_declarations( + n: &Node, + remaining: Option<usize>, + enclosing: Option<usize>, + out: &mut Vec<Declaration>, +) { + let mut enclosing = enclosing; + if DECLARATION_TYPES.contains(&n.node_type.as_str()) { + enclosing = register_declaration(n, enclosing, out); + } + let child_remaining = match remaining { + Some(0) => return, + Some(r) => Some(r - 1), + None => None, }; - let target_line = target_start as i64; - if comment_end_line < target_line { - target_line - comment_end_line - } else { - MAGIC_1000 + (comment_end_line - target_line) + for child in &n.children { + walk_declarations(child, child_remaining, enclosing, out); } } -fn is_comment_properly_placed(comment_start: i64, comment_end: i64, target: &Node) -> bool { - let Some((target_start, _)) = node_pos(target) else { - return false; +/// Registers a declaration node and returns the index of the declaration +/// that encloses its children. +fn register_declaration( + n: &Node, + enclosing: Option<usize>, + out: &mut Vec<Declaration>, +) -> Option<usize> { + let Some((start, end)) = node_lines(n) else { + return enclosing; }; - let target_line = target_start as i64; - if comment_end >= target_line { - return false; + let name = declaration_name(n); + // A nameless declaration whose child declaration starts on the same line + // is that child's definition (C lowers `function_definition` over + // `function_declarator`): the child names it and is no declaration of + // its own. + if let Some(parent) = enclosing.map(|i| &mut out[i]) { + if parent.start == start && parent.name.is_empty() { + parent.name = name.unwrap_or_default(); + return enclosing; + } } - let gap = target_line - comment_end; - is_gap_acceptable(comment_start, comment_end, gap) + if name.is_none() && NAMED_ONLY_TYPES.contains(&n.node_type.as_str()) { + return enclosing; + } + out.push(Declaration { + name: name.unwrap_or_default(), + kind: n.node_type.clone(), + start, + end, + anchor: start, + }); + Some(out.len() - 1) } -fn is_gap_acceptable(comment_start: i64, comment_end: i64, gap: i64) -> bool { - if comment_start == comment_end { - gap <= GAP_THRESHOLD_HIGH - } else { - gap <= MAGIC_3 - } -} +// --- comment blocks ----------------------------------------------------------- -fn determine_position(comment_start: i64, comment_end: i64, target: &Node) -> String { - let Some((target_start, _)) = node_pos(target) else { - return UNKNOWN_NAME.to_string(); - }; - let target_line = target_start as i64; - if comment_end < target_line { - return "above".to_string(); - } - if comment_start > target_line { - return "below".to_string(); - } - "inline".to_string() +/// Adjacent comment nodes, each `(text, start_line)`. +struct Block { + start: i64, + text_end: i64, + /// Whether the block opens with a leading documentation comment of a + /// declaration body (a docstring). + leading_doc: bool, + comments: Vec<(String, i64)>, } -fn comment_assessment(is_good: bool) -> String { - if is_good { - "✅ OK".to_string() - } else { - "❌ Not OK".to_string() - } +/// One comment node: its line geometry, text and whether it is a leading +/// documentation comment (the first statement of a declaration body). +struct FoundComment { + start: i64, + text_end: i64, + text: String, + leading_doc: bool, } -fn truncate_comment_body(body: &str) -> String { - // Truncation is by BYTE length (report contract): len > 50 => first 47 - // bytes + "...". - if body.len() > LEN_ARG_50 { - // Back off to the nearest char boundary <= 47 so we never split a - // multi-byte sequence; comment tokens here are ASCII-dominant. - let mut end = 47; - while end > 0 && !body.is_char_boundary(end) { - end -= 1; +fn collect_comment_blocks(root: &Node, find_depth: Option<usize>) -> Vec<Block> { + let mut comments = Vec::new(); + collect_comments(root, find_depth, &mut comments); + comments.sort_by_key(|c| c.start); + + let mut blocks: Vec<Block> = Vec::new(); + for c in comments { + match blocks.last_mut() { + Some(cur) if c.start <= cur.text_end + 1 => { + cur.text_end = cur.text_end.max(c.text_end); + cur.comments.push((c.text, c.start)); + } + _ => blocks.push(Block { + start: c.start, + text_end: c.text_end, + leading_doc: c.leading_doc, + comments: vec![(c.text, c.start)], + }), } - format!("{}...", &body[..end]) - } else { - body.to_string() } + blocks } -fn function_assessment(info: Option<&FunctionInfo>) -> (String, String) { - match info { - Some(i) if i.has_comment => ("✅ Well Documented".to_string(), i.comment_type.clone()), - _ => ("❌ No Comment".to_string(), "None".to_string()), +/// Collects every `Comment` node below `n` (never descending into a comment: +/// nested pieces are one comment), honoring the same depth cap as +/// [`crate::traverse::find_nodes_by_type`]. +fn collect_comments(n: &Node, remaining: Option<usize>, out: &mut Vec<FoundComment>) { + let child_remaining = match remaining { + Some(0) => return, + Some(r) => Some(r - 1), + None => None, + }; + for (index, child) in n.children.iter().enumerate() { + if child.node_type == uast::COMMENT { + if let Some((start, text_end)) = comment_lines(child) { + out.push(FoundComment { + start, + text_end, + text: comment_text(child), + leading_doc: has_role(child, ROLE_DOC) && is_leading_position(n, index), + }); + } + continue; + } + collect_comments(child, child_remaining, out); } } -fn function_type(function: &Node) -> String { - let t = node_type(function); - if t.is_empty() { - "Unknown".to_string() - } else { - t.to_string() +/// Whether child `index` of `parent` is the first statement of a declaration +/// body: the first child of a `Body` block, or a direct child of a +/// declaration preceded only by header nodes (name, parameters, types, +/// decorators, other comments). +fn is_leading_position(parent: &Node, index: usize) -> bool { + if parent.node_type == uast::BLOCK && has_role(parent, ROLE_BODY) { + return index == 0; } + DECLARATION_TYPES.contains(&parent.node_type.as_str()) + && parent.children[..index].iter().all(is_header_node) } -fn function_line_count(function: &Node) -> i64 { - match node_pos(function) { - Some((start, end)) => (end as i64) - (start as i64) + 1, - None => 0, - } +fn is_header_node(n: &Node) -> bool { + HEADER_TYPES.contains(&n.node_type.as_str()) + || [ROLE_NAME, ROLE_PARAMETER, ROLE_TYPE, ROLE_ATTRIBUTE] + .iter() + .any(|role| has_role(n, role)) } -fn safe_div(a: f64, b: f64) -> f64 { - if b == 0.0 { - 0.0 - } else { - a / b +// --- metrics ------------------------------------------------------------------ + +fn compute_metrics( + blocks: &[Block], + declarations: Vec<Declaration>, + config: &CommentConfig, +) -> CommentMetrics { + let spans: Vec<TargetSpan> = declarations + .iter() + .map(|d| TargetSpan { + start: d.start, + end: d.end, + anchor: d.anchor, + }) + .collect(); + let mut doc_lines: Vec<Option<i64>> = vec![None; declarations.len()]; + let mut comments = Vec::new(); + for block in blocks { + let span = BlockSpan { + start: block.start, + text_end: block.text_end, + leading_doc: block.leading_doc, + }; + let (placement, target) = match resolve(span, &spans) { + Resolved::Above(i) => (Placement::Above, Some(i)), + Resolved::Docstring(i) => (Placement::Docstring, Some(i)), + Resolved::Inline(i) => (Placement::Inline, Some(i)), + Resolved::Unassociated => (Placement::Unassociated, None), + }; + if matches!(placement, Placement::Above | Placement::Docstring) { + if let Some(i) = target { + doc_lines[i].get_or_insert(block.start); + } + } + let (target_type, target_name) = target + .map(|i| (declarations[i].kind.clone(), declarations[i].name.clone())) + .unwrap_or_default(); + let score = score_for(placement, &target_type, config); + comments.extend(block.comments.iter().map(|(text, line)| CommentDetail { + text: text.clone(), + line: *line, + placement, + score, + target_type: target_type.clone(), + target_name: target_name.clone(), + })); + } + + let functions: Vec<FunctionInfo> = declarations + .into_iter() + .zip(doc_lines) + .map(|(d, doc_line)| FunctionInfo { + name: d.name, + kind: d.kind, + line: d.start, + lines: d.end - d.start + 1, + doc_line, + }) + .collect(); + + let good_comments = comments.iter().filter(|c| c.is_good()).count() as i64; + let counts = CommentCounts { + total_comments: comments.len() as i64, + good_comments, + bad_comments: comments.len() as i64 - good_comments, + total_functions: functions.len() as i64, + documented_functions: functions.iter().filter(|f| f.is_documented()).count() as i64, + }; + CommentMetrics { + counts, + comments, + functions, } } -fn comment_message(score: f64) -> String { - // Threshold labeler with high=0.8, medium=0.6, low=0.4. The label strings - // are part of the report contract. - if score >= 0.8 { - "Excellent comment quality and placement".to_string() - } else if score >= 0.6 { - "Good comment quality with room for improvement".to_string() - } else if score >= 0.4 { - "Fair comment quality - consider improving placement".to_string() - } else { - "Poor comment quality - significant improvement needed".to_string() +fn score_for(placement: Placement, target_type: &str, config: &CommentConfig) -> f64 { + match placement { + Placement::Above | Placement::Docstring => config.reward_score, + Placement::Inline => config + .penalty_scores + .get(target_type) + .copied() + .unwrap_or(DEFAULT_INLINE_PENALTY), + Placement::Unassociated => UNASSOCIATED_PENALTY, } } -fn build_empty_result() -> GoValue { +// --- report building (routed through cf-gojson) ------------------------------- + +/// Builds the per-file report map: the counters, the derived ratios, the +/// `comments` and `functions` tables and the quality `message`. +#[must_use] +pub fn build_report(metrics: &CommentMetrics) -> GoValue { + let counts = &metrics.counts; + // Dynamic report map => map-origin so cf-gojson byte-sorts keys. let mut m = GoMap::new(MapOrigin::Map); - m.push("total_comments", GoValue::Int(0)); - m.push("good_comments", GoValue::Int(0)); - m.push("bad_comments", GoValue::Int(0)); - m.push("overall_score", GoValue::Float(0.0)); - m.push("total_functions", GoValue::Int(0)); - m.push("documented_functions", GoValue::Int(0)); - m.push("message", GoValue::Str("No comments found".to_string())); + m.push("total_comments", GoValue::Int(counts.total_comments)); + m.push("good_comments", GoValue::Int(counts.good_comments)); + m.push("bad_comments", GoValue::Int(counts.bad_comments)); + m.push("overall_score", GoValue::Float(counts.overall_score())); + m.push( + "good_comments_ratio", + GoValue::Float(counts.overall_score()), + ); + m.push("total_functions", GoValue::Int(counts.total_functions)); + m.push( + "documented_functions", + GoValue::Int(counts.documented_functions), + ); + m.push( + "documentation_coverage", + GoValue::Float(counts.documentation_coverage()), + ); + m.push("comments", comments_table(&metrics.comments)); + m.push("functions", functions_table(&metrics.functions)); + m.push("message", GoValue::Str(counts.message().to_string())); GoValue::Map(m) } -// --- cf-uast-node accessors (the single point of API coupling) -------------- - -/// Returns the node type string. -fn node_type(n: &Node) -> &str { - &n.node_type +fn comments_table(comments: &[CommentDetail]) -> GoValue { + let rows = comments + .iter() + .map(|c| { + let mut m = GoMap::new(MapOrigin::Map); + m.push("line", GoValue::Int(c.line)); + m.push("comment", GoValue::Str(comment_preview(&c.text))); + m.push("placement", GoValue::Str(c.placement.as_str().to_string())); + m.push("target", GoValue::Str(c.target_name.clone())); + m.push("assessment", GoValue::Str(c.assessment().to_string())); + GoValue::Map(m) + }) + .collect(); + GoValue::Array(rows) } -/// Returns the node token string. -fn node_token(n: &Node) -> &str { - &n.token +fn functions_table(functions: &[FunctionInfo]) -> GoValue { + let rows = functions + .iter() + .map(|f| { + let mut m = GoMap::new(MapOrigin::Map); + m.push("function", GoValue::Str(f.name.clone())); + m.push("type", GoValue::Str(f.kind.clone())); + m.push("line", GoValue::Int(f.line)); + m.push("lines", GoValue::Int(f.lines)); + m.push("doc_line", GoValue::Int(f.doc_line.unwrap_or(0))); + m.push("assessment", GoValue::Str(f.assessment().to_string())); + GoValue::Map(m) + }) + .collect(); + GoValue::Array(rows) } -/// Returns `(start_line, end_line)` if the node carries a position. -/// -/// `cf_uast_node::Positions` stores lines as `u64`; the analyzer works in -/// `i64` line space, so callers widen these directly. -fn node_pos(n: &Node) -> Option<(u64, u64)> { - n.pos.as_ref().map(|p| (p.start_line, p.end_line)) +/// The comment text shortened for tables: more than 50 bytes keeps the first +/// 47 (backed off to a char boundary) plus `...`. +#[must_use] +pub fn comment_preview(text: &str) -> String { + if text.len() <= PREVIEW_MAX_BYTES { + return text.to_string(); + } + let mut end = PREVIEW_KEEP_BYTES; + while end > 0 && !text.is_char_boundary(end) { + end -= 1; + } + format!("{}...", &text[..end]) } diff --git a/crates/cf-comments/src/lib.rs b/crates/cf-comments/src/lib.rs index 880dae45..795207d8 100644 --- a/crates/cf-comments/src/lib.rs +++ b/crates/cf-comments/src/lib.rs @@ -1,32 +1,42 @@ -//! Static comment density / quality analysis (analyzer id: `static/comments`). +//! Static comment placement / documentation coverage analysis (analyzer id: +//! `static/comments`). //! -//! The analyzer walks a parsed UAST, groups consecutive `Comment` nodes into -//! blocks (sorted by line), scores each block by its placement relative to the -//! closest function/class/method/interface/struct, and computes aggregate -//! metrics (good/bad counts, overall score, documentation coverage). It is also -//! consumed by the `quality` analyzer. +//! The analyzer walks a parsed UAST, groups adjacent `Comment` nodes into +//! blocks, places every block against the file's declarations (function, +//! method, class, interface, struct) and derives the per-file metrics. It is +//! also consumed by the history `quality` analyzer. //! -//! # Byte-identity +//! # Placement rules //! -//! Per DESIGN §2, all report serialization is routed through -//! [`cf_gojson::GoValue`] — never `serde_json`. The analyzer returns a -//! *map-origin* [`cf_gojson::GoValue::Map`] whose keys the `cf-gojson` encoder -//! byte-sorts at encode time (report-format contract). +//! * **above** — the block ends on the line directly above a declaration +//! (attribute / decorator lines belong to the declaration): it documents +//! that declaration and is a good comment; +//! * **inline** — the block lies inside a declaration's span: a body comment; +//! * **unassociated** — anything else (file headers, orphans, a comment +//! separated from the declaration by a blank line). //! -//! # Module map +//! Documentation is keyed by declaration identity (line + type + name), so +//! same-named declarations are documented independently; a nameless +//! class/struct/interface node is not a declaration; nested comment pieces +//! count as one comment. +//! +//! # Serialization +//! +//! All report serialization is routed through [`cf_gojson::GoValue`] — never +//! `serde_json`. The analyzer returns a *map-origin* +//! [`cf_gojson::GoValue::Map`] whose keys the `cf-gojson` encoder byte-sorts at +//! encode time. //! -//! [`analyzer`] (scoring + report building), [`types`] (data types), -//! [`aggregator`] (cross-file numeric aggregation), [`traverse`] (UAST -//! traversal helpers). The static-pipeline metric view and the terminal/HTML -//! sections (non-binding per DESIGN §2.7) live in the command layer. +//! # Module map //! -//! Compatibility: output bytes are pinned against the reference implementation -//! by the differential gate in `tests/compat`. +//! [`analyzer`] (discovery, metrics, report building), [`placement`] (the +//! pure line rules), [`types`] (data types), [`aggregator`] (cross-file +//! sums), [`traverse`] (UAST traversal helpers). The terminal/HTML sections +//! live in the command layer. //! //! # Example //! -//! A comment placed immediately above a function is scored "good" and counts -//! that function as documented: +//! A comment placed directly above a function documents it: //! //! ``` //! use cf_comments::Analyzer; @@ -70,14 +80,15 @@ pub mod aggregator; pub mod analyzer; +pub mod placement; pub mod traverse; pub mod types; -pub use aggregator::{Aggregator, NumericReport}; -pub use analyzer::{Analyzer, NilRootNode}; +pub use aggregator::{Aggregator, FileReport, FileStamp}; +pub use analyzer::{build_report, comment_preview, Analyzer, NilRootNode}; pub use types::{ - CommentBlock, CommentConfig, CommentDetail, CommentMetrics, CommentReportItem, FunctionInfo, - FunctionReportItem, + quality_message, CommentConfig, CommentCounts, CommentDetail, CommentMetrics, FunctionInfo, + Placement, NO_COMMENTS_MESSAGE, }; #[cfg(test)] @@ -133,17 +144,22 @@ mod tests { .with_position(Some(pos(start, end))) .build() } - fn named(node_type: &str, name: &str, start: u64, end: u64) -> Node { - let ident = Builder::new() + fn ident(name: &str, roles: &[&str]) -> Node { + Builder::new() .with_type("Identifier") .with_token(name) - .with_roles(vec!["Name".into()]) - .build(); - let mut n = Builder::new() + .with_roles(roles.iter().map(|r| (*r).to_string()).collect()) + .build() + } + fn positioned(node_type: &str, start: u64, end: u64) -> Node { + Builder::new() .with_type(node_type) .with_position(Some(pos(start, end))) - .build(); - n.add_child(ident); + .build() + } + fn named(node_type: &str, name: &str, start: u64, end: u64) -> Node { + let mut n = positioned(node_type, start, end); + n.add_child(ident(name, &["Name"])); n } fn function(name: &str, start: u64, end: u64) -> Node { @@ -155,6 +171,25 @@ mod tests { fn method(name: &str, start: u64, end: u64) -> Node { named("Method", name, start, end) } + fn analyze(root: &Node) -> CommentMetrics { + Analyzer::new() + .analyze_metrics(Some(root), None) + .expect("analysis") + } + fn names(metrics: &CommentMetrics) -> Vec<(&str, bool)> { + metrics + .functions + .iter() + .map(|f| (f.name.as_str(), f.is_documented())) + .collect() + } + fn placements(metrics: &CommentMetrics) -> Vec<(i64, Placement, &str)> { + metrics + .comments + .iter() + .map(|c| (c.line, c.placement, c.target_name.as_str())) + .collect() + } #[test] fn analyzer_name() { @@ -165,7 +200,6 @@ mod tests { fn default_config() { let cfg = Analyzer::new().default_config(); assert!((cfg.reward_score - 1.0).abs() < 1e-9); - assert_eq!(cfg.max_comment_length, 500); assert_eq!(cfg.penalty_scores.get("Function"), Some(&-0.5)); assert_eq!(cfg.penalty_scores.get("Method"), Some(&-0.5)); assert_eq!(cfg.penalty_scores.get("Class"), Some(&-0.3)); @@ -182,6 +216,10 @@ mod tests { assert_eq!(get_float(&res, "overall_score"), 0.0); assert_eq!(get_int(&res, "total_functions"), 0); assert_eq!(get_int(&res, "documented_functions"), 0); + assert_eq!( + get(&res, "message"), + &GoValue::Str(NO_COMMENTS_MESSAGE.to_string()) + ); } // analyze must reject a missing root. @@ -202,14 +240,12 @@ mod tests { assert!((get_float(&res, "overall_score") - 1.0).abs() < 1e-9); assert_eq!(get_int(&res, "total_functions"), 1); assert_eq!(get_int(&res, "documented_functions"), 1); + assert!((get_float(&res, "documentation_coverage") - 1.0).abs() < 1e-9); } #[test] fn analyze_bad_comment_placement() { - let mut body = Builder::new() - .with_type("Block") - .with_position(Some(pos(2, 4))) - .build(); + let mut body = positioned("Block", 2, 4); body.add_child(comment("// This is a bad comment", 3, 3)); let mut func = function("testFunction", 1, 5); func.add_child(body); @@ -248,13 +284,18 @@ mod tests { let mut root = file(); root.add_child(comment("// This is a class", 1, 1)); root.add_child(cls); - let res = Analyzer::new().analyze(Some(&root)).unwrap(); - assert_eq!(get_int(&res, "total_comments"), 2); - assert_eq!(get_int(&res, "good_comments"), 2); - assert_eq!(get_int(&res, "bad_comments"), 0); - assert!((get_float(&res, "overall_score") - 1.0).abs() < 1e-9); - assert_eq!(get_int(&res, "total_functions"), 2); - assert_eq!(get_int(&res, "documented_functions"), 2); + let metrics = analyze(&root); + assert_eq!( + placements(&metrics), + vec![ + (1, Placement::Above, "TestClass"), + (4, Placement::Above, "testMethod") + ] + ); + assert_eq!( + names(&metrics), + vec![("TestClass", true), ("testMethod", true)] + ); } #[test] @@ -274,29 +315,253 @@ mod tests { assert_eq!(get_int(&res, "documented_functions"), 0); } - // Function/method/class only; variable excluded. #[test] - fn find_functions_excludes_variable() { + fn zero_comments_keep_the_functions() { + let mut root = file(); + root.add_child(function("Add", 3, 5)); + root.add_child(function("Sub", 7, 9)); + let metrics = analyze(&root); + assert_eq!(metrics.counts.total_comments, 0); + assert_eq!(names(&metrics), vec![("Add", false), ("Sub", false)]); + assert_eq!( + metrics.counts.message(), + "Poor comment quality - significant improvement needed" + ); + } + + #[test] + fn same_named_functions_are_documented_independently() { + let mut root = file(); + root.add_child(comment("// String renders A.", 6, 6)); + root.add_child(method("String", 7, 9)); + root.add_child(method("String", 11, 13)); + let metrics = analyze(&root); + assert_eq!(names(&metrics), vec![("String", true), ("String", false)]); + assert_eq!(metrics.counts.documented_functions, 1); + assert_eq!(metrics.functions[0].doc_line, Some(6)); + assert_eq!(metrics.functions[1].doc_line, None); + } + + #[test] + fn nested_comment_pieces_count_once_and_document_the_next_item() { + // The Rust mapping lowers `/// Documents foo.` on line 3 as a Comment + // spanning 3-4 with two nested Comment pieces; the function starts + // on line 4. + let mut outer = comment("", 3, 4); + outer.add_child(comment("/", 3, 3)); + outer.add_child(comment(" Documents foo.", 3, 4)); + let mut root = file(); + root.add_child(outer); + root.add_child(function("foo", 4, 7)); + let metrics = analyze(&root); + assert_eq!(metrics.counts.total_comments, 1); + assert_eq!(placements(&metrics), vec![(3, Placement::Above, "foo")]); + assert_eq!(metrics.comments[0].text, "/ Documents foo."); + assert_eq!(names(&metrics), vec![("foo", true)]); + } + + #[test] + fn anonymous_struct_is_not_a_declaration() { + let anonymous = Builder::new() + .with_type("Struct") + .with_token("struct{}") + .with_position(Some(pos(3, 3))) + .build(); + let mut root = file(); + root.add_child(anonymous); + root.add_child(named("Struct", "Named", 5, 7)); + let metrics = analyze(&root); + assert_eq!(names(&metrics), vec![("Named", false)]); + } + + #[test] + fn a_multi_line_token_is_never_a_name() { + let func = Builder::new() + .with_type("Function") + .with_token("int f() {\n return 1;\n}") + .with_position(Some(pos(1, 3))) + .build(); + let mut root = file(); + root.add_child(func); + let metrics = analyze(&root); + assert_eq!(names(&metrics), vec![("unknown", false)]); + } + + #[test] + fn a_type_identifier_is_not_a_name() { + let mut func = positioned("Function", 1, 3); + func.add_child(ident("int", &["Type", "Name"])); + func.add_child(ident("add", &["Name"])); + let mut root = file(); + root.add_child(func); + assert_eq!(names(&analyze(&root)), vec![("add", false)]); + } + + #[test] + fn a_same_line_child_declaration_names_its_nameless_parent() { + // C: function_definition (nameless, lines 2-4) over function_declarator + // (line 2, names `add`). + let mut outer = positioned("Function", 2, 4); + outer.add_child(ident("int", &["Type", "Name"])); + outer.add_child(named("Function", "add", 2, 2)); + let mut root = file(); + root.add_child(comment("/* adds */", 1, 1)); + root.add_child(outer); + let metrics = analyze(&root); + assert_eq!(names(&metrics), vec![("add", true)]); + } + + #[test] + fn a_named_parent_keeps_its_same_line_nested_declaration() { + let mut outer = class("A", 1, 1); + outer.add_child(method("m", 1, 1)); + let mut root = file(); + root.add_child(outer); + assert_eq!(names(&analyze(&root)), vec![("A", false), ("m", false)]); + } + + #[test] + fn body_comment_is_inline_for_the_enclosing_function() { + let mut add = function("Add", 3, 6); + add.add_child(comment("// inline comment", 4, 4)); + let mut root = file(); + root.add_child(add); + root.add_child(function("Sub", 8, 10)); + let metrics = analyze(&root); + assert_eq!(placements(&metrics), vec![(4, Placement::Inline, "Add")]); + assert_eq!(names(&metrics), vec![("Add", false), ("Sub", false)]); + } + + #[test] + fn trailing_comment_on_the_declaration_line_is_inline() { + let mut root = file(); + root.add_child(function("f", 1, 1)); + root.add_child(comment("// trailing", 1, 1)); + let metrics = analyze(&root); + assert_eq!(placements(&metrics), vec![(1, Placement::Inline, "f")]); + } + + #[test] + fn a_blank_line_breaks_documentation() { + let mut root = file(); + root.add_child(comment("// Mul multiplies.", 14, 14)); + root.add_child(function("Mul", 16, 18)); + let metrics = analyze(&root); + assert_eq!( + placements(&metrics), + vec![(14, Placement::Unassociated, "")] + ); + assert_eq!(names(&metrics), vec![("Mul", false)]); + } + + #[test] + fn attribute_lines_belong_to_the_declaration() { + let attribute = Builder::new() + .with_type("Attribute") + .with_roles(vec!["Attribute".into()]) + .with_position(Some(pos(2, 2))) + .build(); + let mut root = file(); + root.add_child(comment("/// Doc f.", 1, 1)); + root.add_child(attribute); + root.add_child(function("f", 3, 5)); + let metrics = analyze(&root); + assert_eq!(names(&metrics), vec![("f", true)]); + } + + #[test] + fn adjacent_comments_form_one_block() { + let mut root = file(); + root.add_child(comment("// line 1", 1, 1)); + root.add_child(comment("// line 2", 2, 2)); + root.add_child(function("f", 3, 5)); + let metrics = analyze(&root); + assert_eq!(metrics.counts.total_comments, 2); + assert_eq!(metrics.counts.good_comments, 2); + assert_eq!(metrics.functions[0].doc_line, Some(1)); + } + + #[test] + fn report_tables_carry_placement_and_assessment() { + let mut root = file(); + root.add_child(comment("// doc", 1, 1)); + root.add_child(function("f", 2, 4)); + let res = Analyzer::new().analyze(Some(&root)).unwrap(); + let comments = match get(&res, "comments") { + GoValue::Array(rows) => rows.clone(), + other => panic!("comments not an array: {other:?}"), + }; + assert_eq!( + get(&comments[0], "placement"), + &GoValue::Str("above".to_string()) + ); + assert_eq!(get(&comments[0], "target"), &GoValue::Str("f".to_string())); + let functions = match get(&res, "functions") { + GoValue::Array(rows) => rows.clone(), + other => panic!("functions not an array: {other:?}"), + }; + assert_eq!(get_int(&functions[0], "line"), 2); + assert_eq!(get_int(&functions[0], "lines"), 3); + assert_eq!(get_int(&functions[0], "doc_line"), 1); + assert_eq!( + get(&functions[0], "assessment"), + &GoValue::Str("✅ Well Documented".to_string()) + ); + } + + #[test] + fn comment_preview_shortens_long_text() { + let long = "x".repeat(60); + assert_eq!(comment_preview(&long), format!("{}...", "x".repeat(47))); + assert_eq!(comment_preview("short"), "short"); + } + + // Declarations are found at any depth; a variable is not one. + #[test] + fn find_declarations_excludes_variable() { let mut root = file(); root.add_child(typed("Function")); root.add_child(typed("Method")); root.add_child(typed("Class")); root.add_child(typed("Variable")); - let funcs = traverse::find_nodes_by_type(&root, &["Function", "Method", "Class"]); - assert_eq!(funcs.len(), 3); + let found = traverse::find_nodes_by_type( + &root, + &["Function", "Method", "Class"], + None, + traverse::MatchDescent::Continue, + ); + assert_eq!(found.len(), 3); } - // Comments are found at any depth, in document order. + // Comments are found at any depth, in document order, stopping at each. #[test] - fn find_comments_in_document_order() { + fn find_comments_in_document_order_without_descending() { let mut func = typed("Function"); func.add_child(comment("// Function comment", 5, 5)); + let mut nested = comment("", 1, 1); + nested.add_child(comment("// piece", 1, 1)); let mut root = file(); - root.add_child(comment("// Root comment", 1, 1)); + root.add_child(nested); root.add_child(func); - let comments = traverse::find_nodes_by_type(&root, &["Comment"]); + let comments = + traverse::find_nodes_by_type(&root, &["Comment"], None, traverse::MatchDescent::Stop); assert_eq!(comments.len(), 2); - assert_eq!(comments[0].token, "// Root comment"); + assert_eq!(traverse::comment_text(comments[0]), "// piece"); assert_eq!(comments[1].token, "// Function comment"); } + + #[test] + fn depth_cap_limits_discovery() { + let mut inner = typed("Block"); + inner.add_child(function("deep", 2, 3)); + let mut outer = function("shallow", 1, 4); + outer.add_child(inner); + let mut root = file(); + root.add_child(outer); + let capped = Analyzer::new() + .analyze_metrics(Some(&root), Some(1)) + .expect("analysis"); + assert_eq!(names(&capped), vec![("shallow", false)]); + assert_eq!(analyze(&root).functions.len(), 2); + } } diff --git a/crates/cf-comments/src/placement.rs b/crates/cf-comments/src/placement.rs new file mode 100644 index 00000000..5f489934 --- /dev/null +++ b/crates/cf-comments/src/placement.rs @@ -0,0 +1,219 @@ +//! Placement rules: where a comment block sits relative to the declarations +//! of its file. Pure line arithmetic, independent of the UAST. +//! +//! A block is tested in this order: +//! 1. **docstring** — it is a leading documentation comment (the first +//! statement of a declaration body, a Python docstring) and lies inside a +//! declaration's span: it documents the innermost such declaration; +//! 2. **above** — it starts before a declaration's anchor line and its text +//! reaches the line directly above that anchor (no blank line between); +//! the anchor is the declaration's first line, extended upward over +//! attribute / decorator lines that end directly above it; +//! 3. **inline** — it lies inside a declaration's span (the innermost one); +//! 4. **unassociated** — anything else. + +/// A declaration's line geometry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TargetSpan { + /// First line of the declaration. + pub start: i64, + /// Last line of the declaration. + pub end: i64, + /// The line a documenting comment must end directly above. + pub anchor: i64, +} + +/// A comment block's line geometry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BlockSpan { + /// First line of the block. + pub start: i64, + /// Last line holding comment text. + pub text_end: i64, + /// Whether the block is a leading documentation comment of a declaration + /// body (a docstring), as opposed to a comment found anywhere else. + pub leading_doc: bool, +} + +/// The outcome of placing a block; indices refer to the targets slice. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Resolved { + /// The block documents the declaration at this index. + Above(usize), + /// The block is the docstring of the declaration at this index. + Docstring(usize), + /// The block is a body comment of the declaration at this index. + Inline(usize), + /// The block belongs to no declaration. + Unassociated, +} + +/// Places `block` against `targets`. +#[must_use] +pub fn resolve(block: BlockSpan, targets: &[TargetSpan]) -> Resolved { + if block.leading_doc { + if let Some(i) = enclosing_target(block, targets) { + return Resolved::Docstring(i); + } + } + if let Some(i) = documented_target(block, targets) { + return Resolved::Above(i); + } + if let Some(i) = enclosing_target(block, targets) { + return Resolved::Inline(i); + } + Resolved::Unassociated +} + +/// The declaration whose anchor the block ends directly above (the earliest +/// such anchor when several qualify). +fn documented_target(block: BlockSpan, targets: &[TargetSpan]) -> Option<usize> { + targets + .iter() + .enumerate() + .filter(|(_, t)| block.start < t.anchor && block.text_end >= t.anchor - 1) + .min_by_key(|(_, t)| t.anchor) + .map(|(i, _)| i) +} + +/// The innermost declaration whose span contains the block. +fn enclosing_target(block: BlockSpan, targets: &[TargetSpan]) -> Option<usize> { + targets + .iter() + .enumerate() + .filter(|(_, t)| t.start <= block.start && block.text_end <= t.end) + .max_by_key(|(i, t)| (t.start, *i)) + .map(|(i, _)| i) +} + +/// The anchor line of a declaration starting at `start`: `start`, extended +/// upward over attribute spans `(start, end)` that end directly above the +/// current anchor. +#[must_use] +pub fn anchor_line(start: i64, attributes: &[(i64, i64)]) -> i64 { + let mut anchor = start; + loop { + let bridged = attributes + .iter() + .filter(|(_, end)| *end == anchor - 1) + .map(|(s, _)| *s) + .min(); + match bridged { + Some(s) if s < anchor => anchor = s, + _ => return anchor, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn target(start: i64, end: i64) -> TargetSpan { + TargetSpan { + start, + end, + anchor: start, + } + } + + fn block(start: i64, text_end: i64) -> BlockSpan { + BlockSpan { + start, + text_end, + leading_doc: false, + } + } + + fn docstring(start: i64, text_end: i64) -> BlockSpan { + BlockSpan { + start, + text_end, + leading_doc: true, + } + } + + /// A docstring documents the declaration whose body holds it, even when + /// it also ends directly above a nested declaration. + #[test] + fn docstring_documents_its_enclosing_declaration_not_the_next_one() { + let targets = [target(1, 6), target(3, 6)]; + assert_eq!(resolve(docstring(2, 2), &targets), Resolved::Docstring(0)); + } + + /// A docstring outside every declaration (a module docstring) is + /// unassociated. + #[test] + fn module_docstring_is_unassociated() { + assert_eq!( + resolve(docstring(1, 1), &[target(3, 5)]), + Resolved::Unassociated + ); + } + + #[test] + fn directly_above_documents() { + assert_eq!(resolve(block(1, 1), &[target(2, 4)]), Resolved::Above(0)); + } + + #[test] + fn multi_line_block_directly_above_documents() { + assert_eq!(resolve(block(1, 3), &[target(4, 6)]), Resolved::Above(0)); + } + + #[test] + fn one_blank_line_breaks_documentation() { + assert_eq!( + resolve(block(1, 1), &[target(3, 5)]), + Resolved::Unassociated + ); + } + + #[test] + fn inside_a_span_is_inline_for_the_innermost() { + let targets = [target(1, 20), target(5, 10)]; + assert_eq!(resolve(block(7, 7), &targets), Resolved::Inline(1)); + assert_eq!(resolve(block(15, 15), &targets), Resolved::Inline(0)); + } + + #[test] + fn above_a_member_wins_over_the_enclosing_class() { + let targets = [target(1, 20), target(5, 10)]; + assert_eq!(resolve(block(4, 4), &targets), Resolved::Above(1)); + } + + #[test] + fn same_line_as_the_declaration_is_inline() { + assert_eq!(resolve(block(2, 2), &[target(2, 2)]), Resolved::Inline(0)); + } + + #[test] + fn span_running_onto_the_declaration_line_still_documents() { + assert_eq!(resolve(block(3, 4), &[target(4, 7)]), Resolved::Above(0)); + } + + #[test] + fn after_the_last_declaration_is_unassociated() { + assert_eq!( + resolve(block(9, 9), &[target(2, 4)]), + Resolved::Unassociated + ); + } + + #[test] + fn attributes_bridge_the_anchor() { + assert_eq!(anchor_line(5, &[(4, 4), (3, 3)]), 3); + assert_eq!(anchor_line(5, &[(2, 2)]), 5); + assert_eq!(anchor_line(5, &[]), 5); + } + + #[test] + fn anchor_is_what_the_comment_must_touch() { + let t = TargetSpan { + start: 3, + end: 5, + anchor: 2, + }; + assert_eq!(resolve(block(1, 1), &[t]), Resolved::Above(0)); + } +} diff --git a/crates/cf-comments/src/traverse.rs b/crates/cf-comments/src/traverse.rs index 3809f492..5ef717b5 100644 --- a/crates/cf-comments/src/traverse.rs +++ b/crates/cf-comments/src/traverse.rs @@ -1,46 +1,73 @@ -//! Generic UAST traversal helpers used by the comments analyzer. -//! -//! These live here (rather than in the shared `cf-analyzers-common` crate) so -//! the crate's only dependency on shared traversal logic is this small, -//! self-contained module; it can be re-pointed at `cf-analyzers-common` when -//! convenient. +//! UAST traversal helpers used by the comments analyzer: node discovery, +//! declaration naming and comment text geometry. use cf_uast_node::Node; -/// Returns, in pre-order, all descendants of `root` (including `root`) whose -/// node type is one of `types`, in document (pre-order, children-in-order) -/// order. +use crate::types::{uast, ROLE_NAME, ROLE_TYPE}; + +/// Whether a walk continues below a node that matched. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MatchDescent { + /// Keep walking into a matched node's children (nested declarations). + Continue, + /// Stop at a matched node: its children are part of it (a comment node + /// lowered with nested comment pieces is one comment). + Stop, +} + +/// Returns, in document (pre-order) order, every node under `root` (root +/// included) whose type is one of `types`. /// -/// Note on depth: the STATIC comments surface runs the reference traverser -/// uncapped (`maxDepth <= 0` disables the limit), so this unbounded walk -/// reproduces the static node set byte-for-byte. The HISTORY `quality` -/// analyzer's comments instance runs with `maxDepth = 10` — use -/// [`find_nodes_by_type_capped`] there (measured against the live reference -/// binary: nodes at depth <= 10 from the root, root at depth 0, are matched; -/// deeper nodes are never matched). -pub fn find_nodes_by_type<'a>(root: &'a Node, types: &[&str]) -> Vec<&'a Node> { +/// With `max_depth = Some(cap)`, only nodes at depth `<= cap` below the root +/// (root at depth 0) are matched; the history `quality` analyzer runs with a +/// cap, the static surface runs uncapped. +pub fn find_nodes_by_type<'a>( + root: &'a Node, + types: &[&str], + max_depth: Option<usize>, + descent: MatchDescent, +) -> Vec<&'a Node> { let mut out = Vec::new(); - walk(root, types, None, &mut out); + walk( + root, + &|n| types.contains(&n.node_type.as_str()), + max_depth, + descent, + &mut out, + ); out } -/// Depth-capped [`find_nodes_by_type`]: matches only nodes at depth -/// `<= max_depth` below `root` (`root` at depth 0). The reference traverser -/// filters MATCHES by depth (it still descends), so pruning the walk at the -/// cap is output-equivalent. -pub fn find_nodes_by_type_capped<'a>( +/// Returns, in document order, every node under `root` carrying `role`, +/// stopping at each match (a role-bearing node's children share its lines). +pub fn find_nodes_with_role<'a>( root: &'a Node, - types: &[&str], - max_depth: usize, + role: &str, + max_depth: Option<usize>, ) -> Vec<&'a Node> { let mut out = Vec::new(); - walk(root, types, Some(max_depth), &mut out); + walk( + root, + &|n| has_role(n, role), + max_depth, + MatchDescent::Stop, + &mut out, + ); out } -fn walk<'a>(n: &'a Node, types: &[&str], remaining: Option<usize>, out: &mut Vec<&'a Node>) { - if types.contains(&n.node_type.as_str()) { +fn walk<'a>( + n: &'a Node, + matches: &dyn Fn(&Node) -> bool, + remaining: Option<usize>, + descent: MatchDescent, + out: &mut Vec<&'a Node>, +) { + if matches(n) { out.push(n); + if descent == MatchDescent::Stop { + return; + } } let child_remaining = match remaining { Some(0) => return, @@ -48,44 +75,92 @@ fn walk<'a>(n: &'a Node, types: &[&str], remaining: Option<usize>, out: &mut Vec None => None, }; for child in &n.children { - walk(child, types, child_remaining, out); + walk(child, matches, child_remaining, descent, out); } } -/// Extracts an entity (function/class/…) name from a target node. -/// -/// Resolution order (report contract): -/// 1. the `name` prop — decisive if the KEY EXISTS, **even when empty**: an -/// existing-but-empty value short-circuits to "no name" (the caller's -/// `unknown`) and never falls through to the token/children probes. -/// 2. the node's own `token` (if non-empty); -/// 3. `children[0].token`, else `children[0]`'s `name` prop. +/// Whether `n` carries `role`. +#[must_use] +pub fn has_role(n: &Node, role: &str) -> bool { + n.roles.iter().any(|r| r == role) +} + +/// Resolves a declaration's own name: its non-empty `name` prop, else the +/// token of its first direct `Identifier` child that carries the `Name` role +/// without the `Type` role (a type identifier names a type, not the +/// declaration). A token spanning several lines is never a name. /// -/// Returns `None` when no name can be derived (the caller substitutes -/// `"unknown"`). -pub fn extract_entity_name(target: &Node) -> Option<String> { - // Step 1: the "name" prop — key presence is decisive (see doc comment). - if let Some(value) = target.props.get("name") { - return if value.is_empty() { - None - } else { - Some(value.clone()) - }; +/// Returns `None` when the node names nothing itself. +#[must_use] +pub fn declaration_name(node: &Node) -> Option<String> { + if let Some(value) = node.props.get("name") { + if !value.is_empty() { + return Some(value.clone()); + } } - // Step 2: the node's own token. - if !target.token.is_empty() { - return Some(target.token.clone()); + node.children + .iter() + .find(|c| { + c.node_type == uast::IDENTIFIER + && has_role(c, ROLE_NAME) + && !has_role(c, ROLE_TYPE) + && is_single_line(&c.token) + }) + .map(|c| c.token.clone()) +} + +fn is_single_line(token: &str) -> bool { + !token.is_empty() && !token.contains('\n') +} + +/// The comment's text: its own token, else the tokens of its nested comment +/// pieces concatenated in order. +#[must_use] +pub fn comment_text(node: &Node) -> String { + if !node.token.is_empty() { + return node.token.clone(); } - // Step 3: children[0] — token first, then its "name" prop. - if let Some(child) = target.children.first() { - if !child.token.is_empty() { - return Some(child.token.clone()); - } - if let Some(value) = child.props.get("name") { - if !value.is_empty() { - return Some(value.clone()); - } - } + node.children + .iter() + .filter(|c| c.node_type == uast::COMMENT) + .map(comment_text) + .collect::<String>() +} + +/// `(start_line, text_end_line)` of a comment node, or `None` without a +/// position. +/// +/// The text end is the last line that holds comment text, decided by the +/// node's own token (a token of N lines ends N-1 lines after the start), +/// else by its nested comment pieces, else by the span end. It never leaves +/// the node's span, so a span that runs onto the following declaration's line +/// (a mapping artefact) cannot move the comment onto that declaration. +#[must_use] +pub fn comment_lines(node: &Node) -> Option<(i64, i64)> { + let pos = node.pos.as_ref()?; + let start = pos.start_line as i64; + let end = (pos.end_line as i64).max(start); + let text_end = text_end_line(node).unwrap_or(end); + Some((start, text_end.clamp(start, end))) +} + +fn text_end_line(node: &Node) -> Option<i64> { + let pos = node.pos.as_ref()?; + if !node.token.is_empty() { + let extra = node.token.lines().count().max(1) as i64 - 1; + return Some(pos.start_line as i64 + extra); } - None + node.children + .iter() + .filter(|c| c.node_type == uast::COMMENT) + .filter_map(text_end_line) + .max() +} + +/// `(start_line, end_line)` of any node, or `None` without a position. +#[must_use] +pub fn node_lines(node: &Node) -> Option<(i64, i64)> { + let pos = node.pos.as_ref()?; + let start = pos.start_line as i64; + Some((start, (pos.end_line as i64).max(start))) } diff --git a/crates/cf-comments/src/types.rs b/crates/cf-comments/src/types.rs index 1ae1c03e..69b69651 100644 --- a/crates/cf-comments/src/types.rs +++ b/crates/cf-comments/src/types.rs @@ -5,12 +5,10 @@ use std::collections::BTreeMap; -use cf_uast_node::Node; - /// UAST node-type strings used by the comments analyzer. /// /// The string values are the contract; [`cf_uast_node::Type`] is a string -/// newtype carrying exactly these. +/// alias carrying exactly these. pub mod uast { /// Comment node type. pub const COMMENT: &str = "Comment"; @@ -28,37 +26,92 @@ pub mod uast { pub const VARIABLE: &str = "Variable"; /// Identifier node type (used for name extraction). pub const IDENTIFIER: &str = "Identifier"; + /// Block node type (a declaration body carries the `Body` role). + pub const BLOCK: &str = "Block"; + /// Parameter node type (part of a declaration header). + pub const PARAMETER: &str = "Parameter"; + /// Pattern node type (a return-type / parameter pattern in the header). + pub const PATTERN: &str = "Pattern"; + /// Type-annotation node type (part of a declaration header). + pub const TYPE_ANNOTATION: &str = "TypeAnnotation"; + /// Decorator node type (part of a declaration header). + pub const DECORATOR: &str = "Decorator"; + /// Attribute node type (part of a declaration header). + pub const ATTRIBUTE: &str = "Attribute"; } /// The `Name` role string. pub const ROLE_NAME: &str = "Name"; +/// The `Type` role string: an identifier carrying it names a type, never the +/// declaration it sits in (C return types, Go receiver types). +pub const ROLE_TYPE: &str = "Type"; +/// The `Attribute` role string: attribute / decorator / annotation lines +/// that belong to the declaration they precede. +pub const ROLE_ATTRIBUTE: &str = "Attribute"; +/// The `Doc` role string: a documentation comment (`///`, `/** */`, a +/// Python docstring). +pub const ROLE_DOC: &str = "Doc"; +/// The `Body` role string: the block holding a declaration's statements. +pub const ROLE_BODY: &str = "Body"; +/// The `Parameter` role string (declaration header). +pub const ROLE_PARAMETER: &str = "Parameter"; + +/// Name reported for a declaration whose name cannot be resolved. +pub const UNKNOWN_NAME: &str = "unknown"; + +/// Where a comment block sits relative to the declarations of its file. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Placement { + /// The block ends on the line directly above a declaration (attribute + /// lines included): it documents that declaration. + Above, + /// The block is a documentation comment that is the first statement of + /// a declaration body (a Python docstring): it documents that + /// declaration. + Docstring, + /// The block lies inside a declaration's span: a body comment. + Inline, + /// The block is attached to no declaration. + Unassociated, +} + +impl Placement { + /// The report string for this placement. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Placement::Above => "above", + Placement::Docstring => "docstring", + Placement::Inline => "inline", + Placement::Unassociated => "unassociated", + } + } +} /// Per-analysis configuration. #[derive(Debug, Clone, PartialEq)] pub struct CommentConfig { - /// Score awarded to a well-placed comment. + /// Score awarded to a comment that documents a declaration. pub reward_score: f64, - /// Maximum comment length (informational). - pub max_comment_length: i64, - /// Penalty scores keyed by target node type. + /// Penalty for an inline comment, keyed by the enclosing declaration's + /// node type. pub penalty_scores: BTreeMap<String, f64>, } impl CommentConfig { /// Returns the default configuration (report contract): - /// `reward_score = 1.0`, `max_comment_length = 500`, penalties - /// `Function/Method -0.5`, `Class/Interface/Struct -0.3`, - /// `Variable/Assignment/Call -0.1`. + /// `reward_score = 1.0`, penalties `Function/Method -0.5`, + /// `Class/Interface/Struct -0.3`, `Variable/Assignment/Call -0.1`. /// /// ``` /// use cf_comments::CommentConfig; /// let cfg = CommentConfig::default_config(); /// assert_eq!(cfg.reward_score, 1.0); - /// assert_eq!(cfg.max_comment_length, 500); /// assert_eq!(cfg.penalty_scores.get("Function"), Some(&-0.5)); /// assert_eq!(cfg.penalty_scores.get("Class"), Some(&-0.3)); /// assert_eq!(cfg.penalty_scores.get("Variable"), Some(&-0.1)); /// ``` + #[must_use] pub fn default_config() -> Self { let mut penalty_scores = BTreeMap::new(); penalty_scores.insert(uast::FUNCTION.to_string(), -0.5); @@ -71,106 +124,185 @@ impl CommentConfig { penalty_scores.insert("Call".to_string(), -0.1); CommentConfig { reward_score: 1.0, - max_comment_length: 500, penalty_scores, } } } -/// A contiguous block of comment nodes. -#[derive(Debug, Clone)] -pub struct CommentBlock { - /// The comment nodes composing this block, in line order. - pub comments: Vec<Node>, - /// 1-based start line of the first comment. - pub start_line: i64, - /// 1-based end line of the last comment. - pub end_line: i64, - /// Concatenated comment text (tokens joined with `"\n"`). - pub full_text: String, -} - -/// Per-comment analysis detail. +/// Per-comment analysis detail (one row per comment node). #[derive(Debug, Clone, PartialEq)] pub struct CommentDetail { - /// Node type of the comment. - pub kind: String, - /// Comment token text. - pub token: String, - /// Quality score. + /// Comment text (the node's token, or its nested comment tokens). + pub text: String, + /// 1-based line the comment starts on. + pub line: i64, + /// Placement relative to the file's declarations. + pub placement: Placement, + /// Quality score (reward when `above`, a penalty otherwise). pub score: f64, - /// Whether the comment is considered good. - pub is_good: bool, - /// Target node type, if any. + /// Type of the declaration the comment is attached to (empty when + /// unassociated). pub target_type: String, - /// Target name, if any. + /// Name of the declaration the comment is attached to (empty when + /// unassociated). pub target_name: String, - /// Relative position (`above`/`below`/`inline`/`unassociated`/`unknown`). - pub position: String, - /// 1-based line number of the comment. - pub line_number: i64, } -/// Documentation status for a single function. -#[derive(Debug, Clone, PartialEq)] +/// Assessment of a comment that documents a declaration. +pub const COMMENT_OK: &str = "✅ OK"; +/// Assessment of a comment that documents nothing. +pub const COMMENT_NOT_OK: &str = "❌ Not OK"; +/// Assessment of a documented declaration. +pub const FUNCTION_DOCUMENTED: &str = "✅ Well Documented"; +/// Assessment of an undocumented declaration. +pub const FUNCTION_UNDOCUMENTED: &str = "❌ No Comment"; + +impl CommentDetail { + /// Whether the comment documents a declaration. + #[must_use] + pub fn is_good(&self) -> bool { + matches!(self.placement, Placement::Above | Placement::Docstring) + } + + /// The table assessment string. + #[must_use] + pub fn assessment(&self) -> &'static str { + if self.is_good() { + COMMENT_OK + } else { + COMMENT_NOT_OK + } + } +} + +/// Documentation status of a single declaration (function, method, class, +/// interface or struct). +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FunctionInfo { - /// Function name. + /// Declaration name (`unknown` when unresolved). pub name: String, - /// Function node type. + /// Declaration node type. pub kind: String, - /// Whether the function has an associated good comment. - pub has_comment: bool, - /// The comment type, when documented. - pub comment_type: String, + /// 1-based line of the declaration. + pub line: i64, + /// Number of source lines the declaration spans. + pub lines: i64, + /// Line of the comment block documenting it; `None` when undocumented. + pub doc_line: Option<i64>, } -/// Aggregate metrics for a file. -#[derive(Debug, Clone)] -pub struct CommentMetrics { - /// Total number of comment details. +impl FunctionInfo { + /// Whether a comment block documents this declaration. + #[must_use] + pub fn is_documented(&self) -> bool { + self.doc_line.is_some() + } + + /// The table assessment string. + #[must_use] + pub fn assessment(&self) -> &'static str { + if self.is_documented() { + FUNCTION_DOCUMENTED + } else { + FUNCTION_UNDOCUMENTED + } + } +} + +/// The five counters every comments report is built from. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CommentCounts { + /// Number of comment nodes. pub total_comments: i64, - /// Count of good comments. + /// Comments that document a declaration. pub good_comments: i64, - /// Count of bad comments. + /// Comments that do not (`inline` or `unassociated`). pub bad_comments: i64, - /// Overall quality score (`good / total`). - pub overall_score: f64, - /// All comment details. - pub comment_details: Vec<CommentDetail>, - /// Function name -> documentation info. - pub function_summary: BTreeMap<String, FunctionInfo>, - /// Total number of functions. + /// Number of declarations. pub total_functions: i64, - /// Number of documented functions. + /// Declarations with a documenting comment. pub documented_functions: i64, } -/// A row of the detailed comments table. -#[derive(Debug, Clone, PartialEq)] -pub struct CommentReportItem { - /// Comment line number. - pub line: i64, - /// (Truncated) comment text. - pub comment: String, - /// Placement string. - pub placement: String, - /// Target name. - pub target: String, - /// Assessment string. - pub assessment: String, +/// Quality-message thresholds over the good-comment ratio. +const EXCELLENT_MIN: f64 = 0.8; +const GOOD_MIN: f64 = 0.6; +const FAIR_MIN: f64 = 0.4; + +/// Message for a report with neither comments nor declarations. +pub const NO_COMMENTS_MESSAGE: &str = "No comments found"; + +impl CommentCounts { + /// Good-comment ratio (`good / total`), 0 when there are no comments. + #[must_use] + pub fn overall_score(&self) -> f64 { + safe_div(self.good_comments as f64, self.total_comments as f64) + } + + /// Documentation coverage (`documented / total`), 0 when there are no + /// declarations. + #[must_use] + pub fn documentation_coverage(&self) -> f64 { + safe_div( + self.documented_functions as f64, + self.total_functions as f64, + ) + } + + /// The quality message: the threshold label of + /// [`overall_score`](Self::overall_score) (`>= 0.8` excellent, `>= 0.6` + /// good, `>= 0.4` fair, else poor), or [`NO_COMMENTS_MESSAGE`] when the + /// report has neither comments nor declarations. + #[must_use] + pub fn message(&self) -> &'static str { + if self.total_comments == 0 && self.total_functions == 0 { + return NO_COMMENTS_MESSAGE; + } + quality_message(self.overall_score()) + } + + /// Adds another report's counters to these. + pub fn add(&mut self, other: &CommentCounts) { + self.total_comments += other.total_comments; + self.good_comments += other.good_comments; + self.bad_comments += other.bad_comments; + self.total_functions += other.total_functions; + self.documented_functions += other.documented_functions; + } } -/// A row of the detailed functions table. -#[derive(Debug, Clone, PartialEq)] -pub struct FunctionReportItem { - /// Function name. - pub function: String, - /// Function type. - pub kind: String, - /// Function line count. - pub lines: i64, - /// Comment type / "None". - pub comment: String, - /// Assessment string. - pub assessment: String, +/// The threshold label of a good-comment ratio. +#[must_use] +pub fn quality_message(score: f64) -> &'static str { + if score >= EXCELLENT_MIN { + "Excellent comment quality and placement" + } else if score >= GOOD_MIN { + "Good comment quality with room for improvement" + } else if score >= FAIR_MIN { + "Fair comment quality - consider improving placement" + } else { + "Poor comment quality - significant improvement needed" + } +} + +/// `a / b`, or 0 when `b` is 0. +#[must_use] +pub fn safe_div(a: f64, b: f64) -> f64 { + if b == 0.0 { + 0.0 + } else { + a / b + } +} + +/// One file's analysis: counters plus the per-comment and per-declaration +/// rows they were computed from. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct CommentMetrics { + /// The counters. + pub counts: CommentCounts, + /// One row per comment node, in line order. + pub comments: Vec<CommentDetail>, + /// One row per declaration, in document order. + pub functions: Vec<FunctionInfo>, } diff --git a/crates/cf-comments/tests/real_trees.rs b/crates/cf-comments/tests/real_trees.rs new file mode 100644 index 00000000..c71d6a8f --- /dev/null +++ b/crates/cf-comments/tests/real_trees.rs @@ -0,0 +1,429 @@ +//! Placement and coverage rules proven on REAL parsed trees: every fixture +//! goes through `cf_uast::Parser` (the tree-sitter grammar + the language +//! mapping), so these tests pin what users get, not what hand-built nodes say. +//! +//! Rules under test (see `site/explanation/comments.md`): +//! * a function is documented iff a comment block ends on the line directly +//! above its declaration (attribute / decorator lines belong to the +//! declaration); +//! * a comment inside a declaration's span is `inline`; +//! * anything else is `unassociated`; +//! * documentation is keyed by declaration identity, never by name. + +use cf_comments::Analyzer; +use cf_gojson::GoValue; +use cf_uast::Parser; + +/// Analyzes `source` as `filename` and returns the per-file report map. +fn analyze(filename: &str, source: &str) -> GoValue { + let root = Parser::new() + .parse(filename, source.as_bytes()) + .expect("fixture parses"); + Analyzer::new().analyze(Some(&root)).expect("analysis") +} + +fn int(report: &GoValue, key: &str) -> i64 { + match report.as_map().and_then(|m| m.get(key)) { + Some(GoValue::Int(n)) => *n, + other => panic!("{key} is not an int: {other:?}"), + } +} + +fn rows<'a>(report: &'a GoValue, key: &str) -> &'a [GoValue] { + match report.as_map().and_then(|m| m.get(key)) { + Some(GoValue::Array(items)) => items, + other => panic!("{key} is not an array: {other:?}"), + } +} + +fn str_field<'a>(row: &'a GoValue, key: &str) -> &'a str { + match row.as_map().and_then(|m| m.get(key)) { + Some(GoValue::Str(s)) => s, + other => panic!("{key} is not a string: {other:?}"), + } +} + +fn int_field(row: &GoValue, key: &str) -> i64 { + match row.as_map().and_then(|m| m.get(key)) { + Some(GoValue::Int(n)) => *n, + other => panic!("{key} is not an int: {other:?}"), + } +} + +/// `(function name, documented?)` for every row of the `functions` table. +fn function_status(report: &GoValue) -> Vec<(String, bool)> { + rows(report, "functions") + .iter() + .map(|row| { + ( + str_field(row, "function").to_string(), + str_field(row, "assessment") == "✅ Well Documented", + ) + }) + .collect() +} + +/// `(line, placement, target)` for every row of the `comments` table. +fn comment_placements(report: &GoValue) -> Vec<(i64, String, String)> { + rows(report, "comments") + .iter() + .map(|row| { + ( + int_field(row, "line"), + str_field(row, "placement").to_string(), + str_field(row, "target").to_string(), + ) + }) + .collect() +} + +const GO_NO_COMMENTS: &str = "package a + +func Add(a, b int) int { + return a + b +} + +func Sub(a, b int) int { + return a - b +} +"; + +#[test] +fn zero_comment_file_keeps_its_functions() { + let report = analyze("a.go", GO_NO_COMMENTS); + assert_eq!(int(&report, "total_comments"), 0); + assert_eq!(int(&report, "total_functions"), 2); + assert_eq!(int(&report, "documented_functions"), 0); + assert_eq!( + function_status(&report), + vec![("Add".to_string(), false), ("Sub".to_string(), false)] + ); +} + +const GO_DUPLICATE_NAMES: &str = "package a + +type A struct{} +type B struct{} + +// String renders A. +func (A) String() string { + return \"A\" +} + +func (B) String() string { + return \"B\" +} +"; + +#[test] +fn duplicate_names_are_documented_independently() { + let report = analyze("a.go", GO_DUPLICATE_NAMES); + assert_eq!(int(&report, "documented_functions"), 1); + let strings: Vec<bool> = function_status(&report) + .into_iter() + .filter(|(name, _)| name == "String") + .map(|(_, documented)| documented) + .collect(); + assert_eq!(strings, vec![true, false], "only A.String is documented"); +} + +const RUST_DOC_COMMENT: &str = "/// Documents foo. +pub fn foo() -> i32 { + 1 +} + +pub fn bar() -> i32 { + 2 +} +"; + +#[test] +fn rust_doc_comment_documents_the_next_item_exactly_once() { + let report = analyze("a.rs", RUST_DOC_COMMENT); + assert_eq!(int(&report, "total_comments"), 1); + assert_eq!(int(&report, "good_comments"), 1); + assert_eq!(int(&report, "total_functions"), 2); + assert_eq!(int(&report, "documented_functions"), 1); + assert_eq!( + function_status(&report), + vec![("foo".to_string(), true), ("bar".to_string(), false)] + ); +} + +const RUST_ATTRIBUTE_BETWEEN: &str = "/// Doc f. +#[inline] +pub fn f() -> i32 { + 1 +} +"; + +#[test] +fn rust_attribute_between_doc_and_item_keeps_the_item_documented() { + let report = analyze("a.rs", RUST_ATTRIBUTE_BETWEEN); + assert_eq!(int(&report, "total_comments"), 1); + assert_eq!(int(&report, "documented_functions"), 1); +} + +const GO_BODY_COMMENT: &str = "package a + +func Add(a, b int) int { + // inline comment + return a + b +} + +func Sub(a, b int) int { + return a - b // trailing +} +"; + +#[test] +fn comments_inside_a_body_are_inline_for_that_function() { + let report = analyze("a.go", GO_BODY_COMMENT); + assert_eq!( + comment_placements(&report), + vec![ + (4, "inline".to_string(), "Add".to_string()), + (9, "inline".to_string(), "Sub".to_string()), + ] + ); + assert_eq!(int(&report, "good_comments"), 0); + assert_eq!(int(&report, "documented_functions"), 0); +} + +const GO_BLANK_LINE_GAPS: &str = "package a + +// Doc with one blank line below. + +func A() int { + return 1 +} + +// Doc with two blank lines below. + + +func B() int { + return 2 +} + +// multi-line doc block line 1 +// multi-line doc block line 2 + + +func C() int { + return 3 +} + +// Directly above D. +func D() int { + return 4 +} +"; + +#[test] +fn a_blank_line_between_comment_and_declaration_breaks_documentation() { + let report = analyze("a.go", GO_BLANK_LINE_GAPS); + assert_eq!(int(&report, "total_comments"), 5); + assert_eq!(int(&report, "good_comments"), 1); + assert_eq!(int(&report, "documented_functions"), 1); + assert_eq!( + function_status(&report), + vec![ + ("A".to_string(), false), + ("B".to_string(), false), + ("C".to_string(), false), + ("D".to_string(), true), + ] + ); + let placements: Vec<String> = comment_placements(&report) + .into_iter() + .map(|(_, placement, _)| placement) + .collect(); + assert_eq!( + placements, + vec![ + "unassociated", + "unassociated", + "unassociated", + "unassociated", + "above" + ] + ); +} + +const JAVA_CLASS: &str = "/** Javadoc for A. */ +public class A { + /** Javadoc for foo. */ + public int foo() { + // inside foo + return 1; + } + + public int bar() { + return 2; // trailing + } + + /* plain block */ + public int baz() { + return 3; + } +} +"; + +#[test] +fn java_class_body_is_not_a_declaration_and_member_docs_are_above() { + let report = analyze("A.java", JAVA_CLASS); + assert_eq!(int(&report, "total_functions"), 4, "A, foo, bar, baz"); + assert_eq!( + function_status(&report), + vec![ + ("A".to_string(), true), + ("foo".to_string(), true), + ("bar".to_string(), false), + ("baz".to_string(), true), + ] + ); + assert_eq!( + comment_placements(&report), + vec![ + (1, "above".to_string(), "A".to_string()), + (3, "above".to_string(), "foo".to_string()), + (5, "inline".to_string(), "foo".to_string()), + (10, "inline".to_string(), "bar".to_string()), + (13, "above".to_string(), "baz".to_string()), + ] + ); +} + +const PYTHON_MODULE: &str = "# module comment +\"\"\"Module docstring.\"\"\" + + +# documents foo +def foo(): + \"\"\"Docstring of foo.\"\"\" + return 1 + + +def bar(): + # inside bar + return 2 + + +# documents K +@dataclass +class K: + \"\"\"Class docstring.\"\"\" + + # documents m + def m(self): + return 3 +"; + +#[test] +fn python_hash_comments_and_docstrings_document_their_declarations() { + let report = analyze("a.py", PYTHON_MODULE); + assert_eq!( + int(&report, "total_comments"), + 8, + "5 hash comments + module, foo and K docstrings" + ); + assert_eq!(int(&report, "total_functions"), 4, "foo, bar, K, m"); + assert_eq!( + function_status(&report), + vec![ + ("foo".to_string(), true), + ("bar".to_string(), false), + ("K".to_string(), true), + ("m".to_string(), true), + ] + ); + let placements: Vec<String> = comment_placements(&report) + .into_iter() + .map(|(_, placement, _)| placement) + .collect(); + assert_eq!( + placements, + vec![ + "unassociated", // # module comment + "unassociated", // module docstring: no enclosing declaration + "above", // # documents foo + "docstring", // foo's docstring + "inline", // # inside bar + "above", // # documents K + "docstring", // K's docstring + "above", // # documents m + ] + ); +} + +const PYTHON_DOCSTRING_ONLY: &str = "def foo(): + \"\"\"Documents foo.\"\"\" + return 1 + + +def bar(): + x = 1 + \"\"\"A stray string, not a docstring.\"\"\" + return x + + +class K: + \"\"\"Documents K.\"\"\" + + def m(self): + \"\"\"Documents m.\"\"\" + return 2 +"; + +/// A docstring (the first statement of a body) documents its declaration; a +/// string literal later in the body is an inline comment of that body. +#[test] +fn python_docstrings_document_only_as_the_first_body_statement() { + let report = analyze("d.py", PYTHON_DOCSTRING_ONLY); + assert_eq!(int(&report, "total_functions"), 4, "foo, bar, K, m"); + assert_eq!(int(&report, "documented_functions"), 3); + assert_eq!( + function_status(&report), + vec![ + ("foo".to_string(), true), + ("bar".to_string(), false), + ("K".to_string(), true), + ("m".to_string(), true), + ] + ); + let placements: Vec<(i64, String, String)> = comment_placements(&report); + let by_line: Vec<(i64, &str, &str)> = placements + .iter() + .map(|(line, p, t)| (*line, p.as_str(), t.as_str())) + .collect(); + assert_eq!( + by_line, + vec![ + (2, "docstring", "foo"), + (8, "inline", "bar"), + (13, "docstring", "K"), + (16, "docstring", "m"), + ] + ); +} + +const C_FUNCTIONS: &str = "/* adds */ +int add(int a, int b) { + return a + b; +} + +int sub(int a, int b) { + return a - b; +} +"; + +#[test] +fn c_function_names_are_identifiers_not_source_text() { + let report = analyze("a.c", C_FUNCTIONS); + let names: Vec<String> = function_status(&report) + .into_iter() + .map(|(name, _)| name) + .collect(); + assert_eq!(names, vec!["add".to_string(), "sub".to_string()]); + assert_eq!(int(&report, "documented_functions"), 1); +} diff --git a/crates/cf-complexity/Cargo.toml b/crates/cf-complexity/Cargo.toml index 28d0e134..99e68441 100644 --- a/crates/cf-complexity/Cargo.toml +++ b/crates/cf-complexity/Cargo.toml @@ -8,23 +8,27 @@ repository.workspace = true publish.workspace = true # cf-complexity — static cyclomatic/cognitive/nesting complexity analyzer -# (analyzer id: static/complexity). Also used by the quality analyzer. +# (analyzer id: static/complexity). Also used by the history quality analyzer. # -# Report serialization routes exclusively through cf-gojson (and cf-goyaml) so -# the machine-format bytes (json/yaml/ndjson/timeseries/compact/bin) match the -# report contract byte-for-byte. See DESIGN.md §2. +# The crate owns the math (counting rules, thresholds, aggregation, ordering) +# and the UAST bridge; the cf-commands handlers own I/O and serialization. +# Report values are built as cf-gojson GoValue trees (never serde_json). [dependencies] -# Report-contract value model + encoder (DESIGN §2.2). The analyzer builds its -# report as a GoValue tree; serialization is owned by the report layer. -# NEVER serde_json. +# Report-contract value model + encoder (DESIGN §2.2). cf-gojson = { workspace = true } -# Pinned unstable sort (pdqsort) for tie ordering, shared across analyzers. -cf-gosort = { workspace = true } -# On-demand stack growth for the recursive complexity walk — the trees come -# from cf-uast, which already needs a segmented stack to build them (>10k -# nesting levels on real sources). +# The canonical UAST node model: `node::Node::from_uast` is the one bridge +# every report format shares. +cf-uast-node = { path = "../cf-uast-node" } +# On-demand stack growth for the recursive walks — the trees come from +# cf-uast, which already needs a segmented stack to build them (>10k nesting +# levels on real sources). stacker = "0.1.24" +[dev-dependencies] +# Fixture tests parse real multi-language sources and assert hand-computed +# metrics end-to-end from source text. +cf-uast = { path = "../cf-uast" } + [lints.clippy] pedantic = "warn" diff --git a/crates/cf-complexity/src/discover.rs b/crates/cf-complexity/src/discover.rs new file mode 100644 index 00000000..c573804e --- /dev/null +++ b/crates/cf-complexity/src/discover.rs @@ -0,0 +1,386 @@ +//! Function discovery and naming. +//! +//! A function is a `Function`/`Method` node (or any node carrying both the +//! `Function` and `Declaration` roles) that has a body. Declaration-only +//! nodes — C prototypes and declarators, Go function types and interface +//! methods, Rust trait signatures, Java abstract/interface methods — are +//! never reported. +//! +//! Nesting rules: +//! +//! * A *named* function nested inside another function (Python `def` in +//! `def`, JS/Rust inner functions, `const g = function () {}`) is its own +//! unit; its body is excluded from the enclosing function's metrics. +//! * An *anonymous* function literal nested inside a function (a JS callback +//! `function (y) { ... }`, a Go/Rust closure) belongs to the enclosing +//! function and raises its cognitive nesting, exactly like a `Lambda`. +//! * A `Function` whose body is itself an anonymous `Function`/`Lambda` child +//! (the JS `const f = (x) => {}` / `const g = function () {}` shape) owns +//! that child as its body: the wrapper never becomes a second function. + +use crate::node::{role, uast, Node}; + +/// Display name for a function without a recoverable name. +pub const ANONYMOUS_FUNCTION_NAME: &str = "anonymous"; + +/// Longest plausible function name; longer "names" are source slices picked +/// up by a mapping gap and are rejected. +const MAX_NAME_LEN: usize = 200; + +/// Node types that only ever appear in a signature. +const SIGNATURE_TYPES: &[&str] = &[ + uast::PARAMETER, + uast::IDENTIFIER, + uast::COMMENT, + "TypeAnnotation", + "Decorator", + "Annotation", + "Modifier", +]; + +/// Node types whose presence proves a function has executable content even +/// without a `Body`-role block (expression-bodied members). +const CODE_TYPES: &[&str] = &[ + uast::BLOCK, + uast::IF, + uast::LOOP, + uast::SWITCH, + uast::MATCH, + uast::CASE, + uast::TRY, + uast::CATCH, + uast::CALL, + uast::BINARY_OP, + uast::UNARY_OP, + uast::LAMBDA, + uast::BREAK, + uast::CONTINUE, + "Return", + "Assignment", + "Variable", + "Literal", + "Throw", + "Yield", + "Await", + "Index", + "Field", +]; + +/// Reports whether `n` is shaped like a function declaration. +pub(crate) fn is_function_candidate(n: &Node) -> bool { + n.has_any_type(&[uast::FUNCTION, uast::METHOD]) + || n.has_all_roles(&[role::FUNCTION, role::DECLARATION]) +} + +/// The child that holds a function's executable body: the `Body`-role child, +/// else the first `Block`, else an anonymous `Function`/`Lambda` child that +/// the node merely wraps (JS arrow/function-expression declarations). +pub(crate) fn body_child(n: &Node) -> Option<&Node> { + if let Some(body) = n.children.iter().find(|c| c.has_role(role::BODY)) { + return Some(body); + } + if let Some(block) = n.children.iter().find(|c| c.is_type(uast::BLOCK)) { + return Some(block); + } + n.children.iter().find(|c| { + c.is_type(uast::LAMBDA) + || (c.has_any_type(&[uast::FUNCTION, uast::METHOD]) + && !has_name(c, None) + && !is_declaration_only(c)) + }) +} + +/// True when `n` declares a signature without any executable content. +pub(crate) fn is_declaration_only(n: &Node) -> bool { + body_child(n).is_none() && !has_code_outside_signature(n) +} + +/// Pre-order scan for a [`CODE_TYPES`] node, skipping signature subtrees +/// (parameter lists carry default-value literals that are not code). +fn has_code_outside_signature(n: &Node) -> bool { + let mut stack: Vec<&Node> = n.children.iter().collect(); + while let Some(node) = stack.pop() { + if node.has_any_type(SIGNATURE_TYPES) { + continue; + } + if node.has_any_type(CODE_TYPES) { + return true; + } + stack.extend(node.children.iter()); + } + false +} + +/// True when the name-role child of `n` is the function's own body wrapper +/// (the inner anonymous function of `const g = function () {}`). +pub(crate) fn is_body_wrapper(parent: &Node, child: &Node) -> bool { + child.has_any_type(&[uast::FUNCTION, uast::METHOD, uast::LAMBDA]) + && body_child(parent).is_some_and(|b| std::ptr::eq(b, child)) +} + +/// Finds every reportable function below `root` in pre-order. +/// +/// `find_depth` caps discovery: only candidates at depth `<= cap` below the +/// root (root at depth 0) are found; the history `quality` analyzer runs with +/// a cap, the static surfaces without one. +pub(crate) fn find_functions<'a>( + root: &'a Node, + source: Option<&[u8]>, + find_depth: Option<usize>, +) -> Vec<&'a Node> { + struct Frame<'a> { + node: &'a Node, + depth: usize, + inside_function: bool, + is_wrapper: bool, + } + + let mut found: Vec<&'a Node> = Vec::new(); + let mut stack = vec![Frame { + node: root, + depth: 0, + inside_function: false, + is_wrapper: false, + }]; + while let Some(frame) = stack.pop() { + let n = frame.node; + let within_cap = find_depth.map_or(true, |cap| frame.depth <= cap); + let is_found = within_cap + && !frame.is_wrapper + && is_function_candidate(n) + && !is_declaration_only(n) + && (!frame.inside_function || has_name(n, source)); + if is_found { + found.push(n); + } + for child in n.children.iter().rev() { + stack.push(Frame { + node: child, + depth: frame.depth + 1, + inside_function: frame.inside_function || is_found, + is_wrapper: is_found && is_body_wrapper(n, child), + }); + } + } + found +} + +/// True when a plausible name can be recovered for `n`. +pub(crate) fn has_name(n: &Node, source: Option<&[u8]>) -> bool { + recover_name(n, source).is_some() +} + +/// The display name of a function: a `name`-like prop, else its `Name`-role +/// identifier (through a C-style declarator child), else an identifier in +/// the signature, else [`ANONYMOUS_FUNCTION_NAME`]. Multi-line or oversized +/// tokens are never names. +pub(crate) fn function_name(n: &Node, source: Option<&[u8]>) -> String { + recover_name(n, source).unwrap_or_else(|| ANONYMOUS_FUNCTION_NAME.to_string()) +} + +fn recover_name(n: &Node, source: Option<&[u8]>) -> Option<String> { + for key in ["name", "function_name", "method_name"] { + if let Some(name) = n.prop(key).and_then(plausible_name) { + return Some(name); + } + } + if let Some(name) = direct_name_identifier(n, source) { + return Some(name); + } + // C/C++ declarator: `Function{Identifier(Type), Function{Identifier(Name), List}, Block}`. + for declarator in n + .children + .iter() + .filter(|c| c.has_any_type(&[uast::FUNCTION, uast::METHOD]) && is_declaration_only(c)) + { + if let Some(name) = direct_name_identifier(declarator, source) { + return Some(name); + } + } + signature_name_identifier(n, source) +} + +/// The text of a `Name`-role, non-`Type` identifier among the direct children. +fn direct_name_identifier(n: &Node, source: Option<&[u8]>) -> Option<String> { + n.children + .iter() + .filter(|c| is_name_identifier(c)) + .find_map(|c| identifier_text(c, source)) +} + +/// Last resort: the first `Name`-role identifier anywhere in the signature +/// part (every child before the body), skipping parameter lists (by type or +/// by role: JS wraps its parameters in a `Parameter`-role `Synthetic`). +fn signature_name_identifier(n: &Node, source: Option<&[u8]>) -> Option<String> { + let body = body_child(n); + let mut stack: Vec<&Node> = n + .children + .iter() + .rev() + .filter(|c| !body.is_some_and(|b| std::ptr::eq(b, *c))) + .collect(); + while let Some(node) = stack.pop() { + if node.is_type(uast::PARAMETER) || node.has_role(role::PARAMETER) { + continue; + } + if is_name_identifier(node) { + if let Some(name) = identifier_text(node, source) { + return Some(name); + } + } + stack.extend(node.children.iter().rev()); + } + None +} + +fn is_name_identifier(n: &Node) -> bool { + n.is_type(uast::IDENTIFIER) && n.has_role(role::NAME) && !n.has_role(role::TYPE) +} + +/// An identifier's text: its token, else its source span. +pub(crate) fn identifier_text(n: &Node, source: Option<&[u8]>) -> Option<String> { + if let Some(name) = plausible_name(&n.token) { + return Some(name); + } + n.span_text(source).and_then(plausible_name) +} + +fn plausible_name(raw: &str) -> Option<String> { + let name = raw.trim(); + if name.is_empty() || name.len() > MAX_NAME_LEN || name.contains('\n') { + return None; + } + Some(name.to_string()) +} + +#[cfg(test)] +mod tests { + use super::{find_functions, function_name, is_declaration_only, ANONYMOUS_FUNCTION_NAME}; + use crate::node::{role, uast, Node}; + + fn func(name: Option<&str>, children: Vec<Node>) -> Node { + let mut f = Node::new(uast::FUNCTION) + .with_roles(vec![role::FUNCTION, role::DECLARATION]) + .with_children(children); + if let Some(n) = name { + f = f.with_prop("name", n); + } + f + } + + fn body() -> Node { + Node::new(uast::BLOCK).with_roles(vec![role::BODY]) + } + + #[test] + fn declaration_only_functions_are_not_found() { + let proto = func(Some("proto"), vec![Node::new(uast::PARAMETER)]); + assert!(is_declaration_only(&proto)); + let root = Node::new(uast::FILE).with_children(vec![proto]); + assert!(find_functions(&root, None, None).is_empty()); + } + + #[test] + fn expression_bodied_member_counts_as_a_function() { + let member = func( + Some("Expr"), + vec![Node::new(uast::SYNTHETIC).with_children(vec![Node::new(uast::IF)])], + ); + assert!(!is_declaration_only(&member)); + } + + #[test] + fn c_declarator_is_not_a_second_function_and_names_the_outer() { + let declarator = func( + None, + vec![ + Node::with_token(uast::IDENTIFIER, "c_decisions").with_roles(vec![role::NAME]), + Node::new("List"), + ], + ); + let outer = func( + None, + vec![ + Node::with_token(uast::IDENTIFIER, "int").with_roles(vec![role::TYPE, role::NAME]), + declarator, + body(), + ], + ); + let root = Node::new(uast::FILE).with_children(vec![outer]); + let found = find_functions(&root, None, None); + assert_eq!(found.len(), 1); + assert_eq!(function_name(found[0], None), "c_decisions"); + } + + #[test] + fn multi_line_tokens_are_never_names() { + let f = Node::with_token(uast::FUNCTION, "int f() {\n return 1;\n}") + .with_roles(vec![role::FUNCTION, role::DECLARATION]) + .with_children(vec![body()]); + assert_eq!(function_name(&f, None), ANONYMOUS_FUNCTION_NAME); + } + + #[test] + fn name_comes_from_source_span_when_the_token_is_empty() { + let ident = Node::new(uast::IDENTIFIER) + .with_roles(vec![role::NAME]) + .with_pos(crate::node::Positions { + start_offset: 3, + end_offset: 8, + ..Default::default() + }); + let f = func(None, vec![ident, body()]); + assert_eq!(function_name(&f, Some(b"fn outer() {}")), "outer"); + assert_eq!(function_name(&f, None), ANONYMOUS_FUNCTION_NAME); + } + + #[test] + fn named_nested_functions_are_separate_but_anonymous_ones_are_not() { + let named_inner = func(Some("inner"), vec![body()]); + let anonymous_inner = func(None, vec![body()]); + let outer = func( + Some("outer"), + vec![body().with_children(vec![named_inner, anonymous_inner])], + ); + let root = Node::new(uast::FILE).with_children(vec![outer]); + let names: Vec<String> = find_functions(&root, None, None) + .into_iter() + .map(|f| function_name(f, None)) + .collect(); + assert_eq!(names, vec!["outer", "inner"]); + } + + #[test] + fn top_level_anonymous_function_is_found() { + let root = Node::new(uast::FILE).with_children(vec![func(None, vec![body()])]); + assert_eq!(find_functions(&root, None, None).len(), 1); + } + + #[test] + fn body_wrapper_function_is_not_a_second_function() { + // const g = function () {} : Function(g){Identifier, Function{Block}}. + let inner = func(None, vec![body()]); + let g = func( + Some("g"), + vec![ + Node::with_token(uast::IDENTIFIER, "g").with_roles(vec![role::NAME]), + inner, + ], + ); + let root = Node::new(uast::FILE).with_children(vec![g]); + let found = find_functions(&root, None, None); + assert_eq!(found.len(), 1); + assert_eq!(function_name(found[0], None), "g"); + } + + #[test] + fn find_depth_caps_discovery() { + let deep = func(Some("deep"), vec![body()]); + let wrapper = Node::new(uast::BLOCK).with_children(vec![deep]); + let shallow = func(Some("shallow"), vec![body()]); + let root = Node::new(uast::FILE).with_children(vec![shallow, wrapper]); + assert_eq!(find_functions(&root, None, Some(1)).len(), 1); + assert_eq!(find_functions(&root, None, Some(2)).len(), 2); + assert_eq!(find_functions(&root, None, None).len(), 2); + } +} diff --git a/crates/cf-complexity/src/gosort.rs b/crates/cf-complexity/src/gosort.rs deleted file mode 100644 index 4acc8c30..00000000 --- a/crates/cf-complexity/src/gosort.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Re-export of the shared unstable-sort port (`cf_gosort`). -//! -//! The report contract pins tie ordering to a specific pattern-defeating -//! quicksort (pdqsort) permutation; the implementation lives in the shared -//! [`cf_gosort`] crate so every analyzer reproduces the exact element -//! movement. This module re-exports it for use within `cf-complexity`. - -pub use cf_gosort::go_sort_slice; diff --git a/crates/cf-complexity/src/lib.rs b/crates/cf-complexity/src/lib.rs index dbc70419..df84c5be 100644 --- a/crates/cf-complexity/src/lib.rs +++ b/crates/cf-complexity/src/lib.rs @@ -1,46 +1,42 @@ //! Static complexity analyzer (cyclomatic / cognitive / nesting). //! //! Walks a UAST tree, computes per-function cyclomatic complexity, cognitive -//! complexity (`SonarSource` methodology), nesting depth, decision points, -//! and lines of code, and returns a deterministic result map. +//! complexity (`SonarSource` model), nesting depth, decision points and lines +//! of code, and returns a deterministic result. The counting rules live in +//! [`metrics`](crate::metrics) (module docs), discovery and naming in +//! [`discover`](crate::discover), every threshold in [`thresholds`], and the +//! multi-file aggregate view in [`report`]. //! -//! # Output shape +//! Pass the file's source bytes through [`AnalysisOptions::source`] whenever +//! you have them: operators, identifier names and lines of code are recovered +//! from the source when the UAST does not carry them. //! -//! On success the returned [`cf_gojson::GoValue`] object carries the keys -//! `analyzer_name`, `total_functions`, `average_complexity`, `max_complexity`, -//! `total_complexity`, `cognitive_complexity`, `nesting_depth`, -//! `decision_points`, `functions`, and `message`. The `functions` array holds -//! one object per function with the per-function metric keys plus the -//! assessment strings (`complexity_assessment`, `cognitive_assessment`, -//! `nesting_assessment`). +//! # Output shape //! -//! For a missing root or a tree with no functions, the empty-result shape is -//! returned (`total_functions`, `average_complexity`, `max_complexity`, +//! [`Analyzer::analyze`] returns a map-origin [`cf_gojson::GoValue`] object +//! with `analyzer_name`, `total_functions`, `average_complexity`, +//! `max_complexity`, `total_complexity`, `cognitive_complexity` (sum), +//! `nesting_depth` (max), `decision_points`, `functions` (one object per +//! function with the metric keys plus the assessment strings) and `message`. +//! A missing root or a tree with no functions yields the empty shape +//! (`total_functions`, `average_complexity`, `max_complexity`, //! `total_complexity`, `message`). //! -//! Because [`cf_gojson::GoValue`] objects are map-origin here, their keys -//! byte-sort on encode (report-format contract). Serialization itself is owned -//! by the report layer; this crate only builds the value tree. The static -//! pipeline's aggregated rendering view lives in [`report`]. -//! -//! Compatibility: output bytes are pinned against the reference implementation -//! by the differential gate in `tests/compat`. -//! //! # Example //! -//! Build a tiny UAST with one function containing an `if`, then read the -//! aggregate scalars off the returned report value: -//! //! ``` //! use cf_complexity::Analyzer; //! use cf_complexity::node::{uast, role, Node}; //! use cf_gojson::GoValue; //! //! // function foo() { if (...) { ... } } +//! let body = Node::new(uast::BLOCK) +//! .with_roles(vec![role::BODY]) +//! .with_children(vec![Node::new(uast::IF)]); //! let func = Node::new(uast::FUNCTION) //! .with_roles(vec![role::FUNCTION, role::DECLARATION]) //! .with_prop("name", "foo") -//! .with_children(vec![Node::new(uast::IF)]); +//! .with_children(vec![body]); //! let root = Node::new(uast::FILE).with_children(vec![func]); //! //! let report = Analyzer.analyze(Some(&root)); @@ -48,6 +44,7 @@ //! assert_eq!(m.get("total_functions"), Some(&GoValue::Int(1))); //! // cyclomatic = 1 (base) + 1 (the `if`) = 2. //! assert_eq!(m.get("total_complexity"), Some(&GoValue::Int(2))); +//! assert_eq!(m.get("cognitive_complexity"), Some(&GoValue::Int(1))); //! assert_eq!(m.get("analyzer_name").unwrap().as_str(), Some("complexity")); //! ``` //! @@ -61,21 +58,23 @@ //! let m = report.as_map().unwrap(); //! assert_eq!(m.get("total_functions"), Some(&GoValue::Int(0))); //! assert_eq!(m.get("message").unwrap().as_str(), Some("No AST provided")); -//! // The empty shape omits the `functions` array and `analyzer_name`. //! assert!(m.get("functions").is_none()); //! ``` -// Metric counts (function counts, line counts, complexity sums) are far below -// the f64 mantissa / i64 range, and the int->float divisions are part of the -// frozen report math; the pedantic cast lints add noise without value here. +// Metric counts are far below the f64 mantissa / i64 range; the pedantic +// cast lints add noise without value here. #![allow(clippy::cast_possible_wrap, clippy::cast_precision_loss)] -pub mod gosort; +mod discover; +mod metrics; pub mod node; pub mod report; +pub mod thresholds; + +use std::cmp::Ordering; use cf_gojson::{GoMap, GoValue, MapOrigin}; -use node::{uast, Node}; +use node::Node; /// The complexity analyzer. Stateless: all configuration is fixed by the /// report contract. @@ -93,34 +92,35 @@ pub struct FunctionMetrics { pub cognitive_complexity: i64, /// Maximum nesting depth. pub nesting_depth: i64, - /// Decision points (`max(cyclomatic-1, 0)`). + /// Decision points (`cyclomatic - 1`). pub decision_points: i64, - /// Estimated lines of code. + /// Lines of code (non-blank, non-comment-only lines of the span). pub lines_of_code: i64, + /// 1-based start line (0 when the node has no position). + pub line: i64, } -const ANONYMOUS_FUNCTION_NAME: &str = "anonymous"; - -// Assessment thresholds (report contract). -const CYCLOMATIC_GREEN: i64 = 1; -const CYCLOMATIC_YELLOW: i64 = 5; -const COMPLEXITY_THRESHOLD_HIGH: i64 = 5; // cognitive "low" boundary -const MAGIC10: i64 = 10; // cognitive "medium" boundary -const DEPTH_THRESHOLD_HIGH: i64 = 3; // nesting "shallow" boundary -const MAGIC5: i64 = 5; // nesting "moderate" boundary -const AVG_COMPLEXITY_THRESHOLD_HIGH: f64 = 3.0; -const MAGIC7P0: f64 = 7.0; +/// Optional inputs of an analysis run. +#[derive(Debug, Default, Clone, Copy)] +pub struct AnalysisOptions<'a> { + /// The analyzed file's bytes: enables operator, name and line-of-code + /// recovery from the source. + pub source: Option<&'a [u8]>, + /// Discovery depth cap: only functions at depth `<= cap` below the root + /// (root at depth 0) are found. The history `quality` analyzer caps at + /// 10; the static surfaces run uncapped. + pub find_depth: Option<usize>, +} -const MSG_EXCELLENT: &str = "Excellent complexity - functions are simple and maintainable"; -const MSG_GOOD: &str = "Good complexity - functions are reasonably maintainable"; -const MSG_FAIR: &str = "Fair complexity - some functions could be simplified"; -const MSG_HIGH: &str = "High complexity - functions are complex and should be refactored"; +const ANALYZER_NAME: &str = "complexity"; +const MSG_NO_AST: &str = "No AST provided"; +const MSG_NO_FUNCTIONS: &str = "No functions found"; impl Analyzer { /// Analyzer name. #[must_use] pub fn name(&self) -> &'static str { - "complexity" + ANALYZER_NAME } /// CLI flag. @@ -129,100 +129,123 @@ impl Analyzer { "complexity-analysis" } - /// Performs complexity analysis and returns the report object as a - /// [`GoValue`]. + /// Performs complexity analysis without source text and returns the + /// report object as a [`GoValue`]. #[must_use] pub fn analyze(&self, root: Option<&Node>) -> GoValue { - self.analyze_with_find_depth(root, None) + self.analyze_with(root, &AnalysisOptions::default()) } - /// [`analyze`](Self::analyze) with the reference shared traverser's - /// `maxDepth` applied to the FUNCTION DISCOVERY walk only: with - /// `Some(cap)`, only function nodes at depth `<= cap` below the root - /// (root at depth 0) are found; per-function metric computation over each - /// found function's whole subtree is unchanged. - /// - /// The history `quality` analyzer instantiates its components with - /// `maxDepth = 10` (the static surfaces run uncapped). + /// [`analyze`](Self::analyze) with a discovery depth cap (see + /// [`AnalysisOptions::find_depth`]). #[must_use] pub fn analyze_with_find_depth( &self, root: Option<&Node>, find_depth: Option<usize>, ) -> GoValue { + self.analyze_with( + root, + &AnalysisOptions { + source: None, + find_depth, + }, + ) + } + + /// Performs complexity analysis with the given options. + #[must_use] + pub fn analyze_with(&self, root: Option<&Node>, opts: &AnalysisOptions) -> GoValue { let Some(root) = root else { - return build_empty_result("No AST provided"); + return build_empty_result(MSG_NO_AST); }; - - let functions = find_functions_capped(root, find_depth); + let functions = self.function_metrics_with(Some(root), opts); if functions.is_empty() { - return build_empty_result("No functions found"); + return build_empty_result(MSG_NO_FUNCTIONS); } - - let (function_metrics, totals) = calculate_all_function_metrics(&functions); - let avg_complexity = calculate_average_complexity(&totals, function_metrics.len()); - let message = get_complexity_message(avg_complexity); - - build_result( - function_metrics.len(), - avg_complexity, - &totals, - &function_metrics, - message, - ) + build_result(&functions) } - /// Computes per-function metrics (in the deterministic report sort order) - /// without building the report map. Useful for the quality analyzer and - /// for direct testing. - /// - /// The result is sorted by the report predicate: cyclomatic descending, - /// then cognitive descending, then name ascending. A `None` root yields an - /// empty vector. + /// Computes per-function metrics without source text, in + /// [`compare_metrics`] order. A `None` root yields an empty vector. /// /// ``` /// use cf_complexity::Analyzer; /// use cf_complexity::node::{uast, role, Node}; /// - /// // `simple` has cyclomatic 1; `branchy` has an `if` (cyclomatic 2). + /// let body = |children| Node::new(uast::BLOCK).with_roles(vec![role::BODY]).with_children(children); /// let simple = Node::new(uast::FUNCTION) /// .with_roles(vec![role::FUNCTION, role::DECLARATION]) - /// .with_prop("name", "simple"); + /// .with_prop("name", "simple") + /// .with_children(vec![body(vec![])]); /// let branchy = Node::new(uast::FUNCTION) /// .with_roles(vec![role::FUNCTION, role::DECLARATION]) /// .with_prop("name", "branchy") - /// .with_children(vec![Node::new(uast::IF)]); + /// .with_children(vec![body(vec![Node::new(uast::IF)])]); /// let root = Node::new(uast::FILE).with_children(vec![simple, branchy]); /// /// let metrics = Analyzer.function_metrics(Some(&root)); - /// // Higher cyclomatic first. /// assert_eq!(metrics[0].name, "branchy"); /// assert_eq!(metrics[0].cyclomatic_complexity, 2); /// assert_eq!(metrics[1].name, "simple"); /// assert_eq!(metrics[1].cyclomatic_complexity, 1); - /// /// assert!(Analyzer.function_metrics(None).is_empty()); /// ``` #[must_use] pub fn function_metrics(&self, root: Option<&Node>) -> Vec<FunctionMetrics> { - match root { - None => Vec::new(), - Some(r) => { - let functions = find_functions(r); - calculate_all_function_metrics(&functions).0 - } - } + self.function_metrics_with(root, &AnalysisOptions::default()) + } + + /// Computes per-function metrics with the given options, in + /// [`compare_metrics`] order. + #[must_use] + pub fn function_metrics_with( + &self, + root: Option<&Node>, + opts: &AnalysisOptions, + ) -> Vec<FunctionMetrics> { + let Some(root) = root else { + return Vec::new(); + }; + let index = opts.source.map(metrics::LineIndex::new); + let mut out: Vec<FunctionMetrics> = + discover::find_functions(root, opts.source, opts.find_depth) + .into_iter() + .map(|fn_node| function_metrics_of(fn_node, opts.source, index.as_ref())) + .collect(); + out.sort_by(compare_metrics); + out + } +} + +fn function_metrics_of( + fn_node: &Node, + source: Option<&[u8]>, + index: Option<&metrics::LineIndex>, +) -> FunctionMetrics { + let name = discover::function_name(fn_node, source); + let measured = metrics::measure(fn_node, &name, source); + FunctionMetrics { + name, + cyclomatic_complexity: measured.cyclomatic, + cognitive_complexity: measured.cognitive, + nesting_depth: measured.nesting, + decision_points: (measured.cyclomatic - 1).max(0), + lines_of_code: metrics::lines_of_code(fn_node, source, index), + line: fn_node.pos.as_ref().map_or(0, |p| i64::from(p.start_line)), } } -/// Aggregated totals across all functions. -#[derive(Debug, Default, Clone, Copy)] -struct Totals { - cyclomatic: i64, - cognitive: i64, - nesting: i64, - decisions: i64, - max: i64, +/// The per-file ordering: cyclomatic descending, cognitive descending, +/// nesting descending, then name and start line ascending. +#[must_use] +pub fn compare_metrics(a: &FunctionMetrics, b: &FunctionMetrics) -> Ordering { + b.cyclomatic_complexity + .cmp(&a.cyclomatic_complexity) + .then(b.cognitive_complexity.cmp(&a.cognitive_complexity)) + .then(b.nesting_depth.cmp(&a.nesting_depth)) + .then_with(|| a.name.cmp(&b.name)) + .then(a.line.cmp(&b.line)) } fn build_empty_result(message: &str) -> GoValue { @@ -235,832 +258,66 @@ fn build_empty_result(message: &str) -> GoValue { GoValue::Map(m) } -fn build_result( - function_count: usize, - avg_complexity: f64, - totals: &Totals, - function_metrics: &[FunctionMetrics], - message: &str, -) -> GoValue { - let mut functions_arr: Vec<GoValue> = Vec::with_capacity(function_metrics.len()); - for fm in function_metrics { - let mut fo = GoMap::new(MapOrigin::Map); - fo.insert("name", GoValue::Str(fm.name.clone())); - fo.insert( - "cyclomatic_complexity", - GoValue::Int(fm.cyclomatic_complexity), - ); - fo.insert( - "cognitive_complexity", - GoValue::Int(fm.cognitive_complexity), - ); - fo.insert("nesting_depth", GoValue::Int(fm.nesting_depth)); - fo.insert("lines_of_code", GoValue::Int(fm.lines_of_code)); - fo.insert( - "complexity_assessment", - GoValue::Str(get_complexity_assessment(fm.cyclomatic_complexity).to_string()), - ); - fo.insert( - "cognitive_assessment", - GoValue::Str(get_cognitive_assessment(fm.cognitive_complexity).to_string()), - ); - fo.insert( - "nesting_assessment", - GoValue::Str(get_nesting_assessment(fm.nesting_depth).to_string()), - ); - functions_arr.push(GoValue::Map(fo)); - } +/// One `functions` entry: the metric keys plus the assessment strings. +fn function_value(fm: &FunctionMetrics) -> GoValue { + let mut fo = GoMap::new(MapOrigin::Map); + fo.insert("name", GoValue::Str(fm.name.clone())); + fo.insert( + "cyclomatic_complexity", + GoValue::Int(fm.cyclomatic_complexity), + ); + fo.insert( + "cognitive_complexity", + GoValue::Int(fm.cognitive_complexity), + ); + fo.insert("nesting_depth", GoValue::Int(fm.nesting_depth)); + fo.insert("lines_of_code", GoValue::Int(fm.lines_of_code)); + fo.insert( + "complexity_assessment", + GoValue::Str(thresholds::complexity_assessment(fm.cyclomatic_complexity).to_string()), + ); + fo.insert( + "cognitive_assessment", + GoValue::Str(thresholds::cognitive_assessment(fm.cognitive_complexity).to_string()), + ); + fo.insert( + "nesting_assessment", + GoValue::Str(thresholds::nesting_assessment(fm.nesting_depth).to_string()), + ); + GoValue::Map(fo) +} + +fn build_result(functions: &[FunctionMetrics]) -> GoValue { + let inputs: Vec<report::FunctionInput> = functions + .iter() + .map(|m| report::FunctionInput::stamped(m, "", "", "")) + .collect(); + let agg = report::aggregate(&inputs); let mut m = GoMap::new(MapOrigin::Map); - m.insert("analyzer_name", GoValue::Str("complexity".to_string())); - m.insert("total_functions", GoValue::Int(function_count as i64)); - m.insert("average_complexity", GoValue::Float(avg_complexity)); - m.insert("max_complexity", GoValue::Int(totals.max)); - m.insert("total_complexity", GoValue::Int(totals.cyclomatic)); - m.insert("cognitive_complexity", GoValue::Int(totals.cognitive)); - m.insert("nesting_depth", GoValue::Int(totals.nesting)); - m.insert("decision_points", GoValue::Int(totals.decisions)); - m.insert("functions", GoValue::Array(functions_arr)); - m.insert("message", GoValue::Str(message.to_string())); + m.insert("analyzer_name", GoValue::Str(ANALYZER_NAME.to_string())); + m.insert("total_functions", GoValue::Int(agg.total_functions)); + m.insert("average_complexity", GoValue::Float(agg.average_complexity)); + m.insert("max_complexity", GoValue::Int(agg.max_complexity)); + m.insert("total_complexity", GoValue::Int(agg.total_complexity)); + m.insert( + "cognitive_complexity", + GoValue::Int(agg.cognitive_complexity), + ); + m.insert("nesting_depth", GoValue::Int(agg.nesting_depth)); + m.insert("decision_points", GoValue::Int(agg.decision_points)); + m.insert( + "functions", + GoValue::Array(functions.iter().map(function_value).collect()), + ); + m.insert("message", GoValue::Str(agg.message.to_string())); GoValue::Map(m) } -/// Finds all function nodes. -/// -/// Nodes are collected by type (`Function`, `Method`) and by role (`Function`), -/// deduplicated by identity, then filtered through [`is_function_node`]. The -/// reference implementation's dedup-set iteration order is nondeterministic, -/// but the caller sorts the resulting metrics deterministically, so collecting -/// in a stable pre-order is equivalent; the final order is fixed by -/// [`calculate_all_function_metrics`]. -fn find_functions(root: &Node) -> Vec<&Node> { - find_functions_capped(root, None) -} - -/// [`find_functions`] with the optional discovery depth cap (see -/// [`Analyzer::analyze_with_find_depth`]). -fn find_functions_capped(root: &Node, find_depth: Option<usize>) -> Vec<&Node> { - let mut by_type: Vec<&Node> = Vec::new(); - let mut by_role: Vec<&Node> = Vec::new(); - if let Some(cap) = find_depth { - root.find_nodes_by_type_capped(&[uast::FUNCTION, uast::METHOD], cap, &mut by_type); - root.find_nodes_by_roles_capped(&[node::role::FUNCTION], cap, &mut by_role); - } else { - root.find_nodes_by_type(&[uast::FUNCTION, uast::METHOD], &mut by_type); - root.find_nodes_by_roles(&[node::role::FUNCTION], &mut by_role); - } - - let mut seen: Vec<*const Node> = Vec::new(); - let mut functions: Vec<&Node> = Vec::new(); - for n in by_type.into_iter().chain(by_role) { - let ptr = std::ptr::from_ref::<Node>(n); - if seen.contains(&ptr) { - continue; - } - seen.push(ptr); - if is_function_node(n) { - functions.push(n); - } - } - functions -} - -/// A function node is a `Function`/`Method` type, OR carries both the -/// `Function` and `Declaration` roles. -fn is_function_node(n: &Node) -> bool { - n.has_any_type(&[uast::FUNCTION, uast::METHOD]) - || n.has_all_roles(&[node::role::FUNCTION, node::role::DECLARATION]) -} - -/// Computes per-function metrics plus totals, sorted by the report contract's -/// exact predicate (cyclomatic desc, then cognitive desc, then name asc). -fn calculate_all_function_metrics(functions: &[&Node]) -> (Vec<FunctionMetrics>, Totals) { - let mut metrics: Vec<FunctionMetrics> = Vec::with_capacity(functions.len()); - let mut totals = Totals::default(); - - for fn_node in functions { - let m = calculate_function_metrics(fn_node); - update_totals(&mut totals, &m); - metrics.push(m); - } - - // The report contract is pinned to an UNSTABLE sort (pdqsort), not a stable - // one. For functions that tie on every key (same cyclomatic, cognitive, and - // name — e.g. several identically-named methods in one file), a stable - // `sort_by` would preserve input order while pdqsort permutes them. - // Reproduce the exact element movement with the shared pdqsort port so tie - // ordering matches byte-for-byte in formats whose final sort key (e.g. the - // YAML `function_complexity` cyclomatic-only sort) does not otherwise break - // those ties. - gosort::go_sort_slice(&mut metrics, |left, right| { - if left.cyclomatic_complexity != right.cyclomatic_complexity { - return left.cyclomatic_complexity > right.cyclomatic_complexity; - } - if left.cognitive_complexity != right.cognitive_complexity { - return left.cognitive_complexity > right.cognitive_complexity; - } - left.name < right.name - }); - - (metrics, totals) -} - -fn update_totals(totals: &mut Totals, m: &FunctionMetrics) { - totals.cyclomatic += m.cyclomatic_complexity; - totals.cognitive += m.cognitive_complexity; - totals.nesting += m.nesting_depth; - totals.decisions += m.decision_points; - if m.cyclomatic_complexity > totals.max { - totals.max = m.cyclomatic_complexity; - } -} - -fn calculate_function_metrics(fn_node: &Node) -> FunctionMetrics { - let name = extract_function_name(fn_node); - let cyclomatic = calculate_cyclomatic_complexity(fn_node); - FunctionMetrics { - name, - cyclomatic_complexity: cyclomatic, - cognitive_complexity: calculate_cognitive_complexity(fn_node), - nesting_depth: calculate_nesting_depth(fn_node), - decision_points: (cyclomatic - 1).max(0), - lines_of_code: estimate_lines_of_code(fn_node), - } -} - -fn calculate_average_complexity(totals: &Totals, function_count: usize) -> f64 { - if function_count == 0 { - return 0.0; - } - totals.cyclomatic as f64 / function_count as f64 -} - -// --- Cyclomatic complexity --- - -fn calculate_cyclomatic_complexity(fn_node: &Node) -> i64 { - let ctx = function_source_context(fn_node); - let mut complexity: i64 = 1; - fn_node.visit_pre_order(&mut |n| { - if std::ptr::eq(n, fn_node) { - return; - } - if is_decision_point_with_source(n, &ctx) { - complexity += 1; - } - }); - complexity -} - -/// Builds the source context from a function node. -fn function_source_context(fn_node: &Node) -> FunctionSourceContext<'_> { - match &fn_node.pos { - Some(pos) if !fn_node.token.is_empty() => FunctionSourceContext { - source: fn_node.token.as_bytes(), - start_offset: pos.start_offset, - }, - _ => FunctionSourceContext { - source: &[], - start_offset: 0, - }, - } -} - -/// Like [`is_decision_point`] but recovers a `BinaryOp`'s operator from the -/// function source slice when token/props omit it. -fn is_decision_point_with_source(target: &Node, ctx: &FunctionSourceContext) -> bool { - match target.node_type.as_str() { - uast::IF | uast::LOOP | uast::CATCH => true, - uast::CASE => !is_default_case(target), - uast::BINARY_OP => { - let op = ctx.binary_operator(target); - !op.is_empty() && is_logical_operator_token(&op) - } - _ => false, - } -} - -/// The AST-metadata variant of the decision-point predicate: reads the -/// `BinaryOp` operator only from the `operator` prop. Production code uses -/// [`is_decision_point_with_source`]; this variant exists for the unit tests -/// that exercise the prop-only classification. -#[cfg(test)] -fn is_decision_point(target: &Node) -> bool { - match target.node_type.as_str() { - uast::IF | uast::LOOP | uast::CATCH => true, - uast::CASE => !is_default_case(target), - uast::BINARY_OP => { - let op = target.prop("operator").unwrap_or(""); - if op.is_empty() { - return false; - } - is_logical_operator_token(op) - } - _ => false, - } -} - -// --- Cognitive complexity (SonarSource / gocognit model) --- -// -// The calculator walks the function's children with a `walk_node` recursion -// that tracks structural nesting, recovers binary operators (from token, -// props, or the function source slice via the source context), and counts: -// * a `nesting + 1` increment for each `if` (non-else-if), loop, switch, -// try, catch, match (structural + nesting penalty); -// * a flat `+1` for an `else if` and for an `else` block; -// * logical-operator *sequence* increments -// (`add_logical_sequence_complexity`): +1 for the first run of logical -// operators in an if condition, +1 for each change of operator kind along -// the flattened operator stream; -// * `+1` for a recursive call (call whose name equals the function name); -// * lambda bodies raise nesting by one. - -/// Holds the function's original source bytes and its start offset, so binary -/// operators can be recovered from the source when the UAST omits them. -struct FunctionSourceContext<'a> { - source: &'a [u8], - start_offset: u32, -} - -fn calculate_cognitive_complexity(fn_node: &Node) -> i64 { - let ctx = function_source_context(fn_node); - let function_name = extract_cognitive_function_name(fn_node); - - let mut complexity: i64 = 0; - for (idx, child) in fn_node.children.iter().enumerate() { - walk_node( - child, - fn_node, - idx, - 0, - &ctx, - &function_name, - &mut complexity, - ); - } - complexity -} - -/// Recursion depth tracks source nesting, which on real inputs exceeds 10k -/// levels (cf-uast builds these trees on a segmented stack for that reason) — -/// so every level grows the stack on demand instead of trusting the fixed -/// 8 MiB main stack. -#[allow(clippy::too_many_arguments)] -fn walk_node( - curr: &Node, - parent: &Node, - child_idx: usize, - nesting: i64, - ctx: &FunctionSourceContext, - function_name: &str, - complexity: &mut i64, -) { - stacker::maybe_grow(64 * 1024, 16 * 1024 * 1024, || { - walk_node_inner( - curr, - parent, - child_idx, - nesting, - ctx, - function_name, - complexity, - ); - }); -} - -#[allow(clippy::too_many_arguments)] -fn walk_node_inner( - curr: &Node, - parent: &Node, - child_idx: usize, - nesting: i64, - ctx: &FunctionSourceContext, - function_name: &str, - complexity: &mut i64, -) { - match curr.node_type.as_str() { - uast::IF => { - process_if_node( - curr, - parent, - child_idx, - nesting, - ctx, - function_name, - complexity, - ); - return; - } - uast::LOOP | uast::SWITCH | uast::TRY | uast::CATCH | uast::MATCH => { - *complexity += nesting + 1; - for (idx, child) in curr.children.iter().enumerate() { - walk_node( - child, - curr, - idx, - nesting + 1, - ctx, - function_name, - complexity, - ); - } - return; - } - uast::LAMBDA => { - for (idx, child) in curr.children.iter().enumerate() { - walk_node( - child, - curr, - idx, - nesting + 1, - ctx, - function_name, - complexity, - ); - } - return; - } - uast::CALL => { - if is_recursive_call(curr, function_name) { - *complexity += 1; - } - } - _ => {} - } - - for (idx, child) in curr.children.iter().enumerate() { - walk_node(child, curr, idx, nesting, ctx, function_name, complexity); - } -} - -#[allow(clippy::too_many_arguments)] -fn process_if_node( - if_node: &Node, - parent: &Node, - child_idx: usize, - nesting: i64, - ctx: &FunctionSourceContext, - function_name: &str, - complexity: &mut i64, -) { - if is_else_if_cognitive(parent, if_node, child_idx) { - *complexity += 1; - } else { - *complexity += nesting + 1; - } - - if !if_node.children.is_empty() { - add_logical_sequence_complexity(&if_node.children[0], ctx, complexity); - walk_node( - &if_node.children[0], - if_node, - 0, - nesting, - ctx, - function_name, - complexity, - ); - } - - if if_node.children.len() > 1 { - walk_node( - &if_node.children[1], - if_node, - 1, - nesting + 1, - ctx, - function_name, - complexity, - ); - } - - for idx in 2..if_node.children.len() { - let child = &if_node.children[idx]; - // Sonar/gocognit: an `else` branch (a Block in the else slot) adds one - // structural increment; an `else if` is handled by the nested walk. - if child.node_type == uast::BLOCK { - *complexity += 1; - } - walk_node(child, if_node, idx, nesting, ctx, function_name, complexity); - } -} - -/// Adds the logical-sequence increments: +1 for the first logical-operator run -/// in the condition, +1 for each change of operator kind along the flattened -/// left-to-right operator stream. -fn add_logical_sequence_complexity(expr: &Node, ctx: &FunctionSourceContext, complexity: &mut i64) { - let mut operators: Vec<String> = Vec::new(); - collect_logical_operators(expr, ctx, &mut operators); - if operators.is_empty() { - return; - } - *complexity += 1; - let mut last_op = operators[0].clone(); - for op in &operators[1..] { - if *op != last_op { - *complexity += 1; - last_op.clone_from(op); - } - } -} - -/// Stack-guarded like [`walk_node`]: expression chains (`a && b && c && ...`) -/// nest one level per operator in generated sources. -fn collect_logical_operators( - curr: &Node, - ctx: &FunctionSourceContext, - operators: &mut Vec<String>, -) { - stacker::maybe_grow(64 * 1024, 16 * 1024 * 1024, || { - collect_logical_operators_inner(curr, ctx, operators); - }); -} - -fn collect_logical_operators_inner( - curr: &Node, - ctx: &FunctionSourceContext, - operators: &mut Vec<String>, -) { - if curr.node_type == uast::BINARY_OP && curr.children.len() >= 2 { - collect_logical_operators(&curr.children[0], ctx, operators); - let op = ctx.binary_operator(curr); - if is_logical_operator_token(&op) { - operators.push(op); - } - collect_logical_operators(&curr.children[1], ctx, operators); - return; - } - for child in &curr.children { - collect_logical_operators(child, ctx, operators); - } -} - -impl FunctionSourceContext<'_> { - /// Recovers a `BinaryOp`'s operator: token, then props, then a best-effort - /// recovery from the source slice between the operands. - fn binary_operator(&self, n: &Node) -> String { - if !n.token.is_empty() { - let op = normalize_operator_text(&n.token); - if !op.is_empty() { - return op; - } - } - if let Some(p) = n.prop("operator") { - let op = normalize_operator_text(p); - if !op.is_empty() { - return op; - } - } - self.binary_operator_from_offsets(n) - } - - fn binary_operator_from_offsets(&self, n: &Node) -> String { - if self.source.is_empty() || n.children.len() < 2 { - return String::new(); - } - let (left, right) = (&n.children[0], &n.children[1]); - let (Some(lp), Some(rp)) = (&left.pos, &right.pos) else { - return String::new(); - }; - if rp.start_offset <= lp.end_offset || lp.end_offset < self.start_offset { - return String::new(); - } - let start = (lp.end_offset - self.start_offset) as usize; - let end = (rp.start_offset - self.start_offset) as usize; - if start >= end || end > self.source.len() { - return String::new(); - } - let segment = String::from_utf8_lossy(&self.source[start..end]); - normalize_operator_text(&segment) - } -} - -/// Extracts the function name for recursive-call detection only. -/// -/// This is deliberately NARROWER than [`extract_function_name`]: it consults -/// only the entity-name chain (`name` prop -> token -> first child's token -> -/// first child's `name` prop) plus a final `name`-prop check, and returns `""` -/// otherwise. Critically it does NOT consult a `Name`-role child token. Using -/// the broader extraction here would surface a name for functions the report -/// contract treats as unnamed, which over-counts recursive calls and inflates -/// the cognitive total. -fn extract_cognitive_function_name(fn_node: &Node) -> String { - if let Some(n) = extract_entity_name(fn_node) { - if !n.is_empty() { - return n; - } - } - if let Some(v) = fn_node.prop("name") { - if !v.is_empty() { - return v.to_string(); - } - } - String::new() -} - -fn is_recursive_call(call_node: &Node, function_name: &str) -> bool { - if function_name.is_empty() { - return false; - } - let call_name = extract_call_name(call_node); - !call_name.is_empty() && call_name == function_name -} - -/// Extracts a call's target name: `name` prop, then a `Name`-role child token, -/// then the first child's token. -fn extract_call_name(call_node: &Node) -> String { - if let Some(name) = call_node.prop("name") { - if !name.is_empty() { - return name.to_string(); - } - } - for child in &call_node.children { - if child.has_any_role(&[node::role::NAME]) && !child.token.is_empty() { - return child.token.clone(); - } - } - if let Some(first) = call_node.children.first() { - if !first.token.is_empty() { - return first.token.clone(); - } - } - String::new() -} - -/// Normalizes raw operator text: trim, strip all whitespace, then keep only -/// the recognized logical/comparison operators. -fn normalize_operator_text(raw: &str) -> String { - if raw.is_empty() { - return String::new(); - } - let compact: String = raw.trim().chars().filter(|c| !c.is_whitespace()).collect(); - match compact.as_str() { - "&&" | "||" | "and" | "or" | "AND" | "OR" | "<" | ">" | "<=" | ">=" | "==" | "!=" => { - compact - } - _ => String::new(), - } -} - -// --- Nesting depth --- - -fn calculate_nesting_depth(fn_node: &Node) -> i64 { - let mut max_depth: i64 = 0; - for (idx, child) in fn_node.children.iter().enumerate() { - walk_nesting(child, 0, Some(fn_node), idx, &mut max_depth); - } - max_depth -} - -fn walk_nesting( - curr: &Node, - depth: i64, - parent: Option<&Node>, - child_idx: usize, - max_depth: &mut i64, -) { - let mut current_depth = depth; - if is_nesting_node(curr) && !is_else_if_node(parent, curr, child_idx) { - current_depth += 1; - if current_depth > *max_depth { - *max_depth = current_depth; - } - } - for (idx, child) in curr.children.iter().enumerate() { - walk_nesting(child, current_depth, Some(curr), idx, max_depth); - } -} - -/// Nesting nodes: `If`, `Loop`, `Switch`, `Try`, `Catch`. -fn is_nesting_node(target: &Node) -> bool { - matches!( - target.node_type.as_str(), - uast::IF | uast::LOOP | uast::SWITCH | uast::TRY | uast::CATCH - ) -} - -// --- Lines of code --- - -fn estimate_lines_of_code(fn_node: &Node) -> i64 { - if let Some(pos) = &fn_node.pos { - if pos.end_line >= pos.start_line { - return i64::from(pos.end_line - pos.start_line) + 1; - } - } - let mut loc: i64 = 0; - fn_node.visit_pre_order(&mut |n| { - if !n.token.is_empty() { - let lines = n.token.matches('\n').count() as i64 + 1; - loc += lines; - } - }); - loc -} - -// --- Name extraction --- - -/// Extracts the display name for a function node. -/// -/// The FIRST branch is the entity-name chain (`name` prop -> token -> first -/// child's token -> first child's `name` prop). For anonymous functions whose -/// props lack `name`, this surfaces the first child's token (the full -/// parameter/receiver signature, e.g. `"(action cgotesting.Action)"` or -/// `"()"`) — reference-implementation behavior, pinned by the differential -/// gate. Then the prop fallbacks, then a `Name`-role child token, then -/// `"anonymous"`. -fn extract_function_name(fn_node: &Node) -> String { - if let Some(n) = extract_entity_name(fn_node) { - if !n.is_empty() { - return n; - } - } - if let Some(n) = extract_name_from_props(fn_node) { - return n; - } - // Name-role child token fallback. - let mut name_nodes: Vec<&Node> = Vec::new(); - fn_node.find_nodes_by_roles(&[node::role::NAME], &mut name_nodes); - if let Some(first) = name_nodes.first() { - let token = first.token.trim(); - if !token.is_empty() { - return token.to_string(); - } - } - ANONYMOUS_FUNCTION_NAME.to_string() -} - -/// The shared entity-name chain: `name` prop -> own token -> first child's -/// token -> first child's `name` prop. Each branch returns `Some(value)` when -/// the source is present (even empty), so the caller's `!is_empty()` guard -/// preserves the "present but empty means stop" semantics. Note: unlike -/// [`extract_name_from_props`], the prop lookup here is NOT trimmed and does -/// not consider `function_name`/`method_name`. -fn extract_entity_name(n: &Node) -> Option<String> { - if let Some(v) = n.prop("name") { - return Some(v.to_string()); - } - if !n.token.is_empty() { - return Some(n.token.clone()); - } - if let Some(child) = n.children.first() { - if !child.token.is_empty() { - return Some(child.token.clone()); - } - if let Some(v) = child.prop("name") { - return Some(v.to_string()); - } - } - None -} - -fn extract_name_from_props(fn_node: &Node) -> Option<String> { - for key in ["name", "function_name", "method_name"] { - if let Some(v) = fn_node.prop(key) { - let trimmed = v.trim(); - if !trimmed.is_empty() { - return Some(trimmed.to_string()); - } - } - } - None -} - -// --- Flow helpers --- - -/// True for a switch `default` case: trim + LOWERCASE the case node's OWN -/// token, then test for the `default` prefix. It must NOT inspect children: a -/// non-default `case X:` whose body begins with an identifier like -/// `defaultedPod` would otherwise be misclassified as the default case and -/// dropped from the cyclomatic decision count (an off-by-1 observed on real -/// input before this was pinned). -fn is_default_case(n: &Node) -> bool { - n.token.trim().to_lowercase().starts_with("default") -} - -/// True if `curr` is the else-if continuation of a parent `If`. -/// -/// An `If` is an else-if continuation only at child index >= 2 (after -/// [condition(0), then-block(1)]). Using `> 0` would wrongly treat a braceless -/// nested `if (a) if (b)` (inner `If` at index 1) as an else-if, skipping its -/// nesting increment and under-counting `nesting_depth` by 1 on C-style code. -/// Matches [`is_else_if_cognitive`]. -fn is_else_if_node(parent: Option<&Node>, curr: &Node, child_idx: usize) -> bool { - let Some(parent) = parent else { - return false; - }; - if parent.node_type != uast::IF || curr.node_type != uast::IF { - return false; - } - child_idx >= 2 -} - -/// Logical operators, including the uppercase `AND`/`OR` forms used by some -/// languages. -fn is_logical_operator_token(op: &str) -> bool { - matches!(op.trim(), "&&" | "||" | "and" | "or" | "AND" | "OR") -} - -/// The else-if predicate for the cognitive calculator: an `If` nested as the -/// third-or-later child (index >= 2) of another `If` (the else-if slot). -fn is_else_if_cognitive(parent: &Node, child: &Node, child_idx: usize) -> bool { - parent.node_type == uast::IF && child.node_type == uast::IF && child_idx >= 2 -} - -// --- Assessment / message helpers --- - -fn get_complexity_level(complexity: i64) -> &'static str { - if complexity <= CYCLOMATIC_GREEN { - "green" - } else if complexity <= CYCLOMATIC_YELLOW { - "yellow" - } else { - "red" - } -} - -/// Cyclomatic-complexity assessment label — exported for the aggregated -/// raw-report builder used by `--format plot` / report.json. -/// -/// `<= 1` is simple, `<= 5` is moderate, otherwise complex: -/// -/// ``` -/// use cf_complexity::get_complexity_assessment; -/// assert_eq!(get_complexity_assessment(1), "🟢 Simple"); -/// assert_eq!(get_complexity_assessment(5), "🟡 Moderate"); -/// assert_eq!(get_complexity_assessment(6), "🔴 Complex"); -/// ``` -#[must_use] -pub fn get_complexity_assessment(complexity: i64) -> &'static str { - match get_complexity_level(complexity) { - "green" => "🟢 Simple", - "yellow" => "🟡 Moderate", - "red" => "🔴 Complex", - _ => "⚪ Unknown", - } -} - -/// Cognitive-complexity assessment label — exported for the aggregated -/// raw-report builder. -/// -/// `<= 5` is low, `<= 10` is medium, otherwise high: -/// -/// ``` -/// use cf_complexity::get_cognitive_assessment; -/// assert_eq!(get_cognitive_assessment(5), "🟢 Low"); -/// assert_eq!(get_cognitive_assessment(10), "🟡 Medium"); -/// assert_eq!(get_cognitive_assessment(11), "🔴 High"); -/// ``` -#[must_use] -pub fn get_cognitive_assessment(complexity: i64) -> &'static str { - if complexity <= COMPLEXITY_THRESHOLD_HIGH { - "🟢 Low" - } else if complexity <= MAGIC10 { - "🟡 Medium" - } else { - "🔴 High" - } -} - -/// Nesting-depth assessment label — exported for the aggregated raw-report -/// builder. -/// -/// `<= 3` is shallow, `<= 5` is moderate, otherwise deep: -/// -/// ``` -/// use cf_complexity::get_nesting_assessment; -/// assert_eq!(get_nesting_assessment(3), "🟢 Shallow"); -/// assert_eq!(get_nesting_assessment(5), "🟡 Moderate"); -/// assert_eq!(get_nesting_assessment(6), "🔴 Deep"); -/// ``` -#[must_use] -pub fn get_nesting_assessment(depth: i64) -> &'static str { - if depth <= DEPTH_THRESHOLD_HIGH { - "🟢 Shallow" - } else if depth <= MAGIC5 { - "🟡 Moderate" - } else { - "🔴 Deep" - } -} - -fn get_complexity_message(avg_complexity: f64) -> &'static str { - if avg_complexity <= 1.0 { - MSG_EXCELLENT - } else if avg_complexity <= AVG_COMPLEXITY_THRESHOLD_HIGH { - MSG_GOOD - } else if avg_complexity <= MAGIC7P0 { - MSG_FAIR - } else { - MSG_HIGH - } -} - #[cfg(test)] mod tests { - use super::{ - calculate_cognitive_complexity, calculate_cyclomatic_complexity, calculate_nesting_depth, - extract_function_name, is_decision_point, Analyzer, - }; - use crate::node::{uast, Node}; + use super::{AnalysisOptions, Analyzer}; + use crate::node::{role, uast, Node}; use cf_gojson::GoValue; fn obj_get<'a>(v: &'a GoValue, key: &str) -> &'a GoValue { @@ -1076,193 +333,50 @@ mod tests { } } - fn as_array(v: &GoValue) -> &Vec<GoValue> { - match v { - GoValue::Array(a) => a, - _ => panic!("not an array"), - } + fn body(children: Vec<Node>) -> Node { + Node::new(uast::BLOCK) + .with_roles(vec![role::BODY]) + .with_children(children) } - #[test] - fn analyzer_basic_name() { - assert_eq!(Analyzer.name(), "complexity"); + fn func(name: &str, children: Vec<Node>) -> Node { + Node::new(uast::FUNCTION) + .with_roles(vec![role::FUNCTION, role::DECLARATION]) + .with_prop("name", name) + .with_children(vec![body(children)]) } #[test] - fn analyzer_flag() { + fn analyzer_identity() { + assert_eq!(Analyzer.name(), "complexity"); assert_eq!(Analyzer.flag(), "complexity-analysis"); } #[test] - fn nil_root_total_functions_zero() { - let result = Analyzer.analyze(None); - assert_eq!(as_int(obj_get(&result, "total_functions")), 0); - } - - #[test] - fn simple_function() { - let mut function_node = Node::new(uast::FUNCTION).with_roles(vec![ - crate::node::role::FUNCTION, - crate::node::role::DECLARATION, - ]); - let name_node = Node::with_token(uast::IDENTIFIER, "simpleFunction") - .with_roles(vec![crate::node::role::NAME]); - function_node.add_child(name_node); - - let mut root = Node::new(uast::FILE); - root.add_child(function_node); - - let result = Analyzer.analyze(Some(&root)); - assert_eq!(as_int(obj_get(&result, "total_functions")), 1); - assert_eq!(as_int(obj_get(&result, "total_complexity")), 1); - } - - #[test] - fn extract_function_name_with_and_without_name() { - let mut function_node = Node::new(uast::FUNCTION); - let name_node = Node::with_token(uast::IDENTIFIER, "testFunction") - .with_roles(vec![crate::node::role::NAME]); - function_node.add_child(name_node); - assert_eq!(extract_function_name(&function_node), "testFunction"); - - let anon = Node::new(uast::FUNCTION); - assert_eq!(extract_function_name(&anon), "anonymous"); - } - - #[test] - fn is_decision_point_classification() { - for t in [uast::IF, uast::LOOP, uast::CASE, uast::CATCH] { - assert!( - is_decision_point(&Node::new(t)), - "{t} should be a decision point" - ); - } - let default_case = Node::with_token(uast::CASE, "default:\n\treturn 0"); - assert!(!is_decision_point(&default_case)); + fn nil_root_and_no_functions_use_the_empty_shape() { + let none = Analyzer.analyze(None); + assert_eq!(as_int(obj_get(&none, "total_functions")), 0); + assert_eq!(obj_get(&none, "message").as_str(), Some("No AST provided")); - assert!(!is_decision_point(&Node::new(uast::IDENTIFIER))); - - let logical = Node::new(uast::BINARY_OP).with_prop("operator", "&&"); - assert!(is_decision_point(&logical)); - - let arithmetic = Node::new(uast::BINARY_OP).with_prop("operator", "+"); - assert!(!is_decision_point(&arithmetic)); - } - - #[test] - fn with_if_statement() { - let mut function_node = Node::new(uast::FUNCTION).with_roles(vec![ - crate::node::role::FUNCTION, - crate::node::role::DECLARATION, - ]); - function_node.add_child( - Node::with_token(uast::IDENTIFIER, "testFunction") - .with_roles(vec![crate::node::role::NAME]), + let root = Node::new(uast::FILE); + let empty = Analyzer.analyze(Some(&root)); + assert_eq!( + obj_get(&empty, "message").as_str(), + Some("No functions found") ); - function_node.add_child(Node::new(uast::IF).with_roles(vec![crate::node::role::CONDITION])); - - let mut root = Node::new(uast::FILE); - root.add_child(function_node); - - let result = Analyzer.analyze(Some(&root)); - assert_eq!(as_int(obj_get(&result, "total_complexity")), 2); - } - - /// The if-processing walks an `If`'s first child (its condition slot) at - /// the SAME nesting level, so a nested `If` placed at index 0 receives no - /// nesting penalty: outer `if` (+1) + inner `if` at nesting 0 (+1) = 2. - /// Verified against the reference cognitive-complexity calculator for this - /// exact tree. - #[test] - fn cognitive_nested_ifs() { - let inner_if = Node::new(uast::IF).with_roles(vec![crate::node::role::CONDITION]); - let outer_if = Node::new(uast::IF) - .with_roles(vec![crate::node::role::CONDITION]) - .with_children(vec![inner_if]); - let function_node = Node::new(uast::FUNCTION) - .with_roles(vec![crate::node::role::FUNCTION]) - .with_children(vec![outer_if]); - - assert_eq!(calculate_cognitive_complexity(&function_node), 2); + assert!(empty.as_map().unwrap().get("functions").is_none()); } - /// Cyclomatic/cognitive/nesting parity for the canonical `if{loop{if}}` - /// body, reproducing the `SonarSource` model. The loop sits in the outer-if's - /// first-child (condition) slot, walked at nesting 0; its inner `if` is in - /// the loop's first-child slot, walked at nesting 1. - /// cognitive = if(+1) + loop(+1) + inner-if(nesting 1 → +2) = 4. Verified - /// against the reference cognitive-complexity calculator. #[test] - fn nested_if_loop_if_metrics() { - let inner_if = Node::new(uast::IF); - let loop_node = Node::new(uast::LOOP).with_children(vec![inner_if]); - let outer_if = Node::new(uast::IF).with_children(vec![loop_node]); - let func = Node::new(uast::FUNCTION) - .with_roles(vec![ - crate::node::role::FUNCTION, - crate::node::role::DECLARATION, - ]) - .with_children(vec![outer_if]); - - // cyclomatic: 1 + if + loop + if = 4 - assert_eq!(calculate_cyclomatic_complexity(&func), 4); - // cognitive: if(+1) + loop(+1) + inner-if@nesting1(+2) = 4 - assert_eq!(calculate_cognitive_complexity(&func), 4); - // nesting: if->loop->if = 3 - assert_eq!(calculate_nesting_depth(&func), 3); - } - - /// Else-if chains are not counted as additional nesting: a child `If` is an - /// else-if continuation only at child index >= 2, after - /// [condition(0), then-block(1)]. - #[test] - fn else_if_does_not_increase_nesting() { - // outer if with child0 = condition, child1 = then-block, child2 = - // else-if; the else-if (index 2) must not add nesting. - let cond = Node::new(uast::IDENTIFIER); - let block = Node::new(uast::BLOCK); - let else_if = Node::new(uast::IF); - let outer = Node::new(uast::IF).with_children(vec![cond, block, else_if]); - let func = Node::new(uast::FUNCTION) - .with_roles(vec![crate::node::role::FUNCTION]) - .with_children(vec![outer]); - // Only the outer if nests => depth 1. - assert_eq!(calculate_nesting_depth(&func), 1); - - // Conversely, an inner `If` at index 1 (braceless `if (a) if (b)`) is - // NOT an else-if continuation and DOES nest => depth 2. - let inner = Node::new(uast::IF); - let cond2 = Node::new(uast::IDENTIFIER); - let outer2 = Node::new(uast::IF).with_children(vec![cond2, inner]); - let func2 = Node::new(uast::FUNCTION) - .with_roles(vec![crate::node::role::FUNCTION]) - .with_children(vec![outer2]); - assert_eq!(calculate_nesting_depth(&func2), 2); - } - - /// Switch counts as a nesting node and a cognitive increment (loop-like). - #[test] - fn switch_is_nesting_and_cognitive() { - let sw = Node::new(uast::SWITCH); - let func = Node::new(uast::FUNCTION) - .with_roles(vec![crate::node::role::FUNCTION]) - .with_children(vec![sw]); - assert_eq!(calculate_nesting_depth(&func), 1); - assert_eq!(calculate_cognitive_complexity(&func), 1); - } - - /// The success result carries the report-contract keys, including - /// assessments. - #[test] - fn result_shape_has_report_keys_and_assessments() { - let mut func = Node::new(uast::FUNCTION).with_roles(vec![ - crate::node::role::FUNCTION, - crate::node::role::DECLARATION, + fn result_shape_carries_sums_and_assessments() { + let nested = Node::new(uast::IF).with_children(vec![ + Node::new(uast::IDENTIFIER), + body(vec![Node::new(uast::IF)]), + ]); + let root = Node::new(uast::FILE).with_children(vec![ + func("a", vec![nested]), + func("b", vec![Node::new(uast::LOOP)]), ]); - func.add_child(Node::new(uast::IF)); - let mut root = Node::new(uast::FILE); - root.add_child(func); - let result = Analyzer.analyze(Some(&root)); for key in [ "analyzer_name", @@ -1278,39 +392,56 @@ mod tests { ] { assert!(result.as_map().unwrap().contains_key(key), "missing {key}"); } - let functions = as_array(obj_get(&result, "functions")); - assert_eq!(functions.len(), 1); - let f0 = &functions[0]; - assert!(f0.as_map().unwrap().contains_key("complexity_assessment")); - assert!(f0.as_map().unwrap().contains_key("cognitive_assessment")); - assert!(f0.as_map().unwrap().contains_key("nesting_assessment")); + assert_eq!(as_int(obj_get(&result, "total_functions")), 2); + // a: cc 3 (two ifs), cog 1 + 2 = 3, nesting 2; b: cc 2, cog 1, nesting 1. + assert_eq!(as_int(obj_get(&result, "total_complexity")), 5); + assert_eq!(as_int(obj_get(&result, "cognitive_complexity")), 4); + assert_eq!(as_int(obj_get(&result, "nesting_depth")), 2); + assert_eq!(as_int(obj_get(&result, "max_complexity")), 3); + assert_eq!(as_int(obj_get(&result, "decision_points")), 3); + let GoValue::Array(functions) = obj_get(&result, "functions") else { + panic!("functions is not an array") + }; + assert_eq!( + functions[0].as_map().unwrap().get("name").unwrap().as_str(), + Some("a") + ); + for key in [ + "complexity_assessment", + "cognitive_assessment", + "nesting_assessment", + ] { + assert!( + functions[0].as_map().unwrap().contains_key(key), + "missing {key}" + ); + } } - /// Sort order: cyclomatic desc, then cognitive desc, then name asc. #[test] - fn functions_sorted_by_report_predicate() { - // aaa: cyclomatic 1; bbb: cyclomatic 2 (one if). - let func_a = Node::new(uast::FUNCTION) - .with_roles(vec![ - crate::node::role::FUNCTION, - crate::node::role::DECLARATION, - ]) - .with_prop("name", "aaa"); - let func_b = Node::new(uast::FUNCTION) - .with_roles(vec![ - crate::node::role::FUNCTION, - crate::node::role::DECLARATION, - ]) - .with_prop("name", "bbb") - .with_children(vec![Node::new(uast::IF)]); - let root = Node::new(uast::FILE).with_children(vec![func_a, func_b]); + fn functions_sort_by_cyclomatic_cognitive_nesting_then_name() { + let root = Node::new(uast::FILE).with_children(vec![ + func("zeta", vec![Node::new(uast::IF)]), + func("alpha", vec![Node::new(uast::IF)]), + func("beta", vec![]), + ]); + let names: Vec<String> = Analyzer + .function_metrics(Some(&root)) + .into_iter() + .map(|m| m.name) + .collect(); + assert_eq!(names, vec!["alpha", "zeta", "beta"]); + } - let result = Analyzer.analyze(Some(&root)); - let functions = as_array(obj_get(&result, "functions")); - // Higher cyclomatic (bbb) comes first. - assert_eq!( - functions[0].as_map().unwrap().get("name").unwrap().as_str(), - Some("bbb") - ); + #[test] + fn find_depth_caps_discovery_for_the_quality_analyzer() { + let deep = Node::new(uast::BLOCK).with_children(vec![ + Node::new(uast::BLOCK).with_children(vec![func("deep", vec![])]) + ]); + let root = Node::new(uast::FILE).with_children(vec![func("top", vec![]), deep]); + let capped = Analyzer.analyze_with_find_depth(Some(&root), Some(1)); + assert_eq!(as_int(obj_get(&capped, "total_functions")), 1); + let uncapped = Analyzer.analyze_with(Some(&root), &AnalysisOptions::default()); + assert_eq!(as_int(obj_get(&uncapped, "total_functions")), 2); } } diff --git a/crates/cf-complexity/src/metrics.rs b/crates/cf-complexity/src/metrics.rs new file mode 100644 index 00000000..a11ea4b6 --- /dev/null +++ b/crates/cf-complexity/src/metrics.rs @@ -0,0 +1,1024 @@ +//! Per-function metric computation: one walk over a function's subtree +//! produces its cyclomatic complexity, cognitive complexity, and nesting +//! depth; lines of code come from the source span. +//! +//! # Counting rules +//! +//! **Cyclomatic (`McCabe`)**: `1` + one per `if` / `else if` / conditional +//! expression, per loop, per non-default `case`/arm, per `catch`, and per +//! logical operator (`&&`, `||`, `and`, `or`). +//! +//! **Cognitive (`SonarSource`)**: structural increments for `if`, `else if`, +//! `else`, conditional expressions, `switch`/`match`, loops, `catch`, +//! labelled jumps (`break label`, `continue label`, `goto`) and direct +//! recursion; `+1` per sequence of like logical operators (`a && b && c || d` +//! is two); `if`, conditional expressions, `switch`/`match`, loops and +//! `catch` also add the current nesting level, and raise it for what they +//! contain. `else`/`else if` add a flat `+1` and never a nesting increment. +//! Lambdas and anonymous function literals raise nesting for their contents +//! without an increment of their own. `try` adds nothing. The function's own +//! body (including an arrow/function-expression wrapper) is nesting level 0. +//! A named nested function is a separate unit and is skipped here. +//! +//! **Nesting depth**: the maximum depth of nested control-flow blocks — +//! `if`/`else` chains (one level for the whole chain), loops, +//! `switch`/`match`, and `try`/`catch` (one level for the statement). +//! Conditional expressions and lambdas are not blocks. +//! +//! **Lines of code**: non-blank lines inside the function's span that carry +//! something other than comment text; the lines of a nested named function +//! belong to that function, not to the enclosing one. +//! +//! # Operator recovery +//! +//! A binary operator is read from `props["operator"]`, else from the node's +//! own token when that is a bare operator symbol, else from the source text +//! between its two operands. A `Synthetic` node with two operands whose gap +//! is a logical word/symbol (Python `and`/`or` before the mapping contract) +//! is treated as a logical operator too. + +use crate::discover::{ + body_child, has_name, identifier_text, is_body_wrapper, is_declaration_only, + is_function_candidate, ANONYMOUS_FUNCTION_NAME, +}; +use crate::node::{role, uast, Node}; + +/// Stack red zone / growth chunk for the recursive walks (sources nest +/// beyond 10k levels; cf-uast builds those trees on a segmented stack). +const STACK_RED_ZONE: usize = 64 * 1024; +const STACK_GROWTH: usize = 16 * 1024 * 1024; + +/// Longest token that can be a bare operator symbol (`and`, `<<=`). +const MAX_BARE_OPERATOR_LEN: usize = 3; +/// Characters a symbolic operator token is built from. +const OPERATOR_CHARS: &str = "&|!=<>+-*/%^~.?:"; +/// Keywords that open an `if` statement (as opposed to a conditional +/// expression, whose text starts with an operand). +const IF_KEYWORDS: [&str; 4] = ["if", "elif", "unless", "when"]; +/// Keyword that opens an `else` branch in every supported language. +const ELSE_KEYWORD: &str = "else"; +/// Case-label keyword (`case _:` is Python's wildcard arm). +const CASE_KEYWORD: &str = "case"; +/// Default-arm spellings: `default`, `_`, `else` (Kotlin `when`). +const DEFAULT_ARM_WORDS: [&str; 3] = ["default", "_", "else"]; + +/// The three structural metrics of one function. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct Measured { + pub cyclomatic: i64, + pub cognitive: i64, + pub nesting: i64, +} + +/// Byte offsets of every line start, so per-function LOC counting never +/// rescans the file. +pub(crate) struct LineIndex { + starts: Vec<usize>, + len: usize, +} + +impl LineIndex { + pub(crate) fn new(source: &[u8]) -> Self { + let mut starts = vec![0usize]; + starts.extend( + source + .iter() + .enumerate() + .filter(|(_, b)| **b == b'\n') + .map(|(i, _)| i + 1), + ); + LineIndex { + starts, + len: source.len(), + } + } + + /// Byte range of the 1-based `line` (without its newline), clipped to + /// the source. + fn line_range(&self, line: u32) -> Option<std::ops::Range<usize>> { + let idx = (line as usize).checked_sub(1)?; + let start = *self.starts.get(idx)?; + let end = self.starts.get(idx + 1).map_or(self.len, |next| next - 1); + Some(start..end.max(start)) + } +} + +/// Logical operator kinds; a change of kind inside a sequence is a new +/// cognitive increment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Logical { + And, + Or, +} + +/// Cognitive nesting level, structural depth, and whether the walk is inside +/// an already-counted logical-operator sequence. +#[derive(Debug, Clone, Copy)] +struct Frame { + nesting: i64, + depth: i64, + in_sequence: bool, +} + +impl Frame { + const BASE: Frame = Frame { + nesting: 0, + depth: 0, + in_sequence: false, + }; + + fn fresh(self) -> Frame { + Frame { + in_sequence: false, + ..self + } + } + + fn nested(self) -> Frame { + Frame { + nesting: self.nesting + 1, + depth: self.depth + 1, + in_sequence: false, + } + } + + fn deeper_nesting(self) -> Frame { + Frame { + nesting: self.nesting + 1, + in_sequence: false, + ..self + } + } +} + +/// How an `If` node's children split into condition, then-branch, and else +/// branches, or whether it is a conditional expression. +struct IfShape { + ternary: bool, + then_idx: Option<usize>, +} + +/// One else branch: an `else if` continuation (its `If`, through at most one +/// wrapper) or a plain `else`. +enum ElseBranch<'a> { + ElseIf(&'a Node), + Else(&'a Node), +} + +struct Walker<'s> { + source: Option<&'s [u8]>, + function_name: &'s str, + out: Measured, +} + +/// Computes the structural metrics of `fn_node`, named `name` (used for +/// recursion detection; [`ANONYMOUS_FUNCTION_NAME`] disables it). +pub(crate) fn measure(fn_node: &Node, name: &str, source: Option<&[u8]>) -> Measured { + let mut walker = Walker { + source, + function_name: if name == ANONYMOUS_FUNCTION_NAME { + "" + } else { + name + }, + out: Measured { + cyclomatic: 1, + ..Measured::default() + }, + }; + let own_body = body_child(fn_node); + for child in &fn_node.children { + let is_own_wrapper = + own_body.is_some_and(|b| std::ptr::eq(b, child)) && is_body_wrapper(fn_node, child); + if is_own_wrapper { + // The function's own body literal: level 0, never a nested lambda. + walker.walk_children(child, Frame::BASE); + } else { + walker.walk(child, Some(fn_node), Frame::BASE); + } + } + walker.out +} + +impl Walker<'_> { + fn walk(&mut self, n: &Node, parent: Option<&Node>, frame: Frame) { + stacker::maybe_grow(STACK_RED_ZONE, STACK_GROWTH, || { + self.walk_inner(n, parent, frame); + }); + } + + fn walk_children(&mut self, n: &Node, frame: Frame) { + for child in &n.children { + self.walk(child, Some(n), frame); + } + } + + fn walk_inner(&mut self, n: &Node, parent: Option<&Node>, frame: Frame) { + match n.node_type.as_str() { + uast::IF => self.process_if(n, frame, false), + uast::LOOP => self.process_loop(n, parent, frame), + uast::SWITCH | uast::MATCH => self.process_switch(n, frame), + uast::CASE => self.process_case(n, parent, frame), + uast::TRY => self.process_try(n, frame), + uast::CATCH => self.process_catch(n, parent, frame), + uast::LAMBDA => self.walk_children(n, frame.deeper_nesting()), + uast::FUNCTION | uast::METHOD => self.process_nested_function(n, frame), + uast::CALL => self.process_call(n, frame), + uast::BREAK | uast::CONTINUE => self.process_jump(n), + _ => self.process_expression(n, frame), + } + } + + fn enter_depth(&mut self, depth: i64) { + if depth > self.out.nesting { + self.out.nesting = depth; + } + } + + fn process_if(&mut self, n: &Node, frame: Frame, is_else_if: bool) { + self.out.cyclomatic += 1; + let shape = IfShape::classify(n, self.source); + if shape.ternary { + self.out.cognitive += frame.nesting + 1; + self.walk_children(n, frame.deeper_nesting()); + return; + } + self.out.cognitive += if is_else_if { 1 } else { frame.nesting + 1 }; + let inner = frame.nested(); + self.enter_depth(inner.depth); + let then_idx = shape.then_idx.unwrap_or(n.children.len()); + for (idx, child) in n.children.iter().enumerate() { + match idx.cmp(&then_idx) { + std::cmp::Ordering::Less => self.walk(child, Some(n), frame.fresh()), + std::cmp::Ordering::Equal => self.walk(child, Some(n), inner), + std::cmp::Ordering::Greater => self.process_else(n, child, frame, inner), + } + } + } + + /// One branch after the then-branch: a flat `+1` for `else if` + /// (recursing with the head's frame) or `else` (contents nested). + fn process_else(&mut self, head: &Node, child: &Node, frame: Frame, inner: Frame) { + match classify_else(child) { + ElseBranch::ElseIf(inner_if) => self.process_if(inner_if, frame, true), + ElseBranch::Else(branch) => { + self.out.cognitive += 1; + self.walk(branch, Some(head), inner); + } + } + } + + fn process_loop(&mut self, n: &Node, parent: Option<&Node>, frame: Frame) { + if is_for_clause_header(n, parent) { + self.walk_children(n, frame.fresh()); + return; + } + self.out.cyclomatic += 1; + self.out.cognitive += frame.nesting + 1; + let inner = frame.nested(); + self.enter_depth(inner.depth); + self.walk_children(n, inner); + } + + fn process_switch(&mut self, n: &Node, frame: Frame) { + self.out.cognitive += frame.nesting + 1; + let inner = frame.nested(); + self.enter_depth(inner.depth); + self.walk_children(n, inner); + } + + fn process_case(&mut self, n: &Node, parent: Option<&Node>, frame: Frame) { + let is_pattern = parent.is_some_and(|p| p.is_type(uast::CASE)); + if !is_pattern && !is_default_case(n, self.source) { + self.out.cyclomatic += 1; + } + self.walk_children(n, frame.fresh()); + } + + fn process_try(&mut self, n: &Node, frame: Frame) { + let inner = Frame { + depth: frame.depth + 1, + ..frame.fresh() + }; + self.enter_depth(inner.depth); + self.walk_children(n, inner); + } + + fn process_catch(&mut self, n: &Node, parent: Option<&Node>, frame: Frame) { + self.out.cyclomatic += 1; + self.out.cognitive += frame.nesting + 1; + // The enclosing `try` already opened the statement's nesting level. + let depth = if parent.is_some_and(|p| p.is_type(uast::TRY)) { + frame.depth + } else { + frame.depth + 1 + }; + let inner = Frame { + nesting: frame.nesting + 1, + depth, + in_sequence: false, + }; + self.enter_depth(inner.depth); + self.walk_children(n, inner); + } + + /// A named nested function is a separate unit; an anonymous function + /// literal is a lambda of this function; a declaration-only node is + /// signature noise and is walked through. + fn process_nested_function(&mut self, n: &Node, frame: Frame) { + if !is_function_candidate(n) || is_declaration_only(n) { + self.walk_children(n, frame.fresh()); + } else if !has_name(n, self.source) { + self.walk_children(n, frame.deeper_nesting()); + } + } + + fn process_call(&mut self, n: &Node, frame: Frame) { + if !self.function_name.is_empty() + && callee_name(n, self.source).as_deref() == Some(self.function_name) + { + self.out.cognitive += 1; + } + self.walk_children(n, frame.fresh()); + } + + /// `break label` / `continue label` / `goto label` carry a target child; + /// a plain `break`/`continue` has none and adds nothing. + fn process_jump(&mut self, n: &Node) { + if !n.children.is_empty() { + self.out.cognitive += 1; + } + } + + /// Logical operators: `+1` cyclomatic each; `+1` cognitive per sequence + /// of like operators, counted once at the sequence root. Wrappers + /// (`Synthetic` parentheses, unary `!`) are transparent to a sequence. + fn process_expression(&mut self, n: &Node, frame: Frame) { + if logical_operator(n, self.source).is_some() { + self.out.cyclomatic += 1; + if !frame.in_sequence { + self.out.cognitive += sequence_increments(n, self.source); + } + self.walk_children( + n, + Frame { + in_sequence: true, + ..frame + }, + ); + } else if is_sequence_wrapper(n) { + self.walk_children(n, frame); + } else { + self.walk_children(n, frame.fresh()); + } + } +} + +impl IfShape { + fn classify(n: &Node, source: Option<&[u8]>) -> Self { + let text = n + .span_text(source) + .map(str::trim_start) + .filter(|t| !t.is_empty()) + .or_else(|| Some(n.token.trim_start()).filter(|t| !t.is_empty())); + let has_block = n + .children + .iter() + .any(|c| c.has_role(role::BODY) || c.is_type(uast::BLOCK)); + let ternary = match text { + Some(t) => !starts_with_keyword(t, &IF_KEYWORDS), + None => !has_block && n.children.len() == 3, + }; + let then_idx = n + .children + .iter() + .enumerate() + .skip(1) + .find(|(_, c)| { + (c.has_role(role::BODY) || c.is_type(uast::BLOCK)) && !is_else_wrapper(c) + }) + .map(|(idx, _)| idx) + .or_else(|| (n.children.len() >= 2).then_some(1)); + IfShape { ternary, then_idx } + } +} + +/// True when `text` starts with one of `keywords` as a whole word. +fn starts_with_keyword(text: &str, keywords: &[&str]) -> bool { + keywords.iter().any(|kw| { + text.strip_prefix(kw) + .is_some_and(|rest| !rest.starts_with(|c: char| c.is_alphanumeric() || c == '_')) + }) +} + +/// A wrapper whose token starts with `else` (C/JS lower braceless and +/// `else if` branches into a `Body`-role wrapper carrying that text). +fn is_else_wrapper(n: &Node) -> bool { + starts_with_keyword(n.token.trim_start(), &[ELSE_KEYWORD]) +} + +/// Classifies one child after the then-branch. An `If` is an `else if`; a +/// one-child `Synthetic`/`Block` wrapper around an `If` is an `else if` when +/// it ends where the `If` ends (the `else` keyword is the only extra text), +/// and a braced `else { if ... }` otherwise. +fn classify_else(child: &Node) -> ElseBranch<'_> { + if child.is_type(uast::IF) { + return ElseBranch::ElseIf(child); + } + let is_wrapper = child.has_any_type(&[uast::SYNTHETIC, uast::BLOCK]); + let only_if = child.children.len() == 1 && child.children[0].is_type(uast::IF); + if !is_wrapper || !only_if { + return ElseBranch::Else(child); + } + let inner = &child.children[0]; + let ends_together = match (&child.pos, &inner.pos) { + (Some(outer), Some(inner)) => outer.end_offset == inner.end_offset, + _ => is_else_wrapper(child), + }; + if ends_together { + ElseBranch::ElseIf(inner) + } else { + ElseBranch::Else(child) + } +} + +/// Go lowers a three-clause `for` as `Loop{Loop{init, cond, post}, Block}`: +/// the inner, body-less first child is the clause header, not a loop. +fn is_for_clause_header(n: &Node, parent: Option<&Node>) -> bool { + let Some(parent) = parent else { + return false; + }; + parent.is_type(uast::LOOP) + && parent.children.first().is_some_and(|c| std::ptr::eq(c, n)) + && parent.children.iter().any(|c| c.has_role(role::BODY)) + && !n.children.iter().any(|c| c.has_role(role::BODY)) +} + +/// True for a `default` / `_` / `else` arm, or a `case _:` wildcard. Reads +/// the arm's own token (never its body) and its pattern child. +fn is_default_case(n: &Node, source: Option<&[u8]>) -> bool { + let token = n.token.trim(); + let mut words = token + .split(|c: char| c.is_whitespace() || c == ':') + .filter(|w| !w.is_empty()); + let first = words.next().unwrap_or(""); + if DEFAULT_ARM_WORDS + .iter() + .any(|w| first.eq_ignore_ascii_case(w)) + { + return true; + } + if first.eq_ignore_ascii_case(CASE_KEYWORD) && words.next() == Some("_") { + return true; + } + n.children.first().is_some_and(|c| { + c.is_type(uast::PATTERN) + && c.children.is_empty() + && (c.token.trim() == "_" || c.span_text(source).map(str::trim) == Some("_")) + }) +} + +/// The simple name a call targets: `props["name"]`, the identifier operand, +/// the last member of an access chain, or the call token up to `(`. +fn callee_name(call: &Node, source: Option<&[u8]>) -> Option<String> { + if let Some(name) = call.prop("name").map(str::trim).filter(|s| !s.is_empty()) { + return Some(name.to_string()); + } + if let Some(target) = call.children.first() { + if target.is_type(uast::IDENTIFIER) { + return identifier_text(target, source); + } + if target.has_any_type(&[uast::FIELD, uast::SYNTHETIC]) { + if let Some(last) = target + .children + .iter() + .rev() + .find(|c| c.is_type(uast::IDENTIFIER)) + { + return identifier_text(last, source); + } + } + } + let head = call.token.split('(').next()?.trim(); + let simple = head.rsplit('.').next()?.trim(); + (!simple.is_empty() && !simple.contains(char::is_whitespace)).then(|| simple.to_string()) +} + +/// Nodes a logical-operator sequence looks through: parenthesised operands +/// and unary negation. +fn is_sequence_wrapper(n: &Node) -> bool { + n.has_any_type(&[uast::SYNTHETIC, uast::UNARY_OP]) +} + +/// The logical operator of `n`, if it is a logical binary operation. +fn logical_operator(n: &Node, source: Option<&[u8]>) -> Option<Logical> { + if n.children.len() < 2 { + return None; + } + if n.is_type(uast::BINARY_OP) { + return binary_operator(n, source).and_then(|op| classify_logical(&op)); + } + if n.is_type(uast::SYNTHETIC) && n.children.len() == 2 { + return operator_from_gap(n, source).and_then(|op| classify_logical(&op)); + } + None +} + +fn classify_logical(op: &str) -> Option<Logical> { + match op { + "&&" => Some(Logical::And), + "||" => Some(Logical::Or), + _ if op.eq_ignore_ascii_case("and") => Some(Logical::And), + _ if op.eq_ignore_ascii_case("or") => Some(Logical::Or), + _ => None, + } +} + +/// A `BinaryOp`'s operator: `props["operator"]`, else a bare-operator token, +/// else the source text between its operands. +pub(crate) fn binary_operator(n: &Node, source: Option<&[u8]>) -> Option<String> { + if let Some(op) = n.prop("operator").map(str::trim).filter(|s| !s.is_empty()) { + return Some(op.to_string()); + } + let token = n.token.trim(); + if is_bare_operator(token) { + return Some(token.to_string()); + } + operator_from_gap(n, source) +} + +/// True for a short token made only of operator characters, or a logical +/// word (`and`, `or`, `not`). +fn is_bare_operator(token: &str) -> bool { + if token.is_empty() || token.len() > MAX_BARE_OPERATOR_LEN { + return false; + } + token.chars().all(|c| OPERATOR_CHARS.contains(c)) + || ["and", "or", "not"] + .iter() + .any(|w| token.eq_ignore_ascii_case(w)) +} + +/// Recovers an operator from the source gap between the first two operands: +/// the gap stripped of whitespace and parentheses when that is a bare +/// operator, else the first logical operator found in the gap (comments +/// between operands are skipped over that way). +fn operator_from_gap(n: &Node, source: Option<&[u8]>) -> Option<String> { + let source = source?; + let left = n.children.first()?.pos.as_ref()?; + let right = n.children.get(1)?.pos.as_ref()?; + let (start, end) = (left.end_offset as usize, right.start_offset as usize); + if start >= end || end > source.len() { + return None; + } + let gap = std::str::from_utf8(&source[start..end]).ok()?; + let compact: String = gap + .chars() + .filter(|c| !c.is_whitespace() && *c != '(' && *c != ')') + .collect(); + if is_bare_operator(&compact) { + return Some(compact); + } + if let Some(sym) = ["&&", "||"].iter().find(|s| gap.contains(*s)) { + return Some((*sym).to_string()); + } + gap.split(|c: char| !c.is_alphanumeric() && c != '_') + .find(|w| w.eq_ignore_ascii_case("and") || w.eq_ignore_ascii_case("or")) + .map(str::to_string) +} + +/// Cognitive increments of the logical-operator sequence rooted at `root`: +/// `1` for the sequence plus `1` per change of operator kind along the +/// in-order operator stream. +fn sequence_increments(root: &Node, source: Option<&[u8]>) -> i64 { + let mut ops: Vec<Logical> = Vec::new(); + collect_sequence(root, source, &mut ops); + if ops.is_empty() { + return 0; + } + let changes = ops.windows(2).filter(|w| w[0] != w[1]).count(); + 1 + changes as i64 +} + +fn collect_sequence(n: &Node, source: Option<&[u8]>, ops: &mut Vec<Logical>) { + stacker::maybe_grow(STACK_RED_ZONE, STACK_GROWTH, || { + if let Some(op) = logical_operator(n, source) { + for (idx, child) in n.children.iter().enumerate() { + if idx > 0 { + ops.push(op); + } + collect_sequence(child, source, ops); + } + } else if is_sequence_wrapper(n) { + for child in &n.children { + collect_sequence(child, source, ops); + } + } + }); +} + +/// Non-blank, non-comment-only lines in `fn_node`'s span. Without source the +/// span's line count stands in; without positions the result is 0. +pub(crate) fn lines_of_code( + fn_node: &Node, + source: Option<&[u8]>, + index: Option<&LineIndex>, +) -> i64 { + let Some(pos) = &fn_node.pos else { + return 0; + }; + if pos.end_line < pos.start_line { + return 0; + } + let (Some(source), Some(index)) = (source, index) else { + return i64::from(pos.end_line - pos.start_line) + 1; + }; + let excluded = excluded_ranges(fn_node, Some(source)); + let span = pos.start_offset as usize..pos.end_offset as usize; + (pos.start_line..=pos.end_line) + .filter_map(|line| index.line_range(line)) + .filter(|range| { + let from = range.start.max(span.start); + let to = range.end.min(span.end).min(source.len()); + (from..to).any(|i| !source[i].is_ascii_whitespace() && !in_ranges(&excluded, i)) + }) + .count() as i64 +} + +/// Sorted, merged byte ranges that do not count as this function's code: +/// every `Comment` node and every nested named function in the subtree. +fn excluded_ranges(fn_node: &Node, source: Option<&[u8]>) -> Vec<(usize, usize)> { + let mut ranges: Vec<(usize, usize)> = Vec::new(); + let mut stack: Vec<&Node> = fn_node.children.iter().collect(); + while let Some(n) = stack.pop() { + let is_nested_unit = + is_function_candidate(n) && !is_declaration_only(n) && has_name(n, source); + if n.is_type(uast::COMMENT) || is_nested_unit { + if let Some(p) = &n.pos { + ranges.push((p.start_offset as usize, p.end_offset as usize)); + } + continue; + } + stack.extend(n.children.iter()); + } + ranges.sort_unstable(); + let mut merged: Vec<(usize, usize)> = Vec::with_capacity(ranges.len()); + for (start, end) in ranges { + match merged.last_mut() { + Some(last) if start <= last.1 => last.1 = last.1.max(end), + _ => merged.push((start, end)), + } + } + merged +} + +fn in_ranges(ranges: &[(usize, usize)], offset: usize) -> bool { + let idx = ranges.partition_point(|(start, _)| *start <= offset); + idx > 0 && offset < ranges[idx - 1].1 +} + +#[cfg(test)] +mod tests { + use super::{binary_operator, lines_of_code, measure, LineIndex, Measured}; + use crate::node::{role, uast, Node, Positions}; + + fn func(children: Vec<Node>) -> Node { + Node::new(uast::FUNCTION) + .with_roles(vec![role::FUNCTION, role::DECLARATION]) + .with_prop("name", "f") + .with_children(children) + } + + fn block(children: Vec<Node>) -> Node { + Node::new(uast::BLOCK) + .with_roles(vec![role::BODY]) + .with_children(children) + } + + fn cond() -> Node { + Node::with_token(uast::IDENTIFIER, "x").with_roles(vec![role::NAME]) + } + + fn if_then(then: Vec<Node>) -> Node { + Node::new(uast::IF).with_children(vec![cond(), block(then)]) + } + + fn m(cyclomatic: i64, cognitive: i64, nesting: i64) -> Measured { + Measured { + cyclomatic, + cognitive, + nesting, + } + } + + #[test] + fn empty_function_is_one_zero_zero() { + assert_eq!(measure(&func(vec![block(vec![])]), "f", None), m(1, 0, 0)); + } + + #[test] + fn nested_ifs_add_nesting_increments() { + let f = func(vec![block(vec![if_then(vec![if_then(vec![if_then( + vec![], + )])])])]); + assert_eq!(measure(&f, "f", None), m(4, 6, 3)); + } + + #[test] + fn else_if_is_flat_and_else_contents_are_nested() { + // if x {} else if x {} else { if x {} } + let else_block = block(vec![if_then(vec![])]); + let else_if = Node::new(uast::IF).with_children(vec![cond(), block(vec![]), else_block]); + let head = Node::new(uast::IF).with_children(vec![cond(), block(vec![]), else_if]); + let f = func(vec![block(vec![head])]); + // cyclomatic: 1 + if + else-if + inner if = 4 + // cognitive: if 1 + else-if 1 + else 1 + inner if (nesting 1) 2 = 5 + // nesting: chain 1, inner if 2 + assert_eq!(measure(&f, "f", None), m(4, 5, 2)); + } + + #[test] + fn braced_else_with_one_if_is_an_else_not_an_else_if() { + // Go: Block(else){If}: the block ends after the inner if. + let inner_if = Node::new(uast::IF) + .with_children(vec![cond(), block(vec![])]) + .with_pos(Positions { + start_offset: 10, + end_offset: 20, + ..Default::default() + }); + let else_block = block(vec![inner_if]).with_pos(Positions { + start_offset: 8, + end_offset: 22, + ..Default::default() + }); + let head = Node::new(uast::IF).with_children(vec![cond(), block(vec![]), else_block]); + let f = func(vec![block(vec![head])]); + assert_eq!(measure(&f, "f", None), m(3, 4, 2)); + } + + #[test] + fn wrapped_else_if_sharing_the_end_offset_is_flat() { + // C/JS: Block(else-if wrapper){If} ending exactly where the If ends. + let inner_if = Node::new(uast::IF) + .with_children(vec![cond(), block(vec![])]) + .with_pos(Positions { + start_offset: 13, + end_offset: 20, + ..Default::default() + }); + let wrapper = Node::with_token(uast::BLOCK, "else if (x) {}") + .with_roles(vec![role::BODY]) + .with_children(vec![inner_if]) + .with_pos(Positions { + start_offset: 8, + end_offset: 20, + ..Default::default() + }); + let head = Node::new(uast::IF).with_children(vec![cond(), block(vec![]), wrapper]); + let f = func(vec![block(vec![head])]); + assert_eq!(measure(&f, "f", None), m(3, 2, 1)); + } + + #[test] + fn ternary_is_one_increment_without_else_or_depth() { + let ternary = Node::with_token(uast::IF, "x ? 1 : 2").with_children(vec![ + cond(), + Node::new("Literal"), + Node::new("Literal"), + ]); + let f = func(vec![block(vec![ternary])]); + assert_eq!(measure(&f, "f", None), m(2, 1, 0)); + } + + #[test] + fn loops_switch_and_cases() { + let cases = vec![ + Node::with_token(uast::CASE, "case 1:"), + Node::with_token(uast::CASE, "case 2:"), + Node::with_token(uast::CASE, "default:"), + ]; + let switch = Node::new(uast::SWITCH).with_children(cases); + let lp = Node::new(uast::LOOP).with_children(vec![block(vec![switch])]); + let f = func(vec![block(vec![lp])]); + // cyclomatic: 1 + loop + 2 cases = 4; cognitive: loop 1 + switch 2 = 3 + assert_eq!(measure(&f, "f", None), m(4, 3, 2)); + } + + #[test] + fn default_arm_spellings() { + for token in ["default:", "DEFAULT:", "_ => 0", "case _:", "else ->"] { + let sw = + Node::new(uast::MATCH).with_children(vec![Node::with_token(uast::CASE, token)]); + let f = func(vec![block(vec![sw])]); + assert_eq!(measure(&f, "f", None).cyclomatic, 1, "{token}"); + } + let named = Node::with_token(uast::CASE, "case defaultValue:"); + let sw = Node::new(uast::SWITCH).with_children(vec![named]); + assert_eq!( + measure(&func(vec![block(vec![sw])]), "f", None).cyclomatic, + 2 + ); + } + + #[test] + fn pattern_case_inside_case_is_not_an_arm() { + let arm = Node::with_token(uast::CASE, "case 1:") + .with_children(vec![Node::new(uast::CASE), block(vec![])]); + let mt = Node::new(uast::MATCH).with_children(vec![block(vec![arm])]); + assert_eq!( + measure(&func(vec![block(vec![mt])]), "f", None).cyclomatic, + 2 + ); + } + + #[test] + fn try_adds_no_cognitive_and_catch_is_one_level() { + let catch = Node::new(uast::CATCH).with_children(vec![block(vec![if_then(vec![])])]); + let tr = Node::new(uast::TRY).with_children(vec![block(vec![if_then(vec![])]), catch]); + let f = func(vec![block(vec![tr])]); + // cyclomatic: 1 + if + catch + if = 4 + // cognitive: if-in-try 1 + catch 1 + if-in-catch (nesting 1) 2 = 4 + // nesting: try/catch 1, inner ifs 2 + assert_eq!(measure(&f, "f", None), m(4, 4, 2)); + } + + #[test] + fn logical_sequences_count_once_per_run_of_like_operators() { + let op = |o: &str, l: Node, r: Node| { + Node::new(uast::BINARY_OP) + .with_prop("operator", o) + .with_children(vec![l, r]) + }; + // a && b && c || d + let expr = op("||", op("&&", op("&&", cond(), cond()), cond()), cond()); + let ret = Node::new("Return").with_children(vec![expr]); + let f = func(vec![block(vec![ret])]); + assert_eq!(measure(&f, "f", None), m(4, 2, 0)); + + // a && f(b || c): the call argument starts a new sequence. + let call = Node::new(uast::CALL).with_children(vec![cond(), op("||", cond(), cond())]); + let expr = op("&&", cond(), call); + let f = func(vec![block(vec![ + Node::new("Return").with_children(vec![expr]) + ])]); + assert_eq!(measure(&f, "f", None), m(3, 2, 0)); + + // !(a || b) && c: the negation is transparent. + let neg = Node::new(uast::UNARY_OP).with_children(vec![op("||", cond(), cond())]); + let expr = op("&&", neg, cond()); + let f = func(vec![block(vec![ + Node::new("Return").with_children(vec![expr]) + ])]); + assert_eq!(measure(&f, "f", None), m(3, 2, 0)); + } + + #[test] + fn operator_recovery_order_is_prop_then_token_then_source() { + let src = b"a > 0 && b > 0"; + let operand = |s: u32, e: u32| { + Node::new(uast::BINARY_OP).with_pos(Positions { + start_offset: s, + end_offset: e, + ..Default::default() + }) + }; + let by_prop = Node::new(uast::BINARY_OP) + .with_prop("operator", "||") + .with_children(vec![operand(0, 5), operand(9, 14)]); + assert_eq!(binary_operator(&by_prop, Some(src)).as_deref(), Some("||")); + let by_token = Node::with_token(uast::BINARY_OP, "&&") + .with_children(vec![operand(0, 5), operand(9, 14)]); + assert_eq!(binary_operator(&by_token, None).as_deref(), Some("&&")); + let by_source = Node::with_token(uast::BINARY_OP, "a > 0 && b > 0") + .with_children(vec![operand(0, 5), operand(9, 14)]); + assert_eq!( + binary_operator(&by_source, Some(src)).as_deref(), + Some("&&") + ); + assert_eq!(binary_operator(&by_source, None), None); + let paren = Node::new(uast::BINARY_OP).with_children(vec![operand(1, 5), operand(18, 19)]); + assert_eq!( + binary_operator(&paren, Some(b"(a||b) /* c */ && c")).as_deref(), + Some("&&") + ); + } + + #[test] + fn python_word_operators_through_synthetic_nodes() { + let src = b"if a and b or c:"; + let ident = |s: u32, e: u32| { + Node::new(uast::IDENTIFIER).with_pos(Positions { + start_offset: s, + end_offset: e, + ..Default::default() + }) + }; + let inner = Node::new(uast::SYNTHETIC) + .with_children(vec![ident(3, 4), ident(9, 10)]) + .with_pos(Positions { + start_offset: 3, + end_offset: 10, + ..Default::default() + }); + let outer = Node::new(uast::SYNTHETIC).with_children(vec![inner, ident(14, 15)]); + let f = func(vec![block(vec![ + Node::new(uast::IF).with_children(vec![outer, block(vec![])]) + ])]); + // cyclomatic: 1 + if + and + or = 4; cognitive: if 1 + sequence 1 + change 1 = 3 + assert_eq!(measure(&f, "f", Some(src)), m(4, 3, 1)); + } + + #[test] + fn recursion_labelled_jumps_and_lambdas() { + let call = Node::with_token(uast::CALL, "f(n-1)").with_children(vec![Node::with_token( + uast::IDENTIFIER, + "f", + ) + .with_roles(vec![role::NAME])]); + let other = Node::with_token(uast::CALL, "g(n)").with_children(vec![Node::with_token( + uast::IDENTIFIER, + "g", + ) + .with_roles(vec![role::NAME])]); + let labelled = Node::new(uast::BREAK).with_children(vec![Node::new(uast::IDENTIFIER)]); + let plain = Node::new(uast::BREAK); + let lambda = Node::new(uast::LAMBDA).with_children(vec![block(vec![if_then(vec![])])]); + let f = func(vec![block(vec![call, other, labelled, plain, lambda])]); + // cyclomatic: 1 + if = 2; cognitive: recursion 1 + label 1 + if@1 2 = 4; nesting 1 + assert_eq!(measure(&f, "f", None), m(2, 4, 1)); + } + + #[test] + fn own_body_wrapper_is_level_zero_but_nested_literals_are_not() { + let arrow_body = Node::new(uast::LAMBDA).with_children(vec![block(vec![if_then(vec![])])]); + let arrow = func(vec![cond(), arrow_body]); + assert_eq!(measure(&arrow, "f", None), m(2, 1, 1)); + + let anon = Node::new(uast::FUNCTION) + .with_roles(vec![role::FUNCTION, role::DECLARATION]) + .with_children(vec![block(vec![if_then(vec![])])]); + let named = Node::new(uast::FUNCTION) + .with_roles(vec![role::FUNCTION, role::DECLARATION]) + .with_prop("name", "inner") + .with_children(vec![block(vec![if_then(vec![])])]); + let f = func(vec![block(vec![anon, named])]); + // anonymous literal: if at nesting 1 (+2); named inner: skipped. + assert_eq!(measure(&f, "f", None), m(2, 2, 1)); + } + + #[test] + fn go_for_clause_header_is_not_a_second_loop() { + let header = Node::new(uast::LOOP).with_children(vec![cond()]); + let lp = Node::new(uast::LOOP).with_children(vec![header, block(vec![])]); + assert_eq!(measure(&func(vec![block(vec![lp])]), "f", None), m(2, 1, 1)); + } + + #[test] + fn lines_of_code_skip_blank_and_comment_only_lines() { + let src = b"func f() {\n\t// note\n\n\tx := 1 // trailing\n\t/* a\n\t b */\n}\n"; + let comment = |s: u32, e: u32, line: u32, end_line: u32| { + Node::new(uast::COMMENT).with_pos(Positions { + start_offset: s, + end_offset: e, + start_line: line, + end_line, + ..Default::default() + }) + }; + let f = Node::new(uast::FUNCTION) + .with_pos(Positions { + start_line: 1, + end_line: 7, + start_offset: 0, + end_offset: u32::try_from(src.len()).unwrap() - 1, + ..Default::default() + }) + .with_children(vec![ + comment(12, 19, 2, 2), + comment(29, 40, 4, 4), + comment(42, 55, 5, 6), + ]); + let index = LineIndex::new(src); + // Lines: `func f() {`, `x := 1`, `}` = 3. + assert_eq!(lines_of_code(&f, Some(src), Some(&index)), 3); + assert_eq!( + lines_of_code(&f, None, None), + 7, + "span fallback without source" + ); + assert_eq!(lines_of_code(&Node::new(uast::FUNCTION), None, None), 0); + } + + #[test] + fn deep_nesting_does_not_overflow_the_stack() { + const DEPTH: usize = 20_000; + let mut inner = block(vec![]); + for _ in 0..DEPTH { + inner = block(vec![Node::new(uast::IF).with_children(vec![cond(), inner])]); + } + let got = measure(&func(vec![inner]), "f", None); + assert_eq!(got.nesting, DEPTH as i64); + assert_eq!(got.cyclomatic, DEPTH as i64 + 1); + } +} diff --git a/crates/cf-complexity/src/node.rs b/crates/cf-complexity/src/node.rs index 6ba22850..5095c2f2 100644 --- a/crates/cf-complexity/src/node.rs +++ b/crates/cf-complexity/src/node.rs @@ -2,50 +2,60 @@ //! //! This covers exactly the subset of the UAST node surface the analyzer //! reads: node type, token, roles, string props, children, and source -//! positions, plus the traversal helpers (pre-order visit, type/role -//! predicates, type/role collection) the analyzer calls. +//! positions, plus the traversal helpers the analyzer calls and the bridge +//! from the canonical [`cf_uast_node::Node`] tree ([`Node::from_uast`]). use std::collections::BTreeMap; -/// UAST node type string constants. The byte values flow into reports via -/// node types, so they are part of the report contract. +/// UAST node type string constants (the subset the analyzer inspects). pub mod uast { /// `File` root node type. pub const FILE: &str = "File"; /// `Function` node type. pub const FUNCTION: &str = "Function"; - /// `FunctionDecl` node type. - pub const FUNCTION_DECL: &str = "FunctionDecl"; /// `Method` node type. pub const METHOD: &str = "Method"; - /// `Class` node type. - pub const CLASS: &str = "Class"; /// `Block` node type. pub const BLOCK: &str = "Block"; - /// `If` node type (decision point / nesting). + /// `If` node type: an `if` statement or a conditional expression. pub const IF: &str = "If"; - /// `Loop` node type (decision point / nesting). + /// `Loop` node type (`for` / `while` / `do` / `loop`). pub const LOOP: &str = "Loop"; - /// `Switch` node type (nesting; cognitive increment like a loop). + /// `Switch` node type. pub const SWITCH: &str = "Switch"; - /// `Case` node type (decision point unless `default`). + /// `Match` node type (pattern matching; counted like a switch). + pub const MATCH: &str = "Match"; + /// `Case` node type (a switch/match arm; a decision point unless default). pub const CASE: &str = "Case"; - /// `Try` node type (nesting). + /// `Try` node type. pub const TRY: &str = "Try"; - /// `Catch` node type (decision point / nesting). + /// `Catch` node type (a decision point; a cognitive nesting node). pub const CATCH: &str = "Catch"; /// `BinaryOp` node type (logical operators are decision points). pub const BINARY_OP: &str = "BinaryOp"; - /// `Return` node type. - pub const RETURN: &str = "Return"; + /// `UnaryOp` node type (transparent for boolean-sequence counting). + pub const UNARY_OP: &str = "UnaryOp"; /// `Identifier` node type. pub const IDENTIFIER: &str = "Identifier"; - /// `Match` node type (cognitive: loop-like nesting increment). - pub const MATCH: &str = "Match"; - /// `Lambda` node type (cognitive: increases nesting for its body). + /// `Lambda` node type (raises cognitive nesting for its contents). pub const LAMBDA: &str = "Lambda"; - /// `Call` node type (cognitive: +1 for a recursive call). + /// `Call` node type (`+1` cognitive for a direct recursive call). pub const CALL: &str = "Call"; + /// `Break` node type (`+1` cognitive when it names a label). + pub const BREAK: &str = "Break"; + /// `Continue` node type (`+1` cognitive when it names a label). + pub const CONTINUE: &str = "Continue"; + /// `Synthetic` node type: language-specific wrapper (parentheses, `else` + /// clauses, statement wrappers). + pub const SYNTHETIC: &str = "Synthetic"; + /// `Comment` node type (excluded from lines of code). + pub const COMMENT: &str = "Comment"; + /// `Parameter` node type (signature; never part of a body). + pub const PARAMETER: &str = "Parameter"; + /// `Field` node type (member access chain in a call target). + pub const FIELD: &str = "Field"; + /// `Pattern` node type (match-arm pattern; `_` marks the default arm). + pub const PATTERN: &str = "Pattern"; } /// UAST role string constants. @@ -56,14 +66,14 @@ pub mod role { pub const DECLARATION: &str = "Declaration"; /// `Name` role. pub const NAME: &str = "Name"; + /// `Type` role (a `Name`+`Type` identifier is a type reference, not a name). + pub const TYPE: &str = "Type"; + /// `Body` role (the statement block of a function or branch). + pub const BODY: &str = "Body"; /// `Condition` role. pub const CONDITION: &str = "Condition"; - /// `Argument` role. - pub const ARGUMENT: &str = "Argument"; - /// `Parameter` role. + /// `Parameter` role (a parameter list wrapper is never a name source). pub const PARAMETER: &str = "Parameter"; - /// `Return` role. - pub const RETURN: &str = "Return"; } /// Source positions for a node. @@ -79,7 +89,7 @@ pub struct Positions { pub end_line: u32, /// 0-based end column. pub end_col: u32, - /// Byte offset of the end. + /// Byte offset of the end (exclusive). pub end_offset: u32, } @@ -101,24 +111,32 @@ pub struct Node { pub pos: Option<Positions>, } +/// Stack red zone / growth chunk for the recursive tree conversion: real +/// sources exceed 10k nesting levels (cf-uast builds them on a segmented +/// stack), so the bridge grows its stack on demand as well. +const STACK_RED_ZONE: usize = 64 * 1024; +const STACK_GROWTH: usize = 16 * 1024 * 1024; + impl Node { /// Creates a node of the given type with all other fields empty. #[must_use] pub fn new(node_type: impl Into<String>) -> Self { Node { node_type: node_type.into(), - ..Node::default() + token: String::new(), + roles: Vec::new(), + props: BTreeMap::new(), + children: Vec::new(), + pos: None, } } /// Creates a node with a token (e.g. an [`uast::IDENTIFIER`]). #[must_use] pub fn with_token(node_type: impl Into<String>, token: impl Into<String>) -> Self { - Node { - node_type: node_type.into(), - token: token.into(), - ..Node::default() - } + let mut n = Node::new(node_type); + n.token = token.into(); + n } /// Builder: set children. @@ -154,35 +172,92 @@ impl Node { self } + /// Converts a parsed [`cf_uast_node::Node`] tree into this model. + /// + /// The one bridge every report format shares. Recursion is guarded by + /// `stacker` because the input trees were built on a segmented stack + /// (sources nest beyond 10k levels) and a plain recursive copy would + /// overflow the fixed main-thread stack. + #[must_use] + pub fn from_uast(n: &cf_uast_node::Node) -> Self { + stacker::maybe_grow(STACK_RED_ZONE, STACK_GROWTH, || Self::from_uast_inner(n)) + } + + fn from_uast_inner(n: &cf_uast_node::Node) -> Self { + let mut out = Node::new(n.node_type.clone()); + out.token.clone_from(&n.token); + out.roles.clone_from(&n.roles); + out.props = n + .props + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + out.pos = n.pos.as_ref().map(|p| Positions { + start_line: clamp_u32(p.start_line), + start_col: clamp_u32(p.start_col), + start_offset: clamp_u32(p.start_offset), + end_line: clamp_u32(p.end_line), + end_col: clamp_u32(p.end_col), + end_offset: clamp_u32(p.end_offset), + }); + out.children = n.children.iter().map(Self::from_uast).collect(); + out + } + /// Looks up a string property by key. #[must_use] pub fn prop(&self, key: &str) -> Option<&str> { self.props.get(key).map(String::as_str) } + /// Reports whether this node is of the given type. + #[must_use] + pub fn is_type(&self, node_type: &str) -> bool { + self.node_type == node_type + } + /// Reports whether this node has any of the given types. #[must_use] pub fn has_any_type(&self, types: &[&str]) -> bool { types.iter().any(|t| self.node_type == *t) } + /// Reports whether this node carries the given role. + #[must_use] + pub fn has_role(&self, role: &str) -> bool { + self.roles.iter().any(|own| own == role) + } + /// Reports whether this node has all of the given roles. #[must_use] pub fn has_all_roles(&self, roles: &[&str]) -> bool { - roles.iter().all(|r| self.roles.iter().any(|own| own == r)) + roles.iter().all(|r| self.has_role(r)) } /// Reports whether this node has any of the given roles. #[must_use] pub fn has_any_role(&self, roles: &[&str]) -> bool { - roles.iter().any(|r| self.roles.iter().any(|own| own == r)) + roles.iter().any(|r| self.has_role(r)) + } + + /// The source text covered by this node's span, when both the span and + /// the source are available and the span is well-formed. + #[must_use] + pub fn span_text<'a>(&self, source: Option<&'a [u8]>) -> Option<&'a str> { + let source = source?; + let pos = self.pos.as_ref()?; + let (start, end) = (pos.start_offset as usize, pos.end_offset as usize); + if start >= end || end > source.len() { + return None; + } + std::str::from_utf8(&source[start..end]).ok() } /// Visits this node and all descendants in pre-order. /// /// Iterative (explicit stack): the trees this crate walks were built by /// cf-uast on a segmented stack because real sources exceed 10k nesting - /// levels, so naive recursion here would SIGSEGV on the 8 MiB main stack. + /// levels, so naive recursion here would overflow the main stack. pub fn visit_pre_order<F: FnMut(&Node)>(&self, f: &mut F) { let mut stack = vec![self]; while let Some(node) = stack.pop() { @@ -190,72 +265,95 @@ impl Node { stack.extend(node.children.iter().rev()); } } +} - /// Collects descendants (including `self`) whose type is in `types`, in - /// pre-order. Iterative — see [`Self::visit_pre_order`]. - pub fn find_nodes_by_type<'a>(&'a self, types: &[&str], out: &mut Vec<&'a Node>) { - let mut stack = vec![self]; - while let Some(node) = stack.pop() { - if node.has_any_type(types) { - out.push(node); - } - stack.extend(node.children.iter().rev()); +impl Drop for Node { + /// Iterative drop: a derived (recursive) drop of a 20k-level tree would + /// overflow the stack exactly like the recursive walks it guards against. + fn drop(&mut self) { + let mut stack: Vec<Node> = std::mem::take(&mut self.children); + while let Some(mut n) = stack.pop() { + stack.append(&mut n.children); } } +} - /// Depth-capped [`find_nodes_by_type`](Self::find_nodes_by_type): matches - /// only nodes at depth `<= remaining` below `self` (`self` at depth 0). - /// - /// This mirrors the reference shared traverser's `maxDepth` behaviour: the - /// walk's MATCHING is filtered by depth (deeper nodes are simply never - /// matched), so pruning the descent at the cap is output-equivalent. - pub fn find_nodes_by_type_capped<'a>( - &'a self, - types: &[&str], - remaining: usize, - out: &mut Vec<&'a Node>, - ) { - let mut stack = vec![(self, remaining)]; - while let Some((node, rem)) = stack.pop() { - if node.has_any_type(types) { - out.push(node); - } - if rem == 0 { - continue; - } - stack.extend(node.children.iter().rev().map(|c| (c, rem - 1))); - } +/// Saturating `u64 -> u32` for source positions (a 4 GiB source file is +/// outside the supported input size; the clamp keeps the bridge total). +fn clamp_u32(v: u64) -> u32 { + u32::try_from(v).unwrap_or(u32::MAX) +} + +#[cfg(test)] +mod tests { + use super::{role, uast, Node, Positions}; + + #[test] + fn span_text_reads_the_covered_source() { + let n = Node::new(uast::IDENTIFIER).with_pos(Positions { + start_offset: 4, + end_offset: 7, + ..Positions::default() + }); + assert_eq!(n.span_text(Some(b"let foo = 1;")), Some("foo")); + assert_eq!(n.span_text(None), None); + assert_eq!( + n.span_text(Some(b"let")), + None, + "span past the end is rejected" + ); } - /// Collects descendants (including `self`) having any of `roles`, in - /// pre-order. - pub fn find_nodes_by_roles<'a>(&'a self, roles: &[&str], out: &mut Vec<&'a Node>) { - let mut stack = vec![self]; - while let Some(node) = stack.pop() { - if node.has_any_role(roles) { - out.push(node); - } - stack.extend(node.children.iter().rev()); - } + #[test] + fn from_uast_copies_type_roles_props_and_positions() { + let child = cf_uast_node::Node { + node_type: uast::IDENTIFIER.to_string(), + token: "x".to_string(), + roles: vec![role::NAME.to_string()], + ..cf_uast_node::Node::default() + }; + let mut root = cf_uast_node::Node { + node_type: uast::FUNCTION.to_string(), + pos: Some(cf_uast_node::Positions { + start_line: 1, + end_line: 3, + start_offset: 0, + end_offset: 20, + ..cf_uast_node::Positions::default() + }), + ..cf_uast_node::Node::default() + }; + root.props.insert("name".to_string(), "x".to_string()); + root.children.push(child); + + let got = Node::from_uast(&root); + assert_eq!(got.node_type, uast::FUNCTION); + assert_eq!(got.prop("name"), Some("x")); + assert_eq!(got.pos.as_ref().map(|p| p.end_offset), Some(20)); + assert_eq!(got.children.len(), 1); + assert!(got.children[0].has_role(role::NAME)); + assert_eq!(got.children[0].token, "x"); } - /// Depth-capped [`find_nodes_by_roles`](Self::find_nodes_by_roles): matches - /// only nodes at depth `<= remaining` below `self` (`self` at depth 0). - pub fn find_nodes_by_roles_capped<'a>( - &'a self, - roles: &[&str], - remaining: usize, - out: &mut Vec<&'a Node>, - ) { - let mut stack = vec![(self, remaining)]; - while let Some((node, rem)) = stack.pop() { - if node.has_any_role(roles) { - out.push(node); - } - if rem == 0 { - continue; - } - stack.extend(node.children.iter().rev().map(|c| (c, rem - 1))); + #[test] + fn from_uast_survives_deep_trees() { + const DEPTH: usize = 20_000; + let mut n = cf_uast_node::Node { + node_type: uast::IDENTIFIER.to_string(), + ..cf_uast_node::Node::default() + }; + for _ in 0..DEPTH { + n = cf_uast_node::Node { + node_type: uast::BLOCK.to_string(), + children: vec![n], + ..cf_uast_node::Node::default() + }; } + let got = Node::from_uast(&n); + // The input model drops recursively; keep the test about the bridge. + std::mem::forget(n); + let mut count = 0usize; + got.visit_pre_order(&mut |_| count += 1); + assert_eq!(count, DEPTH + 1); } } diff --git a/crates/cf-complexity/src/report.rs b/crates/cf-complexity/src/report.rs index ede1db78..b4da3d23 100644 --- a/crates/cf-complexity/src/report.rs +++ b/crates/cf-complexity/src/report.rs @@ -1,34 +1,36 @@ -//! Static-analysis report computation for `static/complexity`. +//! The aggregated, multi-file report view (`--format yaml|bin` payload, and +//! the numbers behind the JSON sections report). //! -//! Builds the aggregated metric view that the static pipeline's -//! `--format json|yaml|bin` captures serialize. The streaming static framework -//! (walk → per-file UAST parse → analyzer → stamp → aggregate) is reproduced -//! by the caller; this module takes the aggregated, file-stamped per-function -//! inputs and builds the exact [`cf_gojson::GoValue`] the report contract -//! pins. +//! The static pipeline stamps every function with its file and language and +//! hands the whole list here; this module owns the aggregate math, the +//! deterministic ordering, and the `ComputedMetrics` value tree: //! -//! Field/key ordering follows the report-format contract: the top-level value -//! and the per-function / aggregate values keep declaration order -//! (struct-origin maps), while `distribution` (a string-keyed map) byte-sorts. -//! `function_complexity` and `high_risk_functions` are ordered by the pinned -//! unstable sort (pdqsort) over the aggregation-order input — see -//! [`crate::gosort`]. +//! * `function_complexity`: every function with density and risk level, in +//! [`function_order`]; +//! * `distribution`: `simple` / `moderate` / `complex` counts; +//! * `high_risk_functions`: functions crossing a high threshold, by risk +//! priority then [`function_order`]; +//! * `aggregate`: the [`Aggregate`] scalars. + +use std::cmp::Ordering; -use crate::gosort::go_sort_slice; use cf_gojson::{GoMap, GoValue, MapOrigin}; -/// One function's aggregated, file-stamped complexity input. -/// `source_file` / `language` / `directory` are stamped by the static -/// pipeline. -#[derive(Debug, Clone)] +use crate::thresholds::{ + complexity_message, distribution_bucket, health_score, risk_issues, risk_level, risk_priority, +}; +use crate::FunctionMetrics; + +/// One function's metrics stamped with its file, language and directory. +#[derive(Debug, Clone, PartialEq)] pub struct FunctionInput { /// Function name. pub name: String, - /// Relative source file (stamped; `_source_file`). + /// Source file relative to the analyzed root. pub source_file: String, - /// Language (stamped; `_language`). + /// Detected language. pub language: String, - /// Directory of the source file (stamped; `_directory`). + /// Directory of `source_file` (`.` at the root). pub directory: String, /// Cyclomatic complexity. pub cyclomatic_complexity: i64, @@ -36,262 +38,250 @@ pub struct FunctionInput { pub cognitive_complexity: i64, /// Maximum nesting depth. pub nesting_depth: i64, - /// Estimated lines of code. + /// Lines of code. pub lines_of_code: i64, + /// 1-based line where the function starts (0 when unknown). + pub line: i64, } -/// Aggregated report-level scalars (summed across files by the aggregator). -/// -/// Note: the aggregation pipeline stores `cognitive_complexity` / -/// `nesting_depth` as float sums, which the scalar-parsing step's strict int -/// assertion then drops to 0 — so the aggregate view always reports those two -/// as 0 (reference-implementation behavior, pinned by the differential gate). -#[derive(Debug, Clone, Copy, Default)] -pub struct ReportScalars { - /// Total functions across all files. - pub total_functions: i64, - /// Average complexity (`total_complexity / total_functions`). - pub average_complexity: f64, - /// Maximum per-function cyclomatic complexity across all files. - pub max_complexity: i64, - /// Total cyclomatic complexity across all files. - pub total_complexity: i64, - /// Total decision points across all files. - pub decision_points: i64, -} - -// --- Thresholds (report contract) --- -const CYCLOMATIC_THRESHOLD_HIGH: i64 = 10; -const CYCLOMATIC_THRESHOLD_MODERATE: i64 = 5; -const COGNITIVE_THRESHOLD_HIGH: i64 = 15; -const COGNITIVE_THRESHOLD_MODERATE: i64 = 7; -const NESTING_THRESHOLD_HIGH: i64 = 5; -const NESTING_THRESHOLD_MODERATE: i64 = 3; -const RISK_SCORE_CRITICAL: i64 = 5; -const RISK_SCORE_HIGH: i64 = 3; - -/// The aggregate `message` text keyed by average complexity. Note this differs -/// from the single-file analyzer's message wording for the mid bands; both -/// texts are part of the report contract. -fn build_complexity_message(score: f64) -> &'static str { - if score <= 1.0 { - "Excellent complexity - functions are simple and maintainable" - } else if score <= 3.0 { - "Good complexity - functions have reasonable complexity" - } else if score <= 7.0 { - "Fair complexity - some functions could be simplified" - } else { - "High complexity - functions are complex and should be refactored" +impl FunctionInput { + /// Stamps per-file metrics with their file, language and directory. + #[must_use] + pub fn stamped( + m: &FunctionMetrics, + source_file: &str, + language: &str, + directory: &str, + ) -> Self { + FunctionInput { + name: m.name.clone(), + source_file: source_file.to_string(), + language: language.to_string(), + directory: directory.to_string(), + cyclomatic_complexity: m.cyclomatic_complexity, + cognitive_complexity: m.cognitive_complexity, + nesting_depth: m.nesting_depth, + lines_of_code: m.lines_of_code, + line: m.line, + } } -} -fn classify_function_risk(cyclomatic: i64, cognitive: i64, nesting: i64) -> &'static str { - let mut score = 0; - if cyclomatic >= CYCLOMATIC_THRESHOLD_HIGH { - score += 2; - } else if cyclomatic >= CYCLOMATIC_THRESHOLD_MODERATE { - score += 1; + /// Decision points (`cyclomatic - 1`, never negative). + #[must_use] + pub fn decision_points(&self) -> i64 { + (self.cyclomatic_complexity - 1).max(0) } - if cognitive >= COGNITIVE_THRESHOLD_HIGH { - score += 2; - } else if cognitive >= COGNITIVE_THRESHOLD_MODERATE { - score += 1; + + /// Cyclomatic complexity per line of code (0 without lines). + #[must_use] + pub fn density(&self) -> f64 { + complexity_density(self.cyclomatic_complexity, self.lines_of_code) } - if nesting >= NESTING_THRESHOLD_HIGH { - score += 2; - } else if nesting >= NESTING_THRESHOLD_MODERATE { - score += 1; + + /// Risk level of this function (see [`risk_level`]). + #[must_use] + pub fn risk(&self) -> &'static str { + risk_level( + self.cyclomatic_complexity, + self.cognitive_complexity, + self.nesting_depth, + ) } - if score >= RISK_SCORE_CRITICAL { - "CRITICAL" - } else if score >= RISK_SCORE_HIGH { - "HIGH" - } else if score >= 1 { - "MEDIUM" +} + +/// Cyclomatic complexity per line of code; `0.0` when there are no lines. +#[must_use] +pub fn complexity_density(cyclomatic: i64, lines_of_code: i64) -> f64 { + if lines_of_code > 0 { + cyclomatic as f64 / lines_of_code as f64 } else { - "LOW" + 0.0 } } -/// Risk sort priority (CRITICAL < HIGH < MEDIUM < LOW). -fn risk_priority(level: &str) -> i32 { - match level { - "CRITICAL" => 0, - "HIGH" => 1, - "MEDIUM" => 2, - _ => 3, - } +/// Report-level scalars over all functions. +#[derive(Debug, Clone, PartialEq)] +pub struct Aggregate { + /// Number of functions. + pub total_functions: i64, + /// Function-weighted average cyclomatic complexity + /// (`total_complexity / total_functions`; 0 without functions). + pub average_complexity: f64, + /// Maximum per-function cyclomatic complexity. + pub max_complexity: i64, + /// Sum of cyclomatic complexity. + pub total_complexity: i64, + /// Sum of cognitive complexity. + pub cognitive_complexity: i64, + /// Maximum per-function nesting depth. + pub nesting_depth: i64, + /// Sum of decision points. + pub decision_points: i64, + /// Health score (0..=100) from the average. + pub health_score: f64, + /// Message for the average. + pub message: &'static str, } -/// Distribution bucket label for a cyclomatic value. -fn classify_complexity_level(cyclomatic: i64) -> &'static str { - if cyclomatic <= CYCLOMATIC_THRESHOLD_MODERATE { - "simple" - } else if cyclomatic <= CYCLOMATIC_THRESHOLD_HIGH { - "moderate" +/// Computes the [`Aggregate`] scalars. +/// +/// ``` +/// use cf_complexity::report::{aggregate, FunctionInput}; +/// +/// let f = |name: &str, cc: i64, cog: i64, nest: i64| FunctionInput { +/// name: name.into(), source_file: "a.go".into(), language: "go".into(), +/// directory: ".".into(), cyclomatic_complexity: cc, cognitive_complexity: cog, +/// nesting_depth: nest, lines_of_code: 10, line: 1, +/// }; +/// let agg = aggregate(&[f("a", 3, 2, 1), f("b", 5, 7, 3)]); +/// assert_eq!(agg.total_functions, 2); +/// assert_eq!(agg.total_complexity, 8); +/// assert_eq!(agg.cognitive_complexity, 9); +/// assert_eq!(agg.nesting_depth, 3); +/// assert_eq!(agg.max_complexity, 5); +/// assert_eq!(agg.decision_points, 6); +/// assert!((agg.average_complexity - 4.0).abs() < f64::EPSILON); +/// ``` +#[must_use] +pub fn aggregate(functions: &[FunctionInput]) -> Aggregate { + let total_functions = functions.len() as i64; + let total_complexity: i64 = functions.iter().map(|f| f.cyclomatic_complexity).sum(); + let average_complexity = if total_functions > 0 { + total_complexity as f64 / total_functions as f64 } else { - "complex" + 0.0 + }; + Aggregate { + total_functions, + average_complexity, + max_complexity: functions + .iter() + .map(|f| f.cyclomatic_complexity) + .max() + .unwrap_or(0), + total_complexity, + cognitive_complexity: functions.iter().map(|f| f.cognitive_complexity).sum(), + nesting_depth: functions.iter().map(|f| f.nesting_depth).max().unwrap_or(0), + decision_points: functions.iter().map(FunctionInput::decision_points).sum(), + health_score: health_score(average_complexity), + message: complexity_message(average_complexity), } } -fn calculate_health_score(avg: f64) -> f64 { - if avg <= 1.0 { - 100.0 - } else if avg <= 3.0 { - 80.0 + (3.0 - avg) * 10.0 - } else if avg <= 7.0 { - 50.0 + (7.0 - avg) * 7.5 - } else { - f64::max(0.0, 50.0 - (avg - 7.0) * 5.0) - } +/// The report ordering: cyclomatic descending, cognitive descending, nesting +/// descending, then name, file and line ascending — a total order, so ties +/// are deterministic. +/// +/// ``` +/// use cf_complexity::report::{function_order, FunctionInput}; +/// use std::cmp::Ordering; +/// +/// let f = |name: &str, file: &str, cc: i64| FunctionInput { +/// name: name.into(), source_file: file.into(), language: "go".into(), +/// directory: ".".into(), cyclomatic_complexity: cc, cognitive_complexity: 0, +/// nesting_depth: 0, lines_of_code: 1, line: 1, +/// }; +/// assert_eq!(function_order(&f("a", "x.go", 5), &f("b", "x.go", 2)), Ordering::Less); +/// assert_eq!(function_order(&f("b", "x.go", 2), &f("a", "x.go", 2)), Ordering::Greater); +/// assert_eq!(function_order(&f("a", "a/x.go", 2), &f("a", "b/x.go", 2)), Ordering::Less); +/// ``` +#[must_use] +pub fn function_order(a: &FunctionInput, b: &FunctionInput) -> Ordering { + b.cyclomatic_complexity + .cmp(&a.cyclomatic_complexity) + .then(b.cognitive_complexity.cmp(&a.cognitive_complexity)) + .then(b.nesting_depth.cmp(&a.nesting_depth)) + .then_with(|| a.name.cmp(&b.name)) + .then_with(|| a.source_file.cmp(&b.source_file)) + .then(a.line.cmp(&b.line)) } -/// Inserts a string field only when non-empty (`omitempty` semantics for -/// strings). +/// Inserts a string field only when non-empty. fn insert_omitempty_str(m: &mut GoMap, key: &str, value: &str) { if !value.is_empty() { m.insert(key, GoValue::Str(value.to_string())); } } -/// A `function_complexity` row: a function with its density and risk level. -struct FcRow<'a> { - f: &'a FunctionInput, - density: f64, - risk: &'static str, -} - -/// A `high_risk_functions` row: a function with at least one issue. -struct HrRow<'a> { - f: &'a FunctionInput, - risk: &'static str, - issues: Vec<&'static str>, +fn stamp_fields(o: &mut GoMap, f: &FunctionInput) { + o.insert("name", GoValue::Str(f.name.clone())); + insert_omitempty_str(o, "source_file", &f.source_file); + insert_omitempty_str(o, "language", &f.language); + insert_omitempty_str(o, "directory", &f.directory); + o.insert( + "cyclomatic_complexity", + GoValue::Int(f.cyclomatic_complexity), + ); + o.insert("cognitive_complexity", GoValue::Int(f.cognitive_complexity)); } -/// Builds `function_complexity`: per-function with density + risk, sorted by -/// cyclomatic desc via the pinned pdqsort. +/// `function_complexity`: every function with density and risk, in +/// [`function_order`]. fn build_function_complexity(functions: &[FunctionInput]) -> Vec<GoValue> { - let mut fc: Vec<FcRow> = functions - .iter() + let mut sorted: Vec<&FunctionInput> = functions.iter().collect(); + sorted.sort_by(|a, b| function_order(a, b)); + sorted + .into_iter() .map(|f| { - let density = if f.lines_of_code > 0 { - f.cyclomatic_complexity as f64 / f.lines_of_code as f64 - } else { - 0.0 - }; - let risk = classify_function_risk( - f.cyclomatic_complexity, - f.cognitive_complexity, - f.nesting_depth, - ); - FcRow { f, density, risk } - }) - .collect(); - go_sort_slice(&mut fc, |a, b| { - a.f.cyclomatic_complexity > b.f.cyclomatic_complexity - }); - - fc.iter() - .map(|row| { let mut o = GoMap::new(MapOrigin::Struct); - o.insert("name", GoValue::Str(row.f.name.clone())); - insert_omitempty_str(&mut o, "source_file", &row.f.source_file); - insert_omitempty_str(&mut o, "language", &row.f.language); - insert_omitempty_str(&mut o, "directory", &row.f.directory); - o.insert( - "cyclomatic_complexity", - GoValue::Int(row.f.cyclomatic_complexity), - ); - o.insert( - "cognitive_complexity", - GoValue::Int(row.f.cognitive_complexity), - ); - o.insert("nesting_depth", GoValue::Int(row.f.nesting_depth)); - o.insert("lines_of_code", GoValue::Int(row.f.lines_of_code)); - o.insert("complexity_density", GoValue::Float(row.density)); - o.insert("risk_level", GoValue::Str(row.risk.to_string())); + stamp_fields(&mut o, f); + o.insert("nesting_depth", GoValue::Int(f.nesting_depth)); + o.insert("lines_of_code", GoValue::Int(f.lines_of_code)); + o.insert("complexity_density", GoValue::Float(f.density())); + o.insert("risk_level", GoValue::Str(f.risk().to_string())); GoValue::Map(o) }) .collect() } -/// Builds `distribution`: a string-keyed map (byte-sorted on encode) holding -/// only the buckets with a non-zero count. +/// `distribution`: the non-zero buckets (byte-sorted keys on encode). fn build_distribution(functions: &[FunctionInput]) -> GoMap { - let mut simple = 0i64; - let mut moderate = 0i64; - let mut complex = 0i64; - for f in functions { - match classify_complexity_level(f.cyclomatic_complexity) { - "simple" => simple += 1, - "moderate" => moderate += 1, - _ => complex += 1, - } - } let mut dist = GoMap::new(MapOrigin::Map); - if complex > 0 { - dist.insert("complex", GoValue::Int(complex)); - } - if moderate > 0 { - dist.insert("moderate", GoValue::Int(moderate)); - } - if simple > 0 { - dist.insert("simple", GoValue::Int(simple)); + for bucket in ["complex", "moderate", "simple"] { + let count = functions + .iter() + .filter(|f| distribution_bucket(f.cyclomatic_complexity) == bucket) + .count() as i64; + if count > 0 { + dist.insert(bucket, GoValue::Int(count)); + } } dist } -/// Builds `high_risk_functions`: functions with issues, sorted by risk -/// priority via the pinned pdqsort. +/// `high_risk_functions`: functions with at least one issue, by risk +/// priority then [`function_order`]. fn build_high_risk_functions(functions: &[FunctionInput]) -> Vec<GoValue> { - let mut hr: Vec<HrRow> = Vec::new(); - for f in functions { - let mut issues: Vec<&'static str> = Vec::new(); - if f.cyclomatic_complexity >= CYCLOMATIC_THRESHOLD_HIGH { - issues.push("High cyclomatic complexity"); - } - if f.cognitive_complexity >= COGNITIVE_THRESHOLD_HIGH { - issues.push("High cognitive complexity"); - } - if f.nesting_depth >= NESTING_THRESHOLD_HIGH { - issues.push("Deep nesting"); - } - if issues.is_empty() { - continue; - } - let risk = classify_function_risk( - f.cyclomatic_complexity, - f.cognitive_complexity, - f.nesting_depth, - ); - hr.push(HrRow { f, risk, issues }); - } - go_sort_slice(&mut hr, |a, b| { - risk_priority(a.risk) < risk_priority(b.risk) + let mut rows: Vec<(&FunctionInput, Vec<&'static str>)> = functions + .iter() + .map(|f| { + ( + f, + risk_issues( + f.cyclomatic_complexity, + f.cognitive_complexity, + f.nesting_depth, + ), + ) + }) + .filter(|(_, issues)| !issues.is_empty()) + .collect(); + rows.sort_by(|(a, _), (b, _)| { + risk_priority(a.risk()) + .cmp(&risk_priority(b.risk())) + .then_with(|| function_order(a, b)) }); - - hr.iter() - .map(|row| { + rows.into_iter() + .map(|(f, issues)| { let mut o = GoMap::new(MapOrigin::Struct); - o.insert("name", GoValue::Str(row.f.name.clone())); - insert_omitempty_str(&mut o, "source_file", &row.f.source_file); - insert_omitempty_str(&mut o, "language", &row.f.language); - insert_omitempty_str(&mut o, "directory", &row.f.directory); - o.insert( - "cyclomatic_complexity", - GoValue::Int(row.f.cyclomatic_complexity), - ); - o.insert( - "cognitive_complexity", - GoValue::Int(row.f.cognitive_complexity), - ); - o.insert("risk_level", GoValue::Str(row.risk.to_string())); + stamp_fields(&mut o, f); + o.insert("risk_level", GoValue::Str(f.risk().to_string())); o.insert( "issues", GoValue::Array( - row.issues - .iter() - .map(|s| GoValue::Str((*s).to_string())) + issues + .into_iter() + .map(|s| GoValue::Str(s.to_string())) .collect(), ), ); @@ -300,71 +290,48 @@ fn build_high_risk_functions(functions: &[FunctionInput]) -> Vec<GoValue> { .collect() } -/// Builds the `aggregate` struct value. -fn build_aggregate(scalars: &ReportScalars) -> GoMap { - let mut agg = GoMap::new(MapOrigin::Struct); - agg.insert("total_functions", GoValue::Int(scalars.total_functions)); - agg.insert( - "average_complexity", - GoValue::Float(scalars.average_complexity), +/// `aggregate`: the [`Aggregate`] scalars as a struct-origin map. +fn build_aggregate(agg: &Aggregate) -> GoMap { + let mut m = GoMap::new(MapOrigin::Struct); + m.insert("total_functions", GoValue::Int(agg.total_functions)); + m.insert("average_complexity", GoValue::Float(agg.average_complexity)); + m.insert("max_complexity", GoValue::Int(agg.max_complexity)); + m.insert("total_complexity", GoValue::Int(agg.total_complexity)); + m.insert( + "cognitive_complexity", + GoValue::Int(agg.cognitive_complexity), ); - agg.insert("max_complexity", GoValue::Int(scalars.max_complexity)); - agg.insert("total_complexity", GoValue::Int(scalars.total_complexity)); - // The aggregator's float-summed cognitive/nesting are dropped to 0 by the - // strict int assertion in scalar parsing (see ReportScalars docs). - agg.insert("cognitive_complexity", GoValue::Int(0)); - agg.insert("nesting_depth", GoValue::Int(0)); - agg.insert("decision_points", GoValue::Int(scalars.decision_points)); - agg.insert( - "health_score", - GoValue::Float(calculate_health_score(scalars.average_complexity)), - ); - agg.insert( - "message", - GoValue::Str(build_complexity_message(scalars.average_complexity).to_string()), - ); - agg + m.insert("nesting_depth", GoValue::Int(agg.nesting_depth)); + m.insert("decision_points", GoValue::Int(agg.decision_points)); + m.insert("health_score", GoValue::Float(agg.health_score)); + m.insert("message", GoValue::Str(agg.message.to_string())); + m } -/// Builds the full computed-metrics value tree (the bin/json payload root). -/// -/// `functions` is the aggregation-order list (walk order × per-file analyzer -/// order); the per-metric unstable sorts are applied here exactly as the -/// report contract pins them. -/// -/// The returned struct-origin map carries, in declaration order, -/// `function_complexity`, `distribution`, `high_risk_functions`, and -/// `aggregate`: +/// Builds the `ComputedMetrics` value tree (the yaml/bin payload root): +/// `function_complexity`, `distribution`, `high_risk_functions`, `aggregate` +/// in that declaration order. /// /// ``` -/// use cf_complexity::report::{computed_metrics, FunctionInput, ReportScalars}; +/// use cf_complexity::report::{computed_metrics, FunctionInput}; +/// use cf_gojson::GoValue; /// /// let funcs = vec![FunctionInput { -/// name: "foo".into(), -/// source_file: "a.go".into(), -/// language: "go".into(), -/// directory: ".".into(), -/// cyclomatic_complexity: 2, -/// cognitive_complexity: 1, -/// nesting_depth: 1, -/// lines_of_code: 10, +/// name: "foo".into(), source_file: "a.go".into(), language: "go".into(), +/// directory: ".".into(), cyclomatic_complexity: 2, cognitive_complexity: 1, +/// nesting_depth: 1, lines_of_code: 10, line: 3, /// }]; -/// let scalars = ReportScalars { -/// total_functions: 1, -/// average_complexity: 2.0, -/// max_complexity: 2, -/// total_complexity: 2, -/// decision_points: 1, -/// }; -/// -/// let value = computed_metrics(&funcs, &scalars); +/// let value = computed_metrics(&funcs); /// let m = value.as_map().unwrap(); /// for key in ["function_complexity", "distribution", "high_risk_functions", "aggregate"] { /// assert!(m.contains_key(key), "missing {key}"); /// } +/// let agg = m.get("aggregate").unwrap().as_map().unwrap(); +/// assert_eq!(agg.get("cognitive_complexity"), Some(&GoValue::Int(1))); +/// assert_eq!(agg.get("nesting_depth"), Some(&GoValue::Int(1))); /// ``` #[must_use] -pub fn computed_metrics(functions: &[FunctionInput], scalars: &ReportScalars) -> GoValue { +pub fn computed_metrics(functions: &[FunctionInput]) -> GoValue { let mut root = GoMap::new(MapOrigin::Struct); root.insert( "function_complexity", @@ -375,6 +342,134 @@ pub fn computed_metrics(functions: &[FunctionInput], scalars: &ReportScalars) -> "high_risk_functions", GoValue::Array(build_high_risk_functions(functions)), ); - root.insert("aggregate", GoValue::Map(build_aggregate(scalars))); + root.insert( + "aggregate", + GoValue::Map(build_aggregate(&aggregate(functions))), + ); GoValue::Map(root) } + +#[cfg(test)] +mod tests { + use super::{aggregate, computed_metrics, function_order, FunctionInput}; + use crate::thresholds::{MSG_EXCELLENT, RISK_CRITICAL, RISK_LOW}; + use cf_gojson::GoValue; + + fn f(name: &str, file: &str, cc: i64, cog: i64, nest: i64) -> FunctionInput { + FunctionInput { + name: name.into(), + source_file: file.into(), + language: "go".into(), + directory: ".".into(), + cyclomatic_complexity: cc, + cognitive_complexity: cog, + nesting_depth: nest, + lines_of_code: 4, + line: 1, + } + } + + fn arr<'a>(v: &'a GoValue, key: &str) -> &'a [GoValue] { + match v.as_map().unwrap().get(key) { + Some(GoValue::Array(a)) => a, + other => panic!("{key}: not an array: {other:?}"), + } + } + + fn name_of(v: &GoValue) -> &str { + v.as_map().unwrap().get("name").unwrap().as_str().unwrap() + } + + #[test] + fn empty_input_yields_zero_aggregate_and_empty_lists() { + let agg = aggregate(&[]); + assert_eq!(agg.total_functions, 0); + assert!(agg.average_complexity.abs() < f64::EPSILON); + assert_eq!(agg.message, MSG_EXCELLENT); + let v = computed_metrics(&[]); + assert!(arr(&v, "function_complexity").is_empty()); + assert!(arr(&v, "high_risk_functions").is_empty()); + assert!(v + .as_map() + .unwrap() + .get("distribution") + .unwrap() + .as_map() + .unwrap() + .is_empty()); + } + + #[test] + fn function_complexity_is_sorted_by_the_total_order() { + let input = vec![ + f("same", "b/x.go", 2, 1, 1), + f("same", "a/x.go", 2, 1, 1), + f("zed", "a/x.go", 2, 1, 1), + f("top", "a/x.go", 12, 20, 6), + ]; + let v = computed_metrics(&input); + let names: Vec<(&str, &str)> = arr(&v, "function_complexity") + .iter() + .map(|row| { + ( + name_of(row), + row.as_map() + .unwrap() + .get("source_file") + .unwrap() + .as_str() + .unwrap(), + ) + }) + .collect(); + assert_eq!( + names, + vec![ + ("top", "a/x.go"), + ("same", "a/x.go"), + ("same", "b/x.go"), + ("zed", "a/x.go") + ] + ); + let top = &arr(&v, "function_complexity")[0]; + assert_eq!( + top.as_map().unwrap().get("risk_level").unwrap().as_str(), + Some(RISK_CRITICAL) + ); + assert_eq!( + arr(&v, "function_complexity")[1] + .as_map() + .unwrap() + .get("risk_level") + .unwrap() + .as_str(), + Some(RISK_LOW) + ); + assert_eq!(arr(&v, "high_risk_functions").len(), 1); + assert_eq!(name_of(&arr(&v, "high_risk_functions")[0]), "top"); + } + + #[test] + fn ordering_is_total_and_antisymmetric() { + let a = f("a", "x.go", 3, 3, 1); + let b = f("a", "x.go", 3, 2, 1); + assert_eq!(function_order(&a, &b), std::cmp::Ordering::Less); + assert_eq!(function_order(&b, &a), std::cmp::Ordering::Greater); + assert_eq!(function_order(&a, &a), std::cmp::Ordering::Equal); + } + + #[test] + fn distribution_counts_only_non_empty_buckets() { + let v = computed_metrics(&[f("a", "x.go", 1, 0, 0), f("b", "x.go", 11, 0, 0)]); + let dist = v + .as_map() + .unwrap() + .get("distribution") + .unwrap() + .as_map() + .unwrap(); + assert_eq!(dist.get("simple"), Some(&GoValue::Int(1))); + assert_eq!(dist.get("complex"), Some(&GoValue::Int(1))); + assert!(dist.get("moderate").is_none()); + } +} diff --git a/crates/cf-complexity/src/thresholds.rs b/crates/cf-complexity/src/thresholds.rs new file mode 100644 index 00000000..01d26052 --- /dev/null +++ b/crates/cf-complexity/src/thresholds.rs @@ -0,0 +1,424 @@ +//! The single definition of every complexity threshold, label, and message. +//! +//! Every report format (the single-file analyzer value, the aggregated +//! `function_complexity` / `high_risk_functions` view, the JSON sections +//! report, and the terminal renderings) classifies through these functions, +//! so a number means the same thing everywhere. The values are documented in +//! `site/analyzers/complexity.md`. + +/// Cyclomatic complexity at or above which a function is "high" risk and is +/// listed in `high_risk_functions`. +pub const CYCLOMATIC_HIGH: i64 = 10; +/// Cyclomatic complexity at or above which a function is "moderate" risk. +pub const CYCLOMATIC_MODERATE: i64 = 5; +/// Cognitive complexity at or above which a function is "high" risk. +pub const COGNITIVE_HIGH: i64 = 15; +/// Cognitive complexity at or above which a function is "moderate" risk. +pub const COGNITIVE_MODERATE: i64 = 7; +/// Nesting depth at or above which a function is "high" risk. +pub const NESTING_HIGH: i64 = 5; +/// Nesting depth at or above which a function is "moderate" risk. +pub const NESTING_MODERATE: i64 = 3; + +/// Risk score (sum of the three dimension scores) for `CRITICAL`. +const RISK_SCORE_CRITICAL: i64 = 5; +/// Risk score for `HIGH`. +const RISK_SCORE_HIGH: i64 = 3; +/// Dimension score for crossing a "high" threshold. +const DIMENSION_SCORE_HIGH: i64 = 2; +/// Dimension score for crossing a "moderate" threshold. +const DIMENSION_SCORE_MODERATE: i64 = 1; + +/// Per-function assessment: cyclomatic `<= 1` is simple. +const ASSESS_CYCLOMATIC_SIMPLE: i64 = 1; +/// Per-function assessment: cyclomatic `<= 5` is moderate, above is complex. +const ASSESS_CYCLOMATIC_MODERATE: i64 = 5; +/// Per-function assessment: cognitive `<= 5` is low. +const ASSESS_COGNITIVE_LOW: i64 = 5; +/// Per-function assessment: cognitive `<= 10` is medium, above is high. +const ASSESS_COGNITIVE_MEDIUM: i64 = 10; +/// Per-function assessment: nesting `<= 3` is shallow. +const ASSESS_NESTING_SHALLOW: i64 = 3; +/// Per-function assessment: nesting `<= 5` is moderate, above is deep. +const ASSESS_NESTING_MODERATE: i64 = 5; + +/// Average-complexity band: `<= 1.0` is excellent. +const AVERAGE_EXCELLENT: f64 = 1.0; +/// Average-complexity band: `<= 3.0` is good. +const AVERAGE_GOOD: f64 = 3.0; +/// Average-complexity band: `<= 5.0` is fair (section score only). +const AVERAGE_FAIR: f64 = 5.0; +/// Average-complexity band: `<= 7.0` is moderate (fair message band). +const AVERAGE_MODERATE: f64 = 7.0; +/// Average-complexity band: `<= 10.0` is poor (section score only). +const AVERAGE_POOR: f64 = 10.0; + +/// Section scores (0..=1) per average-complexity band. +const SCORE_EXCELLENT: f64 = 1.0; +const SCORE_GOOD: f64 = 0.8; +const SCORE_FAIR: f64 = 0.6; +const SCORE_MODERATE: f64 = 0.4; +const SCORE_POOR: f64 = 0.2; +const SCORE_CRITICAL: f64 = 0.1; + +/// Health score (0..=100) anchors per average-complexity band. +const HEALTH_PERFECT: f64 = 100.0; +const HEALTH_GOOD_BASE: f64 = 80.0; +const HEALTH_GOOD_SLOPE: f64 = 10.0; +const HEALTH_MODERATE_BASE: f64 = 50.0; +const HEALTH_MODERATE_SLOPE: f64 = 7.5; +const HEALTH_HIGH_SLOPE: f64 = 5.0; + +/// Issue severity: cyclomatic `>= 11` is poor. +const SEVERITY_POOR_MIN: i64 = 11; +/// Issue severity: cyclomatic `>= 6` is fair, below is good. +const SEVERITY_FAIR_MIN: i64 = 6; + +/// Terminal distribution buckets (cyclomatic upper bounds). +const DIST_SIMPLE_MAX: i64 = 5; +const DIST_MODERATE_MAX: i64 = 10; +const DIST_COMPLEX_MAX: i64 = 20; + +/// The aggregate `message` texts, keyed by average cyclomatic complexity. +pub const MSG_EXCELLENT: &str = "Excellent complexity - functions are simple and maintainable"; +/// See [`MSG_EXCELLENT`]. +pub const MSG_GOOD: &str = "Good complexity - functions have reasonable complexity"; +/// See [`MSG_EXCELLENT`]. +pub const MSG_FAIR: &str = "Fair complexity - some functions could be simplified"; +/// See [`MSG_EXCELLENT`]. +pub const MSG_HIGH: &str = "High complexity - functions are complex and should be refactored"; + +/// Risk level names, in priority order. +pub const RISK_CRITICAL: &str = "CRITICAL"; +/// See [`RISK_CRITICAL`]. +pub const RISK_HIGH: &str = "HIGH"; +/// See [`RISK_CRITICAL`]. +pub const RISK_MEDIUM: &str = "MEDIUM"; +/// See [`RISK_CRITICAL`]. +pub const RISK_LOW: &str = "LOW"; + +/// Issue texts attached to `high_risk_functions` entries. +pub const ISSUE_HIGH_CYCLOMATIC: &str = "High cyclomatic complexity"; +/// See [`ISSUE_HIGH_CYCLOMATIC`]. +pub const ISSUE_HIGH_COGNITIVE: &str = "High cognitive complexity"; +/// See [`ISSUE_HIGH_CYCLOMATIC`]. +pub const ISSUE_DEEP_NESTING: &str = "Deep nesting"; + +/// Terminal distribution bucket labels. +pub const DIST_LABEL_SIMPLE: &str = "Simple (1-5)"; +/// See [`DIST_LABEL_SIMPLE`]. +pub const DIST_LABEL_MODERATE: &str = "Moderate (6-10)"; +/// See [`DIST_LABEL_SIMPLE`]. +pub const DIST_LABEL_COMPLEX: &str = "Complex (11-20)"; +/// See [`DIST_LABEL_SIMPLE`]. +pub const DIST_LABEL_VERY_COMPLEX: &str = "Very Complex (>20)"; + +/// Issue severity names. +pub const SEVERITY_GOOD: &str = "good"; +/// See [`SEVERITY_GOOD`]. +pub const SEVERITY_FAIR: &str = "fair"; +/// See [`SEVERITY_GOOD`]. +pub const SEVERITY_POOR: &str = "poor"; + +/// Cyclomatic-complexity assessment label (`<= 1` simple, `<= 5` moderate, +/// otherwise complex): +/// +/// ``` +/// use cf_complexity::thresholds::complexity_assessment; +/// assert_eq!(complexity_assessment(1), "🟢 Simple"); +/// assert_eq!(complexity_assessment(5), "🟡 Moderate"); +/// assert_eq!(complexity_assessment(6), "🔴 Complex"); +/// ``` +#[must_use] +pub fn complexity_assessment(cyclomatic: i64) -> &'static str { + if cyclomatic <= ASSESS_CYCLOMATIC_SIMPLE { + "🟢 Simple" + } else if cyclomatic <= ASSESS_CYCLOMATIC_MODERATE { + "🟡 Moderate" + } else { + "🔴 Complex" + } +} + +/// Cognitive-complexity assessment label (`<= 5` low, `<= 10` medium, +/// otherwise high): +/// +/// ``` +/// use cf_complexity::thresholds::cognitive_assessment; +/// assert_eq!(cognitive_assessment(5), "🟢 Low"); +/// assert_eq!(cognitive_assessment(10), "🟡 Medium"); +/// assert_eq!(cognitive_assessment(11), "🔴 High"); +/// ``` +#[must_use] +pub fn cognitive_assessment(cognitive: i64) -> &'static str { + if cognitive <= ASSESS_COGNITIVE_LOW { + "🟢 Low" + } else if cognitive <= ASSESS_COGNITIVE_MEDIUM { + "🟡 Medium" + } else { + "🔴 High" + } +} + +/// Nesting-depth assessment label (`<= 3` shallow, `<= 5` moderate, +/// otherwise deep): +/// +/// ``` +/// use cf_complexity::thresholds::nesting_assessment; +/// assert_eq!(nesting_assessment(3), "🟢 Shallow"); +/// assert_eq!(nesting_assessment(5), "🟡 Moderate"); +/// assert_eq!(nesting_assessment(6), "🔴 Deep"); +/// ``` +#[must_use] +pub fn nesting_assessment(depth: i64) -> &'static str { + if depth <= ASSESS_NESTING_SHALLOW { + "🟢 Shallow" + } else if depth <= ASSESS_NESTING_MODERATE { + "🟡 Moderate" + } else { + "🔴 Deep" + } +} + +/// The dimension score of one metric against its moderate/high thresholds. +fn dimension_score(value: i64, moderate: i64, high: i64) -> i64 { + if value >= high { + DIMENSION_SCORE_HIGH + } else if value >= moderate { + DIMENSION_SCORE_MODERATE + } else { + 0 + } +} + +/// Classifies a function's risk level from its three metrics. +/// +/// Each dimension scores 2 at or above its high threshold (cyclomatic 10, +/// cognitive 15, nesting 5), 1 at or above its moderate threshold (5, 7, 3), +/// else 0; the sum maps to `CRITICAL` (>= 5), `HIGH` (>= 3), `MEDIUM` (>= 1) +/// or `LOW`: +/// +/// ``` +/// use cf_complexity::thresholds::risk_level; +/// assert_eq!(risk_level(2, 1, 1), "LOW"); +/// assert_eq!(risk_level(8, 12, 3), "HIGH"); // 1 + 1 + 1 +/// assert_eq!(risk_level(15, 22, 5), "CRITICAL"); // 2 + 2 + 2 +/// assert_eq!(risk_level(5, 1, 1), "MEDIUM"); +/// ``` +#[must_use] +pub fn risk_level(cyclomatic: i64, cognitive: i64, nesting: i64) -> &'static str { + let score = dimension_score(cyclomatic, CYCLOMATIC_MODERATE, CYCLOMATIC_HIGH) + + dimension_score(cognitive, COGNITIVE_MODERATE, COGNITIVE_HIGH) + + dimension_score(nesting, NESTING_MODERATE, NESTING_HIGH); + if score >= RISK_SCORE_CRITICAL { + RISK_CRITICAL + } else if score >= RISK_SCORE_HIGH { + RISK_HIGH + } else if score >= DIMENSION_SCORE_MODERATE { + RISK_MEDIUM + } else { + RISK_LOW + } +} + +/// Sort priority of a risk level (`CRITICAL` first). +#[must_use] +pub fn risk_priority(level: &str) -> u8 { + match level { + RISK_CRITICAL => 0, + RISK_HIGH => 1, + RISK_MEDIUM => 2, + _ => 3, + } +} + +/// The issues a function raises for `high_risk_functions`; empty when it +/// crosses none of the high thresholds. +#[must_use] +pub fn risk_issues(cyclomatic: i64, cognitive: i64, nesting: i64) -> Vec<&'static str> { + let mut issues = Vec::new(); + if cyclomatic >= CYCLOMATIC_HIGH { + issues.push(ISSUE_HIGH_CYCLOMATIC); + } + if cognitive >= COGNITIVE_HIGH { + issues.push(ISSUE_HIGH_COGNITIVE); + } + if nesting >= NESTING_HIGH { + issues.push(ISSUE_DEEP_NESTING); + } + issues +} + +/// The three-bucket distribution key of the aggregated report +/// (`simple` <= 5, `moderate` <= 10, `complex` above). +#[must_use] +pub fn distribution_bucket(cyclomatic: i64) -> &'static str { + if cyclomatic <= CYCLOMATIC_MODERATE { + "simple" + } else if cyclomatic <= CYCLOMATIC_HIGH { + "moderate" + } else { + "complex" + } +} + +/// The four terminal distribution buckets, in display order. +pub const TERMINAL_BUCKETS: [&str; 4] = [ + DIST_LABEL_SIMPLE, + DIST_LABEL_MODERATE, + DIST_LABEL_COMPLEX, + DIST_LABEL_VERY_COMPLEX, +]; + +/// Index into [`TERMINAL_BUCKETS`] for a cyclomatic value. +#[must_use] +pub fn terminal_bucket(cyclomatic: i64) -> usize { + if cyclomatic <= DIST_SIMPLE_MAX { + 0 + } else if cyclomatic <= DIST_MODERATE_MAX { + 1 + } else if cyclomatic <= DIST_COMPLEX_MAX { + 2 + } else { + 3 + } +} + +/// Issue severity of a function row (`poor` >= 11, `fair` >= 6, else `good`). +#[must_use] +pub fn severity(cyclomatic: i64) -> &'static str { + if cyclomatic >= SEVERITY_POOR_MIN { + SEVERITY_POOR + } else if cyclomatic >= SEVERITY_FAIR_MIN { + SEVERITY_FAIR + } else { + SEVERITY_GOOD + } +} + +/// The aggregate message for an average cyclomatic complexity +/// (`<= 1` excellent, `<= 3` good, `<= 7` fair, else high): +/// +/// ``` +/// use cf_complexity::thresholds::{complexity_message, MSG_GOOD}; +/// assert_eq!(complexity_message(2.5), MSG_GOOD); +/// ``` +#[must_use] +pub fn complexity_message(average: f64) -> &'static str { + if average <= AVERAGE_EXCELLENT { + MSG_EXCELLENT + } else if average <= AVERAGE_GOOD { + MSG_GOOD + } else if average <= AVERAGE_MODERATE { + MSG_FAIR + } else { + MSG_HIGH + } +} + +/// The 0..=1 section score for an average cyclomatic complexity +/// (`<= 1` 1.0, `<= 3` 0.8, `<= 5` 0.6, `<= 7` 0.4, `<= 10` 0.2, else 0.1). +#[must_use] +pub fn section_score(average: f64) -> f64 { + if average <= AVERAGE_EXCELLENT { + SCORE_EXCELLENT + } else if average <= AVERAGE_GOOD { + SCORE_GOOD + } else if average <= AVERAGE_FAIR { + SCORE_FAIR + } else if average <= AVERAGE_MODERATE { + SCORE_MODERATE + } else if average <= AVERAGE_POOR { + SCORE_POOR + } else { + SCORE_CRITICAL + } +} + +/// The 0..=100 health score for an average cyclomatic complexity: 100 at or +/// below 1.0, a linear 80..100 band to 3.0, a linear 50..80 band to 7.0, then +/// 5 points off per unit above 7.0 (floored at 0). +#[must_use] +pub fn health_score(average: f64) -> f64 { + if average <= AVERAGE_EXCELLENT { + HEALTH_PERFECT + } else if average <= AVERAGE_GOOD { + HEALTH_GOOD_BASE + (AVERAGE_GOOD - average) * HEALTH_GOOD_SLOPE + } else if average <= AVERAGE_MODERATE { + HEALTH_MODERATE_BASE + (AVERAGE_MODERATE - average) * HEALTH_MODERATE_SLOPE + } else { + (HEALTH_MODERATE_BASE - (average - AVERAGE_MODERATE) * HEALTH_HIGH_SLOPE).max(0.0) + } +} + +#[cfg(test)] +mod tests { + use super::{ + distribution_bucket, health_score, risk_issues, risk_level, risk_priority, section_score, + severity, terminal_bucket, ISSUE_DEEP_NESTING, ISSUE_HIGH_COGNITIVE, ISSUE_HIGH_CYCLOMATIC, + RISK_CRITICAL, RISK_HIGH, RISK_LOW, RISK_MEDIUM, + }; + + #[test] + fn risk_levels_follow_the_documented_score_bands() { + assert_eq!(risk_level(1, 0, 0), RISK_LOW); + assert_eq!(risk_level(5, 0, 0), RISK_MEDIUM); + assert_eq!(risk_level(10, 0, 0), RISK_MEDIUM); + assert_eq!(risk_level(10, 7, 0), RISK_HIGH); + assert_eq!(risk_level(10, 15, 0), RISK_HIGH); + assert_eq!(risk_level(10, 15, 3), RISK_CRITICAL); + assert_eq!(risk_level(8, 12, 3), RISK_HIGH); + assert_eq!(risk_level(15, 22, 5), RISK_CRITICAL); + } + + #[test] + fn risk_priority_orders_critical_first() { + assert!(risk_priority(RISK_CRITICAL) < risk_priority(RISK_HIGH)); + assert!(risk_priority(RISK_HIGH) < risk_priority(RISK_MEDIUM)); + assert!(risk_priority(RISK_MEDIUM) < risk_priority(RISK_LOW)); + assert_eq!(risk_priority("unknown"), risk_priority(RISK_LOW)); + } + + #[test] + fn risk_issues_list_each_crossed_high_threshold() { + assert!(risk_issues(9, 14, 4).is_empty()); + assert_eq!(risk_issues(10, 0, 0), vec![ISSUE_HIGH_CYCLOMATIC]); + assert_eq!( + risk_issues(10, 15, 5), + vec![ + ISSUE_HIGH_CYCLOMATIC, + ISSUE_HIGH_COGNITIVE, + ISSUE_DEEP_NESTING + ] + ); + } + + #[test] + fn buckets_and_severity_share_the_cyclomatic_bands() { + assert_eq!(distribution_bucket(5), "simple"); + assert_eq!(distribution_bucket(6), "moderate"); + assert_eq!(distribution_bucket(11), "complex"); + assert_eq!(terminal_bucket(5), 0); + assert_eq!(terminal_bucket(10), 1); + assert_eq!(terminal_bucket(20), 2); + assert_eq!(terminal_bucket(21), 3); + assert_eq!(severity(5), "good"); + assert_eq!(severity(6), "fair"); + assert_eq!(severity(11), "poor"); + } + + #[test] + fn scores_are_monotone_in_the_average() { + let averages = [ + 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 12.0, 20.0, + ]; + for pair in averages.windows(2) { + assert!(section_score(pair[0]) >= section_score(pair[1])); + assert!(health_score(pair[0]) >= health_score(pair[1])); + } + assert!((health_score(1.0) - 100.0).abs() < f64::EPSILON); + assert!((health_score(3.0) - 80.0).abs() < f64::EPSILON); + assert!((health_score(7.0) - 50.0).abs() < f64::EPSILON); + assert!(health_score(100.0).abs() < f64::EPSILON); + } +} diff --git a/crates/cf-complexity/tests/fixtures.rs b/crates/cf-complexity/tests/fixtures.rs new file mode 100644 index 00000000..f7a4b6c0 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures.rs @@ -0,0 +1,350 @@ +//! Hand-computed metrics asserted end-to-end from SOURCE: each fixture file +//! under `tests/fixtures/` (shared with the CLI e2e suite) is parsed with the +//! real `cf_uast::Parser`, bridged, and measured. The +//! expected numbers follow the counting rules documented in +//! `site/explanation/complexity.md` (`McCabe` cyclomatic, `SonarSource` +//! cognitive, block nesting depth, non-blank non-comment lines of code). + +use cf_complexity::node::Node; +use cf_complexity::{AnalysisOptions, Analyzer, FunctionMetrics}; +use cf_uast::Parser; + +/// `(name, cyclomatic, cognitive, nesting, lines_of_code)`. +type Expected = (&'static str, i64, i64, i64, i64); + +fn measure(file: &str, source: &str) -> Vec<FunctionMetrics> { + let tree = Parser::new() + .parse(file, source.as_bytes()) + .unwrap_or_else(|e| panic!("parse {file}: {e:?}")); + let root = Node::from_uast(&tree); + Analyzer.function_metrics_with( + Some(&root), + &AnalysisOptions { + source: Some(source.as_bytes()), + find_depth: None, + }, + ) +} + +fn assert_functions(file: &str, source: &str, expected: &[Expected]) { + let got = measure(file, source); + let mut rows: Vec<(String, i64, i64, i64, i64)> = got + .iter() + .map(|m| { + ( + m.name.clone(), + m.cyclomatic_complexity, + m.cognitive_complexity, + m.nesting_depth, + m.lines_of_code, + ) + }) + .collect(); + rows.sort(); + let mut want: Vec<(String, i64, i64, i64, i64)> = expected + .iter() + .map(|(n, cc, cog, nest, loc)| ((*n).to_string(), *cc, *cog, *nest, *loc)) + .collect(); + want.sort(); + assert_eq!(rows, want, "{file}: (name, cc, cognitive, nesting, loc)"); + for m in &got { + assert_eq!( + m.decision_points, + m.cyclomatic_complexity - 1, + "{file}: decision points of {}", + m.name + ); + assert!(m.line > 0, "{file}: {} has no start line", m.name); + } +} + +const GO_FIXTURE: &str = include_str!("fixtures/cc.go"); + +#[test] +fn go_fixture() { + assert_functions( + "cc.go", + GO_FIXTURE, + &[ + ("decisions", 8, 7, 1, 22), + ("nested", 4, 6, 3, 10), + ("elseNest", 3, 4, 2, 10), + ("loopCond", 4, 3, 1, 7), + ("fact", 2, 2, 1, 6), + ("withClosure", 3, 3, 1, 12), + ("boolSeq", 4, 2, 0, 3), + ("manyCases", 5, 1, 1, 14), + ("simple", 1, 0, 0, 3), + ("Method", 2, 1, 1, 6), + ], + ); +} + +const GO_EXTRAS: &str = include_str!("fixtures/extras.go"); + +#[test] +fn go_extras_jumps_select_type_switch_closures_recursion_and_declarations() { + assert_functions( + "extras.go", + GO_EXTRAS, + &[ + // loops 1 + 2, ifs at nesting 2: 3 each, two labelled jumps. + ("labeled", 6, 14, 3, 18), + // select: two non-default cases; no switch node, so no cognitive. + ("sel", 3, 0, 0, 10), + ("typeSwitch", 3, 1, 1, 9), + // if inside a closure inside a closure: nesting 2 -> +3. + ("nestedClosure", 2, 3, 1, 11), + ("paren", 3, 2, 0, 3), + ("chain", 4, 4, 1, 11), + ("rec", 2, 2, 1, 6), + ("Rec", 2, 2, 1, 6), + ("commented", 1, 0, 0, 3), + ], + ); +} + +const PY_FIXTURE: &str = include_str!("fixtures/cc.py"); + +#[test] +fn python_fixture() { + assert_functions( + "cc.py", + PY_FIXTURE, + &[ + ("py_decisions", 8, 8, 1, 17), + ("py_nested", 4, 6, 3, 6), + ("method", 2, 1, 1, 4), + ], + ); +} + +const PY_EXTRAS: &str = include_str!("fixtures/extras.py"); + +#[test] +fn python_nested_def_is_its_own_function() { + assert_functions( + "extras.py", + PY_EXTRAS, + &[ + // if 1, elif 1, else 1, inner if at nesting 1: 2, `or` 1, while 1. + ("outer", 6, 7, 2, 11), + ("inner", 2, 1, 1, 4), + ], + ); +} + +const JS_FIXTURE: &str = include_str!("fixtures/cc.js"); + +#[test] +fn javascript_fixture() { + assert_functions( + "cc.js", + JS_FIXTURE, + &[ + ("jsDecisions", 11, 10, 1, 12), + ("jsNested", 4, 6, 3, 10), + ("arrow", 2, 1, 1, 1), + ], + ); +} + +const JS_EXTRAS: &str = include_str!("fixtures/extras.js"); + +#[test] +fn javascript_nested_functions_callbacks_and_labels() { + assert_functions( + "extras.js", + JS_EXTRAS, + &[ + // if 1, inner 2, else 1, inner 2, do 1, for 1, if 2, break label 1, + // ternary in the arrow callback at nesting 1: 2. + ("outer", 8, 13, 2, 11), + ("inner", 2, 1, 1, 4), + ("g", 2, 1, 0, 1), + ], + ); +} + +const RS_FIXTURE: &str = include_str!("fixtures/cc.rs"); + +#[test] +fn rust_fixture() { + assert_functions( + "cc.rs", + RS_FIXTURE, + &[ + ("rs_decisions", 9, 8, 1, 8), + ("rs_nested", 4, 6, 3, 10), + ("method", 2, 1, 1, 4), + ], + ); +} + +const RS_EXTRAS: &str = include_str!("fixtures/extras.rs"); + +#[test] +fn rust_nested_fn_closures_labels_and_trait_declarations() { + assert_functions( + "extras.rs", + RS_EXTRAS, + &[ + // closure if@1 2 + else 1, if-let 1, inner 2, else 1, inner 2, + // for 1, for 2, if 3, continue label 1, match 1. + ("outer", 10, 17, 3, 11), + ("inner", 2, 2, 1, 3), + ("d", 1, 0, 0, 1), + ], + ); +} + +const JAVA_FIXTURE: &str = include_str!("fixtures/Foo.java"); + +#[test] +fn java_nested_method() { + let got = measure("Foo.java", JAVA_FIXTURE); + let nested = got + .iter() + .find(|m| m.name == "nested") + .expect("nested method found"); + assert_eq!( + ( + nested.cyclomatic_complexity, + nested.cognitive_complexity, + nested.nesting_depth, + nested.lines_of_code + ), + (4, 6, 3, 10) + ); + assert_eq!(got.len(), 2, "two methods: {got:?}"); +} + +/// Java `switch` arms lower to `Case` nodes and the ternary to a +/// conditional `If` (the cf-uast-mappings shape contract), so every arm and +/// the `?:` count as decision points. +#[test] +fn java_decisions_method() { + assert_functions( + "Foo.java", + JAVA_FIXTURE, + &[("decisions", 11, 10, 1, 10), ("nested", 4, 6, 3, 10)], + ); +} + +const JAVA_EXTRAS: &str = include_str!("fixtures/Probe.java"); + +#[test] +fn java_abstract_and_interface_methods_are_not_functions() { + assert_functions( + "Probe.java", + JAVA_EXTRAS, + &[ + // lambda if@1 2, if 1, inner 2, else 1, inner 2, do 1, for 1, + // && 1, if@1 2, break label 1, if 1, sequence 2. + ("outer", 12, 17, 2, 12), + ], + ); +} + +const C_FIXTURE: &str = include_str!("fixtures/cc.c"); + +#[test] +fn c_fixture() { + assert_functions( + "cc.c", + C_FIXTURE, + &[ + // goto is a jump to a label: +1 cognitive. + ("c_decisions", 10, 10, 1, 11), + ("c_nested", 4, 6, 3, 10), + ], + ); +} + +const C_EXTRAS: &str = include_str!("fixtures/extras.c"); + +#[test] +fn c_braceless_branches_prototypes_and_negation() { + assert_functions( + "extras.c", + C_EXTRAS, + &[ + // if 1, inner 2, else 1, inner 2, if 1, braceless inner if 2, + // if 1, else-if 1, else 1, do 1, for 1, if@1 2, sequence 2. + ("outer", 13, 18, 2, 12), + ], + ); +} + +const IFINIT_FIXTURE: &str = include_str!("fixtures/i.go"); + +#[test] +fn go_if_with_init_statement() { + assert_functions( + "i.go", + IFINIT_FIXTURE, + &[ + ("initIf", 2, 1, 1, 6), + ("initIfNested", 3, 3, 2, 8), + ("errIf", 2, 1, 1, 6), + ("doIt", 1, 0, 0, 1), + ], + ); +} + +const DEFCASE_FIXTURE: &str = include_str!("fixtures/d.go"); + +#[test] +fn go_case_named_default_is_a_real_case() { + assert_functions( + "d.go", + DEFCASE_FIXTURE, + &[("caseNamedDefault", 4, 1, 1, 12)], + ); +} + +const GEN_FIXTURE: &str = include_str!("fixtures/gen.go"); + +#[test] +fn go_generics_func_types_and_closures() { + assert_functions( + "gen.go", + GEN_FIXTURE, + &[ + ("Push", 2, 1, 1, 5), + // `f func(T) U` is a parameter type, never a function. + ("Map", 2, 1, 1, 7), + // deferred/go closures: if@1 2, for@1 2. + ("outer", 3, 4, 1, 11), + ], + ); +} + +/// 20k nested `if`s through parse -> bridge -> measure. This crate's walks +/// grow their stack on demand (proven on the default test stack by the unit +/// tests); the parser's own tree, however, drops recursively, so the whole +/// pipeline runs on a thread sized for that drop. +#[test] +fn deep_nesting_is_measured_without_overflowing() { + const DEPTH: usize = 20_000; + const STACK_FOR_PARSER_TREE_DROP: usize = 256 * 1024 * 1024; + let mut src = String::from("package main\n\nfunc deep(x int) {\n"); + for _ in 0..DEPTH { + src.push_str("if true {\n"); + } + for _ in 0..DEPTH { + src.push_str("}\n"); + } + src.push_str("}\n"); + let got = std::thread::Builder::new() + .stack_size(STACK_FOR_PARSER_TREE_DROP) + .spawn(move || measure("deep.go", &src)) + .expect("spawn") + .join() + .expect("deep measurement thread"); + assert_eq!(got.len(), 1); + assert_eq!(got[0].name, "deep"); + let depth = i64::try_from(DEPTH).expect("depth fits i64"); + assert_eq!(got[0].cyclomatic_complexity, depth + 1); + assert_eq!(got[0].nesting_depth, depth); +} diff --git a/crates/cf-complexity/tests/fixtures/Foo.java b/crates/cf-complexity/tests/fixtures/Foo.java new file mode 100644 index 00000000..12c96e64 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/Foo.java @@ -0,0 +1,25 @@ +public class Foo { + // cyclomatic 11, cognitive 10 + public int decisions(int a, int b, int[] xs) { + int r = 0; + if (a > 0 && b > 0) { r++; } else if (a < 0 || b < 0) { r--; } else { r = 2; } + for (int x : xs) { r += x; } + while (r > 100) { r--; } + int t = a > 0 ? 1 : 2; + try { r = r / a; } catch (ArithmeticException e) { r = 0; } + switch (r) { case 1: r = 10; break; case 2: r = 20; break; default: r = 0; } + return r + t; + } + + // cyclomatic 4, cognitive 6, nesting 3 + public int nested(boolean a, boolean b, boolean c) { + if (a) { + if (b) { + if (c) { + return 1; + } + } + } + return 0; + } +} diff --git a/crates/cf-complexity/tests/fixtures/Probe.java b/crates/cf-complexity/tests/fixtures/Probe.java new file mode 100644 index 00000000..f21854b8 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/Probe.java @@ -0,0 +1,16 @@ +public abstract class Probe { + abstract int a(int x); + interface I { int i(int x); } + int outer(int a) { + Runnable r = () -> { if (a > 0) { System.out.println(a); } }; + if (a > 0) { + if (a > 1) { return 1; } + } else { + if (a < 0) { return -1; } + } + do { a--; } while (a > 0); + outer: for (int i = 0; i < 3 && a > 0; i++) { if (i == 2) { break outer; } } + if (a > 0 && a < 10 || a == 1) { return 1; } + return 0; + } +} diff --git a/crates/cf-complexity/tests/fixtures/cc.c b/crates/cf-complexity/tests/fixtures/cc.c new file mode 100644 index 00000000..9d28ed6c --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/cc.c @@ -0,0 +1,24 @@ +/* cyclomatic 10, cognitive 9 (+1 if goto counted) */ +int c_decisions(int a, int b, int *xs, int n) { + int r = 0; + if (a > 0 && b > 0) { r++; } else if (a < 0 || b < 0) { r--; } else { r = 2; } + for (int i = 0; i < n; i++) { r += xs[i]; } + while (r > 100) { r--; } + int t = a ? 1 : 2; + switch (r) { case 1: r = 10; break; case 2: r = 20; break; default: r = 0; } + goto done; +done: + return r + t; +} + +/* cyclomatic 4, cognitive 6, nesting 3 */ +int c_nested(int a, int b, int c) { + if (a) { + if (b) { + if (c) { + return 1; + } + } + } + return 0; +} diff --git a/crates/cf-complexity/tests/fixtures/cc.go b/crates/cf-complexity/tests/fixtures/cc.go new file mode 100644 index 00000000..662bf84c --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/cc.go @@ -0,0 +1,116 @@ +package main + +// cyclomatic 8, cognitive 7, nesting 1 +func decisions(a, b int, xs []int) int { + r := 0 + if a > 0 && b > 0 { + r++ + } else if a < 0 || b < 0 { + r-- + } else { + r = 2 + } + for _, x := range xs { + r += x + } + switch r { + case 1: + r = 10 + case 2: + r = 20 + default: + r = 0 + } + return r +} + +// cyclomatic 4, cognitive 6, nesting 3 +func nested(a, b, c bool) int { + if a { + if b { + if c { + return 1 + } + } + } + return 0 +} + +// cyclomatic 3, cognitive 4 (if+1, else+1, inner if nested => +2), nesting 2 +func elseNest(a, b bool) int { + if a { + return 1 + } else { + if b { + return 2 + } + } + return 0 +} + +// cyclomatic 4 (for, &&, ||), cognitive 3 (for +1, && seq +1, || change +1) +func loopCond(a, b bool, n int) int { + r := 0 + for i := 0; i < n && a || b; i++ { + r++ + } + return r +} + +// cyclomatic 2, cognitive 2 (if +1, recursion +1) +func fact(n int) int { + if n <= 1 { + return 1 + } + return n * fact(n-1) +} + +// cyclomatic 3 (if in closure + for), cognitive 3 (if in lambda @nesting1 => +2, for +1) +func withClosure(xs []int) int { + total := 0 + f := func(x int) { + if x > 0 { + total += x + } + } + for _, x := range xs { + f(x) + } + return total +} + +// cyclomatic 4 (&&,&&,||), cognitive 2 (&& seq +1, || change +1) +func boolSeq(a, b, c, d bool) bool { + return a && b && c || d +} + +// cyclomatic 5, cognitive 1, nesting 1 +func manyCases(n int) string { + switch n { + case 1: + return "a" + case 2: + return "b" + case 3: + return "c" + case 4: + return "d" + default: + return "z" + } +} + +// cyclomatic 1, cognitive 0, nesting 0 +func simple() int { + return 1 +} + +type T struct{} + +// method: cyclomatic 2, cognitive 1 +func (t *T) Method(a bool) int { + if a { + return 1 + } + return 0 +} diff --git a/crates/cf-complexity/tests/fixtures/cc.js b/crates/cf-complexity/tests/fixtures/cc.js new file mode 100644 index 00000000..c4bfa311 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/cc.js @@ -0,0 +1,28 @@ +// cyclomatic 11, cognitive 10 +function jsDecisions(a, b, xs) { + let r = 0; + if (a > 0 && b > 0) { r++; } + else if (a < 0 || b < 0) { r--; } + else { r = 2; } + for (const x of xs) { r += x; } + while (r > 100) { r--; } + const t = a ? 1 : 2; + try { r = r / a; } catch (e) { r = 0; } + switch (r) { case 1: r = 10; break; case 2: r = 20; break; default: r = 0; } + return r + t; +} + +// cyclomatic 4, cognitive 6, nesting 3 +function jsNested(a, b, c) { + if (a) { + if (b) { + if (c) { + return 1; + } + } + } + return 0; +} + +// arrow fn with if: cyclomatic 2 +const arrow = (x) => { if (x) { return 1; } return 0; }; diff --git a/crates/cf-complexity/tests/fixtures/cc.py b/crates/cf-complexity/tests/fixtures/cc.py new file mode 100644 index 00000000..c4019dbf --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/cc.py @@ -0,0 +1,35 @@ +# cyclomatic 8, cognitive 8 +def py_decisions(a, b, xs): + r = 0 + if a > 0 and b > 0: + r += 1 + elif a < 0 or b < 0: + r -= 1 + else: + r = 2 + for x in xs: + r += x + while r > 100: + r -= 1 + try: + r = r / a + except ZeroDivisionError: + r = 0 + return r + + +# cyclomatic 4, cognitive 6, nesting 3 +def py_nested(a, b, c): + if a: + if b: + if c: + return 1 + return 0 + + +class K: + # cyclomatic 2, cognitive 1 + def method(self, a): + if a: + return 1 + return 0 diff --git a/crates/cf-complexity/tests/fixtures/cc.rs b/crates/cf-complexity/tests/fixtures/cc.rs new file mode 100644 index 00000000..d3f17fb1 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/cc.rs @@ -0,0 +1,30 @@ +// cyclomatic 9 (if,&&,else-if,||,for,while,2 arms), cognitive 8 +fn rs_decisions(a: i32, b: i32, xs: &[i32]) -> i32 { + let mut r = 0; + if a > 0 && b > 0 { r += 1; } else if a < 0 || b < 0 { r -= 1; } else { r = 2; } + for x in xs { r += x; } + while r > 100 { r -= 1; } + match r { 1 => r = 10, 2 => r = 20, _ => r = 0 } + r +} + +// cyclomatic 4, cognitive 6, nesting 3 +fn rs_nested(a: bool, b: bool, c: bool) -> i32 { + if a { + if b { + if c { + return 1; + } + } + } + 0 +} + +struct S; +impl S { + // cyclomatic 2, cognitive 1 + fn method(&self, a: bool) -> i32 { + if a { return 1; } + 0 + } +} diff --git a/crates/cf-complexity/tests/fixtures/d.go b/crates/cf-complexity/tests/fixtures/d.go new file mode 100644 index 00000000..5b6142a0 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/d.go @@ -0,0 +1,15 @@ +package main + +// cyclomatic 4: 1 + case defaultValue + case 2 + case 3 (default excluded) +func caseNamedDefault(x, defaultValue int) int { + switch x { + case defaultValue: + return 1 + case 2: + return 2 + case 3: + return 3 + default: + return 0 + } +} diff --git a/crates/cf-complexity/tests/fixtures/extras.c b/crates/cf-complexity/tests/fixtures/extras.c new file mode 100644 index 00000000..6ec60a69 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/extras.c @@ -0,0 +1,14 @@ +int proto(int a); + +int outer(int a, int b) { + if (a) { + if (a > 1) { return 1; } + } else { + if (a < 0) { return -1; } + } + if (a) if (b) return 2; + if (a) b++; else if (b) a++; else a--; + do { a--; } while (a > 0); + for (;;) { if (a++ > 10) break; } + return (a || b) && !a; +} diff --git a/crates/cf-complexity/tests/fixtures/extras.go b/crates/cf-complexity/tests/fixtures/extras.go new file mode 100644 index 00000000..a3d5b4aa --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/extras.go @@ -0,0 +1,100 @@ +package main + +type Doer interface { + Do(x int) int +} + +type Fn func(int) int + +func labeled(xs []int) int { + r := 0 +outer: + for _, x := range xs { + for y := 0; y < x; y++ { + if y == 3 { + continue outer + } + if y == 5 { + break outer + } + if y == 7 { + break + } + } + } + return r +} + +func sel(a, b chan int) int { + select { + case v := <-a: + return v + case v := <-b: + return v + default: + return 0 + } +} + +func typeSwitch(v interface{}) int { + switch t := v.(type) { + case int: + return t + case string: + return len(t) + } + return 0 +} + +func nestedClosure() func() int { + return func() int { + f := func() int { + if true { + return 1 + } + return 0 + } + return f() + } +} + +func paren(a, b, c bool) bool { + return (a || b) && c +} + +func chain(a, b, c int) int { + if a > 0 { + return 1 + } else if b > 0 { + return 2 + } else if c > 0 { + return 3 + } else { + return 4 + } +} + +func rec(n int) int { + if n > 0 { + return rec(n - 1) + } + return 0 +} + +type S struct{} + +func (s *S) Rec(n int) int { + if n > 0 { + return s.Rec(n - 1) + } + return 0 +} + +// commented has a comment line and a blank line inside: loc counts neither. +func commented(a int) int { + // explain + + return a /* trailing */ +} + +func asmStub(a, b int) int diff --git a/crates/cf-complexity/tests/fixtures/extras.js b/crates/cf-complexity/tests/fixtures/extras.js new file mode 100644 index 00000000..b9c05e4d --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/extras.js @@ -0,0 +1,15 @@ +function outer(a) { + function inner(b) { + if (b) { return 1; } + return 0; + } + const g = function (x) { return x ? 1 : 2; }; + if (a) { + if (a > 1) { return inner(a); } + } else { + if (a < 0) { return g(a); } + } + do { a--; } while (a > 0); + loop: for (const k of [1]) { if (k) { break loop; } } + return [1].map(x => x > 0 ? 1 : 2).length; +} diff --git a/crates/cf-complexity/tests/fixtures/extras.py b/crates/cf-complexity/tests/fixtures/extras.py new file mode 100644 index 00000000..cd2d0949 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/extras.py @@ -0,0 +1,16 @@ +def outer(a, b): + def inner(c): + if c: + return 1 + return 0 + + if a: + pass + elif b: + pass + else: + if a or b: + pass + while a: + a -= 1 + return inner(a) diff --git a/crates/cf-complexity/tests/fixtures/extras.rs b/crates/cf-complexity/tests/fixtures/extras.rs new file mode 100644 index 00000000..9f13ae96 --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/extras.rs @@ -0,0 +1,16 @@ +fn outer(a: Option<i32>, b: bool) -> i32 { + fn inner(c: bool) -> i32 { + if c { 1 } else { 0 } + } + let g = |x: bool| if x { 1 } else { 2 }; + if let Some(v) = a { + if v > 1 { return inner(b); } + } else { + if b { return g(b); } + } + let mut n = 0; + 'outer: for i in 0..3 { for j in 0..3 { if i == j { continue 'outer; } } } + match n { x if x > 1 => 1, 2 | 3 => 2, _ => 0 } +} + +trait T { fn m(&self) -> i32; fn d(&self) -> i32 { 1 } } diff --git a/crates/cf-complexity/tests/fixtures/gen.go b/crates/cf-complexity/tests/fixtures/gen.go new file mode 100644 index 00000000..1c633eca --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/gen.go @@ -0,0 +1,36 @@ +package main + +type Stack[T any] struct{ items []T } + +func (s *Stack[T]) Push(v T) { + if v != nil { + s.items = append(s.items, v) + } +} + +func Map[T any, U any](xs []T, f func(T) U) []U { + out := make([]U, 0, len(xs)) + for _, x := range xs { + out = append(out, f(x)) + } + return out +} + +var pkgClosure = func(x int) int { + if x > 0 { + return 1 + } + return 0 +} + +func outer() { + defer func() { + if r := recover(); r != nil { + panic(r) + } + }() + go func() { + for { + } + }() +} diff --git a/crates/cf-complexity/tests/fixtures/i.go b/crates/cf-complexity/tests/fixtures/i.go new file mode 100644 index 00000000..c090864a --- /dev/null +++ b/crates/cf-complexity/tests/fixtures/i.go @@ -0,0 +1,29 @@ +package main + +// cyclomatic 2, cognitive 1, nesting 1 +func initIf(m map[string]int) int { + if v, ok := m["a"]; ok { + return v + } + return 0 +} + +// cyclomatic 3, cognitive 3 (if +1, inner if nested +2), nesting 2 +func initIfNested(m map[string]int) int { + if v, ok := m["a"]; ok { + if v > 1 { + return v + } + } + return 0 +} + +// cyclomatic 2, cognitive 1 +func errIf() error { + if err := doIt(); err != nil { + return err + } + return nil +} + +func doIt() error { return nil } diff --git a/crates/cf-composition/Cargo.toml b/crates/cf-composition/Cargo.toml index acfa40fd..ac9a298d 100644 --- a/crates/cf-composition/Cargo.toml +++ b/crates/cf-composition/Cargo.toml @@ -8,18 +8,10 @@ repository.workspace = true publish = false [dependencies] -# Report-format JSON serialization (byte-identity contract, pinned by -# tests/compat). NEVER serde_json for output. +# Report-format JSON values (GoValue / GoMap). Never serde_json for output. cf-gojson = { workspace = true } -# Report-format YAML serialization (same contract). NEVER serde_yaml for output. -cf-goyaml = { workspace = true } -# enry v2.1.0 language data (languages-by-extension) — drives the -# configuration-file predicate byte-for-byte instead of a hand-rolled -# extension list. +# enry v2.1.0 language data: drives the configuration / prose / source +# predicates from the real extension and filename tables. cf-langpath = { path = "../cf-langpath" } -# enry v2.1.0 vendor regex table (`enry.IsVendor` reproduction) — drives the -# vendor predicate byte-for-byte instead of a hand-rolled prefix subset. +# enry v2.1.0 vendor regex table (`enry.IsVendor` reproduction). cf-pathfilter = { path = "../cf-pathfilter" } - -[dev-dependencies] -serde_json = { workspace = true } diff --git a/crates/cf-composition/src/aggregator.rs b/crates/cf-composition/src/aggregator.rs index 9891e186..942e5428 100644 --- a/crates/cf-composition/src/aggregator.rs +++ b/crates/cf-composition/src/aggregator.rs @@ -1,111 +1,83 @@ -//! Composition aggregator. -//! -//! Accumulates per-file classification results into a breakdown / percentages / -//! total-files report. - -use std::collections::HashMap; +//! Composition aggregator: accumulates per-file classes into the +//! [`CompositionReport`] (total files + per-class counts) and its raw +//! `breakdown` / `percentages` / `total_files` report map. use cf_gojson::{GoMap, GoValue, MapOrigin}; -use crate::category::{CategoryCounts, ALL_CATEGORIES}; +use crate::category::{ClassCounts, FileClass, ALL_FILE_CLASSES}; -/// Report key for the per-category counts map. +/// Report key for the per-class counts map. pub const KEY_BREAKDOWN: &str = "breakdown"; -/// Report key for the per-category percentages map. +/// Report key for the per-class percentages map. pub const KEY_PERCENTAGE: &str = "percentages"; /// Report key for the total file count. pub const KEY_TOTAL_FILES: &str = "total_files"; -/// Report key holding the per-file category string. -pub const KEY_CATEGORY: &str = "category"; /// 100.0, used to convert a fraction to a percentage. const PERCENT_MULTIPLIER: f64 = 100.0; -/// Aggregates file composition results across multiple files. -/// -/// Per-file report retention for HTML rendering is a framework concern and is -/// not observable in the machine-format report this crate is responsible for, -/// so it is intentionally omitted here. -#[derive(Debug, Default)] -pub struct Aggregator { - counts: CategoryCounts, - total_files: i64, +/// The aggregated composition of a file tree. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CompositionReport { + /// Total files classified. + pub total_files: i64, + /// Per-class counts. + pub counts: ClassCounts, } -impl Aggregator { - /// Creates a new composition aggregator. +impl CompositionReport { + /// Files of `class`. #[must_use] - pub fn new() -> Self { - Self::default() + pub fn count(&self, class: FileClass) -> i64 { + self.counts.get(class) } - /// Accumulates per-file classification results. - /// - /// `results` maps analyzer name -> single-file report whose `category` key - /// holds the classification string. Every report increments `total_files`; - /// a present-and-string `category` additionally increments the - /// per-category counter. A missing or non-string `category` is skipped - /// (the file is still counted). - pub fn aggregate(&mut self, results: &HashMap<String, HashMap<String, GoValue>>) { - for report in results.values() { - self.total_files += 1; - - if let Some(GoValue::Str(cat)) = report.get(KEY_CATEGORY) { - self.counts.increment(cat); - } + /// Share of `class` as a fraction in `[0, 1]` (`0.0` with no files). + #[must_use] + #[allow(clippy::cast_precision_loss)] // file counts are far below 2^52 + pub fn fraction(&self, class: FileClass) -> f64 { + if self.total_files == 0 { + return 0.0; } + self.count(class) as f64 / self.total_files as f64 } - /// Convenience entry point used in tests: aggregate a single category by - /// its wire string. - pub fn aggregate_category(&mut self, category: &str) { - self.total_files += 1; - self.counts.increment(category); + /// Share of `class` as a percentage in `[0, 100]`. + #[must_use] + pub fn percent(&self, class: FileClass) -> f64 { + self.fraction(class) * PERCENT_MULTIPLIER } - /// Builds the aggregated composition report. - /// - /// The returned [`GoMap`] is a *map-origin* container, so `cf-gojson` sorts - /// its keys by raw UTF-8 bytes at encode time (`breakdown`, `percentages`, - /// `total_files`), per the report contract for dynamic maps. The nested - /// `breakdown` / `percentages` maps are likewise map-origin and - /// byte-sorted. - /// - /// Percentages are only emitted when `total_files > 0` (an empty repo - /// yields an empty `percentages` map — pinned report behaviour). + /// Builds the raw aggregated report: a map-origin [`GoMap`] (keys + /// byte-sort on encode) with `breakdown` (every class, zero counts + /// included), `percentages` (`count / total * 100`, empty when no file + /// was classified) and `total_files`. /// /// ``` - /// use cf_composition::Aggregator; + /// use cf_composition::{Aggregator, Category, FileClass}; /// use cf_gojson::GoValue; /// /// let mut agg = Aggregator::new(); - /// agg.aggregate_category("source"); - /// agg.aggregate_category("source"); - /// agg.aggregate_category("vendor"); + /// agg.add(Category::Source.into()); + /// agg.add(Category::Source.into()); + /// agg.add(FileClass::Other); /// - /// let result = agg.get_result(); - /// assert_eq!(result.get("total_files"), Some(&GoValue::Int(3))); - /// let breakdown = result.get("breakdown").unwrap().as_map().unwrap(); + /// let raw = agg.report().to_raw_report(); + /// assert_eq!(raw.get("total_files"), Some(&GoValue::Int(3))); + /// let breakdown = raw.get("breakdown").unwrap().as_map().unwrap(); /// assert_eq!(breakdown.get("source"), Some(&GoValue::Int(2))); - /// assert_eq!(breakdown.get("vendor"), Some(&GoValue::Int(1))); + /// assert_eq!(breakdown.get("other"), Some(&GoValue::Int(1))); /// ``` #[must_use] - pub fn get_result(&self) -> GoMap { - // All three containers are dynamic maps, so they are map-origin and - // byte-sort their keys at encode time. + pub fn to_raw_report(&self) -> GoMap { let mut breakdown = GoMap::new(MapOrigin::Map); let mut percentages = GoMap::new(MapOrigin::Map); - - for cat in ALL_CATEGORIES { - let count = self.counts.get(cat); - breakdown.push(cat.as_str(), GoValue::Int(count)); - + for class in ALL_FILE_CLASSES { + breakdown.push(class.as_str(), GoValue::Int(self.count(class))); if self.total_files > 0 { - let pct = (count as f64) / (self.total_files as f64) * PERCENT_MULTIPLIER; - percentages.push(cat.as_str(), GoValue::Float(pct)); + percentages.push(class.as_str(), GoValue::Float(self.percent(class))); } } - let mut report = GoMap::new(MapOrigin::Map); report.push(KEY_BREAKDOWN, GoValue::Map(breakdown)); report.push(KEY_PERCENTAGE, GoValue::Map(percentages)); @@ -113,3 +85,67 @@ impl Aggregator { report } } + +/// Aggregates file classes across a file tree. +#[derive(Debug, Default, Clone, Copy)] +pub struct Aggregator { + report: CompositionReport, +} + +impl Aggregator { + /// Creates an empty aggregator. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Counts one file of `class`. + pub fn add(&mut self, class: FileClass) { + self.report.total_files += 1; + self.report.counts.increment(class); + } + + /// The aggregated report so far. + #[must_use] + pub fn report(&self) -> CompositionReport { + self.report + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::category::Category; + use cf_gojson::Encoder; + + #[test] + fn empty_report_has_no_percentages() { + let raw = Aggregator::new().report().to_raw_report(); + let json = Encoder::marshal().encode_to_string(&GoValue::Map(raw)); + assert_eq!( + json, + r#"{"breakdown":{"binary":0,"configuration":0,"documentation":0,"dotfile":0,"generated":0,"image":0,"other":0,"source":0,"vendor":0},"percentages":{},"total_files":0}"# + ); + } + + #[test] + fn counts_fractions_and_percentages() { + let mut agg = Aggregator::new(); + for class in [ + Category::Source.into(), + Category::Source.into(), + Category::Source.into(), + Category::Vendor.into(), + FileClass::Other, + ] { + agg.add(class); + } + let r = agg.report(); + assert_eq!(r.total_files, 5); + assert_eq!(r.count(Category::Source.into()), 3); + assert!((r.fraction(Category::Vendor.into()) - 0.2).abs() < 1e-12); + assert!((r.percent(FileClass::Other) - 20.0).abs() < 1e-12); + let sum: f64 = ALL_FILE_CLASSES.iter().map(|c| r.percent(*c)).sum(); + assert!((sum - 100.0).abs() < 1e-9, "percentages sum to {sum}"); + } +} diff --git a/crates/cf-composition/src/analyzer.rs b/crates/cf-composition/src/analyzer.rs deleted file mode 100644 index 4fcbdcc8..00000000 --- a/crates/cf-composition/src/analyzer.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Composition analyzer. -//! -//! Implements the `static/composition` raw-file analyzer: it classifies each -//! file via [`Classifier`] and renders aggregated reports in every machine -//! format. -//! -//! # Compatibility -//! -//! All report serialization routes through `cf-gojson` / `cf-goyaml`, never -//! serde; the bytes are pinned against the reference binary by -//! `tests/compat`: -//! -//! * `json` / `text` / `plot` — `Encoder::indented(" ")` (two-space indent, -//! HTML escaping ON, trailing newline). -//! * `bin` — the CFB1 envelope (`b"CFB1"` + LE u32 length + compact JSON -//! payload, no trailing newline). -//! * `yaml` — `cf-goyaml` (4-space-indent YAML with byte-sorted map keys). - -use std::collections::HashMap; -use std::io::{self, Write}; - -use cf_gojson::{Encoder, GoMap, GoValue, MapOrigin}; - -use crate::aggregator::{Aggregator, KEY_CATEGORY}; -use crate::classifier::Classifier; -use crate::report_section::{CompositionReport, ReportSection}; - -/// Analyzer name. -pub const ANALYZER_NAME: &str = "composition"; -/// CLI flag name. -pub const ANALYZER_FLAG: &str = "composition"; -/// Fully-qualified analyzer ID. -pub const ANALYZER_ID: &str = "static/composition"; -/// Human description. -pub const ANALYZER_DESCRIPTION: &str = - "Classifies files by type (source, vendor, generated, docs, config, binary, image) using enry."; - -/// CFB1 magic prefix for the binary envelope. -const BINARY_MAGIC: &[u8; 4] = b"CFB1"; - -/// Implements the raw-file composition analyzer. -#[derive(Debug, Default, Clone)] -pub struct Analyzer { - classifier: Classifier, -} - -impl Analyzer { - /// Creates a new composition analyzer. - #[must_use] - pub fn new() -> Self { - Self { - classifier: Classifier::new(), - } - } - - /// Returns the analyzer name. - #[must_use] - pub fn name(&self) -> &'static str { - ANALYZER_NAME - } - - /// Returns the CLI flag name. - #[must_use] - pub fn flag(&self) -> &'static str { - ANALYZER_FLAG - } - - /// Returns the analyzer ID (`static/composition`). - #[must_use] - pub fn id(&self) -> &'static str { - ANALYZER_ID - } - - /// Returns the analyzer description. - #[must_use] - pub fn description(&self) -> &'static str { - ANALYZER_DESCRIPTION - } - - /// Returns the metric thresholds. Composition is informational, so there - /// are none. - #[must_use] - pub fn thresholds(&self) -> Option<()> { - None - } - - /// Returns the available configuration options. Composition has none. - #[must_use] - pub fn list_configuration_options(&self) -> Vec<()> { - Vec::new() - } - - /// Applies configuration facts. Composition takes none and never errors. - /// - /// # Errors - /// Never fails; the `Result` keeps the analyzer-interface signature. - #[allow(clippy::unused_self)] - pub fn configure(&self, _facts: &HashMap<String, GoValue>) -> Result<(), CompositionError> { - Ok(()) - } - - /// Creates a fresh aggregator. - #[must_use] - pub fn create_aggregator(&self) -> Aggregator { - Aggregator::new() - } - - /// Classifies a single file by path and content, returning a single-key - /// report mapping `category` to the classification string. - #[must_use] - pub fn analyze_file_content(&self, path: &str, content: &[u8]) -> GoMap { - let category = self.classifier.classify(path, content); - // Per-file reports are dynamic string-keyed maps, hence map-origin. - let mut report = GoMap::new(MapOrigin::Map); - report.push(KEY_CATEGORY, GoValue::Str(category.as_str().to_string())); - report - } - - /// Creates a report section from a decoded composition report. - #[must_use] - pub fn create_report_section(&self, report: CompositionReport) -> ReportSection { - ReportSection::new(report) - } - - /// Writes human-readable text output (same bytes as the JSON format). - /// - /// # Errors - /// Propagates writer I/O errors. - pub fn format_report(&self, report: &GoMap, writer: &mut dyn Write) -> io::Result<()> { - encode_json(report, writer) - } - - /// Writes indented JSON output. - /// - /// # Errors - /// Propagates writer I/O errors. - pub fn format_report_json(&self, report: &GoMap, writer: &mut dyn Write) -> io::Result<()> { - encode_json(report, writer) - } - - /// Writes plot output (same bytes as the JSON format). - /// - /// # Errors - /// Propagates writer I/O errors. - pub fn format_report_plot(&self, report: &GoMap, writer: &mut dyn Write) -> io::Result<()> { - self.format_report_json(report, writer) - } - - /// Writes the CFB1 binary envelope: `b"CFB1"` + payload length as - /// little-endian `u32` + compact JSON payload (HTML escaping ON, **no** - /// trailing newline). - /// - /// # Errors - /// Propagates writer I/O errors. - pub fn format_report_binary(&self, report: &GoMap, writer: &mut dyn Write) -> io::Result<()> { - // `Encoder::compact()`: compact, HTML-escape ON, no trailing newline — - // exactly the CFB1 payload encoding. - let payload = Encoder::compact().encode_to_vec(&GoValue::Map(report.clone())); - - let len = u32::try_from(payload.len()).unwrap_or(u32::MAX); - writer.write_all(BINARY_MAGIC)?; - writer.write_all(&len.to_le_bytes())?; - writer.write_all(&payload)?; - Ok(()) - } - - /// Writes YAML output by delegating to `cf-goyaml::marshal` (default - /// 4-space indent). The report is a map-origin [`GoMap`], so its keys - /// (`breakdown`, `percentages`, `total_files`) and the nested category maps - /// are byte-sorted, per the report contract for dynamic maps. - /// - /// # Errors - /// Propagates writer I/O errors. - pub fn format_report_yaml(&self, report: &GoMap, writer: &mut dyn Write) -> io::Result<()> { - let bytes = cf_goyaml::marshal(&GoValue::Map(report.clone())); - writer.write_all(&bytes) - } -} - -/// Encodes a report as indented JSON per the report contract: two-space -/// indent, HTML escaping ON, byte-sorted map keys, shortest-round-trip float -/// rendering, and a single trailing newline. -fn encode_json(report: &GoMap, writer: &mut dyn Write) -> io::Result<()> { - // The streaming-encoder report contract appends a single trailing newline. - // `Encoder::indented` defaults trailing-newline OFF, so it must be turned - // on explicitly here. - let bytes = Encoder::indented(" ") - .with_trailing_newline(true) - .encode_to_vec(&GoValue::Map(report.clone())); - writer.write_all(&bytes) -} - -/// Error type for the (currently infallible) configuration path. -#[derive(Debug)] -pub struct CompositionError(pub String); - -impl std::fmt::Display for CompositionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for CompositionError {} diff --git a/crates/cf-composition/src/category.rs b/crates/cf-composition/src/category.rs index 2ab6e053..736a0450 100644 --- a/crates/cf-composition/src/category.rs +++ b/crates/cf-composition/src/category.rs @@ -1,11 +1,8 @@ //! File classification categories. //! -//! The `Category` constants and canonical ordering shared by composition and -//! file-history analysis. This crate hosts the minimal surface it depends on; -//! when the file-history crate is fully built out, these can move there and be -//! re-exported. - -use std::collections::HashMap; +//! [`Category`] is the 8-way classification shared with file-history +//! analysis; [`FileClass`] is what the composition analyzer reports — every +//! `Category` plus [`FileClass::Other`] for files that match no rule. /// A file classification type. /// @@ -47,79 +44,156 @@ impl Category { } } - /// Parses a wire string back into a [`Category`]. - /// - /// Returns `None` for any string that is not a known category. Round-trips - /// with [`Category::as_str`]: + /// Returns the capitalized display name (`Source`, `Dotfile`, …). + #[must_use] + pub const fn display_name(self) -> &'static str { + match self { + Category::Source => "Source", + Category::Vendor => "Vendor", + Category::Generated => "Generated", + Category::Documentation => "Documentation", + Category::Configuration => "Configuration", + Category::Image => "Image", + Category::DotFile => "Dotfile", + Category::Binary => "Binary", + } + } +} + +/// A composition bucket: one of the shared [`Category`] values, or `Other` +/// for a file that matches no classification rule (unknown extension, no +/// recognizable language). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FileClass { + /// One of the shared categories. + Category(Category), + /// Matches no rule. + Other, +} + +/// Wire string of [`FileClass::Other`]. +pub const OTHER_STR: &str = "other"; +/// Display name of [`FileClass::Other`]. +const OTHER_DISPLAY: &str = "Other"; + +impl FileClass { + /// Returns the wire string (`"source"`, …, `"other"`). /// /// ``` - /// use cf_composition::Category; - /// assert_eq!(Category::from_str("source"), Some(Category::Source)); - /// assert_eq!(Category::from_str(Category::Binary.as_str()), Some(Category::Binary)); - /// assert_eq!(Category::from_str("nope"), None); + /// use cf_composition::{Category, FileClass}; + /// assert_eq!(FileClass::Category(Category::Vendor).as_str(), "vendor"); + /// assert_eq!(FileClass::Other.as_str(), "other"); /// ``` #[must_use] - // The lookup is fallible-by-`Option` (not `Result`), so the inherent - // method intentionally does not implement `std::str::FromStr`. - #[allow(clippy::should_implement_trait)] - pub fn from_str(value: &str) -> Option<Category> { - match value { - "source" => Some(Category::Source), - "vendor" => Some(Category::Vendor), - "generated" => Some(Category::Generated), - "documentation" => Some(Category::Documentation), - "configuration" => Some(Category::Configuration), - "image" => Some(Category::Image), - "dotfile" => Some(Category::DotFile), - "binary" => Some(Category::Binary), - _ => None, + pub const fn as_str(self) -> &'static str { + match self { + FileClass::Category(c) => c.as_str(), + FileClass::Other => OTHER_STR, + } + } + + /// Returns the capitalized display name (`Vendor`, `Other`, …). + #[must_use] + pub const fn display_name(self) -> &'static str { + match self { + FileClass::Category(c) => c.display_name(), + FileClass::Other => OTHER_DISPLAY, } } + + /// Position of this class in [`ALL_FILE_CLASSES`]. + #[must_use] + pub fn index(self) -> usize { + ALL_FILE_CLASSES + .iter() + .position(|c| *c == self) + .expect("every FileClass is listed in ALL_FILE_CLASSES") + } +} + +impl From<Category> for FileClass { + fn from(category: Category) -> Self { + FileClass::Category(category) + } } -/// The canonical category order for display and charting: -/// source, documentation, configuration, vendor, generated, dotfile, image, -/// binary. This ordering is load-bearing — it governs the order of distribution -/// items and issues in the report section, and the iteration order in -/// [`crate::aggregator::Aggregator::get_result`]. -pub const ALL_CATEGORIES: [Category; 8] = [ - Category::Source, - Category::Documentation, - Category::Configuration, - Category::Vendor, - Category::Generated, - Category::DotFile, - Category::Image, - Category::Binary, +/// The canonical class order for reports and charts: source, documentation, +/// configuration, vendor, generated, dotfile, image, binary, other. It +/// governs the order of distribution items and issues in the report section +/// and the iteration order of the aggregated report. +pub const ALL_FILE_CLASSES: [FileClass; 9] = [ + FileClass::Category(Category::Source), + FileClass::Category(Category::Documentation), + FileClass::Category(Category::Configuration), + FileClass::Category(Category::Vendor), + FileClass::Category(Category::Generated), + FileClass::Category(Category::DotFile), + FileClass::Category(Category::Image), + FileClass::Category(Category::Binary), + FileClass::Other, ]; -/// Tracks counts per file category. -/// -/// Unknown category strings (those not in [`ALL_CATEGORIES`]) are still counted -/// under their raw string key so the "skips invalid category" behaviour holds: -/// the count is recorded but never surfaces in -/// [`crate::aggregator::Aggregator::get_result`] because the result only -/// iterates the known categories. -#[derive(Debug, Default, Clone)] -pub struct CategoryCounts { - counts: HashMap<String, i64>, +/// Per-class file counts. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct ClassCounts { + counts: [i64; ALL_FILE_CLASSES.len()], } -impl CategoryCounts { +impl ClassCounts { /// Creates an empty counter. #[must_use] pub fn new() -> Self { Self::default() } - /// Increases the count for a category string by one. - pub fn increment(&mut self, category: &str) { - *self.counts.entry(category.to_string()).or_insert(0) += 1; + /// Increases the count for `class` by one. + pub fn increment(&mut self, class: FileClass) { + self.counts[class.index()] += 1; + } + + /// Returns the count for `class` (zero if never incremented). + #[must_use] + pub fn get(&self, class: FileClass) -> i64 { + self.counts[class.index()] } - /// Returns the count for a category (zero if never incremented). + /// Sum over every class. #[must_use] - pub fn get(&self, category: Category) -> i64 { - self.counts.get(category.as_str()).copied().unwrap_or(0) + pub fn total(&self) -> i64 { + self.counts.iter().sum() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wire_strings_and_display_names() { + assert_eq!(Category::DotFile.as_str(), "dotfile"); + assert_eq!(Category::DotFile.display_name(), "Dotfile"); + assert_eq!(FileClass::Other.display_name(), "Other"); + assert_eq!(FileClass::from(Category::Image).as_str(), "image"); + } + + #[test] + fn every_class_is_listed_once() { + for (i, class) in ALL_FILE_CLASSES.iter().enumerate() { + assert_eq!(class.index(), i); + } + assert_eq!(ALL_FILE_CLASSES.len(), 9); + assert_eq!(ALL_FILE_CLASSES[8], FileClass::Other); + } + + #[test] + fn counts_increment_and_total() { + let mut c = ClassCounts::new(); + c.increment(FileClass::Other); + c.increment(FileClass::Other); + c.increment(Category::Source.into()); + assert_eq!(c.get(FileClass::Other), 2); + assert_eq!(c.get(Category::Source.into()), 1); + assert_eq!(c.get(Category::Binary.into()), 0); + assert_eq!(c.total(), 3); } } diff --git a/crates/cf-composition/src/classifier.rs b/crates/cf-composition/src/classifier.rs index cb469816..386a0489 100644 --- a/crates/cf-composition/src/classifier.rs +++ b/crates/cf-composition/src/classifier.rs @@ -1,44 +1,72 @@ //! File classifier. //! -//! [`Classifier::classify`] is a fixed-priority cascade over enry-style +//! [`Classifier::classify_file`] is a fixed-priority cascade over enry-style //! predicates plus generated-file checks: //! //! ```text //! // first match wins: -//! if content is non-empty and looks binary: Binary -//! if the path has an image extension: Image -//! if the path matches a vendoring convention: Vendor -//! if the path matches a generated-file rule: Generated -//! if non-empty content carries a generated marker: Generated -//! if the path matches a documentation convention: Documentation -//! if the extension maps to a config language: Configuration -//! if the base name is a dotfile: DotFile -//! else: Source +//! if content is non-empty and contains a NUL byte: Binary +//! if the path has an image extension: Image +//! if the path matches a vendoring convention: Vendor +//! if the path matches a generated-file rule: Generated +//! if non-empty content carries a generated marker: Generated +//! if the path matches a documentation convention +//! or its extension is a prose one (.md, .rst, …): Documentation +//! if the extension maps to a configuration language: Configuration +//! if the base name is a dotfile: DotFile +//! if enry recognizes a language (name, extension, +//! shebang or modeline): Source +//! else: Other //! ``` //! -//! The branch ORDER is load-bearing (a file matching several predicates is -//! classified by the first matching branch) and is part of the pinned -//! classification behaviour — preserve the exact sequence. +//! The branch ORDER is load-bearing: a file matching several predicates is +//! classified by the first matching branch. Paths must be **relative to the +//! analyzed root** — the documentation and vendor matchers anchor on path +//! prefixes such as `docs/`. //! -//! # Data parity -//! -//! The `is_vendor` / `is_documentation` predicates are driven by generated -//! regex tables from `src-d/enry` v2.1.0 (extracted from github/linguist), and -//! the generated-file rules match the ones `cf-pathfilter` vendors. For full -//! byte-identity on a real corpus those tables should be sourced from the -//! workspace's already-vendored copies — `cf-pathfilter` (enry vendor regexes + -//! generated rules) and `cf-langpath` (the enry v2.1.0 extension table). The -//! predicates below reproduce enry's algorithms and the high-frequency data -//! subset exercised by the unit tests; widening the embedded tables changes -//! only the data slices these functions consult, not their structure. -//! -//! The fully-data-exact predicates here are the ones enry implements *without* -//! a data table: [`enry::is_image`] (4 hard-coded extensions), -//! [`enry::is_dotfile`] (dot-prefixed cleaned base name), [`enry::is_binary`] -//! (8000-byte NUL sniff), and [`enry::is_configuration`] (extension -> one of -//! `{INI, JSON, TOML, YAML, XML, SQL}`). +//! The vendor table ([`cf_pathfilter::is_vendor`]) and the language tables +//! ([`cf_langpath`]) are the vendored enry v2.1.0 data; the documentation +//! path matchers reproduce enry's regex table exactly without a regex +//! dependency. + +use crate::category::{Category, FileClass}; -use crate::category::Category; +/// Extensions of the linguist *prose* languages (AsciiDoc, Creole, +/// Markdown, MediaWiki, Org, Pod, RDoc, RMarkdown, Rich Text Format, +/// Textile, Wikitext, reStructuredText, Text): a file with one of these +/// extensions is documentation even outside a documentation directory. The +/// list is pinned here because enry's extension table resolves an extension +/// to its FIRST language (`.md` → "GCC Machine Description"), which hides +/// the prose type. +const PROSE_EXTENSIONS: [&str; 24] = [ + ".adoc", + ".asc", + ".asciidoc", + ".creole", + ".markdown", + ".md", + ".mdown", + ".mediawiki", + ".mkd", + ".mkdn", + ".mkdown", + ".org", + ".pod", + ".rdoc", + ".rest", + ".rmd", + ".ron", + ".rst", + ".rtf", + ".text", + ".textile", + ".txt", + ".wiki", + ".wikitext", +]; + +/// enry's configuration-language set. +const CONFIGURATION_LANGUAGES: [&str; 6] = ["XML", "JSON", "TOML", "YAML", "INI", "SQL"]; /// Classifies files into categories using enry heuristics. /// @@ -53,54 +81,93 @@ impl Classifier { Classifier } - /// Determines the category of a file from its path and content. - /// - /// The cascade runs branch-for-branch in the pinned order, including the - /// non-empty-content guards on the binary and generated-content checks (so - /// empty content never triggers Binary or content-Generated). + /// Determines the composition class of a file from its root-relative + /// path and (header) content — the cascade in the module docs. /// /// ``` - /// use cf_composition::{Classifier, Category}; + /// use cf_composition::{Classifier, Category, FileClass}; /// /// let c = Classifier::new(); - /// assert_eq!(c.classify("pkg/main.go", b"package main\n"), Category::Source); - /// assert_eq!(c.classify("vendor/foo/bar.go", b"package bar\n"), Category::Vendor); - /// assert_eq!(c.classify("docs/README.md", b"# Hi\n"), Category::Documentation); - /// assert_eq!(c.classify("logo.png", &[]), Category::Image); + /// assert_eq!(c.classify_file("pkg/main.go", b"package main\n"), Category::Source.into()); + /// assert_eq!(c.classify_file("vendor/foo/bar.go", b"package bar\n"), Category::Vendor.into()); + /// assert_eq!(c.classify_file("docs/guide.md", b"# Hi\n"), Category::Documentation.into()); + /// assert_eq!(c.classify_file("logo.png", &[]), Category::Image.into()); /// // The binary check requires non-empty content. - /// assert_eq!(c.classify("data.bin", &[0, 1, 2, 0xFF, 0, 0]), Category::Binary); + /// assert_eq!(c.classify_file("data.bin", &[0, 1, 2, 0xFF, 0, 0]), Category::Binary.into()); + /// // No rule matches an unknown extension. + /// assert_eq!(c.classify_file("data.xyz", b"hello\n"), FileClass::Other); /// ``` #[must_use] - // Two arms both yield `Category::Generated` (generated-by-path then - // generated-by-content), which clippy flags but is load-bearing for the - // pinned branch order and readability. - #[allow(clippy::if_same_then_else)] - pub fn classify(&self, path: &str, content: &[u8]) -> Category { - if !content.is_empty() && enry::is_binary(content) { + pub fn classify_file(&self, path: &str, content: &[u8]) -> FileClass { + let category = if !content.is_empty() && enry::is_binary(content) { Category::Binary } else if enry::is_image(path) { Category::Image } else if enry::is_vendor(path) { Category::Vendor - } else if pathfilter::is_generated_path(path) { - Category::Generated - } else if !content.is_empty() && pathfilter::is_generated_content(content) { + } else if pathfilter::is_generated_path(path) + || (!content.is_empty() && pathfilter::is_generated_content(content)) + { Category::Generated - } else if enry::is_documentation(path) { + } else if enry::is_documentation(path) || is_prose(path) { Category::Documentation } else if enry::is_configuration(path) { Category::Configuration } else if enry::is_dotfile(path) { Category::DotFile - } else { + } else if self.language(path, content).is_some() { Category::Source + } else { + return FileClass::Other; + }; + FileClass::Category(category) + } + + /// The 8-way classification shared with file-history analysis: the same + /// cascade as [`Classifier::classify_file`], with + /// [`FileClass::Other`] folded into [`Category::Source`]. + /// + /// ``` + /// use cf_composition::{Classifier, Category}; + /// + /// let c = Classifier::new(); + /// assert_eq!(c.classify("pkg/main.go", b"package main\n"), Category::Source); + /// assert_eq!(c.classify("data.xyz", b"hello\n"), Category::Source); + /// ``` + #[must_use] + pub fn classify(&self, path: &str, content: &[u8]) -> Category { + match self.classify_file(path, content) { + FileClass::Category(c) => c, + FileClass::Other => Category::Source, } } + + /// The enry language of a file (by file name, extension, shebang or + /// modeline), if any. + /// + /// ``` + /// use cf_composition::Classifier; + /// + /// let c = Classifier::new(); + /// assert_eq!(c.language("main.go", b"").as_deref(), Some("Go")); + /// assert_eq!(c.language("run", b"#!/usr/bin/env python3\n").as_deref(), Some("Python")); + /// assert_eq!(c.language("data.xyz", b"hello\n"), None); + /// ``` + #[must_use] + #[allow(clippy::unused_self)] // instance method: the classifier owns language resolution + pub fn language(&self, path: &str, content: &[u8]) -> Option<String> { + cf_langpath::language_by_path_with_content(path, content) + } } -/// Returns the final path element of `path` after cleaning (trailing-slash -/// trimming), for the `/`-separated inputs this classifier sees on its Linux -/// golden target. An empty path yields `"."`; an all-slash path yields `"/"`. +/// The path's extension (case-insensitive) belongs to a prose language. +fn is_prose(path: &str) -> bool { + let extension = ext(path).to_ascii_lowercase(); + PROSE_EXTENSIONS.contains(&extension.as_str()) +} + +/// Returns the final path element of `path` after trimming trailing slashes. +/// An empty path yields `"."`; an all-slash path yields `"/"`. fn base_clean(path: &str) -> &str { if path.is_empty() { return "."; @@ -115,43 +182,33 @@ fn base_clean(path: &str) -> &str { } } -/// Returns the file extension including the leading dot, **case-preserving** -/// (the suffix from the final dot in the base name). Empty when the base name -/// has no dot. Use this where the predicate compares the raw extension (e.g. -/// `enry::is_image`'s case-sensitive check), so an upper-case `.GIF` is not -/// misclassified the way a lower-cased compare would. -fn ext(path: &str) -> String { +/// Returns the file extension including the leading dot, case-preserving +/// (the suffix from the final dot in the base name). Empty when the base +/// name has no dot. +fn ext(path: &str) -> &str { let base = base_clean(path); match base.rfind('.') { - Some(idx) => base[idx..].to_string(), - None => String::new(), + Some(idx) => &base[idx..], + None => "", } } /// The subset of `src-d/enry` v2.1.0 predicates used by the classifier. mod enry { - use super::base_clean; + use super::{base_clean, CONFIGURATION_LANGUAGES}; - /// Content is detected as binary. - /// - /// Exact rule: read up to 8000 bytes and return true iff a NUL byte appears - /// in that window (git's binary detection). + /// Content is detected as binary: a NUL byte within the first 8000 + /// bytes (git's binary detection). pub fn is_binary(content: &[u8]) -> bool { const SNIFF_LEN: usize = 8000; let window = &content[..content.len().min(SNIFF_LEN)]; window.contains(&0u8) } - /// Path has an image extension: exactly `.png`, `.jpg`, `.jpeg`, or `.gif`. - /// - /// The raw extension is compared, so the match is case-sensitive: an - /// upper-case `.GIF` (e.g. ioq3's `RADIANT3.GIF`) is NOT an image and falls - /// through to `source`, which is what the reference binary reports. + /// Path has an image extension: exactly `.png`, `.jpg`, `.jpeg`, or + /// `.gif` (case-sensitive, as enry compares the raw extension). pub fn is_image(path: &str) -> bool { - matches!( - super::ext(path).as_str(), - ".png" | ".jpg" | ".jpeg" | ".gif" - ) + matches!(super::ext(path), ".png" | ".jpg" | ".jpeg" | ".gif") } /// The cleaned base name starts with `.` and is not exactly `.`. @@ -160,40 +217,21 @@ mod enry { base.starts_with('.') && base != "." } - /// The path's extension-only language is a configuration language. - /// - /// Resolves the language via enry's real extension table (vendored in - /// `cf-langpath`) through [`cf_langpath::language_by_extension`], then - /// tests membership in the exact configuration-language set - /// `{XML, JSON, TOML, YAML, INI, SQL}`. This replaced an earlier - /// hand-curated extension list, which both missed entries (`SQL`, and the - /// long XML-family tail) and added wrong ones, so files now bucket exactly - /// as the reference binary does. + /// The path's extension-only language is a configuration language + /// (`XML`, `JSON`, `TOML`, `YAML`, `INI`, `SQL`). pub fn is_configuration(path: &str) -> bool { - const CONFIGURATION_LANGUAGES: [&str; 6] = ["XML", "JSON", "TOML", "YAML", "INI", "SQL"]; - match cf_langpath::language_by_extension(path) { - Some(lang) => CONFIGURATION_LANGUAGES.contains(&lang.as_str()), - None => false, - } + cf_langpath::language_by_extension(path) + .is_some_and(|lang| CONFIGURATION_LANGUAGES.contains(&lang.as_str())) } - /// Path matches a vendoring convention. - /// - /// enry tests the path against its compiled vendor regex table; delegate to - /// [`cf_pathfilter::is_vendor`], the faithful reproduction of - /// `enry.IsVendor` over the full enry v2.1.0 `VendorMatchers` table (an - /// earlier hand-rolled prefix subset here missed table entries like - /// `.travis.yml` / `.appveyor.yml`, misclassifying them once - /// `--include-vendored` lets vendored paths reach the classifier). + /// Path matches a vendoring convention (enry's full `VendorMatchers` + /// table via [`cf_pathfilter::is_vendor`]). pub fn is_vendor(path: &str) -> bool { cf_pathfilter::is_vendor(path) } - /// Path matches a documentation convention — an exact reproduction of - /// enry's documentation matchers. - /// - /// The matcher is an OR of unanchored regex patterns (verbatim from - /// `src-d/enry/v2@v2.1.0`, extracted from github/linguist): + /// Path matches a documentation convention — enry's documentation + /// matchers, an OR of unanchored regex patterns reproduced exactly: /// /// ```text /// ^[Dd]ocs?/ (^|/)[Dd]ocumentation/ (^|/)[Gg]roovydoc/ @@ -203,50 +241,7 @@ mod enry { /// (^|/)INSTALL(\.|$) (^|/)LICEN[CS]E(\.|$) (^|/)[Ll]icen[cs]e(\.|$) /// (^|/)README(\.|$) (^|/)[Rr]eadme(\.|$) ^[Ss]amples?/ /// ``` - /// - /// These are PATH-only (no prose extensions such as `.md`/`.rst` — an - /// earlier version wrongly added those, over-classifying source files as - /// documentation). The patterns are reproduced exactly below without a - /// regex dependency. pub fn is_documentation(path: &str) -> bool { - // `^[Dd]ocs?/` — starts with Doc/ Docs/ doc/ docs/. - if dir_prefix(path, &["Doc/", "Docs/", "doc/", "docs/"]) { - return true; - } - // `(^|/)X/` directory-segment matchers. - if dir_segment(path, &["Documentation/", "documentation/"]) { - return true; - } - if dir_segment(path, &["groovydoc/", "Groovydoc/"]) { - return true; - } - if dir_segment(path, &["javadoc/", "Javadoc/"]) { - return true; - } - // `^[Mm]an/` - if dir_prefix(path, &["man/", "Man/"]) { - return true; - } - // `^[Ee]xamples/` - if dir_prefix(path, &["examples/", "Examples/"]) { - return true; - } - // `^[Dd]emos?/` - if dir_prefix(path, &["demo/", "demos/", "Demo/", "Demos/"]) { - return true; - } - // `(^|/)inst/doc/` - if dir_segment(path, &["inst/doc/"]) { - return true; - } - // `^[Ss]amples?/` - if dir_prefix(path, &["sample/", "samples/", "Sample/", "Samples/"]) { - return true; - } - // Filename matchers: `(^|/)NAME(\.|$)` — NAME anchored at a component - // start, followed by a literal `.` or by the END OF THE PATH. The pred - // receives the exact candidate name (no extension); `file_component` - // owns the `(\.|$)` anchoring. const FILE_NAMES: &[&str] = &[ "CONTRIBUTING", "COPYING", @@ -255,56 +250,52 @@ mod enry { "Readme", "readme", ]; - if file_component(path, |name| FILE_NAMES.contains(&name)) { - return true; - } - // `(^|/)CHANGE(S|LOG)?(\.|$)` — CHANGE / CHANGES / CHANGELOG. - if file_component(path, |name| { - matches!(name, "CHANGE" | "CHANGES" | "CHANGELOG") - }) { - return true; - } - // `(^|/)LICEN[CS]E(\.|$)` and `(^|/)[Ll]icen[cs]e(\.|$)`. - file_component(path, |name| { - matches!( - name, - "LICENCE" | "LICENSE" | "Licence" | "License" | "licence" | "license" - ) - }) + dir_prefix(path, &["Doc/", "Docs/", "doc/", "docs/"]) + || dir_segment(path, &["Documentation/", "documentation/"]) + || dir_segment(path, &["groovydoc/", "Groovydoc/"]) + || dir_segment(path, &["javadoc/", "Javadoc/"]) + || dir_prefix(path, &["man/", "Man/"]) + || dir_prefix(path, &["examples/", "Examples/"]) + || dir_prefix(path, &["demo/", "demos/", "Demo/", "Demos/"]) + || dir_segment(path, &["inst/doc/"]) + || dir_prefix(path, &["sample/", "samples/", "Sample/", "Samples/"]) + || file_component(path, |name| FILE_NAMES.contains(&name)) + || file_component(path, |name| { + matches!(name, "CHANGE" | "CHANGES" | "CHANGELOG") + }) + || file_component(path, |name| { + matches!( + name, + "LICENCE" | "LICENSE" | "Licence" | "License" | "licence" | "license" + ) + }) } - /// Tests `^(prefix)` for any of the given directory prefixes (each ending in - /// `/`), matching the anchored `^[Xx].../` patterns. + /// Tests `^(prefix)` for any of the given directory prefixes (each ending + /// in `/`). fn dir_prefix(path: &str, prefixes: &[&str]) -> bool { prefixes.iter().any(|p| path.starts_with(p)) } - /// Tests `(^|/)segment` for any of the given directory segments (each ending - /// in `/`), matching the `(^|/)X/` patterns. + /// Tests `(^|/)segment` for any of the given directory segments (each + /// ending in `/`). fn dir_segment(path: &str, segments: &[&str]) -> bool { segments .iter() .any(|s| path.starts_with(s) || path.contains(&format!("/{s}"))) } - /// Tests `(^|/)NAME(\.|$)` exactly as the regex does: NAME anchored at a - /// component start, immediately followed by a literal `.` (any component, - /// even a directory like `NAME.d/`) **or by the end of the whole path** (the - /// `$` — i.e. the final component equals NAME exactly). - /// - /// A bare directory component equal to NAME does NOT match: in the path - /// string it is followed by `/`, which `(\.|$)` does not allow. (This was - /// the kubernetes `CHANGELOG/CHANGELOG-1.x.md` divergence: enry classifies - /// those as source — the `CHANGELOG` *directory* never matches — while a - /// component-equality check wrongly made them documentation.) + /// Tests `(^|/)NAME(\.|$)`: NAME anchored at a component start, + /// immediately followed by a literal `.` (any component, even a + /// directory like `NAME.d/`) or by the end of the whole path. A bare + /// directory component equal to NAME does NOT match (it is followed by + /// `/`), so `CHANGELOG/CHANGELOG-1.x.md` is not documentation. fn file_component(path: &str, pred: impl Fn(&str) -> bool) -> bool { let mut comps = path.split('/').peekable(); while let Some(comp) = comps.next() { - // `NAME$`: only the final component, exactly equal. if comps.peek().is_none() && pred(comp) { return true; } - // `NAME.`: the component's prefix up to its first dot. if let Some(idx) = comp.find('.') { if pred(&comp[..idx]) { return true; @@ -315,13 +306,11 @@ mod enry { } } -/// Generated-file checks, mirroring the rules `cf-pathfilter` vendors -/// (`DEFAULT_SUFFIXES`, `DEFAULT_FILENAME_PREFIXES`, `GENERATED_MARKERS`). +/// Generated-file checks, mirroring the rules `cf-pathfilter` vendors. mod pathfilter { use super::base_clean; - /// File suffixes indicating generated code (matches `cf-pathfilter`'s - /// `DEFAULT_SUFFIXES`). + /// File suffixes indicating generated code. const DEFAULT_SUFFIXES: &[&str] = &[ ".pb.go", ".pb.gw.go", @@ -342,11 +331,10 @@ mod pathfilter { ]; /// Filename prefixes (matched against the base name) indicating generated - /// code (matches `cf-pathfilter`'s `DEFAULT_FILENAME_PREFIXES`). + /// code. const DEFAULT_FILENAME_PREFIXES: &[&str] = &["zz_generated", "mock_", "fake_", "wire_gen"]; - /// Byte markers found near the top of generated files (matches - /// `cf-pathfilter`'s `GENERATED_MARKERS`). + /// Byte markers found near the top of generated files. const GENERATED_MARKERS: &[&[u8]] = &[ b"DO NOT EDIT", b"Code generated", @@ -356,8 +344,7 @@ mod pathfilter { b"@generated", ]; - /// How many header bytes to scan for a generated marker (matches - /// `cf-pathfilter`'s `GENERATED_MARKER_SCAN_LIMIT`). + /// How many header bytes to scan for a generated marker. const GENERATED_MARKER_SCAN_LIMIT: usize = 512; /// The path's name matches a generated suffix or filename prefix. @@ -378,17 +365,132 @@ mod pathfilter { let window = &content[..content.len().min(GENERATED_MARKER_SCAN_LIMIT)]; GENERATED_MARKERS .iter() - .any(|m| contains_subslice(window, m)) + .any(|m| window.windows(m.len()).any(|w| w == *m)) } +} - /// Returns whether `haystack` contains `needle` as a contiguous subslice. - fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool { - if needle.is_empty() { - return true; - } - if needle.len() > haystack.len() { - return false; - } - haystack.windows(needle.len()).any(|w| w == needle) +#[cfg(test)] +mod tests { + use super::*; + + fn class(path: &str, content: &[u8]) -> FileClass { + Classifier::new().classify_file(path, content) + } + + fn cat(c: Category) -> FileClass { + FileClass::Category(c) + } + + #[test] + fn every_class_is_reachable() { + assert_eq!(class("main.go", b"package main\n"), cat(Category::Source)); + assert_eq!(class("Makefile", b"all:\n"), cat(Category::Source)); + assert_eq!(class("README.md", b"# T\n"), cat(Category::Documentation)); + assert_eq!( + class("docs/guide.md", b"# G\n"), + cat(Category::Documentation) + ); + assert_eq!(class("notes.txt", b"plain\n"), cat(Category::Documentation)); + assert_eq!( + class("pkg/notes.md", b"# n\n"), + cat(Category::Documentation) + ); + assert_eq!(class("pkg/NOTES.RST", b"n\n"), cat(Category::Documentation)); + assert_eq!( + class("config.yaml", b"k: v\n"), + cat(Category::Configuration) + ); + // enry lists `.sql` under PLSQL first, so SQL files are source, not + // configuration (the configuration set only matches when the + // extension's FIRST language is one of its six). + assert_eq!( + cf_langpath::language_by_extension("schema.sql").as_deref(), + Some("PLSQL") + ); + assert_eq!(class("schema.sql", b"SELECT 1;\n"), cat(Category::Source)); + assert_eq!(class("settings.json", b"{}"), cat(Category::Configuration)); + assert_eq!( + class("vendor/x/y.go", b"package x\n"), + cat(Category::Vendor) + ); + assert_eq!( + class("gen/thing.pb.go", b"// Code generated. DO NOT EDIT.\n"), + cat(Category::Generated) + ); + assert_eq!( + class( + "api.go", + b"// Code generated by tool. DO NOT EDIT.\npackage api\n" + ), + cat(Category::Generated) + ); + assert_eq!(class(".editorconfig", b"[*]\n"), cat(Category::DotFile)); + assert_eq!(class("logo.png", &[]), cat(Category::Image)); + assert_eq!(class("data.bin", &[0, 1, 0xFF, 0]), cat(Category::Binary)); + assert_eq!(class("data.xyz", b"hello world\n"), FileClass::Other); + } + + #[test] + fn empty_content_never_triggers_binary_or_generated_content() { + assert_eq!(class("empty.go", &[]), cat(Category::Source)); + assert_eq!(class("empty.txt", &[]), cat(Category::Documentation)); + } + + #[test] + fn cascade_order_is_first_match_wins() { + // vendor beats documentation and configuration + assert_eq!(class("vendor/README.md", b"# v\n"), cat(Category::Vendor)); + // generated-by-path beats documentation + assert_eq!( + class("docs/x.pb.go", b"package x\n"), + cat(Category::Generated) + ); + // configuration beats dotfile for a YAML dotfile + assert_eq!( + class(".golangci.yml", b"linters:\n"), + cat(Category::Configuration) + ); + // binary content beats every path rule + assert_eq!(class("docs/guide.md", &[0, 1, 0]), cat(Category::Binary)); + } + + #[test] + fn shebang_scripts_are_source() { + assert_eq!( + class("run", b"#!/usr/bin/env python3\nprint(1)\n"), + cat(Category::Source) + ); + } + + #[test] + fn classify_folds_other_into_source() { + let c = Classifier::new(); + assert_eq!(c.classify("data.xyz", b"hello\n"), Category::Source); + assert_eq!(c.classify("vendor/a.go", b""), Category::Vendor); + } + + #[test] + fn documentation_matchers_anchor_like_enry() { + assert!(enry::is_documentation("docs/a.go")); + assert!(!enry::is_documentation("src/docs/a.go")); + assert!(enry::is_documentation("src/Documentation/a.go")); + assert!(enry::is_documentation("LICENSE")); + assert!(enry::is_documentation("pkg/CHANGELOG.md")); + assert!(!enry::is_documentation("CHANGELOG/CHANGELOG-1.x.md")); + } + + #[test] + fn image_extension_is_case_sensitive() { + assert!(enry::is_image("a/b.gif")); + assert!(!enry::is_image("a/b.GIF")); + } + + #[test] + fn base_and_ext_helpers() { + assert_eq!(base_clean(""), "."); + assert_eq!(base_clean("///"), "/"); + assert_eq!(base_clean("a/b/c.go/"), "c.go"); + assert_eq!(ext("a/b.tar.gz"), ".gz"); + assert_eq!(ext("Makefile"), ""); } } diff --git a/crates/cf-composition/src/lib.rs b/crates/cf-composition/src/lib.rs index e9df00c6..ece2a1ee 100644 --- a/crates/cf-composition/src/lib.rs +++ b/crates/cf-composition/src/lib.rs @@ -1,448 +1,110 @@ -//! `cf-composition` — Raw-file composition analyzer (`static/composition`). +//! `cf-composition` — raw-file composition analyzer (`static/composition`). //! -//! Classifies files into `source` / `vendor` / `generated` / `documentation` / -//! `configuration` / `binary` / `image` / `dotfile` using enry-style heuristics, -//! then aggregates per-category counts and percentages into the -//! `static/composition` report. +//! Classifies every walked file into `source` / `documentation` / +//! `configuration` / `vendor` / `generated` / `dotfile` / `image` / `binary` +//! / `other` using enry-style heuristics ([`Classifier`]), then aggregates +//! per-class counts and percentages ([`Aggregator`] → [`CompositionReport`]) +//! and renders the report section, its JSON value and the `--per-file` +//! entries ([`report_section`]). //! -//! # Compatibility -//! -//! All machine-format report serialization routes through `cf-gojson` (the -//! report-format JSON encoder), never `serde_json` — see [`analyzer`]. The -//! aggregated report is a *map-origin* [`cf_gojson::GoMap`], so its keys are -//! byte-sorted at encode time per the report contract for dynamic maps. Output -//! bytes are pinned against the reference binary by `tests/compat`. +//! The analyzer classifies **every** file the walk hands it — vendored and +//! generated files are exactly what it exists to count — so the only walk +//! filter that applies to it is `--languages`. //! //! # Module layout //! -//! * [`category`] — `Category`, `ALL_CATEGORIES`, `CategoryCounts` (the minimal -//! file-classification surface shared with file-history analysis). +//! * [`category`] — `Category` (the 8-way classification shared with +//! file-history analysis), `FileClass` (`Category` + `other`), +//! `ALL_FILE_CLASSES`, `ClassCounts`. //! * [`classifier`] — `Classifier` + the enry predicate subset. -//! * [`aggregator`] — `Aggregator` (per-file results -> aggregate report). -//! * [`report_section`] — `ReportSection` (terminal section data). -//! * [`analyzer`] — `Analyzer` (identity + format entry points). -//! -//! # Example +//! * [`aggregator`] — `Aggregator`, `CompositionReport` and its raw report. +//! * [`report_section`] — `ReportSection`, the JSON value, per-file entries. //! -//! Classify a single file into its [`Category`]: -//! -//! ``` -//! use cf_composition::{Classifier, Category}; +//! All report values are [`cf_gojson`] trees; nothing here uses `serde`. //! -//! let category = Classifier::new().classify("pkg/main.go", b"package main\n"); -//! assert_eq!(category, Category::Source); -//! assert_eq!(category.as_str(), "source"); -//! ``` -//! -//! The analyzer's per-file report is a single-key `category` map, encoded -//! through `cf-gojson`: +//! # Example //! //! ``` -//! use cf_composition::Analyzer; +//! use cf_composition::{Aggregator, Category, Classifier, FileClass}; //! -//! let report = Analyzer::new().analyze_file_content("pkg/main.go", b"package main\n"); -//! let bytes = cf_gojson::Encoder::compact() -//! .encode_to_vec(&cf_gojson::GoValue::Map(report)); -//! assert_eq!(bytes, br#"{"category":"source"}"#); +//! let classifier = Classifier::new(); +//! let mut agg = Aggregator::new(); +//! for (path, content) in [ +//! ("pkg/main.go", &b"package main\n"[..]), +//! ("vendor/x/y.go", b"package x\n"), +//! ("data.xyz", b"hello\n"), +//! ] { +//! agg.add(classifier.classify_file(path, content)); +//! } +//! let report = agg.report(); +//! assert_eq!(report.total_files, 3); +//! assert_eq!(report.count(Category::Vendor.into()), 1); +//! assert_eq!(report.count(FileClass::Other), 1); //! ``` #![forbid(unsafe_code)] pub mod aggregator; -pub mod analyzer; pub mod category; pub mod classifier; pub mod report_section; -pub use aggregator::Aggregator; -pub use analyzer::{Analyzer, ANALYZER_DESCRIPTION, ANALYZER_FLAG, ANALYZER_ID, ANALYZER_NAME}; -pub use category::{Category, CategoryCounts, ALL_CATEGORIES}; +pub use aggregator::{Aggregator, CompositionReport}; +pub use category::{Category, ClassCounts, FileClass, ALL_FILE_CLASSES}; pub use classifier::Classifier; -pub use report_section::{CompositionReport, ReportSection, SECTION_TITLE}; +pub use report_section::{FileRecord, ReportSection, SECTION_TITLE}; + +/// Analyzer name. +pub const ANALYZER_NAME: &str = "composition"; +/// CLI flag name. +pub const ANALYZER_FLAG: &str = "composition"; +/// Fully-qualified analyzer ID. +pub const ANALYZER_ID: &str = "static/composition"; +/// Human description. +pub const ANALYZER_DESCRIPTION: &str = + "Classifies files by type (source, vendor, generated, docs, config, binary, image, other) using enry."; #[cfg(test)] mod tests { use super::*; - use crate::aggregator::{KEY_BREAKDOWN, KEY_CATEGORY, KEY_PERCENTAGE, KEY_TOTAL_FILES}; - use crate::report_section::{ - SCORE_INFO_ONLY, SEVERITY_INFO, SEVERITY_POOR, STATUS_DEFAULT, STATUS_EMPTY, - }; - use std::collections::HashMap; - - // ---- Analyzer identity ---- - - #[test] - fn analyzer_name() { - assert_eq!(Analyzer::new().name(), ANALYZER_NAME); - } - - #[test] - fn analyzer_flag() { - assert_eq!(Analyzer::new().flag(), ANALYZER_FLAG); - } - - #[test] - fn analyzer_descriptor_id() { - assert_eq!(Analyzer::new().id(), ANALYZER_ID); - } - - #[test] - fn analyzer_thresholds_nil() { - assert!(Analyzer::new().thresholds().is_none()); - } - - #[test] - fn analyzer_list_configuration_options_empty() { - assert!(Analyzer::new().list_configuration_options().is_empty()); - } - - #[test] - fn analyzer_configure_no_error() { - assert!(Analyzer::new().configure(&HashMap::new()).is_ok()); - } - - // ---- Classification ---- - - fn classify(path: &str, content: &[u8]) -> Category { - Classifier::new().classify(path, content) - } - - #[test] - fn analyze_content_go_file() { - assert_eq!( - classify("pkg/main.go", b"package main\n\nfunc main() {}\n"), - Category::Source - ); - } - - #[test] - fn analyze_content_vendor_path() { - assert_eq!( - classify("vendor/github.com/foo/bar.go", b"package bar\n"), - Category::Vendor - ); - } - - #[test] - fn analyze_content_markdown() { - assert_eq!( - classify("docs/README.md", b"# Hello\n"), - Category::Documentation - ); - } - - #[test] - fn analyze_content_config_file() { - assert_eq!( - classify(".golangci.yml", b"linters:\n enable:\n"), - Category::Configuration - ); - } - - #[test] - fn analyze_content_binary_content() { - let binary = [0x00u8, 0x01, 0x02, 0xFF, 0xFE, 0x00, 0x00, 0x00]; - assert_eq!(classify("data.bin", &binary), Category::Binary); - } - - #[test] - fn analyze_content_dot_file() { - assert_eq!( - classify(".editorconfig", b"[*]\nindent_style = tab\n"), - Category::DotFile - ); - } - - #[test] - fn analyze_content_image_path() { - assert_eq!(classify("logo.png", &[]), Category::Image); - } #[test] - fn analyze_file_content_report_shape() { - let report = Analyzer::new().analyze_file_content("pkg/main.go", b"package main\n"); - let bytes = cf_gojson::Encoder::compact().encode_to_vec(&cf_gojson::GoValue::Map(report)); - assert_eq!(bytes, br#"{"category":"source"}"#); - } - - // ---- Aggregator ---- - - fn single(category: &str) -> HashMap<String, HashMap<String, cf_gojson::GoValue>> { - let mut report = HashMap::new(); - report.insert( - KEY_CATEGORY.to_string(), - cf_gojson::GoValue::Str(category.to_string()), - ); - let mut results = HashMap::new(); - results.insert(ANALYZER_NAME.to_string(), report); - results - } - - fn decode(report: &cf_gojson::GoMap) -> CompositionReport { - let mut out = CompositionReport::default(); - for (key, value) in report.entries() { - match (key.as_str(), value) { - (KEY_TOTAL_FILES, cf_gojson::GoValue::Int(n)) => out.total_files = *n, - (KEY_BREAKDOWN, cf_gojson::GoValue::Map(m)) => { - for (cat, v) in m.entries() { - if let cf_gojson::GoValue::Int(n) = v { - out.breakdown.insert(cat.clone(), *n); - } - } - } - _ => {} - } - } - out - } - - #[test] - fn aggregator_empty_result() { - let agg = Aggregator::new(); - let result = decode(&agg.get_result()); - assert_eq!(result.total_files, 0); + fn analyzer_identity() { + assert_eq!(ANALYZER_NAME, "composition"); + assert_eq!(ANALYZER_FLAG, "composition"); + assert_eq!(ANALYZER_ID, "static/composition"); } #[test] - fn aggregator_single_file() { - let mut agg = Aggregator::new(); - agg.aggregate(&single(Category::Source.as_str())); - let result = decode(&agg.get_result()); - assert_eq!(result.total_files, 1); - assert_eq!(result.breakdown.get(Category::Source.as_str()), Some(&1)); - } - - #[test] - fn aggregator_multiple_files() { - let mut agg = Aggregator::new(); - let files = [ - Category::Source, - Category::Source, - Category::Source, - Category::Vendor, - Category::Documentation, + fn classify_then_aggregate_covers_every_class_and_sums_to_100() { + let classifier = Classifier::new(); + let files: [(&str, &[u8]); 10] = [ + ("main.go", b"package main\n"), + ("README.md", b"# T\n"), + ("config.yaml", b"k: v\n"), + ("vendor/x/y.go", b"package x\n"), + ("gen/thing.pb.go", b"// Code generated. DO NOT EDIT.\n"), + (".editorconfig", b"[*]\n"), + ("logo.png", b""), + ("data.bin", b"\x00\x01"), + ("data.xyz", b"hello\n"), + ("empty.go", b""), ]; - for cat in files { - agg.aggregate(&single(cat.as_str())); - } - let result = decode(&agg.get_result()); - assert_eq!(result.total_files, files.len() as i64); - assert_eq!(result.breakdown.get(Category::Source.as_str()), Some(&3)); - assert_eq!(result.breakdown.get(Category::Vendor.as_str()), Some(&1)); - assert_eq!( - result.breakdown.get(Category::Documentation.as_str()), - Some(&1) - ); - - // Percentages are present in the encoded report. - let mut pct = HashMap::new(); - for (key, value) in agg.get_result().entries() { - if key == KEY_PERCENTAGE { - if let cf_gojson::GoValue::Map(m) = value { - for (cat, v) in m.entries() { - if let cf_gojson::GoValue::Float(f) = v { - pct.insert(cat.clone(), *f); - } - } - } - } - } - assert!((pct[Category::Source.as_str()] - 60.0).abs() < 0.1); - assert!((pct[Category::Vendor.as_str()] - 20.0).abs() < 0.1); - assert!((pct[Category::Documentation.as_str()] - 20.0).abs() < 0.1); - } - - #[test] - fn aggregator_skips_invalid_category() { let mut agg = Aggregator::new(); - let mut report = HashMap::new(); - report.insert("not_a_category".to_string(), cf_gojson::GoValue::Int(42)); - let mut results = HashMap::new(); - results.insert(ANALYZER_NAME.to_string(), report); - agg.aggregate(&results); - - let result = decode(&agg.get_result()); - // File counted but no known category incremented. - assert_eq!(result.total_files, 1); - assert_eq!(result.breakdown.get(Category::Source.as_str()), Some(&0)); - } - - // ---- Report section ---- - - fn test_report() -> CompositionReport { - let mut breakdown = HashMap::new(); - breakdown.insert(Category::Source.as_str().to_string(), 6); - breakdown.insert(Category::Vendor.as_str().to_string(), 2); - breakdown.insert(Category::Documentation.as_str().to_string(), 1); - breakdown.insert(Category::Binary.as_str().to_string(), 1); - CompositionReport { - total_files: 10, - breakdown, + for (path, content) in files { + agg.add(classifier.classify_file(path, content)); } - } - - #[test] - fn section_title() { - assert_eq!( - ReportSection::new(test_report()).section_title(), - SECTION_TITLE - ); - } - - #[test] - fn section_score_info_only() { - assert!((ReportSection::new(test_report()).score() - SCORE_INFO_ONLY).abs() < 0.001); - } - - #[test] - fn section_status_message() { - assert_eq!( - ReportSection::new(test_report()).status_message(), - STATUS_DEFAULT - ); - } - - #[test] - fn section_status_message_empty() { - assert_eq!( - ReportSection::new(CompositionReport::default()).status_message(), - STATUS_EMPTY - ); - } - - #[test] - fn section_nil_report() { - let s = ReportSection::new(CompositionReport::default()); - assert_eq!(s.section_title(), SECTION_TITLE); - assert_eq!(s.status_message(), STATUS_EMPTY); - } - - #[test] - fn section_key_metrics_count() { - assert_eq!(ReportSection::new(test_report()).key_metrics().len(), 3); - } - - #[test] - fn section_key_metrics_labels() { - let s = ReportSection::new(test_report()); - let m = s.key_metrics(); - assert_eq!(m[0].label, "Total Files"); - assert_eq!(m[1].label, "Source Files"); - assert_eq!(m[2].label, "Source %"); - } - - #[test] - fn section_key_metrics_values() { - let s = ReportSection::new(test_report()); - let m = s.key_metrics(); - assert_eq!(m[0].value, "10"); - assert_eq!(m[1].value, "6"); - assert!(m[2].value.contains("60")); - } - - #[test] - fn section_distribution() { - let s = ReportSection::new(test_report()); - let dist = s.distribution(); - assert_eq!(dist.len(), 4); - // First follows AllCategories order → source. - assert_eq!(dist[0].label, Category::Source.as_str()); - assert_eq!(dist[0].count, 6); - } - - #[test] - fn section_distribution_empty() { - assert!(ReportSection::new(CompositionReport::default()) - .distribution() - .is_empty()); - } - - #[test] - fn section_top_issues() { - assert_eq!(ReportSection::new(test_report()).top_issues(2).len(), 2); - } - - #[test] - fn section_all_issues() { - // 3 non-source categories with counts: vendor, docs, binary. - assert_eq!(ReportSection::new(test_report()).all_issues().len(), 3); - } - - #[test] - fn section_issues_binary_severity_poor() { - let s = ReportSection::new(test_report()); - let issues = s.all_issues(); - let binary = issues - .iter() - .find(|i| i.name == Category::Binary.as_str()) - .expect("binary category must appear in issues"); - assert_eq!(binary.severity, SEVERITY_POOR); - } - - #[test] - fn section_issues_vendor_severity_info() { - let s = ReportSection::new(test_report()); - let issues = s.all_issues(); - let vendor = issues - .iter() - .find(|i| i.name == Category::Vendor.as_str()) - .expect("vendor category must appear in issues"); - assert_eq!(vendor.severity, SEVERITY_INFO); - } - - #[test] - fn section_issues_empty() { - assert!(ReportSection::new(CompositionReport::default()) - .all_issues() - .is_empty()); - } - - // ---- Format paths ---- - - fn category_report(category: &str) -> cf_gojson::GoMap { - let mut m = cf_gojson::GoMap::new(cf_gojson::MapOrigin::Map); - m.push(KEY_CATEGORY, cf_gojson::GoValue::Str(category.to_string())); - m - } - - #[test] - fn format_report_json_contains_source() { - let mut buf = Vec::new(); - Analyzer::new() - .format_report_json(&category_report("source"), &mut buf) - .unwrap(); - assert!(String::from_utf8(buf).unwrap().contains("source")); - } - - #[test] - fn format_report_contains_binary() { - let mut buf = Vec::new(); - Analyzer::new() - .format_report(&category_report("binary"), &mut buf) - .unwrap(); - assert!(String::from_utf8(buf).unwrap().contains("binary")); - } - - #[test] - fn format_report_plot_contains_docs() { - let mut buf = Vec::new(); - Analyzer::new() - .format_report_plot(&category_report("docs"), &mut buf) - .unwrap(); - assert!(String::from_utf8(buf).unwrap().contains("docs")); - } - - #[test] - fn format_report_binary_envelope() { - let mut buf = Vec::new(); - Analyzer::new() - .format_report_binary(&category_report("source"), &mut buf) - .unwrap(); - assert!(!buf.is_empty()); - assert_eq!(&buf[..4], b"CFB1"); - // Payload length (LE u32) matches the compact JSON body. - let len = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize; - assert_eq!(buf.len(), 8 + len); - assert_eq!(&buf[8..], br#"{"category":"source"}"#); + let report = agg.report(); + assert_eq!(report.total_files, 10); + for class in ALL_FILE_CLASSES { + let want = if class == FileClass::Category(Category::Source) { + 2 + } else { + 1 + }; + assert_eq!(report.count(class), want, "{class:?}"); + } + let sum: f64 = ALL_FILE_CLASSES.iter().map(|c| report.percent(*c)).sum(); + assert!((sum - 100.0).abs() < 1e-9, "{sum}"); } } diff --git a/crates/cf-composition/src/report_section.rs b/crates/cf-composition/src/report_section.rs index 09cd5aee..35690e9d 100644 --- a/crates/cf-composition/src/report_section.rs +++ b/crates/cf-composition/src/report_section.rs @@ -1,32 +1,37 @@ -//! Composition report section. -//! -//! Builds the human-facing section model (title, status, key metrics, -//! distribution, issues) from an aggregated composition report. -//! -//! This section feeds terminal/HTML rendering, which is **non-binding, -//! cosmetic** output — not part of the byte-identity contract. The logic is -//! nonetheless unit-tested so the section behaviour stays stable. +//! Composition report section: the human-facing section model (title, +//! status, key metrics, distribution, issues), its `renderer.JSONReport` +//! value (`--format json`, the source of the `text` / `compact` renders) and +//! the `--per-file` entries. -use std::collections::HashMap; +use cf_gojson::{GoMap, GoValue, MapOrigin}; -use crate::category::{Category, ALL_CATEGORIES}; +use crate::aggregator::CompositionReport; +use crate::category::{Category, FileClass, ALL_FILE_CLASSES}; /// Section title shown in rendered output. pub const SECTION_TITLE: &str = "COMPOSITION"; -const METRIC_TOTAL_FILES: &str = "Total Files"; -const METRIC_SOURCE: &str = "Source Files"; -const METRIC_SOURCE_PCT: &str = "Source %"; +/// Metric label: total files. +pub const METRIC_TOTAL_FILES: &str = "Total Files"; +/// Metric label: source files. +pub const METRIC_SOURCE: &str = "Source Files"; +/// Metric label: source share. +pub const METRIC_SOURCE_PCT: &str = "Source %"; +/// Per-file metric label: the file's enry language. +pub const METRIC_LANGUAGE: &str = "Language"; +/// Per-file language value when enry recognizes none. +pub const LANGUAGE_UNKNOWN: &str = "unknown"; /// Status message when at least one file was analyzed. pub const STATUS_DEFAULT: &str = "File composition analysis completed"; /// Status message when no files were analyzed. pub const STATUS_EMPTY: &str = "No files analyzed"; +/// Per-file status prefix: `Classified as <class>`. +pub const STATUS_CLASSIFIED_PREFIX: &str = "Classified as "; /// Score sentinel meaning "informational only" (no pass/fail grade). -/// -/// Kept here as the local constant the section reports until -/// `cf-analyzers-common` exposes the shared value. pub const SCORE_INFO_ONLY: f64 = -1.0; +/// Score label of an info-only section. +pub const SCORE_LABEL_INFO: &str = "Info"; /// Severity string for informational issues. pub const SEVERITY_INFO: &str = "info"; @@ -42,221 +47,399 @@ pub struct Metric { pub value: String, } -/// A category distribution item. +/// A class distribution item. #[derive(Debug, Clone, PartialEq)] pub struct DistributionItem { - /// Category label. + /// Class label (wire string). pub label: String, - /// Share of total files, as a percentage. + /// Share of total files as a fraction in `[0, 1]` (the renderer + /// contract shared by every analyzer; the formatted strings carry `%`). pub percent: f64, /// Absolute file count. pub count: i64, } -/// A reported issue (non-source category breakdown line). +/// A reported issue (non-source class breakdown line). #[derive(Debug, Clone, PartialEq, Eq)] pub struct Issue { - /// Category name. + /// Class name (wire string). pub name: String, - /// Human-formatted "<n> files (<pct>%)" value. + /// Human-formatted `<n> files (<pct>%)` value. pub value: String, /// Severity string. pub severity: String, } -/// Aggregated composition report in the shape the section consumes. -/// -/// This is the decoded form of the aggregator report: a total count plus the -/// per-category breakdown. It deliberately does not depend on `cf-gojson` -/// because the section is cosmetic and is built directly from typed data. -#[derive(Debug, Clone, Default)] -pub struct CompositionReport { - /// Total files analyzed (`total_files`). - pub total_files: i64, - /// Per-category counts (`breakdown`). - pub breakdown: HashMap<String, i64>, +/// One classified file (the `--per-file` entry source). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileRecord { + /// Path relative to the analyzed root. + pub path: String, + /// The file's class. + pub class: FileClass, + /// The file's enry language, if recognized. + pub language: Option<String>, } -/// Implements the composition report section. -#[derive(Debug, Clone)] +/// The composition report section. +#[derive(Debug, Clone, Copy)] pub struct ReportSection { - title: String, - message: String, - score: f64, report: CompositionReport, } -const PERCENT_MULTIPLIER: f64 = 100.0; - impl ReportSection { /// Builds a section from an aggregated composition report. - /// - /// The status message is [`STATUS_EMPTY`] when `total_files == 0`, - /// otherwise [`STATUS_DEFAULT`]. A missing report maps to - /// [`CompositionReport::default`], which yields the empty status. #[must_use] pub fn new(report: CompositionReport) -> Self { - let message = if report.total_files == 0 { - STATUS_EMPTY - } else { - STATUS_DEFAULT - }; - - Self { - title: SECTION_TITLE.to_string(), - message: message.to_string(), - score: SCORE_INFO_ONLY, - report, - } + Self { report } } - /// Returns the section title. + /// The section title. #[must_use] - pub fn section_title(&self) -> &str { - &self.title + pub fn title(&self) -> &'static str { + SECTION_TITLE } - /// Returns the status message. + /// [`STATUS_EMPTY`] when no file was analyzed, else [`STATUS_DEFAULT`]. #[must_use] - pub fn status_message(&self) -> &str { - &self.message + pub fn status_message(&self) -> &'static str { + if self.report.total_files == 0 { + STATUS_EMPTY + } else { + STATUS_DEFAULT + } } - /// Returns the section score (always [`SCORE_INFO_ONLY`]). + /// Always [`SCORE_INFO_ONLY`]: composition is informational. #[must_use] pub fn score(&self) -> f64 { - self.score + SCORE_INFO_ONLY } - /// Returns the ordered key metrics for display: total files, source-file - /// count, and source percentage (formatted to one decimal place with a - /// trailing `%`). + /// Ordered key metrics: total files, source-file count, source share. #[must_use] pub fn key_metrics(&self) -> Vec<Metric> { - let total = self.report.total_files; - let source_count = self.breakdown_count(Category::Source); - + let source = FileClass::Category(Category::Source); vec![ - Metric { - label: METRIC_TOTAL_FILES.to_string(), - value: format_int(total), - }, - Metric { - label: METRIC_SOURCE.to_string(), - value: format_int(source_count), - }, - Metric { - label: METRIC_SOURCE_PCT.to_string(), - value: format_percent(pct(source_count, total)), - }, + metric(METRIC_TOTAL_FILES, self.report.total_files.to_string()), + metric(METRIC_SOURCE, self.report.count(source).to_string()), + metric( + METRIC_SOURCE_PCT, + format_percent(self.report.percent(source)), + ), ] } - /// Returns the category breakdown as distribution items, in - /// [`ALL_CATEGORIES`] order, omitting zero-count categories. - /// - /// Returns an empty vector when no files were analyzed. + /// The class breakdown in [`ALL_FILE_CLASSES`] order, omitting + /// zero-count classes (empty when no file was analyzed). #[must_use] pub fn distribution(&self) -> Vec<DistributionItem> { - let total = self.report.total_files; - if total == 0 { - return Vec::new(); - } - - let mut items = Vec::with_capacity(ALL_CATEGORIES.len()); - for cat in ALL_CATEGORIES { - let count = self.breakdown_count(cat); - if count == 0 { - continue; - } - items.push(DistributionItem { - label: cat.as_str().to_string(), - percent: pct(count, total), - count, - }); - } - items + ALL_FILE_CLASSES + .into_iter() + .filter(|c| self.report.count(*c) > 0) + .map(|c| DistributionItem { + label: c.as_str().to_string(), + percent: self.report.fraction(c), + count: self.report.count(c), + }) + .collect() } - /// Returns up to `n` non-source categories as issues (`n == 0` -> all). + /// Every non-source class with files, as `<count> files (<pct>%)` + /// issues in [`ALL_FILE_CLASSES`] order. #[must_use] - pub fn top_issues(&self, n: usize) -> Vec<Issue> { - self.build_issues(n) + pub fn issues(&self) -> Vec<Issue> { + ALL_FILE_CLASSES + .into_iter() + .filter(|c| *c != FileClass::Category(Category::Source) && self.report.count(*c) > 0) + .map(|c| Issue { + name: c.as_str().to_string(), + value: format!( + "{} files ({})", + self.report.count(c), + format_percent(self.report.percent(c)) + ), + severity: severity_for(c).to_string(), + }) + .collect() } +} - /// Returns all non-source categories as issues. - #[must_use] - pub fn all_issues(&self) -> Vec<Issue> { - self.build_issues(0) +/// Severity of a class: binary files are [`SEVERITY_POOR`], everything else +/// [`SEVERITY_INFO`]. +#[must_use] +pub fn severity_for(class: FileClass) -> &'static str { + match class { + FileClass::Category(Category::Binary) => SEVERITY_POOR, + _ => SEVERITY_INFO, } +} - /// Builds issues for non-source categories with non-zero counts: skips - /// `source`, skips zero counts, formats the value as - /// `"<count> files (<pct>%)"` with one decimal place, and truncates to - /// `limit` when `limit > 0`. - fn build_issues(&self, limit: usize) -> Vec<Issue> { - let total = self.report.total_files; - if total == 0 { - return Vec::new(); - } +fn metric(label: &str, value: String) -> Metric { + Metric { + label: label.to_string(), + value, + } +} - let mut issues = Vec::with_capacity(ALL_CATEGORIES.len()); - for cat in ALL_CATEGORIES { - if cat == Category::Source { - continue; - } - let count = self.breakdown_count(cat); - if count == 0 { - continue; - } - let percent = (count as f64) / (total as f64) * PERCENT_MULTIPLIER; - issues.push(Issue { - name: cat.as_str().to_string(), - value: format!("{count} files ({percent:.1}%)"), - severity: severity_for_category(cat).to_string(), - }); - } +/// Formats a `0..100` percentage to one decimal place with a trailing `%`. +fn format_percent(value: f64) -> String { + format!("{value:.1}%") +} - if limit > 0 && issues.len() > limit { - issues.truncate(limit); - } - issues - } +/// Builds the `renderer.JSONReport` value for the single composition +/// section: `{overall_score_label, sections: [section], overall_score}`, +/// where the section is `{title, score_label, status, metrics, +/// distribution?, issues, files?, score}`. `files` adds one [`file_entry`] +/// per record (the `--per-file` enrichment). +#[must_use] +pub fn json_value(report: CompositionReport, files: Option<&[FileRecord]>) -> GoValue { + let section = ReportSection::new(report); - /// Looks up a category count in the breakdown (zero if absent). - fn breakdown_count(&self, cat: Category) -> i64 { - self.report - .breakdown - .get(cat.as_str()) - .copied() - .unwrap_or(0) + let mut sect = GoMap::new(MapOrigin::Struct); + sect.push("title", GoValue::Str(section.title().to_string())); + sect.push("score_label", GoValue::Str(SCORE_LABEL_INFO.to_string())); + sect.push("status", GoValue::Str(section.status_message().to_string())); + sect.push("metrics", metrics_value(§ion.key_metrics())); + let distribution = section.distribution(); + if !distribution.is_empty() { + sect.push("distribution", distribution_value(&distribution)); } + sect.push("issues", issues_value(§ion.issues())); + if let Some(records) = files { + sect.push( + "files", + GoValue::Array(records.iter().map(file_entry).collect()), + ); + } + sect.push("score", GoValue::Float(section.score())); + + let mut root = GoMap::new(MapOrigin::Struct); + root.push( + "overall_score_label", + GoValue::Str(SCORE_LABEL_INFO.to_string()), + ); + root.push("sections", GoValue::Array(vec![GoValue::Map(sect)])); + root.push("overall_score", GoValue::Float(SCORE_INFO_ONLY)); + GoValue::Map(root) } -/// Returns the severity for a file category: binary is [`SEVERITY_POOR`], -/// everything else is [`SEVERITY_INFO`]. +/// Builds one `--per-file` entry: the file's class as its status +/// (`Classified as vendor`), a one-file breakdown (`Total Files 1`, +/// `<Class> Files 1`, `<Class> % 100.0%`), the enry language, and a +/// single-row distribution. #[must_use] -pub fn severity_for_category(cat: Category) -> &'static str { - match cat { - Category::Binary => SEVERITY_POOR, - _ => SEVERITY_INFO, - } +pub fn file_entry(record: &FileRecord) -> GoValue { + let class = record.class; + let metrics = vec![ + metric(METRIC_TOTAL_FILES, "1".to_string()), + metric(&format!("{} Files", class.display_name()), "1".to_string()), + metric( + &format!("{} %", class.display_name()), + format_percent(100.0), + ), + metric( + METRIC_LANGUAGE, + record + .language + .clone() + .unwrap_or_else(|| LANGUAGE_UNKNOWN.to_string()), + ), + ]; + let distribution = vec![DistributionItem { + label: class.as_str().to_string(), + percent: 1.0, + count: 1, + }]; + + let mut entry = GoMap::new(MapOrigin::Struct); + entry.push("file_path", GoValue::Str(record.path.clone())); + entry.push("score_label", GoValue::Str(SCORE_LABEL_INFO.to_string())); + entry.push( + "status", + GoValue::Str(format!("{STATUS_CLASSIFIED_PREFIX}{}", class.as_str())), + ); + entry.push("metrics", metrics_value(&metrics)); + entry.push("distribution", distribution_value(&distribution)); + entry.push("issues", GoValue::Array(Vec::new())); + entry.push("score", GoValue::Float(SCORE_INFO_ONLY)); + GoValue::Map(entry) } -/// Percentage of `count` out of `total` (0.0 when `total == 0`). -fn pct(count: i64, total: i64) -> f64 { - if total == 0 { - return 0.0; - } - (count as f64) / (total as f64) * PERCENT_MULTIPLIER +fn metrics_value(metrics: &[Metric]) -> GoValue { + GoValue::Array( + metrics + .iter() + .map(|m| { + let mut sm = GoMap::new(MapOrigin::Struct); + sm.push("label", GoValue::Str(m.label.clone())); + sm.push("value", GoValue::Str(m.value.clone())); + GoValue::Map(sm) + }) + .collect(), + ) } -/// Formats an integer for display. -fn format_int(value: i64) -> String { - value.to_string() +fn distribution_value(rows: &[DistributionItem]) -> GoValue { + GoValue::Array( + rows.iter() + .map(|d| { + let mut sd = GoMap::new(MapOrigin::Struct); + sd.push("label", GoValue::Str(d.label.clone())); + sd.push("percent", GoValue::Float(d.percent)); + sd.push("count", GoValue::Int(d.count)); + GoValue::Map(sd) + }) + .collect(), + ) } -/// Formats a percentage to one decimal place with a trailing `%`. -fn format_percent(value: f64) -> String { - format!("{value:.1}%") +fn issues_value(issues: &[Issue]) -> GoValue { + GoValue::Array( + issues + .iter() + .map(|i| { + let mut si = GoMap::new(MapOrigin::Struct); + si.push("name", GoValue::Str(i.name.clone())); + si.push("location", GoValue::Str(String::new())); + si.push("value", GoValue::Str(i.value.clone())); + si.push("severity", GoValue::Str(i.severity.clone())); + GoValue::Map(si) + }) + .collect(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aggregator::Aggregator; + use cf_gojson::Encoder; + + fn sample() -> CompositionReport { + let mut agg = Aggregator::new(); + for class in [ + Category::Source.into(), + Category::Source.into(), + Category::Source.into(), + Category::Source.into(), + Category::Source.into(), + Category::Source.into(), + Category::Vendor.into(), + Category::Vendor.into(), + Category::Documentation.into(), + Category::Binary.into(), + ] { + agg.add(class); + } + agg.report() + } + + #[test] + fn section_title_status_and_score() { + let s = ReportSection::new(sample()); + assert_eq!(s.title(), SECTION_TITLE); + assert_eq!(s.status_message(), STATUS_DEFAULT); + assert!((s.score() - SCORE_INFO_ONLY).abs() < 1e-12); + let empty = ReportSection::new(CompositionReport::default()); + assert_eq!(empty.status_message(), STATUS_EMPTY); + assert!(empty.distribution().is_empty()); + assert!(empty.issues().is_empty()); + } + + #[test] + fn key_metrics() { + let m = ReportSection::new(sample()).key_metrics(); + assert_eq!(m[0], metric(METRIC_TOTAL_FILES, "10".into())); + assert_eq!(m[1], metric(METRIC_SOURCE, "6".into())); + assert_eq!(m[2], metric(METRIC_SOURCE_PCT, "60.0%".into())); + } + + #[test] + fn distribution_follows_class_order_and_skips_zero() { + let d = ReportSection::new(sample()).distribution(); + let labels: Vec<&str> = d.iter().map(|i| i.label.as_str()).collect(); + assert_eq!(labels, vec!["source", "documentation", "vendor", "binary"]); + assert!((d[0].percent - 0.6).abs() < 1e-12); + assert_eq!(d[2].count, 2); + } + + #[test] + fn issues_list_non_source_classes_with_severity() { + let issues = ReportSection::new(sample()).issues(); + assert_eq!(issues.len(), 3); + assert_eq!(issues[0].name, "documentation"); + assert_eq!(issues[0].value, "1 files (10.0%)"); + assert_eq!(issues[0].severity, SEVERITY_INFO); + assert_eq!(issues[2].name, "binary"); + assert_eq!(issues[2].severity, SEVERITY_POOR); + } + + #[test] + fn json_value_shape() { + let json = Encoder::marshal().encode_to_string(&json_value(sample(), None)); + assert!(json.starts_with(r#"{"overall_score_label":"Info","sections":[{"title":"COMPOSITION","score_label":"Info","status":"File composition analysis completed","metrics":[{"label":"Total Files","value":"10"},{"label":"Source Files","value":"6"},{"label":"Source %","value":"60.0%"}],"distribution":[{"label":"source","percent":0.6,"count":6}"#), "{json}"); + assert!(json.contains(r#""issues":[{"name":"documentation","location":"","value":"1 files (10.0%)","severity":"info"}"#), "{json}"); + assert!(!json.contains("\"files\"")); + assert!( + json.ends_with(r#""score":-1}],"overall_score":-1}"#), + "{json}" + ); + } + + #[test] + fn empty_json_omits_distribution() { + let json = + Encoder::marshal().encode_to_string(&json_value(CompositionReport::default(), None)); + assert!(!json.contains("distribution")); + assert!(json.contains(r#""status":"No files analyzed""#)); + assert!(json.contains(r#""issues":[]"#)); + } + + #[test] + fn per_file_entry_carries_class_and_language() { + let record = FileRecord { + path: "vendor/x/y.go".into(), + class: Category::Vendor.into(), + language: Some("Go".into()), + }; + let json = Encoder::marshal().encode_to_string(&file_entry(&record)); + assert_eq!( + json, + r#"{"file_path":"vendor/x/y.go","score_label":"Info","status":"Classified as vendor","metrics":[{"label":"Total Files","value":"1"},{"label":"Vendor Files","value":"1"},{"label":"Vendor %","value":"100.0%"},{"label":"Language","value":"Go"}],"distribution":[{"label":"vendor","percent":1,"count":1}],"issues":[],"score":-1}"# + ); + let other = FileRecord { + path: "data.xyz".into(), + class: FileClass::Other, + language: None, + }; + let json = Encoder::marshal().encode_to_string(&file_entry(&other)); + assert!(json.contains(r#""status":"Classified as other""#)); + assert!(json.contains(r#"{"label":"Other Files","value":"1"}"#)); + assert!(json.contains(r#"{"label":"Language","value":"unknown"}"#)); + } + + #[test] + fn json_value_with_files_lists_every_record() { + let records = vec![ + FileRecord { + path: "a.go".into(), + class: Category::Source.into(), + language: Some("Go".into()), + }, + FileRecord { + path: "b.xyz".into(), + class: FileClass::Other, + language: None, + }, + ]; + let json = Encoder::marshal().encode_to_string(&json_value(sample(), Some(&records))); + assert!(json.contains(r#""files":[{"file_path":"a.go""#), "{json}"); + assert!(json.contains(r#"{"file_path":"b.xyz""#), "{json}"); + // `files` sits between `issues` and `score`. + let files_at = json.find("\"files\"").unwrap(); + let issues_at = json.find("\"issues\"").unwrap(); + assert!(issues_at < files_at); + assert!(json.ends_with(r#""score":-1}],"overall_score":-1}"#)); + } } diff --git a/crates/cf-config/Cargo.toml b/crates/cf-config/Cargo.toml index 96a7790e..1d58b2e1 100644 --- a/crates/cf-config/Cargo.toml +++ b/crates/cf-config/Cargo.toml @@ -1,12 +1,9 @@ -# cf-config — configuration model + layered loader. +# cf-config — the `codefang` configuration file (`.codefang.yaml` + the +# `CODEFANG_*` environment) as the default layer of the `run` flags. # -# Configuration model (analyzer/pipeline/history/checkpoint constants) plus a -# layered loader with precedence flag > env (CODEFANG_) > file > default. -# -# Config is *input*, never machine-format *output*. Per DESIGN.md §2 the -# byte-identity serializers (cf-gojson / cf-goyaml) are reserved for report -# output; reading YAML config here uses serde + serde_yaml, which is the correct -# boundary (no machine-format report bytes are produced by this crate). +# Config is *input*, never machine-format *output*: the byte-identity +# serializers (cf-gojson / cf-goyaml) are reserved for report output, and +# serde_yaml here only parses the file. [package] name = "cf-config" version.workspace = true @@ -15,7 +12,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true publish.workspace = true -description = "Configuration model + loader (analyzer/pipeline/history/checkpoint constants). Used by analyze, mcp, cmd/codefang/commands." +description = "The codefang configuration file: discovery, the CODEFANG_* environment, and the supported keys that default the run flags." [lints.rust] unsafe_code = "forbid" @@ -23,13 +20,8 @@ unsafe_code = "forbid" [dependencies] serde = { workspace = true } serde_yaml = "0.9" -# Derived Display/Error impls for LoadError; the `read config:` / -# `unmarshal config:` / `validate config:` wrappings are part of the CLI -# compatibility contract and are kept byte-identical. +# Derived Display/Error impls for LoadError. thiserror = "1" [dev-dependencies] tempfile = "3" - -[lib] -doctest = false diff --git a/crates/cf-config/src/defaults.rs b/crates/cf-config/src/defaults.rs deleted file mode 100644 index f39daca7..00000000 --- a/crates/cf-config/src/defaults.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Default configuration values. -//! -//! Every `DEFAULT_*` constant here is pinned to the reference CLI's registered -//! default, so a [`crate::Config`] built from defaults is equivalent to what -//! the reference loader produces from an empty config file (compatibility -//! contract). - -// Pipeline default values. - -/// Default worker count (0 = auto-detect downstream). -pub const DEFAULT_PIPELINE_WORKERS: i64 = 0; -/// Default memory budget (empty = downstream auto). -pub const DEFAULT_PIPELINE_MEMORY_BUDGET: &str = ""; -/// Default blob cache size (empty = downstream auto). -pub const DEFAULT_PIPELINE_BLOB_CACHE_SIZE: &str = ""; -/// Default diff cache size. -pub const DEFAULT_PIPELINE_DIFF_CACHE_SIZE: i64 = 0; -/// Default blob arena size (empty = downstream auto). -pub const DEFAULT_PIPELINE_BLOB_ARENA_SIZE: &str = ""; -/// Default commit batch size. -pub const DEFAULT_PIPELINE_COMMIT_BATCH_SIZE: i64 = 0; -/// Default GOGC value. -pub const DEFAULT_PIPELINE_GOGC: i64 = 0; -/// Default ballast size string. -pub const DEFAULT_PIPELINE_BALLAST_SIZE: &str = "0"; - -// Pipeline advanced tuning defaults. - -/// Default UAST spill threshold. -pub const DEFAULT_PIPELINE_UAST_SPILL_THRESHOLD: i64 = 32; -/// Default intra-commit parallel threshold. -pub const DEFAULT_PIPELINE_INTRA_COMMIT_PARALLEL_THRESHOLD: i64 = 4; -/// Default maximum intra-commit workers. -pub const DEFAULT_PIPELINE_MAX_INTRA_COMMIT_WORKERS: i64 = 4; -/// Default maximum UAST blob size (256 KiB). -pub const DEFAULT_PIPELINE_MAX_UAST_BLOB_SIZE: i64 = 256 * 1024; -/// Default UAST parse timeout. -pub const DEFAULT_PIPELINE_UAST_PARSE_TIMEOUT: &str = "10s"; -/// Default maximum changes per commit. -pub const DEFAULT_PIPELINE_MAX_CHANGES_PER_COMMIT: i64 = 10000; -/// Default maximum diff batch size. -pub const DEFAULT_PIPELINE_MAX_DIFF_BATCH_SIZE: i64 = 1000; -/// Default memory budget ratio (percent). -pub const DEFAULT_PIPELINE_MEMORY_BUDGET_RATIO: i64 = 50; -/// Default memory budget cap. -pub const DEFAULT_PIPELINE_MEMORY_BUDGET_CAP: &str = "2GiB"; -/// Default memory limit ratio (percent). -pub const DEFAULT_PIPELINE_MEMORY_LIMIT_RATIO: i64 = 75; -/// Default UAST spill trim interval. -pub const DEFAULT_PIPELINE_UAST_SPILL_TRIM_INTERVAL: i64 = 16; -/// Default native trim interval. -pub const DEFAULT_PIPELINE_NATIVE_TRIM_INTERVAL: i64 = 10; -/// Default maximum streaming buffering. -pub const DEFAULT_PIPELINE_MAX_STREAMING_BUFFERING: i64 = 3; -/// Default drain prefetch timeout. -pub const DEFAULT_PIPELINE_DRAIN_PREFETCH_TIMEOUT: &str = "30s"; -/// Default sampler interval. -pub const DEFAULT_PIPELINE_SAMPLER_INTERVAL: &str = "2s"; -/// Default worker ratio (percent). -pub const DEFAULT_PIPELINE_WORKER_RATIO: i64 = 100; -/// Default UAST worker ratio (percent). -pub const DEFAULT_PIPELINE_UAST_WORKER_RATIO: i64 = 40; -/// Default leaf worker divisor. -pub const DEFAULT_PIPELINE_LEAF_WORKER_DIVISOR: i64 = 3; -/// Default minimum leaf workers. -pub const DEFAULT_PIPELINE_MIN_LEAF_WORKERS: i64 = 4; -/// Default buffer size multiplier. -pub const DEFAULT_PIPELINE_BUFFER_SIZE_MULTIPLIER: i64 = 2; -/// Default budget limit ratio (percent). -pub const DEFAULT_PIPELINE_BUDGET_LIMIT_RATIO: i64 = 95; -/// Default system RAM limit ratio (percent). -pub const DEFAULT_PIPELINE_SYSTEM_RAM_LIMIT_RATIO: i64 = 90; -/// Default static analyzer maximum workers. -pub const DEFAULT_PIPELINE_STATIC_MAX_WORKERS: i64 = 8; -/// Default malloc trim interval. -pub const DEFAULT_PIPELINE_MALLOC_TRIM_INTERVAL: i64 = 50; -/// Default static memory limit ratio (percent). -pub const DEFAULT_PIPELINE_STATIC_MEMORY_LIMIT_RATIO: i64 = 90; -/// Default diff job buffer multiplier. -pub const DEFAULT_PIPELINE_DIFF_JOB_BUFFER_MULTIPLIER: i64 = 10; - -// Burndown analyzer defaults. - -/// Default burndown granularity. -pub const DEFAULT_BURNDOWN_GRANULARITY: i64 = 30; -/// Default burndown sampling. -pub const DEFAULT_BURNDOWN_SAMPLING: i64 = 30; -/// Default burndown track-files flag. -pub const DEFAULT_BURNDOWN_TRACK_FILES: bool = false; -/// Default burndown track-people flag. -pub const DEFAULT_BURNDOWN_TRACK_PEOPLE: bool = false; -/// Default burndown hibernation threshold. -pub const DEFAULT_BURNDOWN_HIBERNATION_THRESHOLD: i64 = 1000; -/// Default burndown hibernation-to-disk flag. -pub const DEFAULT_BURNDOWN_HIBERNATION_TO_DISK: bool = true; -/// Default burndown hibernation directory. -pub const DEFAULT_BURNDOWN_HIBERNATION_DIRECTORY: &str = ""; -/// Default burndown debug flag. -pub const DEFAULT_BURNDOWN_DEBUG: bool = false; -/// Default burndown goroutines. -pub const DEFAULT_BURNDOWN_GOROUTINES: i64 = 0; - -// Couples analyzer defaults. - -/// Default couples high coupling threshold. -pub const DEFAULT_COUPLES_COUPLING_THRESHOLD_HIGH: i64 = 10; -/// Default couples few-ownership threshold. -pub const DEFAULT_COUPLES_OWNERSHIP_FEW_THRESHOLD: i64 = 3; -/// Default couples moderate-ownership threshold. -pub const DEFAULT_COUPLES_OWNERSHIP_MODERATE_THRESHOLD: i64 = 5; -/// Default couples batch coupling threshold. -pub const DEFAULT_COUPLES_BATCH_COUPLING_THRESHOLD: i64 = 100; -/// Default couples HLL precision. -pub const DEFAULT_COUPLES_HLL_PRECISION: i64 = 10; -/// Default couples top-K per file. -pub const DEFAULT_COUPLES_TOP_K_PER_FILE: i64 = 100; -/// Default couples minimum edge weight. -pub const DEFAULT_COUPLES_MIN_EDGE_WEIGHT: i64 = 2; - -// Devs analyzer defaults. - -/// Default devs consider-empty-commits flag. -pub const DEFAULT_DEVS_CONSIDER_EMPTY_COMMITS: bool = false; -/// Default devs anonymize flag. -pub const DEFAULT_DEVS_ANONYMIZE: bool = false; -/// Default devs bus-factor threshold. -pub const DEFAULT_DEVS_BUS_FACTOR_THRESHOLD: f64 = 0.5; -/// Default devs critical risk threshold (percent). -pub const DEFAULT_DEVS_RISK_THRESHOLD_CRITICAL: f64 = 90.0; -/// Default devs high risk threshold (percent). -pub const DEFAULT_DEVS_RISK_THRESHOLD_HIGH: f64 = 80.0; -/// Default devs medium risk threshold (percent). -pub const DEFAULT_DEVS_RISK_THRESHOLD_MEDIUM: f64 = 60.0; -/// Default devs active threshold ratio. -pub const DEFAULT_DEVS_ACTIVE_THRESHOLD_RATIO: f64 = 0.7; -/// Default devs default-active-days. -pub const DEFAULT_DEVS_DEFAULT_ACTIVE_DAYS: i64 = 90; -/// Default devs HLL precision. -pub const DEFAULT_DEVS_HLL_PRECISION: i64 = 14; - -// File history analyzer defaults. - -/// Default file-history critical hotspot threshold. -pub const DEFAULT_FILE_HISTORY_HOTSPOT_CRITICAL: i64 = 50; -/// Default file-history high hotspot threshold. -pub const DEFAULT_FILE_HISTORY_HOTSPOT_HIGH: i64 = 30; -/// Default file-history medium hotspot threshold. -pub const DEFAULT_FILE_HISTORY_HOTSPOT_MEDIUM: i64 = 15; - -// Imports analyzer defaults. - -/// Default imports goroutines. -pub const DEFAULT_IMPORTS_GOROUTINES: i64 = 4; -/// Default imports maximum file size (1 MiB). -pub const DEFAULT_IMPORTS_MAX_FILE_SIZE: i64 = 1 << 20; -/// Default imports maximum dependency-risk rows. -pub const DEFAULT_IMPORTS_MAX_DEPENDENCY_RISK_ROWS: i64 = 30; - -// Sentiment analyzer defaults. - -/// Default sentiment minimum comment length. -pub const DEFAULT_SENTIMENT_MIN_COMMENT_LENGTH: i64 = 20; -/// Default sentiment gap. -pub const DEFAULT_SENTIMENT_GAP: f64 = 0.5; -/// Default sentiment neutralizer weight. -pub const DEFAULT_SENTIMENT_NEUTRALIZER_WEIGHT: f64 = 0.8; -/// Default sentiment maximum weight ratio. -pub const DEFAULT_SENTIMENT_MAX_WEIGHT_RATIO: f64 = 3.0; -/// Default sentiment positive threshold. -pub const DEFAULT_SENTIMENT_POSITIVE_THRESHOLD: f64 = 0.6; -/// Default sentiment negative threshold. -pub const DEFAULT_SENTIMENT_NEGATIVE_THRESHOLD: f64 = 0.4; -/// Default sentiment trend threshold. -pub const DEFAULT_SENTIMENT_TREND_THRESHOLD: f64 = 0.1; -/// Default low-sentiment risk threshold. -pub const DEFAULT_SENTIMENT_LOW_SENTIMENT_RISK_THRESH: f64 = 0.2; - -// Shotness analyzer defaults. - -/// Default shotness structural DSL. -pub const DEFAULT_SHOTNESS_DSL_STRUCT: &str = r#"filter(.roles has "Function")"#; -/// Default shotness name DSL. -pub const DEFAULT_SHOTNESS_DSL_NAME: &str = ".props.name"; - -// Typos analyzer defaults. - -/// Default typos maximum edit distance. -pub const DEFAULT_TYPOS_MAX_DISTANCE: i64 = 4; - -// Anomaly analyzer defaults. - -/// Default anomaly threshold. -pub const DEFAULT_ANOMALY_THRESHOLD: f64 = 2.0; -/// Default anomaly window size. -pub const DEFAULT_ANOMALY_WINDOW_SIZE: i64 = 20; - -// Clones analyzer defaults. - -/// Default clones maximum clone pairs. -pub const DEFAULT_CLONES_MAX_CLONE_PAIRS: i64 = 1000; -/// Default clones number of hashes. -pub const DEFAULT_CLONES_NUM_HASHES: i64 = 128; -/// Default clones number of bands. -pub const DEFAULT_CLONES_NUM_BANDS: i64 = 16; -/// Default clones number of rows. -pub const DEFAULT_CLONES_NUM_ROWS: i64 = 8; -/// Default clones shingle size. -pub const DEFAULT_CLONES_SHINGLE_SIZE: i64 = 5; -/// Default clones type-2 similarity. -pub const DEFAULT_CLONES_SIMILARITY_TYPE2: f64 = 0.8; -/// Default clones type-3 similarity. -pub const DEFAULT_CLONES_SIMILARITY_TYPE3: f64 = 0.5; -/// Default clones yellow threshold ratio. -pub const DEFAULT_CLONES_THRESHOLD_RATIO_YELLOW: f64 = 0.1; -/// Default clones red threshold ratio. -pub const DEFAULT_CLONES_THRESHOLD_RATIO_RED: f64 = 0.3; -/// Default clones yellow threshold pairs. -pub const DEFAULT_CLONES_THRESHOLD_PAIRS_YELLOW: i64 = 5; -/// Default clones red threshold pairs. -pub const DEFAULT_CLONES_THRESHOLD_PAIRS_RED: i64 = 20; - -// Checkpoint defaults. - -/// Default checkpoint enabled flag. -pub const DEFAULT_CHECKPOINT_ENABLED: bool = true; -/// Default checkpoint directory. -pub const DEFAULT_CHECKPOINT_DIR: &str = ""; -/// Default checkpoint resume flag. -pub const DEFAULT_CHECKPOINT_RESUME: bool = true; -/// Default checkpoint clear-previous flag. -pub const DEFAULT_CHECKPOINT_CLEAR_PREV: bool = false; diff --git a/crates/cf-config/src/facts.rs b/crates/cf-config/src/facts.rs deleted file mode 100644 index 6887093d..00000000 --- a/crates/cf-config/src/facts.rs +++ /dev/null @@ -1,297 +0,0 @@ -//! Merge configuration values into the analyzer facts map. -//! -//! The fact-key strings and the apply rules (positive-only for numerics, -//! non-empty for strings, always for bools) are frozen: analyzer wiring -//! depends on them (compatibility contract). - -use crate::types::{ - AnomalyConfig, BurndownConfig, ClonesConfig, Config, CouplesConfig, DevsConfig, - FileHistoryConfig, ImportsConfig, SentimentConfig, ShotnessConfig, -}; -use std::collections::HashMap; - -/// A fact value stored in the analyzer facts map. -/// -/// Numeric facts are stored as `Int` (for integer config fields) or `Float` -/// (for floating-point config fields), strings as `Str`, and booleans as -/// `Bool`. -#[derive(Debug, Clone, PartialEq)] -pub enum FactValue { - /// Integer fact. - Int(i64), - /// Floating-point fact. - Float(f64), - /// String fact. - Str(String), - /// Boolean fact. - Bool(bool), -} - -impl FactValue { - /// Returns the integer value if this fact is an [`FactValue::Int`]. - #[must_use] - pub const fn as_int(&self) -> Option<i64> { - match self { - Self::Int(v) => Some(*v), - _ => None, - } - } - - /// Returns the float value if this fact is a [`FactValue::Float`]. - #[must_use] - pub const fn as_float(&self) -> Option<f64> { - match self { - Self::Float(v) => Some(*v), - _ => None, - } - } - - /// Returns the string value if this fact is a [`FactValue::Str`]. - #[must_use] - pub fn as_str(&self) -> Option<&str> { - match self { - Self::Str(v) => Some(v), - _ => None, - } - } - - /// Returns the boolean value if this fact is a [`FactValue::Bool`]. - #[must_use] - pub const fn as_bool(&self) -> Option<bool> { - match self { - Self::Bool(v) => Some(*v), - _ => None, - } - } -} - -/// The analyzer facts map. -pub type Facts = HashMap<String, FactValue>; - -/// Sets `facts[key] = value` when an integer value is positive (`> 0`). -/// -/// Zero values are skipped so the analyzer keeps its built-in default. -fn apply_positive_int(facts: &mut Facts, key: &str, value: i64) { - if value > 0 { - facts.insert(key.to_owned(), FactValue::Int(value)); - } -} - -/// Sets `facts[key] = value` when a float value is positive (`> 0`). -fn apply_positive_float(facts: &mut Facts, key: &str, value: f64) { - if value > 0.0 { - facts.insert(key.to_owned(), FactValue::Float(value)); - } -} - -/// Sets `facts[key] = value` when a string is non-empty. -fn apply_non_empty(facts: &mut Facts, key: &str, value: &str) { - if !value.is_empty() { - facts.insert(key.to_owned(), FactValue::Str(value.to_owned())); - } -} - -/// Sets `facts[key] = value` unconditionally. -/// -/// Boolean fields are always applied because `false` is a meaningful override. -fn apply_bool(facts: &mut Facts, key: &str, value: bool) { - facts.insert(key.to_owned(), FactValue::Bool(value)); -} - -impl Config { - /// Merges config values into the analyzer `facts` map. - /// - /// Only non-zero numeric/string config values override existing facts - /// (zero means "use analyzer default" and is skipped); boolean fields are - /// always applied because `false` is a meaningful value. - pub fn apply_to_facts(&self, facts: &mut Facts) { - self.apply_burndown_facts(facts); - self.apply_couples_facts(facts); - self.apply_devs_facts(facts); - self.apply_file_history_facts(facts); - self.apply_imports_facts(facts); - self.apply_sentiment_facts(facts); - self.apply_shotness_facts(facts); - self.apply_typos_facts(facts); - self.apply_anomaly_facts(facts); - self.apply_clones_facts(facts); - } - - fn apply_burndown_facts(&self, facts: &mut Facts) { - let bd: &BurndownConfig = &self.history.burndown; - apply_positive_int(facts, "Burndown.Granularity", bd.granularity); - apply_positive_int(facts, "Burndown.Sampling", bd.sampling); - apply_bool(facts, "Burndown.TrackFiles", bd.track_files); - apply_bool(facts, "Burndown.TrackPeople", bd.track_people); - apply_positive_int( - facts, - "Burndown.HibernationThreshold", - bd.hibernation_threshold, - ); - apply_bool(facts, "Burndown.HibernationOnDisk", bd.hibernation_to_disk); - apply_non_empty( - facts, - "Burndown.HibernationDirectory", - &bd.hibernation_directory, - ); - apply_bool(facts, "Burndown.Debug", bd.debug); - apply_positive_int(facts, "Burndown.Goroutines", bd.goroutines); - } - - fn apply_couples_facts(&self, facts: &mut Facts) { - let cp: &CouplesConfig = &self.history.couples; - apply_positive_int( - facts, - "Couples.CouplingThresholdHigh", - cp.coupling_threshold_high, - ); - apply_positive_int( - facts, - "Couples.OwnershipFewThreshold", - cp.ownership_few_threshold, - ); - apply_positive_int( - facts, - "Couples.OwnershipModerateThreshold", - cp.ownership_moderate_threshold, - ); - apply_positive_int( - facts, - "Couples.BatchCouplingThreshold", - cp.batch_coupling_threshold, - ); - apply_positive_int(facts, "Couples.HLLPrecision", cp.hll_precision); - apply_positive_int(facts, "Couples.TopKPerFile", cp.top_k_per_file); - apply_positive_int(facts, "Couples.MinEdgeWeight", cp.min_edge_weight); - } - - fn apply_devs_facts(&self, facts: &mut Facts) { - let dv: &DevsConfig = &self.history.devs; - apply_bool( - facts, - "Devs.ConsiderEmptyCommits", - dv.consider_empty_commits, - ); - apply_bool(facts, "Devs.Anonymize", dv.anonymize); - apply_positive_float(facts, "Devs.BusFactorThreshold", dv.bus_factor_threshold); - apply_positive_float( - facts, - "Devs.RiskThresholdCritical", - dv.risk_threshold_critical, - ); - apply_positive_float(facts, "Devs.RiskThresholdHigh", dv.risk_threshold_high); - apply_positive_float(facts, "Devs.RiskThresholdMedium", dv.risk_threshold_medium); - apply_positive_float( - facts, - "Devs.ActiveThresholdRatio", - dv.active_threshold_ratio, - ); - apply_positive_int(facts, "Devs.DefaultActiveDays", dv.default_active_days); - apply_positive_int(facts, "Devs.HLLPrecision", dv.hll_precision); - } - - fn apply_file_history_facts(&self, facts: &mut Facts) { - let fh: &FileHistoryConfig = &self.history.file_history; - apply_positive_int( - facts, - "FileHistory.HotspotThresholdCritical", - fh.hotspot_threshold_critical, - ); - apply_positive_int( - facts, - "FileHistory.HotspotThresholdHigh", - fh.hotspot_threshold_high, - ); - apply_positive_int( - facts, - "FileHistory.HotspotThresholdMedium", - fh.hotspot_threshold_medium, - ); - } - - fn apply_imports_facts(&self, facts: &mut Facts) { - let im: &ImportsConfig = &self.history.imports; - apply_positive_int(facts, "Imports.Goroutines", im.goroutines); - apply_positive_int(facts, "Imports.MaxFileSize", im.max_file_size); - apply_positive_int( - facts, - "Imports.MaxDependencyRiskRows", - im.max_dependency_risk_rows, - ); - } - - fn apply_sentiment_facts(&self, facts: &mut Facts) { - let se: &SentimentConfig = &self.history.sentiment; - apply_positive_int(facts, "CommentSentiment.MinLength", se.min_comment_length); - apply_positive_float(facts, "CommentSentiment.Gap", se.gap); - apply_positive_float( - facts, - "CommentSentiment.NeutralizerWeight", - se.neutralizer_weight, - ); - apply_positive_float( - facts, - "CommentSentiment.MaxWeightRatio", - se.max_weight_ratio, - ); - apply_positive_float( - facts, - "CommentSentiment.PositiveThreshold", - se.positive_threshold, - ); - apply_positive_float( - facts, - "CommentSentiment.NegativeThreshold", - se.negative_threshold, - ); - apply_positive_float(facts, "CommentSentiment.TrendThreshold", se.trend_threshold); - apply_positive_float( - facts, - "CommentSentiment.LowSentimentRiskThreshold", - se.low_sentiment_risk_thresh, - ); - } - - fn apply_shotness_facts(&self, facts: &mut Facts) { - let sh: &ShotnessConfig = &self.history.shotness; - apply_non_empty(facts, "Shotness.DSLStruct", &sh.dsl_struct); - apply_non_empty(facts, "Shotness.DSLName", &sh.dsl_name); - } - - fn apply_typos_facts(&self, facts: &mut Facts) { - apply_positive_int( - facts, - "TyposDatasetBuilder.MaximumAllowedDistance", - self.history.typos.max_distance, - ); - } - - fn apply_anomaly_facts(&self, facts: &mut Facts) { - let an: &AnomalyConfig = &self.history.anomaly; - apply_positive_float(facts, "TemporalAnomaly.Threshold", an.threshold); - apply_positive_int(facts, "TemporalAnomaly.WindowSize", an.window_size); - } - - fn apply_clones_facts(&self, facts: &mut Facts) { - let cl: &ClonesConfig = &self.history.clones; - apply_positive_int(facts, "Clones.MaxClonePairs", cl.max_clone_pairs); - apply_positive_int(facts, "Clones.NumHashes", cl.num_hashes); - apply_positive_int(facts, "Clones.NumBands", cl.num_bands); - apply_positive_int(facts, "Clones.NumRows", cl.num_rows); - apply_positive_int(facts, "Clones.ShingleSize", cl.shingle_size); - apply_positive_float(facts, "Clones.SimilarityType2", cl.similarity_type2); - apply_positive_float(facts, "Clones.SimilarityType3", cl.similarity_type3); - apply_positive_float( - facts, - "Clones.ThresholdRatioYellow", - cl.threshold_ratio_yellow, - ); - apply_positive_float(facts, "Clones.ThresholdRatioRed", cl.threshold_ratio_red); - apply_positive_int( - facts, - "Clones.ThresholdPairsYellow", - cl.threshold_pairs_yellow, - ); - apply_positive_int(facts, "Clones.ThresholdPairsRed", cl.threshold_pairs_red); - } -} diff --git a/crates/cf-config/src/lib.rs b/crates/cf-config/src/lib.rs index 6a0b0ee5..7c8d8675 100644 --- a/crates/cf-config/src/lib.rs +++ b/crates/cf-config/src/lib.rs @@ -1,748 +1,209 @@ -//! cf-config — configuration model + layered loader. +//! cf-config — the `codefang` configuration file. //! -//! Implements the layered precedence **flag > env `CODEFANG_` > file > -//! default** (CLI compatibility contract) and the analyzer / pipeline / -//! history / checkpoint configuration constants. The crate emits no -//! machine-format report bytes, so it does not route through the `cf-gojson` / -//! `cf-goyaml` serialization crates; `serde_yaml` is used purely to *parse* -//! the `.codefang.yaml` input file. +//! A `.codefang.yaml` file (explicit `--config`, else the working directory, +//! else `$HOME`) and the `CODEFANG_*` environment supply the DEFAULT of the +//! `run` flags listed in [`KEYS`]: the precedence is explicit flag > +//! environment > file > compiled default. Each field of [`Config`] is `None` +//! when no layer sets it, so the compiled defaults live in one place — the +//! flag registrations in `cf-commands`. //! -//! # Layout -//! - [`defaults`] — every `DEFAULT_*` constant. -//! - [`Config`] and the per-section structs + [`Config::validate`]. -//! - [`Config::apply_to_facts`] — merge into the analyzer facts map. -//! - [`load_config`] — the layered loader. +//! Keys a file sets that nothing reads (the Go-era `pipeline:` tuning tree, +//! `checkpoint:`, analyzer thresholds without a flag, …) are returned in +//! [`Loaded::unsupported_keys`] for the caller to warn about; they never fail +//! the load. +//! +//! The crate emits no report bytes; `serde_yaml` only parses the input file. //! //! # Example +//! //! ``` -//! use cf_config::{load_from_yaml_and_env, defaults}; +//! use cf_config::{keys, load, Value}; +//! +//! let dir = tempfile::tempdir().unwrap(); +//! let file = dir.path().join("codefang.yaml"); +//! std::fs::write(&file, "history:\n anomaly:\n window_size: 3\npipeline:\n workers: 8\n") +//! .unwrap(); +//! let env = |name: &str| (name == "CODEFANG_HISTORY_TICK_SIZE").then(|| "6".to_owned()); //! -//! // env overrides file overrides default. -//! let cfg = load_from_yaml_and_env("pipeline:\n workers: 8\n", &|name| { -//! (name == "CODEFANG_PIPELINE_GOGC").then(|| "200".to_owned()) -//! }) -//! .unwrap(); -//! assert_eq!(cfg.pipeline.workers, 8); // from file -//! assert_eq!(cfg.pipeline.gogc, 200); // from env (overrides default 0) -//! assert_eq!(cfg.history.typos.max_distance, defaults::DEFAULT_TYPOS_MAX_DISTANCE); +//! let loaded = load(Some(&file), &[], env).unwrap(); +//! assert_eq!(loaded.unsupported_keys, vec!["pipeline.workers"]); +//! let settings = loaded.config.settings(); +//! assert_eq!(settings[0].key, keys::TICK_SIZE); +//! assert_eq!(settings[0].value, Value::Int(6)); +//! assert_eq!(settings[1].key, keys::ANOMALY_WINDOW_SIZE); //! ``` #![forbid(unsafe_code)] #![warn(missing_docs)] -pub mod defaults; -mod facts; mod loader; mod types; -pub use facts::{FactValue, Facts}; -pub use loader::{ - apply_env_overrides, config_file_path, load_config, load_from_yaml_and_env, LoadError, -}; -pub use types::CheckpointConfig; +pub use loader::{default_search_dirs, env_name, load, LoadError, Loaded, CONFIG_FILE_NAME}; pub use types::{ - AnomalyConfig, BurndownConfig, ClonesConfig, Config, ConfigError, CouplesConfig, DevsConfig, - FileHistoryConfig, HistoryConfig, ImportsConfig, PipelineConfig, SentimentConfig, - ShotnessConfig, TyposConfig, + keys, AnomalyConfig, BurndownConfig, Config, ConfigError, DevsConfig, HistoryConfig, + PipelineConfig, SentimentConfig, Setting, ShotnessConfig, TyposConfig, Value, KEYS, }; -impl Config { - /// Returns the all-zero config (all numeric/string/bool fields zeroed). - /// - /// This is distinct from [`Config::default`], which returns the config - /// populated with the registered defaults. Use `zero()` where unspecified - /// fields must stay zero rather than fall back to defaults. - #[must_use] - pub const fn zero() -> Self { - Self { - analyzers: Vec::new(), - pipeline: PipelineConfig { - workers: 0, - memory_budget: String::new(), - blob_cache_size: String::new(), - diff_cache_size: 0, - blob_arena_size: String::new(), - commit_batch_size: 0, - gogc: 0, - ballast_size: String::new(), - memory_limit: String::new(), - worker_timeout: String::new(), - uast_spill_threshold: 0, - intra_commit_parallel_threshold: 0, - max_intra_commit_workers: 0, - max_uast_blob_size: 0, - uast_parse_timeout: String::new(), - max_changes_per_commit: 0, - max_diff_batch_size: 0, - memory_budget_ratio: 0, - memory_budget_cap: String::new(), - memory_limit_ratio: 0, - uast_spill_trim_interval: 0, - native_trim_interval: 0, - max_streaming_buffering: 0, - drain_prefetch_timeout: String::new(), - sampler_interval: String::new(), - worker_ratio: 0, - uast_worker_ratio: 0, - leaf_worker_divisor: 0, - min_leaf_workers: 0, - buffer_size_multiplier: 0, - budget_limit_ratio: 0, - system_ram_limit_ratio: 0, - static_max_workers: 0, - malloc_trim_interval: 0, - static_memory_limit_ratio: 0, - diff_job_buffer_multiplier: 0, - }, - history: HistoryConfig { - burndown: BurndownConfig { - granularity: 0, - sampling: 0, - track_files: false, - track_people: false, - hibernation_threshold: 0, - hibernation_to_disk: false, - hibernation_directory: String::new(), - debug: false, - goroutines: 0, - }, - couples: CouplesConfig { - coupling_threshold_high: 0, - ownership_few_threshold: 0, - ownership_moderate_threshold: 0, - batch_coupling_threshold: 0, - hll_precision: 0, - top_k_per_file: 0, - min_edge_weight: 0, - }, - devs: DevsConfig { - consider_empty_commits: false, - anonymize: false, - bus_factor_threshold: 0.0, - risk_threshold_critical: 0.0, - risk_threshold_high: 0.0, - risk_threshold_medium: 0.0, - active_threshold_ratio: 0.0, - default_active_days: 0, - hll_precision: 0, - }, - file_history: FileHistoryConfig { - hotspot_threshold_critical: 0, - hotspot_threshold_high: 0, - hotspot_threshold_medium: 0, - }, - imports: ImportsConfig { - goroutines: 0, - max_file_size: 0, - max_dependency_risk_rows: 0, - }, - sentiment: SentimentConfig { - min_comment_length: 0, - gap: 0.0, - neutralizer_weight: 0.0, - max_weight_ratio: 0.0, - positive_threshold: 0.0, - negative_threshold: 0.0, - trend_threshold: 0.0, - low_sentiment_risk_thresh: 0.0, - }, - shotness: ShotnessConfig { - dsl_struct: String::new(), - dsl_name: String::new(), - }, - typos: TyposConfig { max_distance: 0 }, - anomaly: AnomalyConfig { - threshold: 0.0, - window_size: 0, - }, - clones: ClonesConfig { - max_clone_pairs: 0, - num_hashes: 0, - num_bands: 0, - num_rows: 0, - shingle_size: 0, - similarity_type2: 0.0, - similarity_type3: 0.0, - threshold_ratio_yellow: 0.0, - threshold_ratio_red: 0.0, - threshold_pairs_yellow: 0, - threshold_pairs_red: 0, - }, - }, - checkpoint: CheckpointConfig { - enabled: false, - dir: String::new(), - resume: false, - clear_prev: false, - }, - } - } -} - #[cfg(test)] mod tests { use super::*; - /// No env vars set. - fn no_env(_name: &str) -> Option<String> { - None - } - - // ---- loader tests --------------------------------------------------------- - - #[test] - fn load_config_no_file_uses_defaults() { - // Mirrors TestLoadConfig_NoFile_UsesDefaults: an empty YAML file yields - // the registered defaults. - let cfg = load_from_yaml_and_env("", &no_env).unwrap(); - - assert!(cfg.analyzers.is_empty()); - assert_eq!(cfg.pipeline.workers, defaults::DEFAULT_PIPELINE_WORKERS); - assert_eq!(cfg.pipeline.gogc, defaults::DEFAULT_PIPELINE_GOGC); - assert_eq!( - cfg.pipeline.ballast_size, - defaults::DEFAULT_PIPELINE_BALLAST_SIZE - ); - assert_eq!( - cfg.history.burndown.granularity, - defaults::DEFAULT_BURNDOWN_GRANULARITY - ); - assert_eq!( - cfg.history.burndown.sampling, - defaults::DEFAULT_BURNDOWN_SAMPLING - ); - assert_eq!( - cfg.history.burndown.track_files, - defaults::DEFAULT_BURNDOWN_TRACK_FILES - ); - assert_eq!( - cfg.history.burndown.hibernation_threshold, - defaults::DEFAULT_BURNDOWN_HIBERNATION_THRESHOLD - ); - assert_eq!( - cfg.history.devs.consider_empty_commits, - defaults::DEFAULT_DEVS_CONSIDER_EMPTY_COMMITS - ); - assert_eq!(cfg.history.devs.anonymize, defaults::DEFAULT_DEVS_ANONYMIZE); - assert_eq!( - cfg.history.imports.goroutines, - defaults::DEFAULT_IMPORTS_GOROUTINES - ); - assert_eq!( - cfg.history.imports.max_file_size, - defaults::DEFAULT_IMPORTS_MAX_FILE_SIZE - ); - assert_eq!( - cfg.history.sentiment.min_comment_length, - defaults::DEFAULT_SENTIMENT_MIN_COMMENT_LENGTH - ); - assert!((cfg.history.sentiment.gap - defaults::DEFAULT_SENTIMENT_GAP).abs() < 0.001); - assert_eq!( - cfg.history.shotness.dsl_struct, - defaults::DEFAULT_SHOTNESS_DSL_STRUCT - ); - assert_eq!( - cfg.history.shotness.dsl_name, - defaults::DEFAULT_SHOTNESS_DSL_NAME - ); - assert_eq!( - cfg.history.typos.max_distance, - defaults::DEFAULT_TYPOS_MAX_DISTANCE - ); - assert_eq!(cfg.checkpoint.enabled, defaults::DEFAULT_CHECKPOINT_ENABLED); - assert_eq!(cfg.checkpoint.resume, defaults::DEFAULT_CHECKPOINT_RESUME); - } - - #[test] - fn load_config_valid_file_unmarshals() { - // Mirrors TestLoadConfig_ValidFile_Unmarshals. - let content = r#"analyzers: - - burndown - - complexity + /// A document that sets every supported key. + const EVERY_KEY: &str = "\ +analyzers: [static/complexity, history/devs] pipeline: - workers: 8 - memory_budget: "4GB" - blob_cache_size: "512MB" - diff_cache_size: 5000 - commit_batch_size: 200 - gogc: 200 - ballast_size: "256MB" + max_changes_per_commit: 500 + memory_budget: 2GB + chunk_size: 500 history: + tick_size: 12 burndown: granularity: 15 - sampling: 15 + sampling: 10 track_files: true - track_people: true - hibernation_threshold: 2000 + track_people: false devs: consider_empty_commits: true anonymize: true - imports: - goroutines: 8 - max_file_size: 2097152 sentiment: - min_comment_length: 30 - gap: 0.7 + min_comment_length: 0 shotness: - dsl_struct: 'filter(.roles has "Class")' - dsl_name: ".props.identifier" + dsl_struct: 'rfilter(.roles has \"Class\")' + dsl_name: .props.identifier + keep_ended: true typos: max_distance: 3 -checkpoint: - enabled: false - dir: "/tmp/ckpt" - resume: false - clear_prev: true -"#; - let cfg = load_from_yaml_and_env(content, &no_env).unwrap(); - - assert_eq!( - cfg.analyzers, - vec!["burndown".to_owned(), "complexity".to_owned()] - ); - assert_eq!(cfg.pipeline.workers, 8); - assert_eq!(cfg.pipeline.memory_budget, "4GB"); - assert_eq!(cfg.pipeline.blob_cache_size, "512MB"); - assert_eq!(cfg.pipeline.diff_cache_size, 5000); - assert_eq!(cfg.pipeline.commit_batch_size, 200); - assert_eq!(cfg.pipeline.gogc, 200); - assert_eq!(cfg.pipeline.ballast_size, "256MB"); - - assert_eq!(cfg.history.burndown.granularity, 15); - assert_eq!(cfg.history.burndown.sampling, 15); - assert!(cfg.history.burndown.track_files); - assert!(cfg.history.burndown.track_people); - assert_eq!(cfg.history.burndown.hibernation_threshold, 2000); - - assert!(cfg.history.devs.consider_empty_commits); - assert!(cfg.history.devs.anonymize); - - assert_eq!(cfg.history.imports.goroutines, 8); - assert_eq!(cfg.history.imports.max_file_size, 2_097_152); - - assert_eq!(cfg.history.sentiment.min_comment_length, 30); - assert!((cfg.history.sentiment.gap - 0.7).abs() < 0.001); - - assert_eq!( - cfg.history.shotness.dsl_struct, - r#"filter(.roles has "Class")"# - ); - assert_eq!(cfg.history.shotness.dsl_name, ".props.identifier"); - - assert_eq!(cfg.history.typos.max_distance, 3); - - assert!(!cfg.checkpoint.enabled); - assert_eq!(cfg.checkpoint.dir, "/tmp/ckpt"); - assert!(!cfg.checkpoint.resume); - assert!(cfg.checkpoint.clear_prev); - } - - #[test] - fn load_config_explicit_path_overrides() { - // Mirrors TestLoadConfig_ExplicitPath_Overrides via the temp-file path. - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("custom-config.yaml"); - std::fs::write(&path, "pipeline:\n workers: 16\n").unwrap(); - - let cfg = load_config(path.to_str().unwrap()).unwrap(); - assert_eq!(cfg.pipeline.workers, 16); - } - - #[test] - fn load_config_malformed_yaml_returns_error() { - // Mirrors TestLoadConfig_MalformedYAML_ReturnsError. - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("bad.yaml"); - std::fs::write(&path, "pipeline:\n workers: [invalid yaml\n").unwrap(); - - let err = load_config(path.to_str().unwrap()).unwrap_err(); - assert!(err.to_string().contains("read config"), "got: {err}"); - } - - #[test] - fn load_config_unknown_keys_no_error() { - // Mirrors TestLoadConfig_UnknownKeys_NoError: unknown keys are ignored - // (serde ignores unknown fields by default, like the reference loader). - let content = "unknown_section:\n unknown_key: \"value\"\npipeline:\n workers: 4\n"; - let cfg = load_from_yaml_and_env(content, &no_env).unwrap(); - assert_eq!(cfg.pipeline.workers, 4); - } - - #[test] - fn load_config_empty_analyzers_nil_slice() { - // Mirrors TestLoadConfig_EmptyAnalyzers_NilSlice. - let cfg = load_from_yaml_and_env("analyzers: []\n", &no_env).unwrap(); - assert!(cfg.analyzers.is_empty()); - } - - #[test] - fn load_config_partial_config_merges_defaults() { - // Mirrors TestLoadConfig_PartialConfig_MergesDefaults. - let content = "history:\n burndown:\n granularity: 60\n"; - let cfg = load_from_yaml_and_env(content, &no_env).unwrap(); - - assert_eq!(cfg.history.burndown.granularity, 60); - assert_eq!( - cfg.history.burndown.sampling, - defaults::DEFAULT_BURNDOWN_SAMPLING - ); - assert_eq!(cfg.pipeline.workers, defaults::DEFAULT_PIPELINE_WORKERS); - assert_eq!( - cfg.history.typos.max_distance, - defaults::DEFAULT_TYPOS_MAX_DISTANCE - ); - } - - #[test] - fn load_config_env_override_pipeline() { - // Mirrors TestLoadConfig_EnvOverride_Pipeline. - let cfg = load_from_yaml_and_env("", &|name| { - (name == "CODEFANG_PIPELINE_WORKERS").then(|| "32".to_owned()) - }) - .unwrap(); - assert_eq!(cfg.pipeline.workers, 32); - } - - #[test] - fn load_config_env_override_nested_key() { - // Mirrors TestLoadConfig_EnvOverride_NestedKey. - let cfg = load_from_yaml_and_env("", &|name| { - (name == "CODEFANG_HISTORY_BURNDOWN_GRANULARITY").then(|| "60".to_owned()) - }) - .unwrap(); - assert_eq!(cfg.history.burndown.granularity, 60); - } - - #[test] - fn load_config_explicit_path_not_found_returns_error() { - // Mirrors TestLoadConfig_ExplicitPath_NotFound_ReturnsError. - let err = load_config("/nonexistent/path/config.yaml").unwrap_err(); - assert!(matches!(err, LoadError::Read(_)), "got: {err}"); - } - - #[test] - fn env_overrides_file() { - // Precedence check: env > file (flag > env > file > default chain). - let cfg = load_from_yaml_and_env("pipeline:\n workers: 8\n", &|name| { - (name == "CODEFANG_PIPELINE_WORKERS").then(|| "32".to_owned()) - }) - .unwrap(); - assert_eq!(cfg.pipeline.workers, 32); - } - - // ---- types tests ---------------------------------------------------------- - - fn valid_config() -> Config { - let mut c = Config::zero(); - c.pipeline.workers = 4; - c.pipeline.diff_cache_size = 1000; - c.pipeline.commit_batch_size = 100; - c.pipeline.gogc = 200; - c.pipeline.ballast_size = "0".to_owned(); - c.history.burndown.granularity = 30; - c.history.burndown.sampling = 30; - c.history.burndown.hibernation_threshold = 1000; - c.history.imports.goroutines = 4; - c.history.imports.max_file_size = 1 << 20; - c.history.sentiment.min_comment_length = 20; - c.history.sentiment.gap = 0.5; - c.history.typos.max_distance = 4; - c.checkpoint.enabled = true; - c.checkpoint.resume = true; - c - } - - #[test] - fn validate_valid_config_no_error() { - assert!(valid_config().validate().is_ok()); - } - - #[test] - fn validate_zero_config_no_error() { - // Mirrors TestValidate_ZeroConfig_NoError: Config{} is valid. - assert!(Config::zero().validate().is_ok()); - } + anomaly: + threshold: 1.5 + window_size: 5 +"; - #[test] - fn validate_invalid_workers() { - let mut cfg = valid_config(); - cfg.pipeline.workers = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidWorkers)); - } - - #[test] - fn validate_invalid_diff_cache_size() { - let mut cfg = valid_config(); - cfg.pipeline.diff_cache_size = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidDiffCacheSize)); - } - - #[test] - fn validate_invalid_commit_batch_size() { - let mut cfg = valid_config(); - cfg.pipeline.commit_batch_size = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidCommitBatchSize)); - } - - #[test] - fn validate_invalid_gogc() { - let mut cfg = valid_config(); - cfg.pipeline.gogc = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidGogc)); - } - - #[test] - fn validate_invalid_burndown_granularity() { - let mut cfg = valid_config(); - cfg.history.burndown.granularity = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidBurndownGranularity)); - } - - #[test] - fn validate_invalid_burndown_sampling() { - let mut cfg = valid_config(); - cfg.history.burndown.sampling = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidBurndownSampling)); - } - - #[test] - fn validate_invalid_sentiment_min_length() { - let mut cfg = valid_config(); - cfg.history.sentiment.min_comment_length = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidSentimentMinLength)); - } - - #[test] - fn validate_invalid_sentiment_gap_negative() { - let mut cfg = valid_config(); - cfg.history.sentiment.gap = -0.1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidSentimentGap)); - } - - #[test] - fn validate_invalid_sentiment_gap_too_high() { - let mut cfg = valid_config(); - cfg.history.sentiment.gap = 1.1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidSentimentGap)); - } - - #[test] - fn validate_invalid_typos_max_distance() { - let mut cfg = valid_config(); - cfg.history.typos.max_distance = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidTyposMaxDistance)); - } - - #[test] - fn validate_invalid_imports_goroutines() { - let mut cfg = valid_config(); - cfg.history.imports.goroutines = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidImportsGoroutines)); - } - - #[test] - fn validate_invalid_imports_max_file_size() { - let mut cfg = valid_config(); - cfg.history.imports.max_file_size = -1; - assert_eq!(cfg.validate(), Err(ConfigError::InvalidImportsMaxFileSize)); - } - - #[test] - fn config_error_messages_are_frozen() { - // Guards the byte-identical sentinel wording used by `validate config: ...`. - assert_eq!( - ConfigError::InvalidWorkers.message(), - "pipeline.workers must be non-negative" - ); - assert_eq!( - ConfigError::InvalidAnomalyWindowSize.message(), - "history.anomaly.window_size must be at least 2" - ); - assert_eq!( - ConfigError::InvalidCouplesHllPrecision.message(), - "history.couples.hll_precision must be between 4 and 18" - ); - assert_eq!( - LoadError::Validate(ConfigError::InvalidWorkers).to_string(), - "validate config: pipeline.workers must be non-negative" - ); - } - - // ---- apply_to_facts tests ------------------------------------------------- - - const FACT_BURNDOWN_GRANULARITY: &str = "Burndown.Granularity"; - const FACT_BURNDOWN_SAMPLING: &str = "Burndown.Sampling"; - const FACT_BURNDOWN_TRACK_FILES: &str = "Burndown.TrackFiles"; - const FACT_BURNDOWN_TRACK_PEOPLE: &str = "Burndown.TrackPeople"; - const FACT_BURNDOWN_HIBERNATION_THRESHOLD: &str = "Burndown.HibernationThreshold"; - const FACT_BURNDOWN_HIBERNATION_ON_DISK: &str = "Burndown.HibernationOnDisk"; - const FACT_BURNDOWN_HIBERNATION_DIRECTORY: &str = "Burndown.HibernationDirectory"; - const FACT_BURNDOWN_DEBUG: &str = "Burndown.Debug"; - const FACT_BURNDOWN_GOROUTINES: &str = "Burndown.Goroutines"; - const FACT_DEVS_CONSIDER_EMPTY: &str = "Devs.ConsiderEmptyCommits"; - const FACT_DEVS_ANONYMIZE: &str = "Devs.Anonymize"; - const FACT_IMPORTS_GOROUTINES: &str = "Imports.Goroutines"; - const FACT_IMPORTS_MAX_FILE_SIZE: &str = "Imports.MaxFileSize"; - const FACT_SENTIMENT_MIN_LENGTH: &str = "CommentSentiment.MinLength"; - const FACT_SENTIMENT_GAP: &str = "CommentSentiment.Gap"; - const FACT_SHOTNESS_DSL_STRUCT: &str = "Shotness.DSLStruct"; - const FACT_SHOTNESS_DSL_NAME: &str = "Shotness.DSLName"; - const FACT_TYPOS_MAX_DISTANCE: &str = "TyposDatasetBuilder.MaximumAllowedDistance"; - const FACT_ANOMALY_THRESHOLD: &str = "TemporalAnomaly.Threshold"; - const FACT_ANOMALY_WINDOW_SIZE: &str = "TemporalAnomaly.WindowSize"; - - #[test] - fn apply_to_facts_burndown() { - let mut cfg = Config::zero(); - cfg.history.burndown = BurndownConfig { - granularity: 60, - sampling: 60, - track_files: true, - track_people: true, - hibernation_threshold: 2000, - hibernation_to_disk: true, - hibernation_directory: "/tmp/hib".to_owned(), - debug: true, - goroutines: 16, - }; - - let mut facts = Facts::new(); - cfg.apply_to_facts(&mut facts); - - assert_eq!(facts[FACT_BURNDOWN_GRANULARITY].as_int(), Some(60)); - assert_eq!(facts[FACT_BURNDOWN_SAMPLING].as_int(), Some(60)); - assert_eq!(facts[FACT_BURNDOWN_TRACK_FILES].as_bool(), Some(true)); - assert_eq!(facts[FACT_BURNDOWN_TRACK_PEOPLE].as_bool(), Some(true)); - assert_eq!( - facts[FACT_BURNDOWN_HIBERNATION_THRESHOLD].as_int(), - Some(2000) - ); - assert_eq!( - facts[FACT_BURNDOWN_HIBERNATION_ON_DISK].as_bool(), - Some(true) - ); - assert_eq!( - facts[FACT_BURNDOWN_HIBERNATION_DIRECTORY].as_str(), - Some("/tmp/hib") - ); - assert_eq!(facts[FACT_BURNDOWN_DEBUG].as_bool(), Some(true)); - assert_eq!(facts[FACT_BURNDOWN_GOROUTINES].as_int(), Some(16)); - } - - #[test] - fn apply_to_facts_devs() { - let mut cfg = Config::zero(); - cfg.history.devs.consider_empty_commits = true; - cfg.history.devs.anonymize = true; - - let mut facts = Facts::new(); - cfg.apply_to_facts(&mut facts); - - assert_eq!(facts[FACT_DEVS_CONSIDER_EMPTY].as_bool(), Some(true)); - assert_eq!(facts[FACT_DEVS_ANONYMIZE].as_bool(), Some(true)); - } - - #[test] - fn apply_to_facts_imports() { - let mut cfg = Config::zero(); - cfg.history.imports.goroutines = 16; - cfg.history.imports.max_file_size = 4_194_304; - - let mut facts = Facts::new(); - cfg.apply_to_facts(&mut facts); - - assert_eq!(facts[FACT_IMPORTS_GOROUTINES].as_int(), Some(16)); - assert_eq!(facts[FACT_IMPORTS_MAX_FILE_SIZE].as_int(), Some(4_194_304)); - } - - #[test] - fn apply_to_facts_sentiment() { - let mut cfg = Config::zero(); - cfg.history.sentiment.min_comment_length = 40; - cfg.history.sentiment.gap = 0.8; - - let mut facts = Facts::new(); - cfg.apply_to_facts(&mut facts); - - assert_eq!(facts[FACT_SENTIMENT_MIN_LENGTH].as_int(), Some(40)); - assert!((facts[FACT_SENTIMENT_GAP].as_float().unwrap() - 0.8).abs() < 0.001); + fn parse(yaml: &str) -> Config { + serde_yaml::from_str(yaml).unwrap() } #[test] - fn apply_to_facts_shotness() { - let mut cfg = Config::zero(); - cfg.history.shotness.dsl_struct = r#"filter(.roles has "Class")"#.to_owned(); - cfg.history.shotness.dsl_name = ".props.identifier".to_owned(); - - let mut facts = Facts::new(); - cfg.apply_to_facts(&mut facts); - + fn every_key_deserializes_and_is_a_setting_in_key_order() { + let cfg = parse(EVERY_KEY); + assert_eq!(cfg.validate(), Ok(())); + let settings = cfg.settings(); + let keys: Vec<&str> = settings.iter().map(|s| s.key).collect(); + assert_eq!(keys, KEYS); assert_eq!( - facts[FACT_SHOTNESS_DSL_STRUCT].as_str(), - Some(r#"filter(.roles has "Class")"#) + settings[0].value, + Value::List(vec!["static/complexity".into(), "history/devs".into()]) ); - assert_eq!( - facts[FACT_SHOTNESS_DSL_NAME].as_str(), - Some(".props.identifier") - ); - } - - #[test] - fn apply_to_facts_typos() { - let mut cfg = Config::zero(); - cfg.history.typos.max_distance = 6; - - let mut facts = Facts::new(); - cfg.apply_to_facts(&mut facts); - - assert_eq!(facts[FACT_TYPOS_MAX_DISTANCE].as_int(), Some(6)); - } - - #[test] - fn apply_to_facts_anomaly() { - let mut cfg = Config::zero(); - cfg.history.anomaly.threshold = 3.5; - cfg.history.anomaly.window_size = 30; - - let mut facts = Facts::new(); - cfg.apply_to_facts(&mut facts); - - assert!((facts[FACT_ANOMALY_THRESHOLD].as_float().unwrap() - 3.5).abs() < 0.001); - assert_eq!(facts[FACT_ANOMALY_WINDOW_SIZE].as_int(), Some(30)); - } - - #[test] - fn apply_to_facts_zero_values_skips_numeric_overrides() { - // Mirrors TestApplyToFacts_ZeroValues_SkipsNumericOverrides. - let cfg = Config::zero(); - let mut facts = Facts::new(); - facts.insert(FACT_BURNDOWN_GRANULARITY.to_owned(), FactValue::Int(30)); - facts.insert(FACT_TYPOS_MAX_DISTANCE.to_owned(), FactValue::Int(4)); - - cfg.apply_to_facts(&mut facts); - - assert_eq!(facts[FACT_BURNDOWN_GRANULARITY].as_int(), Some(30)); - assert_eq!(facts[FACT_TYPOS_MAX_DISTANCE].as_int(), Some(4)); + assert_eq!(settings[2].value, Value::Text("2GB".into())); + assert_eq!(settings[3].value, Value::Int(500)); + assert_eq!(settings[7].value, Value::Bool(true)); + assert_eq!( + settings[12].value, + Value::Text("rfilter(.roles has \"Class\")".into()) + ); + assert_eq!(settings[14].value, Value::Bool(true)); + assert_eq!(settings[16].value, Value::Float(1.5)); + } + + /// A budget written as a plain byte count is the same setting as its + /// text form. + #[test] + fn a_numeric_memory_budget_is_a_byte_count() { + let cfg = parse("pipeline:\n memory_budget: 1073741824\n"); + assert_eq!( + cfg.settings(), + vec![Setting { + key: keys::MEMORY_BUDGET, + value: Value::Text("1073741824".into()), + }] + ); + } + + #[test] + fn the_default_configuration_sets_nothing() { + assert!(Config::default().settings().is_empty()); + assert_eq!(Config::default().validate(), Ok(())); + } + + #[test] + fn empty_lists_and_blank_queries_count_as_unset() { + let cfg = + parse("analyzers: []\nhistory:\n shotness:\n dsl_struct: ''\n dsl_name: ' '\n"); + assert!(cfg.settings().is_empty()); + } + + #[test] + fn null_values_count_as_unset() { + let cfg = parse("history:\n anomaly:\n window_size:\n"); + assert_eq!(cfg.history.anomaly.window_size, None); + } + + #[test] + fn values_render_as_flag_arguments() { + assert_eq!(Value::Int(-3).to_args(), vec!["-3"]); + assert_eq!(Value::Float(2.0).to_args(), vec!["2"]); + assert_eq!(Value::Float(0.25).to_args(), vec!["0.25"]); + assert_eq!(Value::Bool(false).to_args(), vec!["false"]); + assert_eq!(Value::Text("x y".into()).to_args(), vec!["x y"]); + assert_eq!( + Value::List(vec!["a".into(), "b".into()]).to_args(), + vec!["a", "b"] + ); + } + + #[test] + fn every_rule_is_enforced_with_the_key_named() { + let cases = [ + ( + "pipeline:\n max_changes_per_commit: -1\n", + "pipeline.max_changes_per_commit must be >= 0, got -1", + ), + ( + "pipeline:\n chunk_size: 0\n", + "pipeline.chunk_size must be >= 1, got 0", + ), + ( + "history:\n tick_size: 0\n", + "history.tick_size must be > 0, got 0", + ), + ( + "history:\n burndown:\n granularity: 0\n", + "history.burndown.granularity must be > 0, got 0", + ), + ( + "history:\n burndown:\n sampling: -2\n", + "history.burndown.sampling must be > 0, got -2", + ), + ( + "history:\n sentiment:\n min_comment_length: -1\n", + "history.sentiment.min_comment_length must be >= 0, got -1", + ), + ( + "history:\n typos:\n max_distance: 0\n", + "history.typos.max_distance must be > 0, got 0", + ), + ( + "history:\n anomaly:\n threshold: 0\n", + "history.anomaly.threshold must be > 0, got 0", + ), + ( + "history:\n anomaly:\n threshold: .nan\n", + "history.anomaly.threshold must be > 0, got NaN", + ), + ( + "history:\n anomaly:\n window_size: 1\n", + "history.anomaly.window_size must be >= 2, got 1", + ), + ]; + for (yaml, message) in cases { + let err = parse(yaml).validate().unwrap_err(); + assert_eq!(err.to_string(), message, "{yaml}"); + } } #[test] - fn apply_to_facts_boolean_fields_always_applied() { - // Mirrors TestApplyToFacts_BooleanFields_AlwaysApplied: false overrides true. - let mut cfg = Config::zero(); - cfg.history.burndown.track_files = false; - cfg.history.burndown.track_people = false; - cfg.history.burndown.debug = false; - cfg.history.devs.consider_empty_commits = false; - cfg.history.devs.anonymize = false; - - let mut facts = Facts::new(); - facts.insert(FACT_BURNDOWN_TRACK_FILES.to_owned(), FactValue::Bool(true)); - facts.insert(FACT_BURNDOWN_TRACK_PEOPLE.to_owned(), FactValue::Bool(true)); - - cfg.apply_to_facts(&mut facts); - - assert_eq!(facts[FACT_BURNDOWN_TRACK_FILES].as_bool(), Some(false)); - assert_eq!(facts[FACT_BURNDOWN_TRACK_PEOPLE].as_bool(), Some(false)); - assert_eq!(facts[FACT_DEVS_CONSIDER_EMPTY].as_bool(), Some(false)); - assert_eq!(facts[FACT_DEVS_ANONYMIZE].as_bool(), Some(false)); + fn the_first_violation_in_key_order_is_reported() { + let cfg = + parse("history:\n anomaly:\n window_size: 1\n threshold: -1\n tick_size: 0\n"); + assert_eq!(cfg.validate().unwrap_err().key, keys::TICK_SIZE); } } diff --git a/crates/cf-config/src/loader.rs b/crates/cf-config/src/loader.rs index b955ae64..62527a18 100644 --- a/crates/cf-config/src/loader.rs +++ b/crates/cf-config/src/loader.rs @@ -1,339 +1,504 @@ -//! Layered configuration loader. +//! Locating, reading and layering the configuration. //! -//! Precedence is **flag > env (`CODEFANG_`) > file > default** (CLI -//! compatibility contract): -//! -//! 1. **default** — [`Config::default`] (the registered default values). -//! 2. **file** — `.codefang.yaml` (explicit path, else CWD then `$HOME`), -//! deserialized over the defaults via `serde(default)`. -//! 3. **env** — every `CODEFANG_<UPPER_SNAKE>` variable overrides the matching -//! dotted key (`.` maps to `_` under the `CODEFANG` prefix). -//! 4. **flag** — explicit programmatic overrides applied last (supplied by the -//! caller, since clap lives in `cf-commands`). - -use crate::types::{Config, ConfigError}; -use std::env; +//! Layers, lowest first: the compiled defaults (every field `None`), the +//! config file, the `CODEFANG_*` environment. The caller applies the result +//! as the default of each flag, so an explicit flag wins over all of them. + +use crate::types::{Config, ConfigError, KEYS}; +use serde_yaml::{Mapping, Value as Yaml}; use std::path::{Path, PathBuf}; -/// Config file base name without extension. -const CONFIG_NAME: &str = ".codefang"; -/// Config file extension. -const CONFIG_EXT: &str = "yaml"; -/// Environment variable prefix. -const ENV_PREFIX: &str = "CODEFANG"; +/// The configuration file name searched in the working directory and in +/// `$HOME`. +pub const CONFIG_FILE_NAME: &str = ".codefang.yaml"; -/// Error returned by [`load_config`]. -/// -/// The `Display` wrapping (`read config: ...`, `unmarshal config: ...`, -/// `validate config: ...`) is part of the CLI compatibility contract, so any -/// error-substring assertions continue to hold. +/// Prefix of the environment variable of a key: `CODEFANG_` + the key +/// upper-cased with `.` replaced by `_`. +pub const ENV_PREFIX: &str = "CODEFANG_"; + +/// Why the configuration could not be loaded. #[derive(Debug, thiserror::Error)] pub enum LoadError { - /// Reading the config file failed (`read config: <cause>`). - /// - /// A *missing* file at a searched path is NOT an error (defaults are - /// used), but an explicitly-set path that cannot be read, or a malformed - /// YAML document, is wrapped here. - #[error("read config: {0}")] - Read(String), - /// Deserializing the merged config into the struct failed - /// (`unmarshal config: <cause>`). - #[error("unmarshal config: {0}")] - Unmarshal(String), - /// Validation failed (`validate config: <cause>`). - #[error("validate config: {0}")] - Validate(#[source] ConfigError), + /// The file could not be read. + #[error("cannot read config {}: {cause}", .path.display())] + Read { + /// The file. + path: PathBuf, + /// The I/O error. + cause: std::io::Error, + }, + /// The file is not a YAML mapping of the documented types. + #[error("invalid config {}: {cause}", .path.display())] + Parse { + /// The file. + path: PathBuf, + /// The parser's message (it names the key and the line). + cause: String, + }, + /// A `CODEFANG_*` variable does not hold a value of its key's type. + #[error("invalid environment variable {name}: {cause}")] + Env { + /// The variable name. + name: String, + /// The parser's message. + cause: String, + }, + /// A value breaks its key's rule; `origin` is the file path or the + /// environment variable that set it. + #[error("invalid config {origin}: {error}")] + Invalid { + /// The file path or variable name. + origin: String, + /// The violated rule. + error: ConfigError, + }, +} + +/// A loaded configuration. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Loaded { + /// The file the configuration came from (`None`: no file). + pub path: Option<PathBuf>, + /// The layered configuration. + pub config: Config, + /// Keys the file sets that nothing reads, in document order. + pub unsupported_keys: Vec<String>, } -/// Loads configuration from file, environment variables, and defaults. +/// The default search directories: the working directory, then `$HOME`. +#[must_use] +pub fn default_search_dirs() -> Vec<PathBuf> { + let mut dirs = vec![PathBuf::from(".")]; + if let Some(home) = std::env::var_os("HOME").filter(|h| !h.is_empty()) { + dirs.push(PathBuf::from(home)); + } + dirs +} + +/// Loads the configuration. /// -/// If `config_path` is non-empty it is used as the explicit config file path; -/// otherwise `.codefang.yaml` is searched in CWD and `$HOME`. A missing config -/// file (when searching) is not an error — defaults are used. An explicit -/// path that does not exist *is* an error (reference loader behavior). +/// `explicit` (from `--config`) must be readable; without it the first +/// [`CONFIG_FILE_NAME`] found in `search_dirs` is read, and none at all is +/// not an error. `env` resolves an environment variable name; the variable +/// of a key overrides the file. /// /// # Errors -/// - [`LoadError::Read`] if an explicit config file cannot be read or any -/// located file is malformed YAML. -/// - [`LoadError::Unmarshal`] if the YAML cannot be deserialized into [`Config`]. -/// - [`LoadError::Validate`] if [`Config::validate`] fails. -pub fn load_config(config_path: &str) -> Result<Config, LoadError> { - // Layers 1+2: file over defaults (missing fields fall back via - // serde(default)), or pure defaults when no file is found. - let mut cfg = match read_config_source(config_path)? { - Some(contents) => parse_yaml_over_defaults(&contents)?, - None => Config::default(), +/// +/// [`LoadError`] naming the file, variable or key at fault. +pub fn load<F>( + explicit: Option<&Path>, + search_dirs: &[PathBuf], + env: F, +) -> Result<Loaded, LoadError> +where + F: Fn(&str) -> Option<String>, +{ + let (path, contents) = match explicit { + Some(p) => (Some(p.to_path_buf()), Some(read(p)?)), + None => match discover(search_dirs)? { + Some((p, c)) => (Some(p), Some(c)), + None => (None, None), + }, }; + let file_tree = match (&path, contents) { + (Some(p), Some(c)) => parse_file(p, &c)?, + _ => Yaml::Null, + }; + let mut unsupported_keys = Vec::new(); + collect_unsupported(&file_tree, "", &mut unsupported_keys); - // Layer 3: environment (CODEFANG_*). - apply_env_overrides(&mut cfg, &env_lookup); - - // Validate the merged result. - cfg.validate().map_err(LoadError::Validate)?; + let mut merged = file_tree; + for key in KEYS { + let name = env_name(key); + if let Some(raw) = env(&name) { + let leaf = env_value(key, &raw); + let only = nest(key, leaf.clone()); + let cfg: Config = serde_yaml::from_value(only).map_err(|e| LoadError::Env { + name: name.clone(), + cause: e.to_string(), + })?; + cfg.validate().map_err(|error| LoadError::Invalid { + origin: name.clone(), + error, + })?; + overlay(&mut merged, key, leaf); + } + } + let config: Config = serde_yaml::from_value(merged).map_err(|e| LoadError::Parse { + path: path.clone().unwrap_or_default(), + cause: e.to_string(), + })?; + Ok(Loaded { + path, + config, + unsupported_keys, + }) +} - Ok(cfg) +/// The environment variable of `key`. +#[must_use] +pub fn env_name(key: &str) -> String { + format!("{ENV_PREFIX}{}", key.to_uppercase().replace('.', "_")) } -/// Reads the config file contents, or `None` when searching and no file exists. -/// -/// Returns [`LoadError::Read`] for an explicit path that cannot be read. -fn read_config_source(config_path: &str) -> Result<Option<String>, LoadError> { - if !config_path.is_empty() { - // Explicit path: a missing/unreadable file is an error (reference - // loader behavior). - return std::fs::read_to_string(config_path) - .map(Some) - .map_err(|e| LoadError::Read(e.to_string())); - } +/// Reads `path` whole. +fn read(path: &Path) -> Result<String, LoadError> { + std::fs::read_to_string(path).map_err(|cause| LoadError::Read { + path: path.to_path_buf(), + cause, + }) +} - // Search CWD then $HOME for `.codefang.yaml`. A missing file is fine. - for dir in search_paths() { - let candidate = dir.join(format!("{CONFIG_NAME}.{CONFIG_EXT}")); +/// The first [`CONFIG_FILE_NAME`] in `dirs` and its contents. A missing file +/// moves on to the next directory; any other read failure is an error. +fn discover(dirs: &[PathBuf]) -> Result<Option<(PathBuf, String)>, LoadError> { + for dir in dirs { + let candidate = dir.join(CONFIG_FILE_NAME); match std::fs::read_to_string(&candidate) { - Ok(contents) => return Ok(Some(contents)), + Ok(contents) => return Ok(Some((candidate, contents))), Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => return Err(LoadError::Read(e.to_string())), + Err(cause) => { + return Err(LoadError::Read { + path: candidate, + cause, + }) + } } } - Ok(None) } -/// Returns the config search directories, in order: CWD, then `$HOME`. -fn search_paths() -> Vec<PathBuf> { - let mut paths = vec![PathBuf::from(".")]; - if let Some(home) = home_dir() { - paths.push(home); +/// Parses the file into a YAML tree, checks the documented types and the +/// value rules. A file holding only comments is the empty configuration. +fn parse_file(path: &Path, contents: &str) -> Result<Yaml, LoadError> { + let parse_error = |cause: String| LoadError::Parse { + path: path.to_path_buf(), + cause, + }; + let tree: Yaml = serde_yaml::from_str(contents).map_err(|e| parse_error(e.to_string()))?; + if !matches!(tree, Yaml::Null | Yaml::Mapping(_)) { + return Err(parse_error("the document must be a mapping of keys".into())); } - paths -} - -/// Best-effort `$HOME` resolution. -fn home_dir() -> Option<PathBuf> { - env::var_os("HOME") - .filter(|h| !h.is_empty()) - .map(PathBuf::from) + if tree.is_null() { + return Ok(tree); + } + // Typed parse of the TEXT (not the tree) so the message names the key + // path and the line: `history.anomaly.window_size: invalid type: …`. + let cfg: Config = serde_yaml::from_str(contents).map_err(|e| parse_error(e.to_string()))?; + cfg.validate().map_err(|error| LoadError::Invalid { + origin: path.display().to_string(), + error, + })?; + Ok(tree) } -/// Deserializes YAML over the registered defaults. -/// -/// An empty document yields the all-defaults [`Config`]. Malformed YAML is -/// surfaced as [`LoadError::Read`] (`read config: ...`), matching the -/// reference loader's wrapping of parse errors. -fn parse_yaml_over_defaults(contents: &str) -> Result<Config, LoadError> { - if contents.trim().is_empty() { - return Ok(Config::default()); +/// Appends to `out` every leaf path under `node` (prefixed with `prefix`) +/// that is not a supported key, descending into mappings. +fn collect_unsupported(node: &Yaml, prefix: &str, out: &mut Vec<String>) { + let Yaml::Mapping(map) = node else { + // `pipeline:` with every child commented out is an empty section. + let empty_section = node.is_null() && is_section(prefix); + if !prefix.is_empty() && !empty_section { + out.push(prefix.to_owned()); + } + return; + }; + if map.is_empty() && !prefix.is_empty() && !is_section(prefix) { + out.push(prefix.to_owned()); + } + for (k, v) in map { + let name = match k { + Yaml::String(s) => s.clone(), + other => serde_yaml::to_string(other) + .map(|s| s.trim().to_owned()) + .unwrap_or_default(), + }; + let path = if prefix.is_empty() { + name + } else { + format!("{prefix}.{name}") + }; + if KEYS.contains(&path.as_str()) { + continue; + } + collect_unsupported(v, &path, out); } - // serde(default) on every struct/field merges the file over the registered - // defaults: absent keys fall back to the default values. - serde_yaml::from_str::<Config>(contents).map_err(|e| LoadError::Read(e.to_string())) } -/// Resolves a `CODEFANG_*` environment variable, returning its value if set. -fn env_lookup(name: &str) -> Option<String> { - env::var(name).ok() +/// True when `prefix` is a section holding supported keys (`history`, +/// `history.anomaly`, …). +fn is_section(prefix: &str) -> bool { + KEYS.iter() + .any(|k| k.strip_prefix(prefix).is_some_and(|r| r.starts_with('.'))) } -/// Applies `CODEFANG_*` environment overrides over `cfg` (env > file). -/// -/// `lookup` resolves a full env-var name (e.g. `CODEFANG_PIPELINE_WORKERS`) to -/// its string value; the name is the dotted config key upper-cased with `.` -/// replaced by `_` under the `CODEFANG` prefix. Parameterizing the lookup -/// keeps the function testable without mutating the process environment. -pub fn apply_env_overrides<F>(cfg: &mut Config, lookup: &F) -where - F: Fn(&str) -> Option<String>, -{ - for (key, setter) in ENV_FIELDS { - let env_name = format!("{ENV_PREFIX}_{}", key.to_uppercase().replace('.', "_")); - if let Some(raw) = lookup(&env_name) { - setter(cfg, &raw); - } +/// The YAML value of an environment variable: a comma / whitespace +/// separated list for `analyzers`, the raw text for the string keys, a YAML +/// scalar otherwise (`3`, `true`, `2.5`). +fn env_value(key: &str, raw: &str) -> Yaml { + use crate::types::keys; + if key == keys::SHOTNESS_DSL_STRUCT + || key == keys::SHOTNESS_DSL_NAME + || key == keys::MEMORY_BUDGET + { + return Yaml::String(raw.to_owned()); } - // analyzers (string list) — a single env var is split on whitespace - // (reference loader behavior, kept for parity). - let analyzers_name = format!("{ENV_PREFIX}_ANALYZERS"); - if let Some(raw) = lookup(&analyzers_name) { - cfg.analyzers = parse_string_slice_env(&raw); + if key == keys::ANALYZERS { + let items = raw + .split(|c: char| c == ',' || c.is_whitespace()) + .filter(|s| !s.is_empty()) + .map(|s| Yaml::String(s.to_owned())) + .collect(); + return Yaml::Sequence(items); } + serde_yaml::from_str(raw).unwrap_or_else(|_| Yaml::String(raw.to_owned())) } -/// Parses a string-list env value (whitespace-separated). -fn parse_string_slice_env(raw: &str) -> Vec<String> { - raw.split_whitespace().map(str::to_owned).collect() +/// `leaf` nested under the dotted `key` (`a.b` → `{a: {b: leaf}}`). +fn nest(key: &str, leaf: Yaml) -> Yaml { + key.rsplit('.').fold(leaf, |inner, part| { + let mut map = Mapping::new(); + map.insert(Yaml::String(part.to_owned()), inner); + Yaml::Mapping(map) + }) } -/// Parses an integer env value (best-effort coercion). -/// -/// Invalid integers leave the existing value unchanged: unparsable overrides -/// are conservatively ignored so a typo cannot silently zero a tuned value. -fn set_i64(target: &mut i64, raw: &str) { - if let Ok(v) = raw.trim().parse::<i64>() { - *target = v; +/// Sets the dotted `key` of `tree` to `leaf`, creating (or replacing +/// non-mapping) intermediate sections. +fn overlay(tree: &mut Yaml, key: &str, leaf: Yaml) { + let mut node = tree; + let parts: Vec<&str> = key.split('.').collect(); + for (i, part) in parts.iter().enumerate() { + if !matches!(node, Yaml::Mapping(_)) { + *node = Yaml::Mapping(Mapping::new()); + } + let Yaml::Mapping(map) = node else { + unreachable!("just made a mapping") + }; + let k = Yaml::String((*part).to_owned()); + if i + 1 == parts.len() { + map.insert(k, leaf); + return; + } + node = map.entry(k).or_insert(Yaml::Null); } } -/// Parses a float env value; ignores unparsable input. -fn set_f64(target: &mut f64, raw: &str) { - if let Ok(v) = raw.trim().parse::<f64>() { - *target = v; +#[cfg(test)] +mod tests { + use super::*; + use crate::types::keys; + + fn no_env(_: &str) -> Option<String> { + None } -} -/// Parses a bool env value; ignores unparsable input. -fn set_bool(target: &mut bool, raw: &str) { - match raw.trim().to_ascii_lowercase().as_str() { - "1" | "t" | "true" | "yes" | "y" | "on" => *target = true, - "0" | "f" | "false" | "no" | "n" | "off" => *target = false, - _ => {} + fn write(dir: &Path, name: &str, body: &str) -> PathBuf { + let p = dir.join(name); + std::fs::write(&p, body).unwrap(); + p } -} -/// Sets a string field directly to the raw env value. -fn set_string(target: &mut String, raw: &str) { - raw.clone_into(target); -} + #[test] + fn env_names_follow_the_key() { + assert_eq!( + env_name(keys::ANOMALY_WINDOW_SIZE), + "CODEFANG_HISTORY_ANOMALY_WINDOW_SIZE" + ); + assert_eq!(env_name(keys::ANALYZERS), "CODEFANG_ANALYZERS"); + } -/// Type of a per-key env override setter. -type EnvSetter = fn(&mut Config, &str); + /// The budget variable holds a size, kept as text whatever it looks + /// like. + #[test] + fn the_memory_budget_variable_is_text() { + for raw in ["512MB", "1073741824"] { + let dir = tempfile::tempdir().unwrap(); + let env = + |name: &str| (name == "CODEFANG_PIPELINE_MEMORY_BUDGET").then(|| raw.to_owned()); + let loaded = load(None, &[dir.path().to_path_buf()], env).unwrap(); + assert_eq!(loaded.config.pipeline.memory_budget.as_deref(), Some(raw)); + } + } -/// Full table of `(dotted-key, setter)` pairs for env overrides. -/// -/// [`apply_env_overrides`] derives the env-var name by upper-casing and -/// replacing `.` with `_` under the `CODEFANG_` prefix. Every scalar config -/// field appears here so the env layer is exhaustive. -#[rustfmt::skip] -const ENV_FIELDS: &[(&str, EnvSetter)] = &[ - // pipeline - ("pipeline.workers", |c, v| set_i64(&mut c.pipeline.workers, v)), - ("pipeline.memory_budget", |c, v| set_string(&mut c.pipeline.memory_budget, v)), - ("pipeline.blob_cache_size", |c, v| set_string(&mut c.pipeline.blob_cache_size, v)), - ("pipeline.diff_cache_size", |c, v| set_i64(&mut c.pipeline.diff_cache_size, v)), - ("pipeline.blob_arena_size", |c, v| set_string(&mut c.pipeline.blob_arena_size, v)), - ("pipeline.commit_batch_size", |c, v| set_i64(&mut c.pipeline.commit_batch_size, v)), - ("pipeline.gogc", |c, v| set_i64(&mut c.pipeline.gogc, v)), - ("pipeline.ballast_size", |c, v| set_string(&mut c.pipeline.ballast_size, v)), - ("pipeline.memory_limit", |c, v| set_string(&mut c.pipeline.memory_limit, v)), - ("pipeline.worker_timeout", |c, v| set_string(&mut c.pipeline.worker_timeout, v)), - ("pipeline.uast_spill_threshold", |c, v| set_i64(&mut c.pipeline.uast_spill_threshold, v)), - ("pipeline.intra_commit_parallel_threshold", |c, v| set_i64(&mut c.pipeline.intra_commit_parallel_threshold, v)), - ("pipeline.max_intra_commit_workers", |c, v| set_i64(&mut c.pipeline.max_intra_commit_workers, v)), - ("pipeline.max_uast_blob_size", |c, v| set_i64(&mut c.pipeline.max_uast_blob_size, v)), - ("pipeline.uast_parse_timeout", |c, v| set_string(&mut c.pipeline.uast_parse_timeout, v)), - ("pipeline.max_changes_per_commit", |c, v| set_i64(&mut c.pipeline.max_changes_per_commit, v)), - ("pipeline.max_diff_batch_size", |c, v| set_i64(&mut c.pipeline.max_diff_batch_size, v)), - ("pipeline.memory_budget_ratio", |c, v| set_i64(&mut c.pipeline.memory_budget_ratio, v)), - ("pipeline.memory_budget_cap", |c, v| set_string(&mut c.pipeline.memory_budget_cap, v)), - ("pipeline.memory_limit_ratio", |c, v| set_i64(&mut c.pipeline.memory_limit_ratio, v)), - ("pipeline.uast_spill_trim_interval", |c, v| set_i64(&mut c.pipeline.uast_spill_trim_interval, v)), - ("pipeline.native_trim_interval", |c, v| set_i64(&mut c.pipeline.native_trim_interval, v)), - ("pipeline.max_streaming_buffering", |c, v| set_i64(&mut c.pipeline.max_streaming_buffering, v)), - ("pipeline.drain_prefetch_timeout", |c, v| set_string(&mut c.pipeline.drain_prefetch_timeout, v)), - ("pipeline.sampler_interval", |c, v| set_string(&mut c.pipeline.sampler_interval, v)), - ("pipeline.worker_ratio", |c, v| set_i64(&mut c.pipeline.worker_ratio, v)), - ("pipeline.uast_worker_ratio", |c, v| set_i64(&mut c.pipeline.uast_worker_ratio, v)), - ("pipeline.leaf_worker_divisor", |c, v| set_i64(&mut c.pipeline.leaf_worker_divisor, v)), - ("pipeline.min_leaf_workers", |c, v| set_i64(&mut c.pipeline.min_leaf_workers, v)), - ("pipeline.buffer_size_multiplier", |c, v| set_i64(&mut c.pipeline.buffer_size_multiplier, v)), - ("pipeline.budget_limit_ratio", |c, v| set_i64(&mut c.pipeline.budget_limit_ratio, v)), - ("pipeline.system_ram_limit_ratio", |c, v| set_i64(&mut c.pipeline.system_ram_limit_ratio, v)), - ("pipeline.static_max_workers", |c, v| set_i64(&mut c.pipeline.static_max_workers, v)), - ("pipeline.malloc_trim_interval", |c, v| set_i64(&mut c.pipeline.malloc_trim_interval, v)), - ("pipeline.static_memory_limit_ratio", |c, v| set_i64(&mut c.pipeline.static_memory_limit_ratio, v)), - ("pipeline.diff_job_buffer_multiplier", |c, v| set_i64(&mut c.pipeline.diff_job_buffer_multiplier, v)), - // history.burndown - ("history.burndown.granularity", |c, v| set_i64(&mut c.history.burndown.granularity, v)), - ("history.burndown.sampling", |c, v| set_i64(&mut c.history.burndown.sampling, v)), - ("history.burndown.track_files", |c, v| set_bool(&mut c.history.burndown.track_files, v)), - ("history.burndown.track_people", |c, v| set_bool(&mut c.history.burndown.track_people, v)), - ("history.burndown.hibernation_threshold", |c, v| set_i64(&mut c.history.burndown.hibernation_threshold, v)), - ("history.burndown.hibernation_to_disk", |c, v| set_bool(&mut c.history.burndown.hibernation_to_disk, v)), - ("history.burndown.hibernation_directory", |c, v| set_string(&mut c.history.burndown.hibernation_directory, v)), - ("history.burndown.debug", |c, v| set_bool(&mut c.history.burndown.debug, v)), - ("history.burndown.goroutines", |c, v| set_i64(&mut c.history.burndown.goroutines, v)), - // history.couples - ("history.couples.coupling_threshold_high", |c, v| set_i64(&mut c.history.couples.coupling_threshold_high, v)), - ("history.couples.ownership_few_threshold", |c, v| set_i64(&mut c.history.couples.ownership_few_threshold, v)), - ("history.couples.ownership_moderate_threshold", |c, v| set_i64(&mut c.history.couples.ownership_moderate_threshold, v)), - ("history.couples.batch_coupling_threshold", |c, v| set_i64(&mut c.history.couples.batch_coupling_threshold, v)), - ("history.couples.hll_precision", |c, v| set_i64(&mut c.history.couples.hll_precision, v)), - ("history.couples.top_k_per_file", |c, v| set_i64(&mut c.history.couples.top_k_per_file, v)), - ("history.couples.min_edge_weight", |c, v| set_i64(&mut c.history.couples.min_edge_weight, v)), - // history.devs - ("history.devs.consider_empty_commits", |c, v| set_bool(&mut c.history.devs.consider_empty_commits, v)), - ("history.devs.anonymize", |c, v| set_bool(&mut c.history.devs.anonymize, v)), - ("history.devs.bus_factor_threshold", |c, v| set_f64(&mut c.history.devs.bus_factor_threshold, v)), - ("history.devs.risk_threshold_critical", |c, v| set_f64(&mut c.history.devs.risk_threshold_critical, v)), - ("history.devs.risk_threshold_high", |c, v| set_f64(&mut c.history.devs.risk_threshold_high, v)), - ("history.devs.risk_threshold_medium", |c, v| set_f64(&mut c.history.devs.risk_threshold_medium, v)), - ("history.devs.active_threshold_ratio", |c, v| set_f64(&mut c.history.devs.active_threshold_ratio, v)), - ("history.devs.default_active_days", |c, v| set_i64(&mut c.history.devs.default_active_days, v)), - ("history.devs.hll_precision", |c, v| set_i64(&mut c.history.devs.hll_precision, v)), - // history.file_history - ("history.file_history.hotspot_threshold_critical", |c, v| set_i64(&mut c.history.file_history.hotspot_threshold_critical, v)), - ("history.file_history.hotspot_threshold_high", |c, v| set_i64(&mut c.history.file_history.hotspot_threshold_high, v)), - ("history.file_history.hotspot_threshold_medium", |c, v| set_i64(&mut c.history.file_history.hotspot_threshold_medium, v)), - // history.imports - ("history.imports.goroutines", |c, v| set_i64(&mut c.history.imports.goroutines, v)), - ("history.imports.max_file_size", |c, v| set_i64(&mut c.history.imports.max_file_size, v)), - ("history.imports.max_dependency_risk_rows", |c, v| set_i64(&mut c.history.imports.max_dependency_risk_rows, v)), - // history.sentiment - ("history.sentiment.min_comment_length", |c, v| set_i64(&mut c.history.sentiment.min_comment_length, v)), - ("history.sentiment.gap", |c, v| set_f64(&mut c.history.sentiment.gap, v)), - ("history.sentiment.neutralizer_weight", |c, v| set_f64(&mut c.history.sentiment.neutralizer_weight, v)), - ("history.sentiment.max_weight_ratio", |c, v| set_f64(&mut c.history.sentiment.max_weight_ratio, v)), - ("history.sentiment.positive_threshold", |c, v| set_f64(&mut c.history.sentiment.positive_threshold, v)), - ("history.sentiment.negative_threshold", |c, v| set_f64(&mut c.history.sentiment.negative_threshold, v)), - ("history.sentiment.trend_threshold", |c, v| set_f64(&mut c.history.sentiment.trend_threshold, v)), - ("history.sentiment.low_sentiment_risk_threshold", |c, v| set_f64(&mut c.history.sentiment.low_sentiment_risk_thresh, v)), - // history.shotness - ("history.shotness.dsl_struct", |c, v| set_string(&mut c.history.shotness.dsl_struct, v)), - ("history.shotness.dsl_name", |c, v| set_string(&mut c.history.shotness.dsl_name, v)), - // history.typos - ("history.typos.max_distance", |c, v| set_i64(&mut c.history.typos.max_distance, v)), - // history.anomaly - ("history.anomaly.threshold", |c, v| set_f64(&mut c.history.anomaly.threshold, v)), - ("history.anomaly.window_size", |c, v| set_i64(&mut c.history.anomaly.window_size, v)), - // history.clones - ("history.clones.max_clone_pairs", |c, v| set_i64(&mut c.history.clones.max_clone_pairs, v)), - ("history.clones.num_hashes", |c, v| set_i64(&mut c.history.clones.num_hashes, v)), - ("history.clones.num_bands", |c, v| set_i64(&mut c.history.clones.num_bands, v)), - ("history.clones.num_rows", |c, v| set_i64(&mut c.history.clones.num_rows, v)), - ("history.clones.shingle_size", |c, v| set_i64(&mut c.history.clones.shingle_size, v)), - ("history.clones.similarity_type2", |c, v| set_f64(&mut c.history.clones.similarity_type2, v)), - ("history.clones.similarity_type3", |c, v| set_f64(&mut c.history.clones.similarity_type3, v)), - ("history.clones.threshold_ratio_yellow", |c, v| set_f64(&mut c.history.clones.threshold_ratio_yellow, v)), - ("history.clones.threshold_ratio_red", |c, v| set_f64(&mut c.history.clones.threshold_ratio_red, v)), - ("history.clones.threshold_pairs_yellow", |c, v| set_i64(&mut c.history.clones.threshold_pairs_yellow, v)), - ("history.clones.threshold_pairs_red", |c, v| set_i64(&mut c.history.clones.threshold_pairs_red, v)), - // checkpoint - ("checkpoint.enabled", |c, v| set_bool(&mut c.checkpoint.enabled, v)), - ("checkpoint.dir", |c, v| set_string(&mut c.checkpoint.dir, v)), - ("checkpoint.resume", |c, v| set_bool(&mut c.checkpoint.resume, v)), - ("checkpoint.clear_prev", |c, v| set_bool(&mut c.checkpoint.clear_prev, v)), -]; - -/// Loads configuration from a YAML string plus an env-lookup closure. -/// -/// Test/host seam exposing the same layering as [`load_config`] (default → file -/// → env) without touching the filesystem or process environment. The `flag` -/// layer (highest precedence) is applied by the caller after this returns, -/// completing the `flag > env > file > default` chain. -/// -/// # Errors -/// Same conditions as [`load_config`], minus the filesystem read. -pub fn load_from_yaml_and_env<F>(yaml: &str, lookup: &F) -> Result<Config, LoadError> -where - F: Fn(&str) -> Option<String>, -{ - let mut cfg = parse_yaml_over_defaults(yaml)?; - apply_env_overrides(&mut cfg, lookup); - cfg.validate().map_err(LoadError::Validate)?; - Ok(cfg) -} + #[test] + fn no_file_and_no_env_is_the_empty_configuration() { + let dir = tempfile::tempdir().unwrap(); + let loaded = load(None, &[dir.path().to_path_buf()], no_env).unwrap(); + assert_eq!(loaded, Loaded::default()); + } -/// Returns the canonical config file path within `dir` (`.codefang.yaml`). -#[must_use] -pub fn config_file_path(dir: &Path) -> PathBuf { - dir.join(format!("{CONFIG_NAME}.{CONFIG_EXT}")) + #[test] + fn discovery_takes_the_first_directory_that_has_the_file() { + let first = tempfile::tempdir().unwrap(); + let second = tempfile::tempdir().unwrap(); + write( + second.path(), + CONFIG_FILE_NAME, + "history:\n tick_size: 2\n", + ); + let dirs = [first.path().to_path_buf(), second.path().to_path_buf()]; + let loaded = load(None, &dirs, no_env).unwrap(); + assert_eq!(loaded.path, Some(second.path().join(CONFIG_FILE_NAME))); + assert_eq!(loaded.config.history.tick_size, Some(2)); + + write(first.path(), CONFIG_FILE_NAME, "history:\n tick_size: 1\n"); + let loaded = load(None, &dirs, no_env).unwrap(); + assert_eq!(loaded.config.history.tick_size, Some(1)); + } + + #[test] + fn an_explicit_path_skips_discovery_and_must_exist() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), CONFIG_FILE_NAME, "history:\n tick_size: 1\n"); + let other = write(dir.path(), "other.yaml", "history:\n tick_size: 6\n"); + let dirs = [dir.path().to_path_buf()]; + let loaded = load(Some(&other), &dirs, no_env).unwrap(); + assert_eq!(loaded.config.history.tick_size, Some(6)); + + let missing = dir.path().join("missing.yaml"); + let err = load(Some(&missing), &dirs, no_env).unwrap_err(); + assert!(matches!(err, LoadError::Read { .. }), "{err}"); + assert!(err.to_string().starts_with("cannot read config "), "{err}"); + } + + #[test] + fn a_comment_only_file_is_empty() { + let dir = tempfile::tempdir().unwrap(); + let p = write(dir.path(), "c.yaml", "# nothing\n# analyzers: []\n"); + let loaded = load(Some(&p), &[], no_env).unwrap(); + assert_eq!(loaded.config, Config::default()); + assert!(loaded.unsupported_keys.is_empty()); + } + + #[test] + fn unsupported_keys_are_listed_in_document_order() { + let dir = tempfile::tempdir().unwrap(); + let p = write( + dir.path(), + "c.yaml", + "pipeline:\n workers: 4\n max_changes_per_commit: 5\n\ + checkpoint:\n enabled: true\n dir: x\n\ + history:\n burndown:\n granularity: 7\n debug: true\n clones: {}\n\ + bogus: 1\n", + ); + let loaded = load(Some(&p), &[], no_env).unwrap(); + assert_eq!( + loaded.unsupported_keys, + vec![ + "pipeline.workers", + "checkpoint.enabled", + "checkpoint.dir", + "history.burndown.debug", + "history.clones", + "bogus", + ] + ); + assert_eq!(loaded.config.pipeline.max_changes_per_commit, Some(5)); + assert_eq!(loaded.config.history.burndown.granularity, Some(7)); + } + + #[test] + fn a_section_with_every_child_commented_out_is_not_unsupported() { + let dir = tempfile::tempdir().unwrap(); + let p = write( + dir.path(), + "c.yaml", + "pipeline:\n# workers: 0\nhistory:\n burndown:\ncheckpoint:\n", + ); + let loaded = load(Some(&p), &[], no_env).unwrap(); + assert_eq!(loaded.unsupported_keys, vec!["checkpoint"]); + } + + #[test] + fn a_wrong_type_is_a_parse_error_naming_the_key() { + let dir = tempfile::tempdir().unwrap(); + let p = write( + dir.path(), + "c.yaml", + "history:\n anomaly:\n window_size: many\n", + ); + let err = load(Some(&p), &[], no_env).unwrap_err().to_string(); + assert!(err.starts_with("invalid config "), "{err}"); + assert!(err.contains("history.anomaly.window_size"), "{err}"); + } + + #[test] + fn a_non_mapping_document_is_rejected() { + let dir = tempfile::tempdir().unwrap(); + let p = write(dir.path(), "c.yaml", "- a\n- b\n"); + let err = load(Some(&p), &[], no_env).unwrap_err().to_string(); + assert!(err.contains("must be a mapping"), "{err}"); + } + + #[test] + fn a_rule_violation_names_the_file_and_the_key() { + let dir = tempfile::tempdir().unwrap(); + let p = write( + dir.path(), + "c.yaml", + "history:\n burndown:\n sampling: 0\n", + ); + let err = load(Some(&p), &[], no_env).unwrap_err().to_string(); + assert_eq!( + err, + format!( + "invalid config {}: history.burndown.sampling must be > 0, got 0", + p.display() + ) + ); + } + + #[test] + fn the_environment_overrides_the_file() { + let dir = tempfile::tempdir().unwrap(); + let p = write( + dir.path(), + "c.yaml", + "history:\n anomaly:\n window_size: 20\n threshold: 3\n", + ); + let env = |name: &str| match name { + "CODEFANG_HISTORY_ANOMALY_WINDOW_SIZE" => Some("4".to_owned()), + "CODEFANG_ANALYZERS" => Some("history/devs, history/anomaly".to_owned()), + "CODEFANG_HISTORY_DEVS_ANONYMIZE" => Some("true".to_owned()), + _ => None, + }; + let loaded = load(Some(&p), &[], env).unwrap(); + let cfg = &loaded.config; + assert_eq!(cfg.history.anomaly.window_size, Some(4)); + assert_eq!(cfg.history.anomaly.threshold, Some(3.0)); + assert_eq!(cfg.history.devs.anonymize, Some(true)); + assert_eq!( + cfg.analyzers, + Some(vec![ + "history/devs".to_owned(), + "history/anomaly".to_owned() + ]) + ); + } + + #[test] + fn a_bad_environment_value_names_the_variable() { + let env = |name: &str| (name == "CODEFANG_HISTORY_TICK_SIZE").then(|| "hourly".to_owned()); + let err = load(None, &[], env).unwrap_err().to_string(); + assert!( + err.starts_with("invalid environment variable CODEFANG_HISTORY_TICK_SIZE: "), + "{err}" + ); + + let env = |name: &str| (name == "CODEFANG_HISTORY_TICK_SIZE").then(|| "0".to_owned()); + let err = load(None, &[], env).unwrap_err().to_string(); + assert_eq!( + err, + "invalid config CODEFANG_HISTORY_TICK_SIZE: history.tick_size must be > 0, got 0" + ); + } + + #[test] + fn overlay_replaces_scalars_on_the_path() { + let mut tree: Yaml = serde_yaml::from_str("history: 3\n").unwrap(); + overlay(&mut tree, keys::TICK_SIZE, Yaml::Number(5.into())); + let cfg: Config = serde_yaml::from_value(tree).unwrap(); + assert_eq!(cfg.history.tick_size, Some(5)); + } } diff --git a/crates/cf-config/src/types.rs b/crates/cf-config/src/types.rs index 66773a84..dc756d5e 100644 --- a/crates/cf-config/src/types.rs +++ b/crates/cf-config/src/types.rs @@ -1,880 +1,419 @@ -//! Configuration model and validation. +//! The configuration model: one optional field per supported key. //! -//! Field names, the YAML keys, the validation order, and the sentinel-error -//! messages are all frozen (CLI compatibility contract). +//! A field is `None` when neither the file nor the environment sets it; the +//! consumer then keeps its compiled default. Every supported key is listed +//! in [`KEYS`]; anything else in a file is reported as unsupported by the +//! loader. use serde::Deserialize; use std::fmt; -/// Upper bound for the sentiment gap value. -const SENTIMENT_GAP_MAX: f64 = 1.0; -/// Upper bound for ratio values, 0.0 to 1.0. -const RATIO_MAX: f64 = 1.0; -/// Upper bound for percentage values, 0 to 100. -const PERCENT_MAX: f64 = 100.0; -/// Minimum valid HLL precision. -const MIN_HLL_PRECISION: i64 = 4; -/// Maximum valid HLL precision. -const MAX_HLL_PRECISION: i64 = 18; -/// Minimum valid sliding window for anomaly detection. +/// The dotted name of every supported key. +pub mod keys { + /// Default analyzer selection (`-a`). + pub const ANALYZERS: &str = "analyzers"; + /// Changeset cap (`--max-changes-per-commit`). + pub const MAX_CHANGES_PER_COMMIT: &str = "pipeline.max_changes_per_commit"; + /// History memory budget (`--memory-budget`). + pub const MEMORY_BUDGET: &str = "pipeline.memory_budget"; + /// Commits per history chunk (`--chunk-size`). + pub const CHUNK_SIZE: &str = "pipeline.chunk_size"; + /// Tick width in hours (`--tick-size`). + pub const TICK_SIZE: &str = "history.tick_size"; + /// Burndown ticks per age band (`--granularity`). + pub const BURNDOWN_GRANULARITY: &str = "history.burndown.granularity"; + /// Burndown ticks per sample (`--sampling`). + pub const BURNDOWN_SAMPLING: &str = "history.burndown.sampling"; + /// Burndown per-file survival (`--burndown-files`). + pub const BURNDOWN_TRACK_FILES: &str = "history.burndown.track_files"; + /// Burndown per-developer survival (`--burndown-people`). + pub const BURNDOWN_TRACK_PEOPLE: &str = "history.burndown.track_people"; + /// Devs counts empty commits (`--empty-commits`). + pub const DEVS_CONSIDER_EMPTY_COMMITS: &str = "history.devs.consider_empty_commits"; + /// Anonymized developer names (`--anonymize`). + pub const DEVS_ANONYMIZE: &str = "history.devs.anonymize"; + /// Sentiment minimum comment length (`--min-comment-len`). + pub const SENTIMENT_MIN_COMMENT_LENGTH: &str = "history.sentiment.min_comment_length"; + /// Shotness structural DSL (`--shotness-dsl-struct`). + pub const SHOTNESS_DSL_STRUCT: &str = "history.shotness.dsl_struct"; + /// Shotness name DSL (`--shotness-dsl-name`). + pub const SHOTNESS_DSL_NAME: &str = "history.shotness.dsl_name"; + /// Shotness keeps ended entities (`--shotness-keep-ended`). + pub const SHOTNESS_KEEP_ENDED: &str = "history.shotness.keep_ended"; + /// Typos maximum Levenshtein distance (`--typos-max-distance`). + pub const TYPOS_MAX_DISTANCE: &str = "history.typos.max_distance"; + /// Anomaly Z-score threshold (`--anomaly-threshold`). + pub const ANOMALY_THRESHOLD: &str = "history.anomaly.threshold"; + /// Anomaly trailing window in ticks (`--anomaly-window`). + pub const ANOMALY_WINDOW_SIZE: &str = "history.anomaly.window_size"; +} + +/// Every supported key, in documentation order. +pub const KEYS: &[&str] = &[ + keys::ANALYZERS, + keys::MAX_CHANGES_PER_COMMIT, + keys::MEMORY_BUDGET, + keys::CHUNK_SIZE, + keys::TICK_SIZE, + keys::BURNDOWN_GRANULARITY, + keys::BURNDOWN_SAMPLING, + keys::BURNDOWN_TRACK_FILES, + keys::BURNDOWN_TRACK_PEOPLE, + keys::DEVS_CONSIDER_EMPTY_COMMITS, + keys::DEVS_ANONYMIZE, + keys::SENTIMENT_MIN_COMMENT_LENGTH, + keys::SHOTNESS_DSL_STRUCT, + keys::SHOTNESS_DSL_NAME, + keys::SHOTNESS_KEEP_ENDED, + keys::TYPOS_MAX_DISTANCE, + keys::ANOMALY_THRESHOLD, + keys::ANOMALY_WINDOW_SIZE, +]; + +/// The smallest anomaly window that has a standard deviation. const MIN_ANOMALY_WINDOW_SIZE: i64 = 2; -/// Sentinel errors returned by [`Config::validate`]. -/// -/// The [`fmt::Display`] text of each variant is part of the CLI compatibility -/// contract, so any downstream wrapping (`validate config: <msg>`) matches the -/// reference binary byte-for-byte. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[non_exhaustive] -pub enum ConfigError { - /// `pipeline.workers must be non-negative` - InvalidWorkers, - /// `pipeline.diff_cache_size must be non-negative` - InvalidDiffCacheSize, - /// `pipeline.commit_batch_size must be non-negative` - InvalidCommitBatchSize, - /// `pipeline.gogc must be non-negative` - InvalidGogc, - /// `pipeline.uast_spill_threshold must be non-negative` - InvalidUastSpillThreshold, - /// `pipeline.intra_commit_parallel_threshold must be non-negative` - InvalidIntraCommitParallelThreshold, - /// `pipeline.max_intra_commit_workers must be non-negative` - InvalidMaxIntraCommitWorkers, - /// `pipeline.max_uast_blob_size must be non-negative` - InvalidMaxUastBlobSize, - /// `pipeline.max_changes_per_commit must be non-negative` - InvalidMaxChangesPerCommit, - /// `pipeline.max_diff_batch_size must be non-negative` - InvalidMaxDiffBatchSize, - /// `pipeline.memory_budget_ratio must be between 0 and 100` - InvalidMemoryBudgetRatio, - /// `pipeline.memory_limit_ratio must be between 0 and 100` - InvalidMemoryLimitRatio, - /// `history.burndown.granularity must be positive` - InvalidBurndownGranularity, - /// `history.burndown.sampling must be positive` - InvalidBurndownSampling, - /// `history.couples.coupling_threshold_high must be non-negative` - InvalidCouplingThreshold, - /// `history.couples.ownership_few_threshold must be non-negative` - InvalidOwnershipFewThreshold, - /// `history.couples.ownership_moderate_threshold must be non-negative` - InvalidOwnershipModerateThreshold, - /// `history.couples.hll_precision must be between 4 and 18` - InvalidCouplesHllPrecision, - /// `history.devs.bus_factor_threshold must be between 0 and 1` - InvalidBusFactorThreshold, - /// `history.devs.risk_threshold_critical must be between 0 and 100` - InvalidDevsRiskThresholdCritical, - /// `history.devs.risk_threshold_high must be between 0 and 100` - InvalidDevsRiskThresholdHigh, - /// `history.devs.risk_threshold_medium must be between 0 and 100` - InvalidDevsRiskThresholdMedium, - /// `history.devs.active_threshold_ratio must be between 0 and 1` - InvalidDevsActiveThresholdRatio, - /// `history.devs.default_active_days must be non-negative` - InvalidDevsDefaultActiveDays, - /// `history.devs.hll_precision must be between 4 and 18` - InvalidDevsHllPrecision, - /// `history.file_history.hotspot_threshold_critical must be non-negative` - InvalidHotspotThresholdCritical, - /// `history.file_history.hotspot_threshold_high must be non-negative` - InvalidHotspotThresholdHigh, - /// `history.file_history.hotspot_threshold_medium must be non-negative` - InvalidHotspotThresholdMedium, - /// `history.sentiment.min_comment_length must be positive` - InvalidSentimentMinLength, - /// `history.sentiment.gap must be between 0 and 1` - InvalidSentimentGap, - /// `history.sentiment.neutralizer_weight must be between 0 and 1` - InvalidNeutralizerWeight, - /// `history.sentiment.max_weight_ratio must be non-negative` - InvalidMaxWeightRatio, - /// `history.typos.max_distance must be positive` - InvalidTyposMaxDistance, - /// `history.imports.goroutines must be positive` - InvalidImportsGoroutines, - /// `history.imports.max_file_size must be positive` - InvalidImportsMaxFileSize, - /// `history.imports.max_dependency_risk_rows must be non-negative` - InvalidImportsMaxDependencyRiskRows, - /// `history.anomaly.threshold must be positive` - InvalidAnomalyThreshold, - /// `history.anomaly.window_size must be at least 2` - InvalidAnomalyWindowSize, - /// `history.clones.max_clone_pairs must be non-negative` - InvalidClonesMaxClonePairs, +/// A configured value that violates its key's rule. +#[derive(Debug, Clone, PartialEq)] +pub struct ConfigError { + /// The dotted key. + pub key: &'static str, + /// The rule, e.g. `must be > 0`. + pub rule: &'static str, + /// The rejected value as written. + pub got: String, +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}, got {}", self.key, self.rule, self.got) + } +} + +impl std::error::Error for ConfigError {} + +/// One configured value, typed like the flag it defaults. +#[derive(Debug, Clone, PartialEq)] +pub enum Value { + /// An integer. + Int(i64), + /// A floating-point number. + Float(f64), + /// A boolean. + Bool(bool), + /// A string. + Text(String), + /// A list of strings. + List(Vec<String>), } -impl ConfigError { - /// Returns the frozen sentinel message for this error. +impl Value { + /// The value as command-line argument strings (one per list item). #[must_use] - pub const fn message(self) -> &'static str { + pub fn to_args(&self) -> Vec<String> { match self { - Self::InvalidWorkers => "pipeline.workers must be non-negative", - Self::InvalidDiffCacheSize => "pipeline.diff_cache_size must be non-negative", - Self::InvalidCommitBatchSize => "pipeline.commit_batch_size must be non-negative", - Self::InvalidGogc => "pipeline.gogc must be non-negative", - Self::InvalidUastSpillThreshold => "pipeline.uast_spill_threshold must be non-negative", - Self::InvalidIntraCommitParallelThreshold => { - "pipeline.intra_commit_parallel_threshold must be non-negative" - } - Self::InvalidMaxIntraCommitWorkers => { - "pipeline.max_intra_commit_workers must be non-negative" - } - Self::InvalidMaxUastBlobSize => "pipeline.max_uast_blob_size must be non-negative", - Self::InvalidMaxChangesPerCommit => { - "pipeline.max_changes_per_commit must be non-negative" - } - Self::InvalidMaxDiffBatchSize => "pipeline.max_diff_batch_size must be non-negative", - Self::InvalidMemoryBudgetRatio => { - "pipeline.memory_budget_ratio must be between 0 and 100" - } - Self::InvalidMemoryLimitRatio => { - "pipeline.memory_limit_ratio must be between 0 and 100" - } - Self::InvalidBurndownGranularity => "history.burndown.granularity must be positive", - Self::InvalidBurndownSampling => "history.burndown.sampling must be positive", - Self::InvalidCouplingThreshold => { - "history.couples.coupling_threshold_high must be non-negative" - } - Self::InvalidOwnershipFewThreshold => { - "history.couples.ownership_few_threshold must be non-negative" - } - Self::InvalidOwnershipModerateThreshold => { - "history.couples.ownership_moderate_threshold must be non-negative" - } - Self::InvalidCouplesHllPrecision => { - "history.couples.hll_precision must be between 4 and 18" - } - Self::InvalidBusFactorThreshold => { - "history.devs.bus_factor_threshold must be between 0 and 1" - } - Self::InvalidDevsRiskThresholdCritical => { - "history.devs.risk_threshold_critical must be between 0 and 100" - } - Self::InvalidDevsRiskThresholdHigh => { - "history.devs.risk_threshold_high must be between 0 and 100" - } - Self::InvalidDevsRiskThresholdMedium => { - "history.devs.risk_threshold_medium must be between 0 and 100" - } - Self::InvalidDevsActiveThresholdRatio => { - "history.devs.active_threshold_ratio must be between 0 and 1" - } - Self::InvalidDevsDefaultActiveDays => { - "history.devs.default_active_days must be non-negative" - } - Self::InvalidDevsHllPrecision => "history.devs.hll_precision must be between 4 and 18", - Self::InvalidHotspotThresholdCritical => { - "history.file_history.hotspot_threshold_critical must be non-negative" - } - Self::InvalidHotspotThresholdHigh => { - "history.file_history.hotspot_threshold_high must be non-negative" - } - Self::InvalidHotspotThresholdMedium => { - "history.file_history.hotspot_threshold_medium must be non-negative" - } - Self::InvalidSentimentMinLength => { - "history.sentiment.min_comment_length must be positive" - } - Self::InvalidSentimentGap => "history.sentiment.gap must be between 0 and 1", - Self::InvalidNeutralizerWeight => { - "history.sentiment.neutralizer_weight must be between 0 and 1" - } - Self::InvalidMaxWeightRatio => { - "history.sentiment.max_weight_ratio must be non-negative" - } - Self::InvalidTyposMaxDistance => "history.typos.max_distance must be positive", - Self::InvalidImportsGoroutines => "history.imports.goroutines must be positive", - Self::InvalidImportsMaxFileSize => "history.imports.max_file_size must be positive", - Self::InvalidImportsMaxDependencyRiskRows => { - "history.imports.max_dependency_risk_rows must be non-negative" - } - Self::InvalidAnomalyThreshold => "history.anomaly.threshold must be positive", - Self::InvalidAnomalyWindowSize => "history.anomaly.window_size must be at least 2", - Self::InvalidClonesMaxClonePairs => { - "history.clones.max_clone_pairs must be non-negative" - } + Self::Int(v) => vec![v.to_string()], + Self::Float(v) => vec![v.to_string()], + Self::Bool(v) => vec![v.to_string()], + Self::Text(v) => vec![v.clone()], + Self::List(v) => v.clone(), } } } -impl fmt::Display for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.message()) - } +/// One key the configuration sets. +#[derive(Debug, Clone, PartialEq)] +pub struct Setting { + /// The dotted key (one of [`KEYS`]). + pub key: &'static str, + /// The configured value. + pub value: Value, } -impl std::error::Error for ConfigError {} - -/// Top-level configuration for codefang. -/// -/// `serde(default)` makes every absent YAML key fall back to the field -/// default, merging a partial file over the registered defaults (reference -/// loader behavior). +/// The configuration: every supported key, `None` when unset. #[derive(Debug, Clone, Default, PartialEq, Deserialize)] #[serde(default)] pub struct Config { - /// Analyzer IDs or glob patterns selected for the run. - pub analyzers: Vec<String>, - /// Pipeline resource knobs. + /// Analyzer ids or globs selected when `-a` is absent. + pub analyzers: Option<Vec<String>>, + /// The `pipeline` section. pub pipeline: PipelineConfig, - /// Per-analyzer history configuration. + /// The `history` section. pub history: HistoryConfig, - /// Checkpoint settings. - pub checkpoint: CheckpointConfig, } -/// Pipeline resource knobs. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// The `pipeline` section. +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct PipelineConfig { - /// Worker count. - pub workers: i64, - /// Memory budget string. - pub memory_budget: String, - /// Blob cache size string. - pub blob_cache_size: String, - /// Diff cache size. - pub diff_cache_size: i64, - /// Blob arena size string. - pub blob_arena_size: String, - /// Commit batch size. - pub commit_batch_size: i64, - /// GOGC value. - pub gogc: i64, - /// Ballast size string. - pub ballast_size: String, - /// Memory limit string. - pub memory_limit: String, - /// Worker timeout string. - pub worker_timeout: String, + /// Commits whose tree diff exceeds this many changes are dropped (`0` = + /// the default cap). + pub max_changes_per_commit: Option<i64>, + /// The history memory budget as a size (`2GB`, `512MiB`) or a plain + /// byte count; the flag parses and checks it. + #[serde(deserialize_with = "size_text")] + pub memory_budget: Option<String>, + /// Commits per history chunk. + pub chunk_size: Option<i64>, +} - /// UAST spill threshold. - pub uast_spill_threshold: i64, - /// Intra-commit parallel threshold. - pub intra_commit_parallel_threshold: i64, - /// Maximum intra-commit workers. - pub max_intra_commit_workers: i64, - /// Maximum UAST blob size. - pub max_uast_blob_size: i64, - /// UAST parse timeout string. - pub uast_parse_timeout: String, - /// Maximum changes per commit. - pub max_changes_per_commit: i64, - /// Maximum diff batch size. - pub max_diff_batch_size: i64, - /// Memory budget ratio (percent). - pub memory_budget_ratio: i64, - /// Memory budget cap string. - pub memory_budget_cap: String, - /// Memory limit ratio (percent). - pub memory_limit_ratio: i64, - /// UAST spill trim interval. - pub uast_spill_trim_interval: i64, - /// Native trim interval. - pub native_trim_interval: i64, - /// Maximum streaming buffering. - pub max_streaming_buffering: i64, - /// Drain prefetch timeout string. - pub drain_prefetch_timeout: String, - /// Sampler interval string. - pub sampler_interval: String, - /// Worker ratio (percent). - pub worker_ratio: i64, - /// UAST worker ratio (percent). - pub uast_worker_ratio: i64, - /// Leaf worker divisor. - pub leaf_worker_divisor: i64, - /// Minimum leaf workers. - pub min_leaf_workers: i64, - /// Buffer size multiplier. - pub buffer_size_multiplier: i64, - /// Budget limit ratio (percent). - pub budget_limit_ratio: i64, - /// System RAM limit ratio (percent). - pub system_ram_limit_ratio: i64, - /// Static analyzer maximum workers. - pub static_max_workers: i64, - /// Malloc trim interval. - pub malloc_trim_interval: i64, - /// Static memory limit ratio (percent). - pub static_memory_limit_ratio: i64, - /// Diff job buffer multiplier. - pub diff_job_buffer_multiplier: i64, +/// A size written as text (`2GB`) or as a plain byte count, kept as the +/// text the `--memory-budget` flag reads. +fn size_text<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> { + #[derive(Deserialize)] + #[serde(untagged)] + enum Size { + Text(String), + Bytes(u64), + } + Ok(Option::<Size>::deserialize(d)?.map(|size| match size { + Size::Text(text) => text, + Size::Bytes(bytes) => bytes.to_string(), + })) } -/// Per-analyzer history configuration. +/// The `history` section. #[derive(Debug, Clone, Default, PartialEq, Deserialize)] #[serde(default)] pub struct HistoryConfig { - /// Burndown analyzer settings. + /// Tick width in hours. + pub tick_size: Option<i64>, + /// Burndown settings. pub burndown: BurndownConfig, - /// Couples analyzer settings. - pub couples: CouplesConfig, - /// Devs analyzer settings. + /// Devs settings. pub devs: DevsConfig, - /// File-history analyzer settings. - pub file_history: FileHistoryConfig, - /// Imports analyzer settings. - pub imports: ImportsConfig, - /// Sentiment analyzer settings. + /// Sentiment settings. pub sentiment: SentimentConfig, - /// Shotness analyzer settings. + /// Shotness settings. pub shotness: ShotnessConfig, - /// Typos analyzer settings. + /// Typos settings. pub typos: TyposConfig, - /// Anomaly analyzer settings. + /// Anomaly settings. pub anomaly: AnomalyConfig, - /// Clones analyzer settings. - pub clones: ClonesConfig, } -/// Temporal anomaly detection settings. -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(default)] -pub struct AnomalyConfig { - /// Z-score / deviation threshold. - pub threshold: f64, - /// Sliding window size. - pub window_size: i64, -} - -/// Burndown analyzer settings. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// `history.burndown`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct BurndownConfig { - /// Time-bucket granularity in days. - pub granularity: i64, - /// Sampling interval in days. - pub sampling: i64, - /// Track per-file burndown. - pub track_files: bool, - /// Track per-person burndown. - pub track_people: bool, - /// Hibernation threshold. - pub hibernation_threshold: i64, - /// Hibernate state to disk. - pub hibernation_to_disk: bool, - /// Hibernation directory. - pub hibernation_directory: String, - /// Debug logging. - pub debug: bool, - /// Goroutine count. - pub goroutines: i64, + /// Ticks per age band. + pub granularity: Option<i64>, + /// Ticks per sample. + pub sampling: Option<i64>, + /// Per-file survival. + pub track_files: Option<bool>, + /// Per-developer survival and interactions. + pub track_people: Option<bool>, } -/// Couples analyzer settings. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(default)] -pub struct CouplesConfig { - /// High coupling threshold. - pub coupling_threshold_high: i64, - /// Few-ownership threshold. - pub ownership_few_threshold: i64, - /// Moderate-ownership threshold. - pub ownership_moderate_threshold: i64, - /// Batch coupling threshold. - pub batch_coupling_threshold: i64, - /// HLL precision. - pub hll_precision: i64, - /// Top-K couples kept per file. - pub top_k_per_file: i64, - /// Minimum edge weight. - pub min_edge_weight: i64, -} - -/// Devs analyzer settings. -#[derive(Debug, Clone, PartialEq, Deserialize)] +/// `history.devs`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct DevsConfig { - /// Count empty commits. - pub consider_empty_commits: bool, - /// Anonymize developer identities. - pub anonymize: bool, - /// Bus-factor threshold. - pub bus_factor_threshold: f64, - /// Critical risk threshold (percent). - pub risk_threshold_critical: f64, - /// High risk threshold (percent). - pub risk_threshold_high: f64, - /// Medium risk threshold (percent). - pub risk_threshold_medium: f64, - /// Active threshold ratio. - pub active_threshold_ratio: f64, - /// Default active-days window. - pub default_active_days: i64, - /// HLL precision. - pub hll_precision: i64, + /// Count commits whose filtered diff is empty. + pub consider_empty_commits: Option<bool>, + /// Replace developer names with `Developer-A`, `Developer-B`, …. + pub anonymize: Option<bool>, } -/// File-history analyzer settings. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(default)] -pub struct FileHistoryConfig { - /// Critical hotspot threshold. - pub hotspot_threshold_critical: i64, - /// High hotspot threshold. - pub hotspot_threshold_high: i64, - /// Medium hotspot threshold. - pub hotspot_threshold_medium: i64, -} - -/// Imports analyzer settings. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(default)] -pub struct ImportsConfig { - /// Goroutine count. - pub goroutines: i64, - /// Maximum analyzed file size. - pub max_file_size: i64, - /// Maximum dependency-risk rows. - pub max_dependency_risk_rows: i64, -} - -/// Sentiment analyzer settings. -#[derive(Debug, Clone, PartialEq, Deserialize)] +/// `history.sentiment`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct SentimentConfig { - /// Minimum comment length. - pub min_comment_length: i64, - /// Gap. - pub gap: f64, - /// Neutralizer weight. - pub neutralizer_weight: f64, - /// Maximum weight ratio. - pub max_weight_ratio: f64, - /// Positive threshold. - pub positive_threshold: f64, - /// Negative threshold. - pub negative_threshold: f64, - /// Trend threshold. - pub trend_threshold: f64, - /// Low-sentiment risk threshold. - #[serde(rename = "low_sentiment_risk_threshold")] - pub low_sentiment_risk_thresh: f64, + /// Minimum comment length in characters (`0` disables the filter). + pub min_comment_length: Option<i64>, } -/// Shotness analyzer settings. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// `history.shotness`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct ShotnessConfig { - /// Structural selection DSL. - pub dsl_struct: String, - /// Name extraction DSL. - pub dsl_name: String, + /// UAST DSL query selecting the structural nodes (empty = default). + pub dsl_struct: Option<String>, + /// UAST DSL query resolving node names (empty = default). + pub dsl_name: Option<String>, + /// Keep ended entities in the report, marked. + pub keep_ended: Option<bool>, } -/// Typos analyzer settings. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// `history.typos`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct TyposConfig { - /// Maximum edit distance. - pub max_distance: i64, + /// Maximum Levenshtein distance of a typo-fix pair. + pub max_distance: Option<i64>, } -/// Clones analyzer settings. -#[derive(Debug, Clone, PartialEq, Deserialize)] +/// `history.anomaly`. +#[derive(Debug, Clone, Default, PartialEq, Deserialize)] #[serde(default)] -pub struct ClonesConfig { - /// Maximum clone pairs. - pub max_clone_pairs: i64, - /// Number of `MinHash` hashes. - pub num_hashes: i64, - /// Number of LSH bands. - pub num_bands: i64, - /// Number of LSH rows. - pub num_rows: i64, - /// Shingle size. - pub shingle_size: i64, - /// Type-2 similarity threshold. - pub similarity_type2: f64, - /// Type-3 similarity threshold. - pub similarity_type3: f64, - /// Yellow threshold ratio. - pub threshold_ratio_yellow: f64, - /// Red threshold ratio. - pub threshold_ratio_red: f64, - /// Yellow threshold pairs. - pub threshold_pairs_yellow: i64, - /// Red threshold pairs. - pub threshold_pairs_red: i64, +pub struct AnomalyConfig { + /// Z-score threshold. + pub threshold: Option<f64>, + /// Trailing window in ticks. + pub window_size: Option<i64>, } -/// Checkpoint settings. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(default)] -pub struct CheckpointConfig { - /// Checkpointing enabled. - pub enabled: bool, - /// Checkpoint directory. - pub dir: String, - /// Resume from a previous checkpoint. - pub resume: bool, - /// Clear a previous checkpoint before running. - pub clear_prev: bool, +/// An integer rule: the key, the configured value, the rule text, and the +/// predicate the value must satisfy. +type IntRule = (&'static str, Option<i64>, &'static str, fn(i64) -> bool); + +/// The [`ConfigError`] for the first integer in `rules` that breaks its rule. +fn first_int_violation(rules: &[IntRule]) -> Option<ConfigError> { + rules.iter().find_map(|&(key, value, rule, ok)| { + value.filter(|v| !ok(*v)).map(|v| ConfigError { + key, + rule, + got: v.to_string(), + }) + }) } impl Config { - /// Validates configuration invariants and returns the first error found. - /// - /// Pipeline checks run first, then history; the order is frozen because - /// only the FIRST violation is reported. The zero-value config is valid - /// because every bound is `< 0` (or a `!= 0 && out-of-range` guard). + /// Checks every set value against its key's rule (the same rule the + /// matching flag enforces). /// /// # Errors - /// Returns the first [`ConfigError`] whose invariant is violated. + /// + /// The first [`ConfigError`] in [`KEYS`] order. pub fn validate(&self) -> Result<(), ConfigError> { - self.validate_pipeline()?; - self.validate_history() - } - - const fn validate_pipeline(&self) -> Result<(), ConfigError> { - let p = &self.pipeline; - let pct_max = PERCENT_MAX as i64; - if p.workers < 0 { - return Err(ConfigError::InvalidWorkers); - } - if p.diff_cache_size < 0 { - return Err(ConfigError::InvalidDiffCacheSize); - } - if p.commit_batch_size < 0 { - return Err(ConfigError::InvalidCommitBatchSize); - } - if p.gogc < 0 { - return Err(ConfigError::InvalidGogc); - } - if p.uast_spill_threshold < 0 { - return Err(ConfigError::InvalidUastSpillThreshold); - } - if p.intra_commit_parallel_threshold < 0 { - return Err(ConfigError::InvalidIntraCommitParallelThreshold); - } - if p.max_intra_commit_workers < 0 { - return Err(ConfigError::InvalidMaxIntraCommitWorkers); - } - if p.max_uast_blob_size < 0 { - return Err(ConfigError::InvalidMaxUastBlobSize); - } - if p.max_changes_per_commit < 0 { - return Err(ConfigError::InvalidMaxChangesPerCommit); - } - if p.max_diff_batch_size < 0 { - return Err(ConfigError::InvalidMaxDiffBatchSize); - } - if p.memory_budget_ratio < 0 || p.memory_budget_ratio > pct_max { - return Err(ConfigError::InvalidMemoryBudgetRatio); - } - if p.memory_limit_ratio < 0 || p.memory_limit_ratio > pct_max { - return Err(ConfigError::InvalidMemoryLimitRatio); - } - Ok(()) - } - - fn validate_history(&self) -> Result<(), ConfigError> { let h = &self.history; - if h.burndown.granularity < 0 { - return Err(ConfigError::InvalidBurndownGranularity); - } - if h.burndown.sampling < 0 { - return Err(ConfigError::InvalidBurndownSampling); - } - self.validate_couples()?; - self.validate_devs()?; - self.validate_file_history()?; - self.validate_sentiment()?; - if h.typos.max_distance < 0 { - return Err(ConfigError::InvalidTyposMaxDistance); - } - if h.imports.goroutines < 0 { - return Err(ConfigError::InvalidImportsGoroutines); - } - if h.imports.max_file_size < 0 { - return Err(ConfigError::InvalidImportsMaxFileSize); - } - if h.imports.max_dependency_risk_rows < 0 { - return Err(ConfigError::InvalidImportsMaxDependencyRiskRows); - } - if h.anomaly.threshold < 0.0 { - return Err(ConfigError::InvalidAnomalyThreshold); - } - if h.anomaly.window_size != 0 && h.anomaly.window_size < MIN_ANOMALY_WINDOW_SIZE { - return Err(ConfigError::InvalidAnomalyWindowSize); - } - if h.clones.max_clone_pairs < 0 { - return Err(ConfigError::InvalidClonesMaxClonePairs); - } - Ok(()) - } - - const fn validate_couples(&self) -> Result<(), ConfigError> { - let cp = &self.history.couples; - if cp.coupling_threshold_high < 0 { - return Err(ConfigError::InvalidCouplingThreshold); - } - if cp.ownership_few_threshold < 0 { - return Err(ConfigError::InvalidOwnershipFewThreshold); - } - if cp.ownership_moderate_threshold < 0 { - return Err(ConfigError::InvalidOwnershipModerateThreshold); - } - if cp.hll_precision != 0 - && (cp.hll_precision < MIN_HLL_PRECISION || cp.hll_precision > MAX_HLL_PRECISION) - { - return Err(ConfigError::InvalidCouplesHllPrecision); - } - Ok(()) - } - - fn validate_devs(&self) -> Result<(), ConfigError> { - let dv = &self.history.devs; - if dv.bus_factor_threshold < 0.0 || dv.bus_factor_threshold > RATIO_MAX { - return Err(ConfigError::InvalidBusFactorThreshold); - } - if dv.risk_threshold_critical < 0.0 || dv.risk_threshold_critical > PERCENT_MAX { - return Err(ConfigError::InvalidDevsRiskThresholdCritical); - } - if dv.risk_threshold_high < 0.0 || dv.risk_threshold_high > PERCENT_MAX { - return Err(ConfigError::InvalidDevsRiskThresholdHigh); - } - if dv.risk_threshold_medium < 0.0 || dv.risk_threshold_medium > PERCENT_MAX { - return Err(ConfigError::InvalidDevsRiskThresholdMedium); - } - if dv.active_threshold_ratio < 0.0 || dv.active_threshold_ratio > RATIO_MAX { - return Err(ConfigError::InvalidDevsActiveThresholdRatio); - } - if dv.default_active_days < 0 { - return Err(ConfigError::InvalidDevsDefaultActiveDays); - } - if dv.hll_precision != 0 - && (dv.hll_precision < MIN_HLL_PRECISION || dv.hll_precision > MAX_HLL_PRECISION) + let before_threshold: [IntRule; 7] = [ + ( + keys::MAX_CHANGES_PER_COMMIT, + self.pipeline.max_changes_per_commit, + "must be >= 0", + |v| v >= 0, + ), + ( + keys::CHUNK_SIZE, + self.pipeline.chunk_size, + "must be >= 1", + |v| v >= 1, + ), + (keys::TICK_SIZE, h.tick_size, "must be > 0", |v| v > 0), + ( + keys::BURNDOWN_GRANULARITY, + h.burndown.granularity, + "must be > 0", + |v| v > 0, + ), + ( + keys::BURNDOWN_SAMPLING, + h.burndown.sampling, + "must be > 0", + |v| v > 0, + ), + ( + keys::SENTIMENT_MIN_COMMENT_LENGTH, + h.sentiment.min_comment_length, + "must be >= 0", + |v| v >= 0, + ), + ( + keys::TYPOS_MAX_DISTANCE, + h.typos.max_distance, + "must be > 0", + |v| v > 0, + ), + ]; + if let Some(e) = first_int_violation(&before_threshold) { + return Err(e); + } + if let Some(t) = h + .anomaly + .threshold + .filter(|t| t.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater)) { - return Err(ConfigError::InvalidDevsHllPrecision); - } - Ok(()) - } - - const fn validate_file_history(&self) -> Result<(), ConfigError> { - let fh = &self.history.file_history; - if fh.hotspot_threshold_critical < 0 { - return Err(ConfigError::InvalidHotspotThresholdCritical); - } - if fh.hotspot_threshold_high < 0 { - return Err(ConfigError::InvalidHotspotThresholdHigh); - } - if fh.hotspot_threshold_medium < 0 { - return Err(ConfigError::InvalidHotspotThresholdMedium); - } - Ok(()) + return Err(ConfigError { + key: keys::ANOMALY_THRESHOLD, + rule: "must be > 0", + got: t.to_string(), + }); + } + let window: [IntRule; 1] = [( + keys::ANOMALY_WINDOW_SIZE, + h.anomaly.window_size, + "must be >= 2", + |v| v >= MIN_ANOMALY_WINDOW_SIZE, + )]; + first_int_violation(&window).map_or(Ok(()), Err) } - fn validate_sentiment(&self) -> Result<(), ConfigError> { - let se = &self.history.sentiment; - if se.min_comment_length < 0 { - return Err(ConfigError::InvalidSentimentMinLength); - } - if se.gap < 0.0 || se.gap > SENTIMENT_GAP_MAX { - return Err(ConfigError::InvalidSentimentGap); - } - if se.neutralizer_weight < 0.0 || se.neutralizer_weight > RATIO_MAX { - return Err(ConfigError::InvalidNeutralizerWeight); - } - if se.max_weight_ratio < 0.0 { - return Err(ConfigError::InvalidMaxWeightRatio); - } - Ok(()) - } -} - -// --------------------------------------------------------------------------- -// Default impls -// -// These reproduce the registered defaults, i.e. the config that results from -// an empty .codefang.yaml. Note this is NOT the all-zero value, which is -// obtained with `Config::zero()`. -// --------------------------------------------------------------------------- - -use crate::defaults::*; - -impl Default for PipelineConfig { - fn default() -> Self { - Self { - workers: DEFAULT_PIPELINE_WORKERS, - memory_budget: DEFAULT_PIPELINE_MEMORY_BUDGET.to_owned(), - blob_cache_size: DEFAULT_PIPELINE_BLOB_CACHE_SIZE.to_owned(), - diff_cache_size: DEFAULT_PIPELINE_DIFF_CACHE_SIZE, - blob_arena_size: DEFAULT_PIPELINE_BLOB_ARENA_SIZE.to_owned(), - commit_batch_size: DEFAULT_PIPELINE_COMMIT_BATCH_SIZE, - gogc: DEFAULT_PIPELINE_GOGC, - ballast_size: DEFAULT_PIPELINE_BALLAST_SIZE.to_owned(), - // memory_limit and worker_timeout have no registered default and - // stay at the zero value (empty string). - memory_limit: String::new(), - worker_timeout: String::new(), - uast_spill_threshold: DEFAULT_PIPELINE_UAST_SPILL_THRESHOLD, - intra_commit_parallel_threshold: DEFAULT_PIPELINE_INTRA_COMMIT_PARALLEL_THRESHOLD, - max_intra_commit_workers: DEFAULT_PIPELINE_MAX_INTRA_COMMIT_WORKERS, - max_uast_blob_size: DEFAULT_PIPELINE_MAX_UAST_BLOB_SIZE, - uast_parse_timeout: DEFAULT_PIPELINE_UAST_PARSE_TIMEOUT.to_owned(), - max_changes_per_commit: DEFAULT_PIPELINE_MAX_CHANGES_PER_COMMIT, - max_diff_batch_size: DEFAULT_PIPELINE_MAX_DIFF_BATCH_SIZE, - memory_budget_ratio: DEFAULT_PIPELINE_MEMORY_BUDGET_RATIO, - memory_budget_cap: DEFAULT_PIPELINE_MEMORY_BUDGET_CAP.to_owned(), - memory_limit_ratio: DEFAULT_PIPELINE_MEMORY_LIMIT_RATIO, - uast_spill_trim_interval: DEFAULT_PIPELINE_UAST_SPILL_TRIM_INTERVAL, - native_trim_interval: DEFAULT_PIPELINE_NATIVE_TRIM_INTERVAL, - max_streaming_buffering: DEFAULT_PIPELINE_MAX_STREAMING_BUFFERING, - drain_prefetch_timeout: DEFAULT_PIPELINE_DRAIN_PREFETCH_TIMEOUT.to_owned(), - sampler_interval: DEFAULT_PIPELINE_SAMPLER_INTERVAL.to_owned(), - worker_ratio: DEFAULT_PIPELINE_WORKER_RATIO, - uast_worker_ratio: DEFAULT_PIPELINE_UAST_WORKER_RATIO, - leaf_worker_divisor: DEFAULT_PIPELINE_LEAF_WORKER_DIVISOR, - min_leaf_workers: DEFAULT_PIPELINE_MIN_LEAF_WORKERS, - buffer_size_multiplier: DEFAULT_PIPELINE_BUFFER_SIZE_MULTIPLIER, - budget_limit_ratio: DEFAULT_PIPELINE_BUDGET_LIMIT_RATIO, - system_ram_limit_ratio: DEFAULT_PIPELINE_SYSTEM_RAM_LIMIT_RATIO, - static_max_workers: DEFAULT_PIPELINE_STATIC_MAX_WORKERS, - malloc_trim_interval: DEFAULT_PIPELINE_MALLOC_TRIM_INTERVAL, - static_memory_limit_ratio: DEFAULT_PIPELINE_STATIC_MEMORY_LIMIT_RATIO, - diff_job_buffer_multiplier: DEFAULT_PIPELINE_DIFF_JOB_BUFFER_MULTIPLIER, - } - } -} - -impl Default for AnomalyConfig { - fn default() -> Self { - Self { - threshold: DEFAULT_ANOMALY_THRESHOLD, - window_size: DEFAULT_ANOMALY_WINDOW_SIZE, - } - } -} - -impl Default for BurndownConfig { - fn default() -> Self { - Self { - granularity: DEFAULT_BURNDOWN_GRANULARITY, - sampling: DEFAULT_BURNDOWN_SAMPLING, - track_files: DEFAULT_BURNDOWN_TRACK_FILES, - track_people: DEFAULT_BURNDOWN_TRACK_PEOPLE, - hibernation_threshold: DEFAULT_BURNDOWN_HIBERNATION_THRESHOLD, - hibernation_to_disk: DEFAULT_BURNDOWN_HIBERNATION_TO_DISK, - hibernation_directory: DEFAULT_BURNDOWN_HIBERNATION_DIRECTORY.to_owned(), - debug: DEFAULT_BURNDOWN_DEBUG, - goroutines: DEFAULT_BURNDOWN_GOROUTINES, - } - } -} - -impl Default for CouplesConfig { - fn default() -> Self { - Self { - coupling_threshold_high: DEFAULT_COUPLES_COUPLING_THRESHOLD_HIGH, - ownership_few_threshold: DEFAULT_COUPLES_OWNERSHIP_FEW_THRESHOLD, - ownership_moderate_threshold: DEFAULT_COUPLES_OWNERSHIP_MODERATE_THRESHOLD, - batch_coupling_threshold: DEFAULT_COUPLES_BATCH_COUPLING_THRESHOLD, - hll_precision: DEFAULT_COUPLES_HLL_PRECISION, - top_k_per_file: DEFAULT_COUPLES_TOP_K_PER_FILE, - min_edge_weight: DEFAULT_COUPLES_MIN_EDGE_WEIGHT, - } - } -} - -impl Default for DevsConfig { - fn default() -> Self { - Self { - consider_empty_commits: DEFAULT_DEVS_CONSIDER_EMPTY_COMMITS, - anonymize: DEFAULT_DEVS_ANONYMIZE, - bus_factor_threshold: DEFAULT_DEVS_BUS_FACTOR_THRESHOLD, - risk_threshold_critical: DEFAULT_DEVS_RISK_THRESHOLD_CRITICAL, - risk_threshold_high: DEFAULT_DEVS_RISK_THRESHOLD_HIGH, - risk_threshold_medium: DEFAULT_DEVS_RISK_THRESHOLD_MEDIUM, - active_threshold_ratio: DEFAULT_DEVS_ACTIVE_THRESHOLD_RATIO, - default_active_days: DEFAULT_DEVS_DEFAULT_ACTIVE_DAYS, - hll_precision: DEFAULT_DEVS_HLL_PRECISION, - } - } -} - -impl Default for FileHistoryConfig { - fn default() -> Self { - Self { - hotspot_threshold_critical: DEFAULT_FILE_HISTORY_HOTSPOT_CRITICAL, - hotspot_threshold_high: DEFAULT_FILE_HISTORY_HOTSPOT_HIGH, - hotspot_threshold_medium: DEFAULT_FILE_HISTORY_HOTSPOT_MEDIUM, - } - } -} - -impl Default for ImportsConfig { - fn default() -> Self { - Self { - goroutines: DEFAULT_IMPORTS_GOROUTINES, - max_file_size: DEFAULT_IMPORTS_MAX_FILE_SIZE, - max_dependency_risk_rows: DEFAULT_IMPORTS_MAX_DEPENDENCY_RISK_ROWS, - } - } -} - -impl Default for SentimentConfig { - fn default() -> Self { - Self { - min_comment_length: DEFAULT_SENTIMENT_MIN_COMMENT_LENGTH, - gap: DEFAULT_SENTIMENT_GAP, - neutralizer_weight: DEFAULT_SENTIMENT_NEUTRALIZER_WEIGHT, - max_weight_ratio: DEFAULT_SENTIMENT_MAX_WEIGHT_RATIO, - positive_threshold: DEFAULT_SENTIMENT_POSITIVE_THRESHOLD, - negative_threshold: DEFAULT_SENTIMENT_NEGATIVE_THRESHOLD, - trend_threshold: DEFAULT_SENTIMENT_TREND_THRESHOLD, - low_sentiment_risk_thresh: DEFAULT_SENTIMENT_LOW_SENTIMENT_RISK_THRESH, - } - } -} - -impl Default for ShotnessConfig { - fn default() -> Self { - Self { - dsl_struct: DEFAULT_SHOTNESS_DSL_STRUCT.to_owned(), - dsl_name: DEFAULT_SHOTNESS_DSL_NAME.to_owned(), - } - } -} - -impl Default for TyposConfig { - fn default() -> Self { - Self { - max_distance: DEFAULT_TYPOS_MAX_DISTANCE, - } - } -} - -impl Default for ClonesConfig { - fn default() -> Self { - Self { - max_clone_pairs: DEFAULT_CLONES_MAX_CLONE_PAIRS, - num_hashes: DEFAULT_CLONES_NUM_HASHES, - num_bands: DEFAULT_CLONES_NUM_BANDS, - num_rows: DEFAULT_CLONES_NUM_ROWS, - shingle_size: DEFAULT_CLONES_SHINGLE_SIZE, - similarity_type2: DEFAULT_CLONES_SIMILARITY_TYPE2, - similarity_type3: DEFAULT_CLONES_SIMILARITY_TYPE3, - threshold_ratio_yellow: DEFAULT_CLONES_THRESHOLD_RATIO_YELLOW, - threshold_ratio_red: DEFAULT_CLONES_THRESHOLD_RATIO_RED, - threshold_pairs_yellow: DEFAULT_CLONES_THRESHOLD_PAIRS_YELLOW, - threshold_pairs_red: DEFAULT_CLONES_THRESHOLD_PAIRS_RED, - } - } -} - -impl Default for CheckpointConfig { - fn default() -> Self { - Self { - enabled: DEFAULT_CHECKPOINT_ENABLED, - dir: DEFAULT_CHECKPOINT_DIR.to_owned(), - resume: DEFAULT_CHECKPOINT_RESUME, - clear_prev: DEFAULT_CHECKPOINT_CLEAR_PREV, - } + /// Every key this configuration sets, in [`KEYS`] order. An empty + /// `analyzers` list and an empty shotness query count as unset. + #[must_use] + pub fn settings(&self) -> Vec<Setting> { + let h = &self.history; + let non_empty = |s: &Option<String>| s.clone().filter(|s| !s.trim().is_empty()); + let candidates = [ + ( + keys::ANALYZERS, + self.analyzers + .clone() + .filter(|a| !a.is_empty()) + .map(Value::List), + ), + ( + keys::MAX_CHANGES_PER_COMMIT, + self.pipeline.max_changes_per_commit.map(Value::Int), + ), + ( + keys::MEMORY_BUDGET, + non_empty(&self.pipeline.memory_budget).map(Value::Text), + ), + (keys::CHUNK_SIZE, self.pipeline.chunk_size.map(Value::Int)), + (keys::TICK_SIZE, h.tick_size.map(Value::Int)), + ( + keys::BURNDOWN_GRANULARITY, + h.burndown.granularity.map(Value::Int), + ), + (keys::BURNDOWN_SAMPLING, h.burndown.sampling.map(Value::Int)), + ( + keys::BURNDOWN_TRACK_FILES, + h.burndown.track_files.map(Value::Bool), + ), + ( + keys::BURNDOWN_TRACK_PEOPLE, + h.burndown.track_people.map(Value::Bool), + ), + ( + keys::DEVS_CONSIDER_EMPTY_COMMITS, + h.devs.consider_empty_commits.map(Value::Bool), + ), + (keys::DEVS_ANONYMIZE, h.devs.anonymize.map(Value::Bool)), + ( + keys::SENTIMENT_MIN_COMMENT_LENGTH, + h.sentiment.min_comment_length.map(Value::Int), + ), + ( + keys::SHOTNESS_DSL_STRUCT, + non_empty(&h.shotness.dsl_struct).map(Value::Text), + ), + ( + keys::SHOTNESS_DSL_NAME, + non_empty(&h.shotness.dsl_name).map(Value::Text), + ), + ( + keys::SHOTNESS_KEEP_ENDED, + h.shotness.keep_ended.map(Value::Bool), + ), + ( + keys::TYPOS_MAX_DISTANCE, + h.typos.max_distance.map(Value::Int), + ), + ( + keys::ANOMALY_THRESHOLD, + h.anomaly.threshold.map(Value::Float), + ), + ( + keys::ANOMALY_WINDOW_SIZE, + h.anomaly.window_size.map(Value::Int), + ), + ]; + candidates + .into_iter() + .filter_map(|(key, value)| value.map(|value| Setting { key, value })) + .collect() } } diff --git a/crates/cf-couples/Cargo.toml b/crates/cf-couples/Cargo.toml index 1a7803f5..e80408f3 100644 --- a/crates/cf-couples/Cargo.toml +++ b/crates/cf-couples/Cargo.toml @@ -8,17 +8,15 @@ repository.workspace = true publish = false # cf-couples — file/developer co-change coupling analysis (analyzer id: -# history/couples, non-sequential / parallelizable). +# history/couples). # # Builds the file co-occurrence matrix (files changed in the same commit) and # the developer co-occurrence matrix (developers touching the same file), plus -# per-file ownership cardinality (HyperLogLog), all from sorted indices so the -# output is deterministic. The coupling-strength formula is code-maat's -# `co_changes / avg(revs_a, revs_b)` capped at 1.0. +# per-file ownership cardinality (HyperLogLog), following renames, all from +# sorted indices so the output is deterministic. The coupling-strength formula +# is code-maat's `co_changes / avg(revs_a, revs_b)` capped at 1.0. # -# Report serialization routes exclusively through cf-gojson (and, once wired, -# cf-goyaml); the machine-format bytes (json/yaml/ndjson/timeseries/compact/ -# bin) are pinned against the reference implementation by tests/compat. +# Report serialization routes exclusively through cf-gojson / cf-goyaml. [features] default = ["hll"] diff --git a/crates/cf-couples/src/aggregator.rs b/crates/cf-couples/src/aggregator.rs index 55985121..e727cf17 100644 --- a/crates/cf-couples/src/aggregator.rs +++ b/crates/cf-couples/src/aggregator.rs @@ -1,89 +1,93 @@ -//! In-memory coupling accumulator and pure reduction helpers. -//! -//! The full streaming aggregator runs over a disk-backed spill store, and the -//! consume/fork/merge pipeline driver depends on git tree diffs, identity -//! detection, and Bloom-filtered merge dedup. Those framework crates are not -//! yet wired in, so this module provides the **pure in-memory accumulation -//! and report-building** logic that the streaming aggregator wraps, behind -//! small traits the eventual framework integration can satisfy. See crate -//! TODOs. +//! The in-memory coupling accumulator: sums the per-commit payloads into the +//! file co-occurrence matrix and the per-developer file touches, applies +//! renames as they happen, and reduces the result to the report's file set. use crate::matrix::{ build_files_index, compute_files_matrix, compute_people_matrix, People, RawFiles, }; use crate::metrics::ReportData; use crate::tc::{CommitData, RenamePair}; -use std::collections::BTreeMap; - -/// Minimal interface the streaming aggregator needs from a spill store. -/// -/// Defined here so the in-memory core compiles without `cf-spillstore`. The -/// real integration implements this over the spill store. -pub trait FileSpill { - /// Returns the current in-memory `file -> otherFile -> count` buffer. - fn current(&self) -> &RawFiles; - /// Mutable access to the current buffer. - fn current_mut(&mut self) -> &mut RawFiles; - /// Number of files held in memory. - fn len(&self) -> usize; - /// True when no files are held in memory. - fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -/// A trivial all-in-memory [`FileSpill`] (no disk overflow). Sufficient for -/// tests and small repos; the streaming aggregator swaps in a spilling impl. -#[derive(Debug, Default)] -pub struct MemFileSpill { - files: RawFiles, -} - -impl FileSpill for MemFileSpill { - fn current(&self) -> &RawFiles { - &self.files - } - fn current_mut(&mut self) -> &mut RawFiles { - &mut self.files - } - fn len(&self) -> usize { - self.files.len() - } -} +use std::collections::{BTreeMap, BTreeSet, HashSet}; /// In-memory coupling accumulator. #[derive(Debug, Default)] pub struct Aggregator { - files: MemFileSpill, + files: RawFiles, people: People, people_commits: Vec<i64>, - renames: Vec<RenamePair>, people_number: usize, } impl Aggregator { /// Creates an aggregator sized for `people_number` developers. Slices are - /// `people_number + 1` long to leave room for the "missing author" slot. + /// `people_number + 1` long to leave room for the "missing author" slot; + /// they grow on demand when a later author id appears. #[must_use] pub fn new(people_number: usize) -> Self { Self { - files: MemFileSpill::default(), + files: BTreeMap::new(), people: vec![BTreeMap::new(); people_number + 1], people_commits: vec![0; people_number + 1], - renames: Vec::new(), people_number, } } - /// Ingests a single per-commit payload. + /// Ingests a single per-commit payload: the commit's renames are applied + /// first (so the moved file keeps one identity), then its author touches + /// and its coupling context are accumulated. pub fn add(&mut self, author: usize, data: &CommitData) { self.ensure_capacity(author + 1); + for rename in &data.renames { + self.apply_rename(rename); + } if data.commit_counted { self.people_commits[author] += 1; } self.add_author_files(&data.author_files, author); self.add_file_couplings(&data.coupling_files); - self.renames.extend(data.renames.iter().cloned()); + } + + /// Carries every accumulated count of `from_name` onto `to_name`: the + /// co-change row and column (the diagonal included) and every + /// developer's touch count. A co-change recorded between the two paths + /// themselves (a rename onto a path that was deleted earlier in the + /// walk) is dropped, so the merged diagonal is the sum of both paths' + /// change counts. + fn apply_rename(&mut self, rename: &RenamePair) { + let (from, to) = (rename.from_name.as_str(), rename.to_name.as_str()); + if from == to { + return; + } + if let Some(row) = self.files.remove(from) { + for (other, count) in row { + if other == to { + // The cross term: dropped on both sides. + if let Some(lane) = self.files.get_mut(to) { + lane.remove(from); + } + continue; + } + let key = if other == from { to } else { other.as_str() }; + if other != from { + if let Some(lane) = self.files.get_mut(&other) { + if let Some(moved) = lane.remove(from) { + *lane.entry(to.to_string()).or_insert(0) += moved; + } + } + } + *self + .files + .entry(to.to_string()) + .or_default() + .entry(key.to_string()) + .or_insert(0) += count; + } + } + for touches in &mut self.people { + if let Some(moved) = touches.remove(from) { + *touches.entry(to.to_string()).or_insert(0) += moved; + } + } } /// Merges per-commit author file touches. @@ -96,9 +100,8 @@ impl Aggregator { /// Increments the file co-occurrence matrix for every ordered file pair, /// including the diagonal. fn add_file_couplings(&mut self, coupling_files: &[String]) { - let files = self.files.current_mut(); for a in coupling_files { - let lane = files.entry(a.clone()).or_default(); + let lane = self.files.entry(a.clone()).or_default(); for b in coupling_files { *lane.entry(b.clone()).or_insert(0) += 1; } @@ -116,8 +119,8 @@ impl Aggregator { /// Returns the accumulated raw file coupling map. #[must_use] - pub fn raw_files(&self) -> &RawFiles { - self.files.current() + pub const fn raw_files(&self) -> &RawFiles { + &self.files } /// Returns the accumulated per-developer file maps. @@ -126,22 +129,25 @@ impl Aggregator { &self.people } - /// Builds the dense [`ReportData`] from the accumulated state, restricted - /// to the given current-file set (without the git-tree line counting, - /// which requires libgit2). + /// Builds the dense [`ReportData`] from the accumulated state. /// - /// `current_files`: when `Some`, only these files are retained (callers - /// derive this from the final commit tree); when `None`, all accumulated - /// files are kept. `files_lines_by_name` is supplied by the caller because - /// line counts require reading blobs from the final commit (libgit2). + /// The report lists every file that was coupled or touched in the walk, + /// restricted to `current_files` when given (the files present at the + /// window head; `None` keeps every accumulated file). `files_lines_by_name` + /// supplies the per-file line counts the caller read from the head tree. #[must_use] pub fn build_report( &self, - current_files: Option<&std::collections::HashSet<String>>, + current_files: Option<&HashSet<String>>, files_lines_by_name: &BTreeMap<String, i32>, ) -> ReportData { let (reduced_files, reduced_people) = self.reduce(current_files); - let (files_sequence, files_index) = build_files_index(&reduced_files); + let names: BTreeSet<&str> = reduced_files + .keys() + .chain(reduced_people.iter().flat_map(BTreeMap::keys)) + .map(String::as_str) + .collect(); + let (files_sequence, files_index) = build_files_index(names); let files_lines: Vec<i32> = files_sequence .iter() @@ -168,37 +174,21 @@ impl Aggregator { } } - /// Filters files and people to the current-file set — the public surface - /// the store-record path shares with [`Aggregator::build_report`]. With - /// `current_files = None`, keeps everything (the streaming fallback where - /// the last commit's tree is unavailable). - #[must_use] - pub fn reduced( - &self, - current_files: Option<&std::collections::HashSet<String>>, - ) -> (RawFiles, People) { - self.reduce(current_files) - } - /// Filters files and people to the current-file set. With /// `current_files = None`, keeps everything. - fn reduce( - &self, - current_files: Option<&std::collections::HashSet<String>>, - ) -> (RawFiles, People) { + fn reduce(&self, current_files: Option<&HashSet<String>>) -> (RawFiles, People) { let keep = |f: &str| current_files.map_or(true, |s| s.contains(f)); let mut reduced_files: RawFiles = BTreeMap::new(); - for (file, refmap) in self.files.current() { - if !keep(file) || refmap.is_empty() { + for (file, refmap) in &self.files { + if !keep(file) { continue; } - let mut fmap: BTreeMap<String, i64> = BTreeMap::new(); - for (other, &refval) in refmap { - if refval > 0 && keep(other) { - fmap.insert(other.clone(), refval); - } - } + let fmap: BTreeMap<String, i64> = refmap + .iter() + .filter(|(other, &refval)| refval > 0 && keep(other)) + .map(|(o, &c)| (o.clone(), c)) + .collect(); if !fmap.is_empty() { reduced_files.insert(file.clone(), fmap); } @@ -233,6 +223,17 @@ mod tests { } } + fn rename(from: &str, to: &str) -> RenamePair { + RenamePair { + from_name: from.to_string(), + to_name: to.to_string(), + } + } + + fn current(files: &[&str]) -> HashSet<String> { + files.iter().map(|s| (*s).to_string()).collect() + } + #[test] fn add_builds_symmetric_file_matrix() { let mut agg = Aggregator::new(1); @@ -259,11 +260,8 @@ mod tests { let mut agg = Aggregator::new(1); agg.add(0, &commit(&["a.go", "b.go"])); agg.add(0, &commit(&["a.go", "old.go"])); - let current: std::collections::HashSet<String> = ["a.go".to_string(), "b.go".to_string()] - .into_iter() - .collect(); let lines = BTreeMap::new(); - let report = agg.build_report(Some(¤t), &lines); + let report = agg.build_report(Some(¤t(&["a.go", "b.go"])), &lines); // old.go dropped. assert_eq!(report.files, vec!["a.go", "b.go"]); // a.go self count = 2 (appeared in 2 commits). @@ -273,6 +271,28 @@ mod tests { assert_eq!(report.files_matrix[0][&1], 1); } + /// A file touched only by an author (no coupling context — an oversized + /// commit) is still listed, with an empty coupling row. + #[test] + fn build_report_lists_files_with_only_author_touches() { + let mut agg = Aggregator::new(1); + agg.add( + 0, + &CommitData { + coupling_files: Vec::new(), + author_files: BTreeMap::from([("big.go".to_string(), 1)]), + renames: Vec::new(), + commit_counted: true, + }, + ); + agg.add(1, &commit(&["a.go"])); + let report = agg.build_report(None, &BTreeMap::new()); + assert_eq!(report.files, vec!["a.go", "big.go"]); + assert!(report.files_matrix[1].is_empty()); + assert_eq!(report.people_files[0], vec![1]); + assert_eq!(report.people_matrix[0][&0], 1); + } + #[test] fn ensure_capacity_grows_for_new_author() { let mut agg = Aggregator::new(1); @@ -280,4 +300,84 @@ mod tests { assert!(agg.people().len() >= 6); assert_eq!(agg.people()[5]["x.go"], 1); } + + /// `git mv b d`: d inherits b's row, column, diagonal and owners, plus + /// the rename commit's own touch; b disappears. + #[test] + fn rename_merges_the_old_row_column_and_owners_into_the_new_path() { + let mut agg = Aggregator::new(1); + agg.add(0, &commit(&["a", "b"])); + agg.add(1, &commit(&["a", "b", "c"])); + let mut mv = commit(&["d"]); + mv.renames.push(rename("b", "d")); + agg.add(0, &mv); + + let raw = agg.raw_files(); + assert!(!raw.contains_key("b"), "{raw:?}"); + assert!(!raw["a"].contains_key("b"), "{raw:?}"); + assert!(!raw["c"].contains_key("b"), "{raw:?}"); + assert_eq!(raw["d"]["d"], 3, "b's 2 changes + the rename commit"); + assert_eq!(raw["d"]["a"], 2); + assert_eq!(raw["a"]["d"], 2); + assert_eq!(raw["d"]["c"], 1); + assert_eq!(raw["c"]["d"], 1); + assert_eq!(agg.people()[0].get("b"), None); + assert_eq!(agg.people()[0]["d"], 2); + assert_eq!(agg.people()[1]["d"], 1); + + let report = agg.build_report(Some(¤t(&["a", "c", "d"])), &BTreeMap::new()); + assert_eq!(report.files, vec!["a", "c", "d"]); + assert_eq!(report.files_matrix[0][&2], 2); + assert_eq!(report.files_matrix[2][&2], 3); + } + + /// Renames chain: b → d, then d → e carries everything to e. + #[test] + fn chained_renames_follow_the_latest_path() { + let mut agg = Aggregator::new(1); + agg.add(0, &commit(&["a", "b"])); + let mut first = commit(&["d"]); + first.renames.push(rename("b", "d")); + agg.add(0, &first); + let mut second = commit(&["e"]); + second.renames.push(rename("d", "e")); + agg.add(0, &second); + + let raw = agg.raw_files(); + assert_eq!(raw.keys().collect::<Vec<_>>(), vec!["a", "e"]); + assert_eq!(raw["e"]["e"], 3); + assert_eq!(raw["e"]["a"], 1); + assert_eq!(raw["a"]["e"], 1); + assert_eq!(agg.people()[0]["e"], 3); + } + + /// A rename whose source has no history (outside the window) and a + /// self-rename are no-ops. + #[test] + fn rename_without_history_is_a_no_op() { + let mut agg = Aggregator::new(1); + let mut data = commit(&["d"]); + data.renames.push(rename("ghost", "d")); + data.renames.push(rename("d", "d")); + agg.add(0, &data); + assert_eq!(agg.raw_files()["d"]["d"], 1); + assert_eq!(agg.people()[0]["d"], 1); + } + + /// A rename onto a path deleted earlier drops the cross term: the merged + /// diagonal is the sum of both paths' change counts. + #[test] + fn rename_onto_a_dead_path_sums_the_change_counts() { + let mut agg = Aggregator::new(1); + agg.add(0, &commit(&["b", "d"])); + agg.add(0, &commit(&["b"])); + let mut mv = commit(&["d"]); + mv.renames.push(rename("b", "d")); + agg.add(0, &mv); + let raw = agg.raw_files(); + assert_eq!(raw.keys().collect::<Vec<_>>(), vec!["d"]); + assert_eq!(raw["d"].keys().collect::<Vec<_>>(), vec!["d"]); + assert_eq!(raw["d"]["d"], 1 + 2 + 1); + assert_eq!(agg.people()[0]["d"], 4); + } } diff --git a/crates/cf-couples/src/lib.rs b/crates/cf-couples/src/lib.rs index 2987262f..f70f25c8 100644 --- a/crates/cf-couples/src/lib.rs +++ b/crates/cf-couples/src/lib.rs @@ -1,34 +1,35 @@ //! `cf-couples` — file/developer co-change coupling analysis. //! -//! Implements the `history/couples` analyzer (non-sequential / -//! parallelizable). From the commit history it derives two co-occurrence -//! ("coupling") matrices and per-file ownership: +//! Implements the math of the `history/couples` analyzer. From the per-commit +//! payloads the history walk produces ([`CommitData`]) it derives two +//! co-occurrence ("coupling") matrices and per-file ownership: //! -//! - **File coupling** — how often a pair of files changed in the same commit +//! - **File coupling** — the number of commits in which a pair of files both +//! changed; the diagonal is a file's own change count //! ([`matrix::compute_files_matrix`], [`metrics::compute_file_coupling`]). -//! - **Developer coupling** — how often a pair of developers touched the same -//! file ([`matrix::compute_people_matrix`], +//! - **Developer coupling** — `sum over files of min(changes_i, changes_j)` +//! for a pair of developers; the diagonal is a developer's own file-change +//! count ([`matrix::compute_people_matrix`], //! [`metrics::compute_developer_coupling`]). -//! - **File ownership** — per-file contributor cardinality via a `HyperLogLog` -//! sketch keyed by `LittleEndian(devID)` +//! - **File ownership** — per-file line count and contributor cardinality //! ([`metrics::compute_file_ownership`]). //! - **Aggregate** summary statistics ([`metrics::compute_aggregate`]). //! //! The coupling-strength formula is code-maat's `co_changes / avg(revs_a, //! revs_b)` capped at `1.0`, where `revs` is the diagonal (self-change) count. //! -//! ## Determinism and byte-identity +//! ## Renames //! -//! All index assignment flows from byte-sorted name lists, so the matrices, -//! sequences, and report are reproducible. Per-commit author-file maps only -//! ever store a count of `1` and accumulate additively, so map iteration -//! order is irrelevant to the result. The HLL sketch ([`cf_alg_hll`]) is -//! bit-identical to the reference implementation. +//! A rename carries the old path's accumulated row, column and ownership +//! onto the new path at the commit it happens +//! ([`aggregator::Aggregator::add`]), so a moved file keeps one identity. +//! +//! ## Determinism //! -//! Machine-format report bytes (json/yaml/ndjson/timeseries/compact/bin) are -//! produced via [`cf_gojson`] in [`report`]; this crate never uses -//! `serde_json` for output. Compatibility: output bytes are pinned against -//! the reference implementation by `tests/compat`. +//! All index assignment flows from byte-sorted name lists, so the matrices, +//! sequences and report are a pure function of the walked payloads. Report +//! bytes are produced via [`cf_gojson`] in [`report`]; this crate never uses +//! `serde_json` for output. #![forbid(unsafe_code)] @@ -36,41 +37,13 @@ pub mod aggregator; pub mod matrix; pub mod metrics; pub mod report; -pub mod report_section; -pub mod store; pub mod tc; -/// Analyzer descriptor constants. -pub mod descriptor { - /// Analyzer id. - pub const ID: &str = "history/couples"; - /// Human-readable description. - pub const DESCRIPTION: &str = - "The result is a square matrix, the value in each cell corresponds to the number of times \ -the pair of files appeared in the same commit or pair of developers committed to the same file."; - /// The analyzer is NOT sequential-only (it can be parallelized). - pub const SEQUENTIAL: bool = false; - /// Analyzer name. - pub const NAME: &str = "Couples"; - /// CLI flag. - pub const FLAG: &str = "couples"; -} - -/// Configuration option keys. -pub mod config { - pub const COUPLING_THRESHOLD_HIGH: &str = "Couples.CouplingThresholdHigh"; - pub const OWNERSHIP_FEW_THRESHOLD: &str = "Couples.OwnershipFewThreshold"; - pub const OWNERSHIP_MODERATE_THRESHOLD: &str = "Couples.OwnershipModerateThreshold"; - pub const BATCH_COUPLING_THRESHOLD: &str = "Couples.BatchCouplingThreshold"; - pub const HLL_PRECISION: &str = "Couples.HLLPrecision"; - pub const TOP_K_PER_FILE: &str = "Couples.TopKPerFile"; - pub const MIN_EDGE_WEIGHT: &str = "Couples.MinEdgeWeight"; -} - -/// Maximum number of files in a commit to consider for coupling analysis. -/// Larger changesets are bulk operations (vendor updates, mass renames, -/// formatting) that produce noise. -pub const COUPLES_MAXIMUM_MEANINGFUL_CONTEXT_SIZE: usize = 200; +/// Maximum number of files a commit may touch and still feed the coupling +/// matrix. A larger changeset is a bulk operation (vendor update, mass +/// rename, formatting) whose co-changes are noise, so it contributes no +/// coupling context — its author still owns every file it touched. +pub const COUPLES_MAXIMUM_MEANINGFUL_CONTEXT_SIZE: usize = 1000; /// Splits an identity string `"name|email"` into its components. /// Thin re-export over [`cf_identity`] so the crate's metric code can call @@ -91,8 +64,8 @@ pub fn split_identity(s: &str) -> (String, String) { pub use metrics::{ bucket_ownership, bucket_ownership_with_thresholds, compute_aggregate, compute_all_metrics, compute_all_metrics_with_options, compute_developer_coupling, compute_file_coupling, - compute_file_ownership, filter_top_devs, sort_ownership_by_risk, AggregateData, - ComputedMetrics, DeveloperCouplingData, FileCouplingData, FileOwnershipData, MetricOptions, - OwnershipBucket, ReportData, COUPLING_THRESHOLD_HIGH, FILE_CONTRIB_HLL_PRECISION, + compute_file_ownership, AggregateData, ComputedMetrics, DeveloperCouplingData, + FileCouplingData, FileOwnershipData, MetricOptions, OwnershipBucket, ReportData, + COUPLING_THRESHOLD_HIGH, FILE_CONTRIB_HLL_PRECISION, }; -pub use tc::{CommitData, CommitSummary, RenamePair, TickData}; +pub use tc::{CommitData, RenamePair}; diff --git a/crates/cf-couples/src/matrix.rs b/crates/cf-couples/src/matrix.rs index 5a14c70a..c973f9e7 100644 --- a/crates/cf-couples/src/matrix.rs +++ b/crates/cf-couples/src/matrix.rs @@ -4,7 +4,7 @@ //! index-keyed matrices that form the analyzer report. All index assignment //! flows from byte-sorted name lists so the output is reproducible. -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; /// Raw file co-occurrence data: `file -> otherFile -> count`. pub type RawFiles = BTreeMap<String, BTreeMap<String, i64>>; @@ -13,18 +13,17 @@ pub type RawFiles = BTreeMap<String, BTreeMap<String, i64>>; /// commits`. pub type People = Vec<BTreeMap<String, i64>>; -/// Builds the byte-sorted file-name sequence and a name→index map. -/// -/// The sequence is sorted in raw byte order (the default `str` ordering), -/// which is the index order the report contract expects. +/// Builds the byte-sorted file-name sequence and a name→index map over +/// `names` (a byte-ordered set, so the sequence is the report's index +/// order). #[must_use] -pub fn build_files_index(files: &RawFiles) -> (Vec<String>, HashMap<String, usize>) { - let mut sequence: Vec<String> = files.keys().cloned().collect(); - sequence.sort(); - let mut index = HashMap::with_capacity(sequence.len()); - for (i, file) in sequence.iter().enumerate() { - index.insert(file.clone(), i); - } +pub fn build_files_index(names: BTreeSet<&str>) -> (Vec<String>, HashMap<String, usize>) { + let sequence: Vec<String> = names.into_iter().map(str::to_string).collect(); + let index = sequence + .iter() + .enumerate() + .map(|(i, file)| (file.clone(), i)) + .collect(); (sequence, index) } @@ -36,7 +35,7 @@ struct DevCommit { } /// Builds the developer co-occurrence matrix and per-developer file-index -/// lists. +/// lists over the files in `files_index`. /// /// Returns `(matrix, people_files)` where: /// * `matrix[i]` maps developer `j` → `sum over files of min(commits_i, @@ -47,7 +46,7 @@ struct DevCommit { /// Both have length `people_number + 1` (one extra slot for the /// "missing author" sentinel). #[must_use] -#[allow(clippy::implicit_hasher)] // public signature is frozen +#[allow(clippy::implicit_hasher)] // the index is the std HashMap `build_files_index` builds pub fn compute_people_matrix( people: &People, files_index: &HashMap<String, usize>, @@ -66,42 +65,32 @@ fn build_people_file_indices( people_number: usize, ) -> Vec<Vec<usize>> { let mut result = vec![Vec::new(); people_number + 1]; - for (i, files) in people.iter().enumerate() { - if i > people_number { - break; - } - for file in files.keys() { - if let Some(&fi) = files_index.get(file) { - result[i].push(fi); - } - } + for (i, files) in people.iter().enumerate().take(people_number + 1) { + result[i] = files + .keys() + .filter_map(|file| files_index.get(file).copied()) + .collect(); result[i].sort_unstable(); } result } -/// Builds the `file -> [(devID, commits)]` inverted index. -/// Only positive commit counts are recorded. +/// Builds the `file index -> [(devID, commits)]` inverted index over the +/// indexed files; only positive commit counts are recorded. fn build_inverted_index( people: &People, - _files_index: &HashMap<String, usize>, + files_index: &HashMap<String, usize>, people_number: usize, -) -> BTreeMap<String, Vec<DevCommit>> { - // The inverted index is keyed by file name regardless of `files_index` - // membership: filtering to indexed files happens separately via the - // per-developer file lists. The `files_index` parameter is retained only - // for signature parity with `compute_people_matrix`; the accumulated - // matrix is identical either way because only co-touched files ever - // produce a non-zero `min(commits_i, commits_j)` contribution. - let mut inverted: BTreeMap<String, Vec<DevCommit>> = BTreeMap::new(); - for (i, files) in people.iter().enumerate() { - if i > people_number { - break; - } +) -> BTreeMap<usize, Vec<DevCommit>> { + let mut inverted: BTreeMap<usize, Vec<DevCommit>> = BTreeMap::new(); + for (i, files) in people.iter().enumerate().take(people_number + 1) { for (file, &commits) in files { + let Some(&index) = files_index.get(file) else { + continue; + }; if commits > 0 { inverted - .entry(file.clone()) + .entry(index) .or_default() .push(DevCommit { dev_id: i, commits }); } @@ -115,7 +104,7 @@ fn build_inverted_index( /// For each file, adds `min(commits_a, commits_b)` for every ordered developer /// pair (including `a == b`). fn accumulate_matrix( - inverted: &BTreeMap<String, Vec<DevCommit>>, + inverted: &BTreeMap<usize, Vec<DevCommit>>, people_number: usize, ) -> Vec<BTreeMap<usize, i64>> { let mut matrix: Vec<BTreeMap<usize, i64>> = vec![BTreeMap::new(); people_number + 1]; @@ -135,9 +124,9 @@ fn accumulate_matrix( /// Builds the file co-occurrence matrix keyed by file index. /// /// `matrix[i]` maps the index of every co-changed file to its count, for the -/// file at sorted position `i`. +/// file at sorted position `i`; a file with no coupling row has an empty map. #[must_use] -#[allow(clippy::implicit_hasher)] // public signature is frozen +#[allow(clippy::implicit_hasher)] // the index is the std HashMap `build_files_index` builds pub fn compute_files_matrix( raw_files: &RawFiles, files_sequence: &[String], @@ -172,10 +161,14 @@ mod tests { .collect() } + fn names(files: &RawFiles) -> BTreeSet<&str> { + files.keys().map(String::as_str).collect() + } + #[test] fn files_index_is_byte_sorted() { let files = raw(&[("b.go", &[]), ("a.go", &[]), ("c.go", &[])]); - let (seq, idx) = build_files_index(&files); + let (seq, idx) = build_files_index(names(&files)); assert_eq!(seq, vec!["a.go", "b.go", "c.go"]); assert_eq!(idx["a.go"], 0); assert_eq!(idx["c.go"], 2); @@ -188,7 +181,7 @@ mod tests { ("a.go", &[("a.go", 2), ("b.go", 1)]), ("b.go", &[("b.go", 2), ("a.go", 1)]), ]); - let (seq, idx) = build_files_index(&files); + let (seq, idx) = build_files_index(names(&files)); let m = compute_files_matrix(&files, &seq, &idx); // a.go = 0, b.go = 1. assert_eq!(m[0][&0], 2); @@ -201,7 +194,7 @@ mod tests { fn people_matrix_uses_min_commits() { // file a.go touched by dev0 (3 commits) and dev1 (5 commits). let files = raw(&[("a.go", &[("a.go", 1)])]); - let (_seq, idx) = build_files_index(&files); + let (_seq, idx) = build_files_index(names(&files)); let mut people: People = vec![BTreeMap::new(), BTreeMap::new()]; people[0].insert("a.go".to_string(), 3); people[1].insert("a.go".to_string(), 5); @@ -216,4 +209,21 @@ mod tests { assert_eq!(pfiles[0], vec![0]); assert_eq!(pfiles[1], vec![0]); } + + /// A touched file outside the index (not at the window head) feeds + /// neither the matrix nor the file lists. + #[test] + fn people_matrix_ignores_unindexed_files() { + let files = raw(&[("a.go", &[("a.go", 1)])]); + let (_seq, idx) = build_files_index(names(&files)); + let mut people: People = vec![BTreeMap::new(), BTreeMap::new()]; + people[0].insert("gone.go".to_string(), 3); + people[1].insert("gone.go".to_string(), 5); + people[1].insert("a.go".to_string(), 1); + let (matrix, pfiles) = compute_people_matrix(&people, &idx, 1); + assert!(matrix[0].is_empty()); + assert_eq!(matrix[1][&1], 1); + assert!(pfiles[0].is_empty()); + assert_eq!(pfiles[1], vec![0]); + } } diff --git a/crates/cf-couples/src/metrics.rs b/crates/cf-couples/src/metrics.rs index e6efb6c6..3c053f4e 100644 --- a/crates/cf-couples/src/metrics.rs +++ b/crates/cf-couples/src/metrics.rs @@ -1,9 +1,7 @@ -//! Couples metrics computation. -//! -//! These types and functions are the data-parity-critical core: they produce -//! the numbers that appear in the machine report. The coupling-strength formula -//! is code-maat's `co_changes / avg(revs_a, revs_b)` capped at `1.0`, where -//! `revs` is the diagonal (self-change) count. +//! Couples metrics computation: the numbers that appear in the report. The +//! coupling-strength formula is code-maat's `co_changes / avg(revs_a, +//! revs_b)` capped at `1.0`, where `revs` is the diagonal (self-change) +//! count. use std::collections::BTreeMap; @@ -132,32 +130,23 @@ pub struct ComputedMetrics { pub aggregate: AggregateData, } -/// Coupling strength: `co_changes / avg(self_i, self_j)`, capped at `1.0`. -/// -/// Public wrapper over the crate-private `coupling_strength` helper, exposed for -/// the [`crate::store`] module so the sparse store path reuses the identical -/// formula. +/// Coupling strength: `co_changes / avg(self_i, self_j)`, capped at `1.0`, +/// with an `avg_revs <= 0 → 0.0` guard. Shared by every metric; the float +/// math is part of the report contract. /// /// ``` -/// use cf_couples::metrics::coupling_strength_pub; +/// use cf_couples::metrics::coupling_strength; /// /// // co_changes / avg(self_i, self_j): 4 / avg(8, 8) = 0.5. -/// assert_eq!(coupling_strength_pub(4, 8, 8), 0.5); +/// assert_eq!(coupling_strength(4, 8, 8), 0.5); /// // Capped at 1.0 even when co_changes exceeds the average. -/// assert_eq!(coupling_strength_pub(100, 2, 2), 1.0); +/// assert_eq!(coupling_strength(100, 2, 2), 1.0); /// // avg_revs <= 0 guard yields 0.0. -/// assert_eq!(coupling_strength_pub(5, 0, 0), 0.0); +/// assert_eq!(coupling_strength(5, 0, 0), 0.0); /// ``` #[must_use] -pub fn coupling_strength_pub(co_changes: i64, self_i: i64, self_j: i64) -> f64 { - coupling_strength(co_changes, self_i, self_j) -} - -/// Coupling strength: `co_changes / avg(self_i, self_j)`, capped at `1.0`, -/// with an `avg_revs <= 0 → 0.0` guard. Shared by every metric; the float -/// math is part of the report contract. #[allow(clippy::cast_precision_loss)] // contractual float math on counts -fn coupling_strength(co_changes: i64, self_i: i64, self_j: i64) -> f64 { +pub fn coupling_strength(co_changes: i64, self_i: i64, self_j: i64) -> f64 { let avg_revs = (self_i + self_j) as f64 / PAIR_COUNT; if avg_revs > 0.0 { (co_changes as f64 / avg_revs).min(1.0) @@ -169,10 +158,8 @@ fn coupling_strength(co_changes: i64, self_i: i64, self_j: i64) -> f64 { /// Computes file coupling pairs from the dense files matrix. /// /// Iterates the upper triangle (`j > i`), skips zero co-changes, and sorts the -/// result by `co_changes` descending. The reference binary uses an unstable -/// sort here; this implementation uses a stable sort, which can differ in tie -/// ordering — see crate TODOs on matching the reference's unstable sort for -/// byte-identity. +/// result by `co_changes` descending (a stable sort: ties keep the byte-sorted +/// file order). #[must_use] pub fn compute_file_coupling(input: &ReportData) -> Vec<FileCouplingData> { let mut result: Vec<FileCouplingData> = Vec::new(); @@ -255,8 +242,7 @@ fn dev_name_email(idx: usize, names: &[String]) -> (String, String) { /// Contributor cardinality comes from per-file `HyperLogLog` sketches keyed by /// `LittleEndian(devID)` (memory-efficient on large repos, and the counts are /// part of the report contract). Without the `hll` feature it counts distinct -/// developer IDs exactly via a set (parity-equivalent for typical inputs, -/// exact for small ones). See crate TODOs. +/// developer IDs exactly via a set. #[must_use] #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // precision is a small positive int pub fn compute_file_ownership(input: &ReportData, opts: MetricOptions) -> Vec<FileOwnershipData> { @@ -324,23 +310,10 @@ fn file_contributor_counts( sets.iter().map(|s| s.len() as i32).collect() } -/// Number of simulated iteration-order passes used to locate the median -/// naive-sum for `avg_coupling_strength` (see [`median_map_order_total`]). -const AVG_STRENGTH_ORDER_PASSES: usize = 101; - -/// Computes aggregate statistics from the dense files matrix. -/// -/// `avg_coupling_strength` is a naive left-to-right f64 sum of per-pair -/// strengths divided by the pair count. The reference implementation -/// accumulates that sum while iterating each row as a hash map whose -/// iteration order is randomized per process, so the reference value wobbles -/// by a few ULPs from run to run (rows are visited in fixed ascending order; -/// only the within-row term order varies). A deterministic port must pick one -/// representative of that measured distribution: this implementation reports -/// the median naive-sum over simulated within-row orders (the distribution's -/// most probable region), computed deterministically from the input — see -/// [`median_map_order_total`]. All integer fields are order-independent and -/// exact. +/// Computes aggregate statistics from the dense files matrix: the pair +/// count, the co-change total, the highly-coupled pair count and +/// `avg_coupling_strength`, the mean of the per-pair strengths (upper +/// triangle, ascending index order). #[must_use] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // report fields are i32 #[allow(clippy::cast_precision_loss)] // contractual float math on counts @@ -348,16 +321,11 @@ pub fn compute_aggregate(input: &ReportData, opts: MetricOptions) -> AggregateDa let mut total_co_changes: i64 = 0; let mut pair_count: i64 = 0; let mut highly_coupled: i32 = 0; + let mut total_strength: f64 = 0.0; let threshold = i64::from(opts.coupling_threshold_high); - // Per-row contributing strengths: rows in ascending file index (the - // reference iterates the row slice in order), within-row ascending column - // as the canonical starting arrangement (the reference's within-row order - // is what varies). - let mut row_strengths: Vec<Vec<f64>> = Vec::new(); for (i, row) in input.files_matrix.iter().enumerate() { let self_i = *row.get(&i).unwrap_or(&0); - let mut strengths: Vec<f64> = Vec::new(); for (&j, &co_changes) in row { if j <= i || co_changes <= 0 { continue; @@ -373,10 +341,7 @@ pub fn compute_aggregate(input: &ReportData, opts: MetricOptions) -> AggregateDa if co_changes >= threshold { highly_coupled += 1; } - strengths.push(coupling_strength(co_changes, self_i, self_j)); - } - if !strengths.is_empty() { - row_strengths.push(strengths); + total_strength += coupling_strength(co_changes, self_i, self_j); } } @@ -385,7 +350,7 @@ pub fn compute_aggregate(input: &ReportData, opts: MetricOptions) -> AggregateDa total_developers: input.reversed_people_dict.len() as i32, total_co_changes, avg_coupling_strength: if pair_count > 0 { - median_map_order_total(&row_strengths) / pair_count as f64 + total_strength / pair_count as f64 } else { 0.0 }, @@ -393,73 +358,6 @@ pub fn compute_aggregate(input: &ReportData, opts: MetricOptions) -> AggregateDa } } -/// Median naive left-to-right f64 total of per-row strength terms over -/// simulated within-row iteration orders. -/// -/// The reference accumulates `total_strength` while iterating each row as a -/// randomized-order hash map, so its total is a random draw from the -/// distribution of naive sums over within-row permutations (float addition is -/// not associative, so different orders round differently by a few ULPs). -/// This helper measures that distribution deterministically: it computes the -/// naive total under [`AVG_STRENGTH_ORDER_PASSES`] pseudo-random within-row -/// orders (fixed seeds, Fisher–Yates over a splitmix64 stream) and returns the -/// median — the most probable reference outcome. The result is a pure -/// function of the input strengths: deterministic, input-varying, and exactly -/// equal to the unique naive sum whenever ordering cannot matter (zero or one -/// term per row). -fn median_map_order_total(row_strengths: &[Vec<f64>]) -> f64 { - // Fast path: ordering cannot affect the sum when every row has a single - // term (the pass loop would produce identical totals). - if row_strengths.iter().all(|r| r.len() < 2) { - let mut s = 0.0; - for row in row_strengths { - for &v in row { - s += v; - } - } - return s; - } - - let mut totals = Vec::with_capacity(AVG_STRENGTH_ORDER_PASSES); - let mut scratch: Vec<f64> = Vec::new(); - for pass in 0..AVG_STRENGTH_ORDER_PASSES { - // splitmix64 stream with a fixed per-pass seed. - let mut state = (pass as u64) - .wrapping_mul(0x9E37_79B9_7F4A_7C15) - .wrapping_add(0x2545_F491_4F6C_DD1D); - let mut next = move || -> u64 { - state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); - let mut z = state; - z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); - z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); - z ^ (z >> 31) - }; - let mut total = 0.0f64; - for row in row_strengths { - if row.len() < 2 { - for &v in row { - total += v; - } - continue; - } - scratch.clear(); - scratch.extend_from_slice(row); - // Fisher–Yates shuffle of the row's terms. - for k in (1..scratch.len()).rev() { - #[allow(clippy::cast_possible_truncation)] // index < row len - let pick = (next() % (k as u64 + 1)) as usize; - scratch.swap(k, pick); - } - for &v in &scratch { - total += v; - } - } - totals.push(total); - } - totals.sort_by(f64::total_cmp); - totals[totals.len() / 2] -} - /// Groups ownership data into contributor-count buckets using the default /// thresholds. #[must_use] @@ -510,50 +408,6 @@ pub fn bucket_ownership_with_thresholds( ] } -/// Returns a copy sorted by contributors ascending (highest risk first). -#[must_use] -pub fn sort_ownership_by_risk(ownership: &[FileOwnershipData]) -> Vec<FileOwnershipData> { - let mut sorted = ownership.to_vec(); - sorted.sort_by(|a, b| a.contributors.cmp(&b.contributors)); - sorted -} - -/// Limits a developer matrix to the top-N developers by diagonal activity. -/// Returns the input unchanged when within the limit. -#[must_use] -pub fn filter_top_devs( - matrix: &[BTreeMap<usize, i64>], - names: &[String], - limit: usize, -) -> (Vec<BTreeMap<usize, i64>>, Vec<String>) { - if names.len() <= limit { - return (matrix.to_vec(), names.to_vec()); - } - let mut devs: Vec<(usize, i64)> = (0..names.len()) - .map(|i| (i, matrix[i].get(&i).copied().unwrap_or(0))) - .collect(); - devs.sort_by(|a, b| b.1.cmp(&a.1)); - let top_n = &devs[..limit]; - - let mut old_to_new = std::collections::HashMap::with_capacity(limit); - let mut new_names = vec![String::new(); limit]; - for (new_idx, &(old_idx, _)) in top_n.iter().enumerate() { - old_to_new.insert(old_idx, new_idx); - new_names[new_idx].clone_from(&names[old_idx]); - } - - let mut new_matrix: Vec<BTreeMap<usize, i64>> = vec![BTreeMap::new(); limit]; - for &(old_i, _) in top_n { - let new_i = old_to_new[&old_i]; - for (&old_j, &val) in &matrix[old_i] { - if let Some(&new_j) = old_to_new.get(&old_j) { - new_matrix[new_i].insert(new_j, val); - } - } - } - (new_matrix, new_names) -} - /// Runs all metrics with default options. #[must_use] pub fn compute_all_metrics(input: &ReportData) -> ComputedMetrics { @@ -651,6 +505,24 @@ mod tests { assert!((agg.avg_coupling_strength - 2.0 / 3.0).abs() < 1e-12); } + /// Three pairs with strengths 1.0, 0.5 and 0.25 average to 7/12 (one + /// left-to-right sum over the upper triangle). + #[test] + fn aggregate_average_is_the_mean_of_the_pair_strengths() { + let input = ReportData { + files: vec!["a".into(), "b".into(), "c".into()], + files_matrix: vec![ + row(&[(0, 2), (1, 2), (2, 1)]), + row(&[(0, 2), (1, 2), (2, 1)]), + row(&[(0, 1), (1, 1), (2, 6)]), + ], + ..Default::default() + }; + let agg = compute_aggregate(&input, MetricOptions::default()); + assert_eq!(agg.total_co_changes, 4); + assert!((agg.avg_coupling_strength - (1.0 + 0.25 + 0.25) / 3.0).abs() < 1e-12); + } + #[test] fn ownership_buckets() { let own = vec![ @@ -687,16 +559,7 @@ mod tests { assert_eq!(buckets[3].count, 1); } - #[test] - fn filter_top_devs_keeps_within_limit() { - let m = vec![row(&[(0, 5)]), row(&[(1, 3)])]; - let names = vec!["a".to_string(), "b".to_string()]; - let (fm, fn_) = filter_top_devs(&m, &names, 5); - assert_eq!(fm.len(), 2); - assert_eq!(fn_, names); - } - - // --- DeveloperCoupling parity tests (mirroring the reference suite) --- + // --- developer coupling --- #[test] fn developer_coupling_single_pair_strength() { @@ -756,13 +619,4 @@ mod tests { }; assert!(compute_developer_coupling(&input).is_empty()); } - - #[test] - fn filter_top_devs_ranks_by_diagonal() { - let m = vec![row(&[(0, 1)]), row(&[(1, 9)]), row(&[(2, 5)])]; - let names = vec!["a".into(), "b".into(), "c".into()]; - let (_fm, fn_) = filter_top_devs(&m, &names, 2); - // top by diagonal: b(9), c(5). - assert_eq!(fn_, vec!["b".to_string(), "c".to_string()]); - } } diff --git a/crates/cf-couples/src/report.rs b/crates/cf-couples/src/report.rs index af29d9a1..7a373534 100644 --- a/crates/cf-couples/src/report.rs +++ b/crates/cf-couples/src/report.rs @@ -1,30 +1,17 @@ //! Report serialization. //! -//! All MACHINE-format report bytes (json, yaml, ndjson, timeseries, compact, -//! bin) are a frozen contract, pinned against the reference implementation by -//! `tests/compat`. Every report-bearing value is built as a -//! [`cf_gojson::GoValue`] tree and serialized through [`cf_gojson::marshal()`] / -//! [`cf_gojson::marshal_indent`], never through `serde_json` (which differs -//! from the report contract on map-key ordering, HTML escaping, and float -//! formatting). YAML reuses the same `GoValue` tree through `cf-goyaml` once -//! that path is wired. -//! -//! Two payload shapes are produced: -//! -//! * [`computed_metrics_to_value`] — the [`ComputedMetrics`] **struct** -//! (`file_coupling`, `developer_coupling`, `file_ownership`, `aggregate`), -//! emitted in declaration order with omit-when-empty honored (struct -//! origin). -//! * [`dense_report_to_value`] — the raw analyzer report, a dynamic map whose -//! keys (and the integer-keyed matrix rows) are byte-sorted at encode time -//! (map origin). +//! The report is built as a [`cf_gojson::GoValue`] tree +//! ([`computed_metrics_to_value`]) and serialized through `cf-gojson` / +//! `cf-goyaml` by the command layer, never through `serde_json` (which +//! differs from the report contract on map-key ordering, HTML escaping and +//! float formatting). The [`ComputedMetrics`] struct is emitted in +//! declaration order (`file_coupling`, `developer_coupling`, +//! `file_ownership`, `aggregate`) with omit-when-empty honored. use crate::metrics::{ AggregateData, ComputedMetrics, DeveloperCouplingData, FileCouplingData, FileOwnershipData, - ReportData, }; -use cf_gojson::{marshal, marshal_indent, GoMap, GoValue}; -use std::collections::BTreeMap; +use cf_gojson::{GoMap, GoValue}; /// Builds the [`GoValue`] tree for [`ComputedMetrics`] (struct origin). /// @@ -33,8 +20,8 @@ use std::collections::BTreeMap; #[must_use] pub fn computed_metrics_to_value(m: &ComputedMetrics) -> GoValue { let mut obj = GoMap::new_struct(); - // Reference: `file_coupling` is a `var`-declared append target — an - // empty-walk run leaves it nil (JSON `null`, YAML `[]`), never `[]`. + // `file_coupling` renders as JSON `null` (YAML `[]`) when no pair + // co-changed, never `[]`. obj.push( "file_coupling", if m.file_coupling.is_empty() { @@ -119,86 +106,10 @@ fn aggregate_to_value(d: &AggregateData) -> GoValue { GoValue::Object(o) } -/// Builds the [`GoValue`] tree for the raw dense analyzer report. -/// -/// The report is a dynamic map, so it is map-origin: `cf-gojson` byte-sorts -/// the keys at encode time, and insertion order here is not load-bearing. -/// Matrix rows are integer-keyed maps, also map-origin, rendering with -/// stringified, byte-sorted integer keys (report-format contract). -#[must_use] -#[allow(clippy::cast_possible_wrap)] // file indices are small -pub fn dense_report_to_value(r: &ReportData) -> GoValue { - let mut obj = GoMap::new_map(); - obj.push("PeopleMatrix", matrix_to_value(&r.people_matrix)); - obj.push( - "PeopleFiles", - GoValue::Array( - r.people_files - .iter() - .map(|row| GoValue::Array(row.iter().map(|&i| GoValue::Int(i as i64)).collect())) - .collect(), - ), - ); - obj.push( - "Files", - GoValue::Array(r.files.iter().map(|s| GoValue::Str(s.clone())).collect()), - ); - obj.push( - "FilesLines", - GoValue::Array( - r.files_lines - .iter() - .map(|&l| GoValue::Int(i64::from(l))) - .collect(), - ), - ); - obj.push("FilesMatrix", matrix_to_value(&r.files_matrix)); - obj.push( - "ReversedPeopleDict", - GoValue::Array( - r.reversed_people_dict - .iter() - .map(|s| GoValue::Str(s.clone())) - .collect(), - ), - ); - GoValue::Object(obj) -} - -/// Converts a slice of index-keyed matrix rows to an array of map-origin -/// objects with stringified integer keys (report-format contract). -fn matrix_to_value(matrix: &[BTreeMap<usize, i64>]) -> GoValue { - GoValue::Array( - matrix - .iter() - .map(|row| { - let mut o = GoMap::new_map(); - for (idx, count) in row { - o.push(idx.to_string(), GoValue::Int(*count)); - } - GoValue::Object(o) - }) - .collect(), - ) -} - -/// Serializes a [`GoValue`] to compact report-contract JSON bytes (no -/// insignificant whitespace, HTML escaping on, no trailing newline). -#[must_use] -pub fn to_go_json(value: &GoValue) -> Vec<u8> { - marshal(value) -} - -/// Serializes a [`GoValue`] to two-space-indented report-contract JSON bytes -/// (the caller appends the trailing newline the run/render path emits). -#[must_use] -pub fn to_go_json_indent(value: &GoValue) -> Vec<u8> { - marshal_indent(value) -} - #[cfg(test)] mod tests { use super::*; + use cf_gojson::marshal; #[test] fn computed_metrics_field_order_and_omitempty() { @@ -265,29 +176,10 @@ mod tests { }, ..Default::default() }; - let bytes = to_go_json(&computed_metrics_to_value(&m)); + let bytes = marshal(&computed_metrics_to_value(&m)); let s = String::from_utf8(bytes).unwrap(); // Struct field declaration order, compact (no spaces), float "0.5". assert!(s.starts_with(r#"{"file_coupling":[{"file1":"a.go","file2":"b.go","co_changes":2,"coupling_strength":0.5}]"#)); assert!(s.contains(r#""aggregate":{"total_files":2,"total_developers":1,"total_co_changes":2,"avg_coupling_strength":0.5,"highly_coupled_pairs":0}"#)); } - - #[test] - fn dense_report_map_keys_byte_sorted() { - let r = ReportData { - files: vec!["a.go".into()], - files_lines: vec![5], - files_matrix: vec![BTreeMap::from([(0usize, 4i64)])], - people_matrix: vec![BTreeMap::from([(0usize, 2i64)])], - people_files: vec![vec![0]], - reversed_people_dict: vec!["Alice|a".into()], - }; - let bytes = to_go_json(&dense_report_to_value(&r)); - let s = String::from_utf8(bytes).unwrap(); - // dynamic-map keys byte-sorted: Files < FilesLines < FilesMatrix < - // PeopleFiles < PeopleMatrix < ReversedPeopleDict. - assert!(s.starts_with(r#"{"Files":["a.go"],"FilesLines":[5],"FilesMatrix":[{"0":4}]"#)); - // matrix-row integer key stringified. - assert!(s.contains(r#""PeopleMatrix":[{"0":2}]"#)); - } } diff --git a/crates/cf-couples/src/report_section.rs b/crates/cf-couples/src/report_section.rs deleted file mode 100644 index 44ad6fbb..00000000 --- a/crates/cf-couples/src/report_section.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! Human-facing report-section logic. -//! -//! Computes the section score, status message, coupling-strength distribution, -//! and per-pair severity. These feed the (cosmetic, non-binding) terminal/HTML -//! summary; the numeric values follow the reference implementation so the -//! section behaves identically. - -use crate::metrics::{ComputedMetrics, FileCouplingData}; - -/// Section title. -pub const REPORT_SECTION_TITLE: &str = "COUPLES"; - -/// Status message when no coupling data is available. -pub const DEFAULT_STATUS_MSG: &str = "No coupling data available"; - -/// Sentinel score used for info-only sections. -/// -/// The canonical constant belongs in `cf-analyze`; the contractual value is -/// `-1.0`. Reconcile when `cf-analyze` lands (see crate TODOs). -pub const SCORE_INFO_ONLY: f64 = -1.0; - -// Distribution thresholds. -const DIST_STRONG_MIN: f64 = 0.7; -const DIST_MODERATE_MIN: f64 = 0.4; -const DIST_WEAK_MIN: f64 = 0.1; - -/// Distribution bucket labels. -pub const DIST_LABEL_STRONG: &str = "Strong (>70%)"; -pub const DIST_LABEL_MOD: &str = "Moderate (40-70%)"; -pub const DIST_LABEL_WEAK: &str = "Weak (10-40%)"; -pub const DIST_LABEL_NONE: &str = "Minimal (<10%)"; - -// Issue severity thresholds. -const ISSUE_SEVERITY_HIGH_MIN: f64 = 0.7; -const ISSUE_SEVERITY_MED_MIN: f64 = 0.4; - -/// Severity labels. The canonical string values belong in `cf-analyze`; the -/// contractual literals are reproduced here until that crate lands. -pub const SEVERITY_POOR: &str = "poor"; -pub const SEVERITY_FAIR: &str = "fair"; -pub const SEVERITY_GOOD: &str = "good"; - -/// Computes the section score and status message. -/// -/// Score is `1.0 - avg_coupling_strength` (clamped to `[0, ∞)`); a higher score -/// means lower coupling. Returns [`SCORE_INFO_ONLY`] / [`DEFAULT_STATUS_MSG`] -/// when there are no files. -#[must_use] -pub fn compute_score(m: &ComputedMetrics) -> (f64, String) { - const GOOD_THRESHOLD: f64 = 0.7; - const FAIR_THRESHOLD: f64 = 0.4; - if m.aggregate.total_files == 0 { - return (SCORE_INFO_ONLY, DEFAULT_STATUS_MSG.to_string()); - } - let mut score = 1.0 - m.aggregate.avg_coupling_strength; - if score < 0.0 { - score = 0.0; - } - let msg = if score >= GOOD_THRESHOLD { - format!( - "Good - low coupling across {} files", - m.aggregate.total_files - ) - } else if score >= FAIR_THRESHOLD { - format!( - "Fair - moderate coupling ({} highly coupled pairs)", - m.aggregate.highly_coupled_pairs - ) - } else { - format!( - "Poor - high coupling ({} highly coupled pairs need attention)", - m.aggregate.highly_coupled_pairs - ) - }; - (score, msg) -} - -/// Coupling-strength distribution counts. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct StrengthDistCounts { - pub strong: i32, - pub moderate: i32, - pub weak: i32, - pub minimal: i32, -} - -/// Categorizes coupling pairs by strength. -#[must_use] -pub fn categorize_strength(couples: &[FileCouplingData]) -> StrengthDistCounts { - let mut counts = StrengthDistCounts::default(); - for cp in couples { - if cp.strength >= DIST_STRONG_MIN { - counts.strong += 1; - } else if cp.strength >= DIST_MODERATE_MIN { - counts.moderate += 1; - } else if cp.strength >= DIST_WEAK_MIN { - counts.weak += 1; - } else { - counts.minimal += 1; - } - } - counts -} - -/// Maps a coupling strength to a severity label. -#[must_use] -pub fn severity_for_strength(strength: f64) -> &'static str { - if strength >= ISSUE_SEVERITY_HIGH_MIN { - SEVERITY_POOR - } else if strength >= ISSUE_SEVERITY_MED_MIN { - SEVERITY_FAIR - } else { - SEVERITY_GOOD - } -} - -/// Computes a fraction `count / total`, returning `0.0` for an empty total. -#[must_use] -pub fn pct(count: i32, total: i32) -> f64 { - if total == 0 { - 0.0 - } else { - f64::from(count) / f64::from(total) - } -} - -#[cfg(test)] -#[allow(clippy::float_cmp)] // exact contract values (score constants, guards) are the point -mod tests { - use super::*; - use crate::metrics::AggregateData; - - fn metrics(total_files: i32, avg: f64, highly: i32) -> ComputedMetrics { - ComputedMetrics { - aggregate: AggregateData { - total_files, - avg_coupling_strength: avg, - highly_coupled_pairs: highly, - ..Default::default() - }, - ..Default::default() - } - } - - #[test] - fn score_no_files_is_info_only() { - let (s, msg) = compute_score(&metrics(0, 0.0, 0)); - assert_eq!(s, SCORE_INFO_ONLY); - assert_eq!(msg, DEFAULT_STATUS_MSG); - } - - #[test] - fn score_good_fair_poor() { - let (s, msg) = compute_score(&metrics(5, 0.1, 0)); - assert!((s - 0.9).abs() < 1e-12); - assert!(msg.starts_with("Good")); - - let (_s, msg) = compute_score(&metrics(5, 0.5, 2)); - assert!(msg.starts_with("Fair")); - - let (_s, msg) = compute_score(&metrics(5, 0.9, 3)); - assert!(msg.starts_with("Poor")); - } - - #[test] - fn distribution_categorization() { - let couples = vec![ - FileCouplingData { - file1: "a".into(), - file2: "b".into(), - co_changes: 1, - strength: 0.8, - }, - FileCouplingData { - file1: "c".into(), - file2: "d".into(), - co_changes: 1, - strength: 0.5, - }, - FileCouplingData { - file1: "e".into(), - file2: "f".into(), - co_changes: 1, - strength: 0.2, - }, - FileCouplingData { - file1: "g".into(), - file2: "h".into(), - co_changes: 1, - strength: 0.05, - }, - ]; - let c = categorize_strength(&couples); - assert_eq!( - c, - StrengthDistCounts { - strong: 1, - moderate: 1, - weak: 1, - minimal: 1 - } - ); - } - - #[test] - fn severity_mapping() { - assert_eq!(severity_for_strength(0.8), SEVERITY_POOR); - assert_eq!(severity_for_strength(0.5), SEVERITY_FAIR); - assert_eq!(severity_for_strength(0.1), SEVERITY_GOOD); - } - - #[test] - fn pct_guards_zero() { - assert_eq!(pct(0, 0), 0.0); - assert_eq!(pct(1, 4), 0.25); - } -} diff --git a/crates/cf-couples/src/store.rs b/crates/cf-couples/src/store.rs deleted file mode 100644 index d106b7f9..00000000 --- a/crates/cf-couples/src/store.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Bounded store-record computation. -//! -//! The store path emits compact, bounded records instead of dense matrices: -//! top-K file coupling pairs, a bounded developer matrix, per-file ownership, -//! and an aggregate summary. The sparse computations here avoid materializing -//! the dense `O(N²)` matrices while producing the same numbers as the dense -//! path. - -use crate::matrix::RawFiles; -use crate::metrics::{ - coupling_strength_pub, AggregateData, FileCouplingData, COUPLING_THRESHOLD_HIGH, -}; -use std::collections::HashMap; - -/// Store record kinds. -pub const KIND_FILE_COUPLING: &str = "file_coupling"; -pub const KIND_DEV_MATRIX: &str = "dev_matrix"; -pub const KIND_OWNERSHIP: &str = "ownership"; -pub const KIND_AGGREGATE: &str = "aggregate"; - -/// A bounded developer coupling matrix for store serialization. -/// JSON keys: `names`, `matrix`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StoreDevMatrix { - pub names: Vec<String>, - pub matrix: Vec<std::collections::BTreeMap<usize, i64>>, -} - -/// Extracts file coupling pairs from the sparse coupling map without -/// materializing a dense matrix. -/// -/// Only upper-triangle pairs (`j > i`) with `count >= min_weight` are kept. -/// Result is unsorted (the caller sorts by `co_changes` descending and -/// applies the top-K limit; see [`top_k_file_coupling`]). -#[must_use] -#[allow(clippy::implicit_hasher)] // public signature is frozen -pub fn compute_sparse_coupling( - reduced_files: &RawFiles, - files_sequence: &[String], - files_index: &HashMap<String, usize>, - min_weight: i64, -) -> Vec<FileCouplingData> { - let mut result = Vec::new(); - for (i, file1) in files_sequence.iter().enumerate() { - let Some(lane) = reduced_files.get(file1) else { - continue; - }; - if lane.is_empty() { - continue; - } - let self_i = lane.get(file1).copied().unwrap_or(0); - for (file2, &co_changes) in lane { - let Some(&j) = files_index.get(file2) else { - continue; - }; - if j <= i { - continue; - } - if co_changes < min_weight { - continue; - } - let self_j = reduced_files - .get(file2) - .and_then(|l| l.get(file2)) - .copied() - .unwrap_or(0); - result.push(FileCouplingData { - file1: file1.clone(), - file2: file2.clone(), - co_changes, - strength: coupling_strength_pub(co_changes, self_i, self_j), - }); - } - } - result -} - -/// Sorts sparse coupling pairs by `co_changes` descending and truncates to -/// `top_k`. -#[must_use] -pub fn top_k_file_coupling( - mut pairs: Vec<FileCouplingData>, - top_k: usize, -) -> Vec<FileCouplingData> { - pairs.sort_by(|a, b| b.co_changes.cmp(&a.co_changes)); - let limit = pairs.len().min(top_k); - pairs.truncate(limit); - pairs -} - -/// Computes aggregate statistics directly from the sparse coupling map. -#[must_use] -#[allow(clippy::implicit_hasher)] // public signature is frozen -#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // report fields are i32 -#[allow(clippy::cast_precision_loss)] // contractual float math on counts -pub fn compute_sparse_aggregate( - reduced_files: &RawFiles, - files_sequence: &[String], - files_index: &HashMap<String, usize>, - reversed_names: &[String], -) -> AggregateData { - let mut total_co_changes: i64 = 0; - let mut pair_count: i64 = 0; - let mut highly_coupled: i32 = 0; - let mut total_strength: f64 = 0.0; - - for file1 in files_sequence { - let i = files_index[file1]; - let Some(lane) = reduced_files.get(file1) else { - continue; - }; - let self_i = lane.get(file1).copied().unwrap_or(0); - for (file2, &co_changes) in lane { - let Some(&j) = files_index.get(file2) else { - continue; - }; - if j <= i || co_changes <= 0 { - continue; - } - let self_j = reduced_files - .get(file2) - .and_then(|l| l.get(file2)) - .copied() - .unwrap_or(0); - total_co_changes += co_changes; - pair_count += 1; - if co_changes >= COUPLING_THRESHOLD_HIGH { - highly_coupled += 1; - } - total_strength += coupling_strength_pub(co_changes, self_i, self_j); - } - } - - AggregateData { - total_files: files_sequence.len() as i32, - total_developers: reversed_names.len() as i32, - total_co_changes, - avg_coupling_strength: if pair_count > 0 { - total_strength / pair_count as f64 - } else { - 0.0 - }, - highly_coupled_pairs: highly_coupled, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::matrix::build_files_index; - use std::collections::BTreeMap; - - fn raw(pairs: &[(&str, &[(&str, i64)])]) -> RawFiles { - pairs - .iter() - .map(|(f, inner)| { - ( - (*f).to_string(), - inner - .iter() - .map(|(o, c)| ((*o).to_string(), *c)) - .collect::<BTreeMap<_, _>>(), - ) - }) - .collect() - } - - #[test] - fn sparse_coupling_respects_min_weight() { - let files = raw(&[ - ("a.go", &[("a.go", 4), ("b.go", 2)]), - ("b.go", &[("b.go", 3), ("a.go", 2)]), - ]); - let (seq, idx) = build_files_index(&files); - let pairs = compute_sparse_coupling(&files, &seq, &idx, 2); - assert_eq!(pairs.len(), 1); - assert_eq!(pairs[0].co_changes, 2); - // raise min_weight above the count -> filtered out. - let none = compute_sparse_coupling(&files, &seq, &idx, 3); - assert!(none.is_empty()); - } - - #[test] - fn top_k_limits_and_sorts() { - let pairs = vec![ - FileCouplingData { - file1: "a".into(), - file2: "b".into(), - co_changes: 1, - strength: 0.0, - }, - FileCouplingData { - file1: "c".into(), - file2: "d".into(), - co_changes: 5, - strength: 0.0, - }, - FileCouplingData { - file1: "e".into(), - file2: "f".into(), - co_changes: 3, - strength: 0.0, - }, - ]; - let top = top_k_file_coupling(pairs, 2); - assert_eq!(top.len(), 2); - assert_eq!(top[0].co_changes, 5); - assert_eq!(top[1].co_changes, 3); - } - - #[test] - fn sparse_aggregate_matches_dense() { - let files = raw(&[ - ("a.go", &[("a.go", 4), ("b.go", 2)]), - ("b.go", &[("b.go", 2), ("a.go", 2)]), - ]); - let (seq, idx) = build_files_index(&files); - let agg = compute_sparse_aggregate(&files, &seq, &idx, &["Alice|a".into()]); - assert_eq!(agg.total_files, 2); - assert_eq!(agg.total_developers, 1); - assert_eq!(agg.total_co_changes, 2); - assert_eq!(agg.highly_coupled_pairs, 0); - assert!((agg.avg_coupling_strength - 2.0 / 3.0).abs() < 1e-12); - } -} diff --git a/crates/cf-couples/src/tc.rs b/crates/cf-couples/src/tc.rs index 1e8e075d..d90604d0 100644 --- a/crates/cf-couples/src/tc.rs +++ b/crates/cf-couples/src/tc.rs @@ -1,4 +1,4 @@ -//! Per-commit and per-tick transfer types. +//! The per-commit payload the history walk hands to the aggregator. use std::collections::BTreeMap; @@ -11,21 +11,12 @@ pub struct RenamePair { pub to_name: String, } -/// Per-commit summary used for timeseries output. -/// -/// JSON field names: `files_touched`, `author_id`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CommitSummary { - /// Number of files in the coupling context. - pub files_touched: usize, - /// Author index. - pub author_id: usize, -} - -/// Per-commit transfer payload emitted by the pipeline consume step. +/// One commit's contribution: the coupling context, the author's file +/// touches and the renames, in change order. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CommitData { - /// Files forming the coupling context (already context-size filtered). + /// Files forming the coupling context — empty for a commit above the + /// changeset cap ([`crate::COUPLES_MAXIMUM_MEANINGFUL_CONTEXT_SIZE`]). pub coupling_files: Vec<String>, /// File name → touch count for this commit's author. /// @@ -33,23 +24,9 @@ pub struct CommitData { /// stored per file and downstream accumulation is additive, so ordering /// does not affect results. pub author_files: BTreeMap<String, i64>, - /// Rename pairs detected in this commit. + /// Rename pairs detected in this commit, applied BEFORE the commit's own + /// touches are accumulated. pub renames: Vec<RenamePair>, /// Whether this commit incremented the author's commit count. pub commit_counted: bool, } - -/// Per-tick aggregated payload. -#[derive(Debug, Clone, Default)] -pub struct TickData { - /// `file -> otherFile -> co-occurrence count`. - pub files: BTreeMap<String, BTreeMap<String, i64>>, - /// Per-author file touch counts, indexed by author ID. - pub people: Vec<BTreeMap<String, i64>>, - /// Per-author commit counts, indexed by author ID. - pub people_commits: Vec<i64>, - /// Renames accumulated during this tick. - pub renames: Vec<RenamePair>, - /// Per-commit summaries keyed by commit hash string. - pub commit_stats: BTreeMap<String, CommitSummary>, -} diff --git a/crates/cf-devs/Cargo.toml b/crates/cf-devs/Cargo.toml index 3560f3c5..2ec1c435 100644 --- a/crates/cf-devs/Cargo.toml +++ b/crates/cf-devs/Cargo.toml @@ -19,7 +19,7 @@ publish = false # # Report serialization routes exclusively through cf-gojson / cf-goyaml; the # machine-format bytes (json/yaml/ndjson/timeseries/compact/bin) are pinned -# against the reference implementation by tests/compat. +# by the report e2e suites in bins/codefang/tests. [dependencies] # Report-format-compatible JSON encoder. NEVER serde_json for output. The @@ -45,3 +45,5 @@ cf-gosort = { path = "../cf-gosort" } pedantic = "warn" [dev-dependencies] +# The YAML encoder, for the json/yaml asymmetry tests of the developer rows. +cf-goyaml = { path = "../cf-goyaml" } diff --git a/crates/cf-devs/src/lib.rs b/crates/cf-devs/src/lib.rs index 27a49d07..8a02b219 100644 --- a/crates/cf-devs/src/lib.rs +++ b/crates/cf-devs/src/lib.rs @@ -24,7 +24,7 @@ //! Machine-format report bytes (json/yaml/ndjson/timeseries/compact/bin) are //! produced via [`cf_gojson`] in [`serialize`]; this crate never uses //! `serde_json` for output. Compatibility: output bytes are pinned against the -//! reference implementation by `tests/compat`. +//! reference implementation by the report e2e suites (`bins/codefang/tests`). #![forbid(unsafe_code)] @@ -850,4 +850,97 @@ mod tests { let pos2 = s.find("\"2\"").unwrap(); assert!(pos10 < pos2, "got: {s}"); } + + /// The secondary developer is emitted whenever one exists — including + /// developer id 0 — and omitted only for a single-contributor language. + #[test] + fn bus_factor_secondary_id_zero_is_emitted() { + let names = vec![ + "alice|alice@example.com".to_string(), + "bob|bob@example.com".to_string(), + ]; + let mut two = LanguageData { + name: "Go".into(), + total_lines: 30, + total_contribution: 30, + ..LanguageData::default() + }; + two.contributors.insert(1, 20); + two.contributors.insert(0, 10); + two.contributor_order = vec![1, 0]; + let mut one = LanguageData { + name: "Python".into(), + total_lines: 5, + total_contribution: 5, + ..LanguageData::default() + }; + one.contributors.insert(1, 5); + one.contributor_order = vec![1]; + + let bf = compute_bus_factor( + &BusFactorInput { + languages: &[two, one], + names: &names, + }, + &MetricOptions::default(), + ); + let go = bf.iter().find(|b| b.language == "Go").unwrap(); + assert_eq!(go.primary_dev_id, 1); + assert_eq!(go.secondary_dev_id, Some(0)); + let go_json = + String::from_utf8(cf_gojson::marshal(&serialize::bus_factor_data_to_go(go))).unwrap(); + assert!(go_json.contains(r#""secondary_dev_id":0"#), "{go_json}"); + assert!( + go_json.contains(r#""secondary_dev_name":"alice""#), + "{go_json}" + ); + + let py = bf.iter().find(|b| b.language == "Python").unwrap(); + assert_eq!(py.secondary_dev_id, None); + let py_json = + String::from_utf8(cf_gojson::marshal(&serialize::bus_factor_data_to_go(py))).unwrap(); + assert!(!py_json.contains("secondary"), "{py_json}"); + } + + /// A developer with no language breakdown renders `languages: null` in + /// JSON and `languages: []` in YAML (the documented asymmetry). + #[test] + fn developer_without_languages_is_null_in_json_and_empty_in_yaml() { + let dev = DeveloperData { + id: 3, + name: "carol".into(), + commits: 1, + ..DeveloperData::default() + }; + let json = + String::from_utf8(cf_gojson::marshal(&serialize::developer_data_to_go(&dev))).unwrap(); + assert!(json.contains(r#""languages":null"#), "{json}"); + let yaml = String::from_utf8(cf_goyaml::marshal(&serialize::developer_data_to_go_yaml( + &dev, + ))) + .unwrap(); + assert!(yaml.contains("languages: []"), "{yaml}"); + assert!(!yaml.contains("email"), "empty email is omitted: {yaml}"); + } + + /// The active window never starts before tick 0: a history shorter than + /// the active period makes every tick active, and the ratio fallback + /// truncates toward zero. + #[test] + fn active_threshold_clamps_to_zero_and_truncates_the_ratio() { + use crate::metrics::compute_active_threshold; + let opts = MetricOptions::default(); + // 90 days of 24-hour ticks reach back 90 ticks. + assert_eq!(compute_active_threshold(100, TICK_SIZE, &opts), 10); + assert_eq!(compute_active_threshold(90, TICK_SIZE, &opts), 0); + assert_eq!(compute_active_threshold(5, TICK_SIZE, &opts), 0); + // One-hour ticks reach back 90 * 24 ticks. + assert_eq!( + compute_active_threshold(3000, TICK_SIZE / 24, &opts), + 3000 - 90 * 24 + ); + // Unknown tick size: the 0.7 ratio of the period, truncated. + assert_eq!(compute_active_threshold(9, 0, &opts), 6); + assert_eq!(compute_active_threshold(0, 0, &opts), 0); + } } diff --git a/crates/cf-devs/src/metrics.rs b/crates/cf-devs/src/metrics.rs index 9e0b397a..5086b735 100644 --- a/crates/cf-devs/src/metrics.rs +++ b/crates/cf-devs/src/metrics.rs @@ -310,7 +310,7 @@ pub fn compute_bus_factor(input: &BusFactorInput, opts: &MetricOptions) -> Vec<B } if let Some(&(id, lines)) = contribs.get(1) { - bf.secondary_dev_id = id; + bf.secondary_dev_id = Some(id); let (n, e) = dev_name_and_email(id, input.names); bf.secondary_dev_name = n; bf.secondary_dev_email = e; diff --git a/crates/cf-devs/src/model.rs b/crates/cf-devs/src/model.rs index 2b285630..3bee6e7a 100644 --- a/crates/cf-devs/src/model.rs +++ b/crates/cf-devs/src/model.rs @@ -3,7 +3,7 @@ //! Field declaration order is part of the report-format contract: the wrapper //! structs are emitted in source order by [`cf_gojson`] (a `GoMap` built //! field-by-field reproduces struct-origin ordering, while dynamic map -//! payloads are byte-sorted on encode). Pinned by `tests/compat`. +//! payloads are byte-sorted on encode). Pinned by the report e2e suites (`bins/codefang/tests`). use std::collections::BTreeMap; @@ -201,8 +201,9 @@ pub struct BusFactorData { pub primary_dev_email: String, /// Primary contributor percentage of total contribution. pub primary_pct: f64, - /// Secondary contributor id (omitted when zero). - pub secondary_dev_id: i64, + /// Secondary contributor id; `None` when the language has one + /// contributor. Emitted whenever present — id 0 is a real developer. + pub secondary_dev_id: Option<i64>, /// Secondary contributor name (omitted when empty). pub secondary_dev_name: String, /// Secondary contributor email (omitted when empty). diff --git a/crates/cf-devs/src/serialize.rs b/crates/cf-devs/src/serialize.rs index 8afb4150..ffaf65fd 100644 --- a/crates/cf-devs/src/serialize.rs +++ b/crates/cf-devs/src/serialize.rs @@ -6,7 +6,7 @@ //! byte-sorts on encode. This module never touches `serde_json`. //! //! Integer map keys serialize as decimal strings sorted lexicographically -//! (report-format contract; pinned by `tests/compat`). We reproduce that +//! (report-format contract; pinned by the report e2e suites (`bins/codefang/tests`)). We reproduce that //! by inserting stringified keys into a map-origin `GoMap`. use std::collections::BTreeMap; @@ -176,11 +176,8 @@ pub fn bus_factor_data_to_go(b: &BusFactorData) -> GoValue { "primary_percentage".to_string(), GoValue::Float(b.primary_pct), ); - if b.secondary_dev_id != 0 { - obj.insert( - "secondary_dev_id".to_string(), - GoValue::Int(b.secondary_dev_id), - ); + if let Some(id) = b.secondary_dev_id { + obj.insert("secondary_dev_id".to_string(), GoValue::Int(id)); } if !b.secondary_dev_name.is_empty() { obj.insert( diff --git a/crates/cf-file-history/Cargo.toml b/crates/cf-file-history/Cargo.toml index 3223b878..573e4f71 100644 --- a/crates/cf-file-history/Cargo.toml +++ b/crates/cf-file-history/Cargo.toml @@ -7,16 +7,12 @@ license.workspace = true repository.workspace = true publish = false -# cf-file-history — per-file change history + file classifier -# (analyzer id: history/file-history). -# -# Compatibility: machine-format report bytes (json/ndjson/timeseries/compact/bin) -# are pinned against the reference binary by tests/compat; all report -# serialization routes through cf-gojson. +# cf-file-history — per-file change history metrics and the file-composition +# breakdown (analyzer id: history/file-history). All report serialization +# routes through cf-gojson. [dependencies] -# Report-format JSON encoder (compatibility contract). Never serde_json for -# report output. +# Report-format JSON encoder. Never serde_json for report output. cf-gojson = { path = "../cf-gojson" } # Risk level string constants (CRITICAL/HIGH/MEDIUM/LOW), shared with the other # analyzers' hotspot logic. diff --git a/crates/cf-file-history/src/category.rs b/crates/cf-file-history/src/category.rs new file mode 100644 index 00000000..72f0adf8 --- /dev/null +++ b/crates/cf-file-history/src/category.rs @@ -0,0 +1,107 @@ +//! The file categories of the composition breakdown. +//! +//! Files are classified by the shared `cf-composition` classifier (path and +//! content cascade: binary, image, vendor, generated, documentation, +//! configuration, dotfile, else source); this module holds the category set +//! the file-history report names. + +/// A file classification category. +/// +/// The string values are part of the report contract (they appear verbatim in +/// machine report keys such as the composition breakdown). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Category { + /// First-party project source code. + Source, + /// Third-party vendored dependencies. + Vendor, + /// Generated code (protobuf, mocks, deepcopy, etc.). + Generated, + /// Documentation (README, CHANGELOG, docs/). + Documentation, + /// Configuration files (YAML/JSON/TOML/...). + Configuration, + /// Image assets (PNG/JPG/GIF). + Image, + /// Dotfiles (.editorconfig, .bashrc, ...). + DotFile, + /// Files with binary content. + Binary, +} + +impl Category { + /// Returns the lowercase string identifier used in report keys. + /// + /// ``` + /// use cf_file_history::Category; + /// + /// assert_eq!(Category::Source.as_str(), "source"); + /// assert_eq!(Category::DotFile.as_str(), "dotfile"); + /// ``` + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Category::Source => "source", + Category::Vendor => "vendor", + Category::Generated => "generated", + Category::Documentation => "documentation", + Category::Configuration => "configuration", + Category::Image => "image", + Category::DotFile => "dotfile", + Category::Binary => "binary", + } + } +} + +/// The canonical order of categories for display and charting: +/// source, documentation, configuration, vendor, generated, dotfile, image, +/// binary. +pub const ALL_CATEGORIES: [Category; 8] = [ + Category::Source, + Category::Documentation, + Category::Configuration, + Category::Vendor, + Category::Generated, + Category::DotFile, + Category::Image, + Category::Binary, +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_categories_lists_each_variant_once() { + let expected = [ + Category::Source, + Category::Vendor, + Category::Generated, + Category::Documentation, + Category::Configuration, + Category::Image, + Category::DotFile, + Category::Binary, + ]; + assert_eq!(ALL_CATEGORIES.len(), expected.len()); + for cat in expected { + assert_eq!( + ALL_CATEGORIES.iter().filter(|c| **c == cat).count(), + 1, + "{cat:?}" + ); + } + } + + #[test] + fn category_as_str_is_stable() { + assert_eq!(Category::Source.as_str(), "source"); + assert_eq!(Category::Vendor.as_str(), "vendor"); + assert_eq!(Category::Generated.as_str(), "generated"); + assert_eq!(Category::Documentation.as_str(), "documentation"); + assert_eq!(Category::Configuration.as_str(), "configuration"); + assert_eq!(Category::Image.as_str(), "image"); + assert_eq!(Category::DotFile.as_str(), "dotfile"); + assert_eq!(Category::Binary.as_str(), "binary"); + } +} diff --git a/crates/cf-file-history/src/classify.rs b/crates/cf-file-history/src/classify.rs deleted file mode 100644 index 3b7d0585..00000000 --- a/crates/cf-file-history/src/classify.rs +++ /dev/null @@ -1,305 +0,0 @@ -//! File classification by category. -//! -//! Files are classified into one of eight [`Category`] values using -//! enry-style language/vendor/documentation heuristics plus generated-path and -//! generated-content detection. Categories are checked in a fixed priority -//! order; the first match wins. -//! -//! # Backend parity -//! -//! The reference classifier delegates its predicates to the `enry` library. -//! The full predicate set (image/vendor/documentation/configuration/dotfile) -//! is not yet available in this tree, so this module routes every predicate -//! through the [`Enry`] and [`GeneratedDetector`] traits. The category *set* -//! and *priority order* are final; a faithful enry port is dropped in by -//! implementing those traits, with no change to the classifier itself. The -//! [`PlaceholderEnry`] backend reproduces only the NUL-byte binary heuristic, -//! classifying everything else as [`Category::Source`]. Tracked as a roadmap -//! item. - -/// A file classification category. -/// -/// The string values are part of the report contract (they appear verbatim in -/// machine report keys such as the composition breakdown). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Category { - /// First-party project source code. - Source, - /// Third-party vendored dependencies. - Vendor, - /// Generated code (protobuf, mocks, deepcopy, etc.). - Generated, - /// Documentation (README, CHANGELOG, docs/). - Documentation, - /// Configuration files (YAML/JSON/TOML/...). - Configuration, - /// Image assets (PNG/JPG/GIF). - Image, - /// Dotfiles (.editorconfig, .bashrc, ...). - DotFile, - /// Files with binary content. - Binary, -} - -impl Category { - /// Returns the lowercase string identifier used in report keys. - /// - /// ``` - /// use cf_file_history::Category; - /// - /// assert_eq!(Category::Source.as_str(), "source"); - /// assert_eq!(Category::DotFile.as_str(), "dotfile"); - /// ``` - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Category::Source => "source", - Category::Vendor => "vendor", - Category::Generated => "generated", - Category::Documentation => "documentation", - Category::Configuration => "configuration", - Category::Image => "image", - Category::DotFile => "dotfile", - Category::Binary => "binary", - } - } -} - -/// The canonical order of categories for display and charting: -/// source, documentation, configuration, vendor, generated, dotfile, image, -/// binary. -pub const ALL_CATEGORIES: [Category; 8] = [ - Category::Source, - Category::Documentation, - Category::Configuration, - Category::Vendor, - Category::Generated, - Category::DotFile, - Category::Image, - Category::Binary, -]; - -/// Returns the canonical [`Category`] order (function form of [`ALL_CATEGORIES`]). -#[must_use] -pub fn all_categories() -> &'static [Category] { - &ALL_CATEGORIES -} - -/// Heuristic predicates supplied by an enry-equivalent backend. -/// -/// These map one-to-one onto the predicates used by the reference classifier. -/// A faithful enry port implements this trait so [`Classifier`] reproduces the -/// contract classification byte-for-byte. -pub trait Enry { - /// Whether `content` is binary. - fn is_binary(&self, content: &[u8]) -> bool; - /// Whether `path` is an image asset. - fn is_image(&self, path: &str) -> bool; - /// Whether `path` is vendored third-party code. - fn is_vendor(&self, path: &str) -> bool; - /// Whether `path` is documentation. - fn is_documentation(&self, path: &str) -> bool; - /// Whether `path` is configuration. - fn is_configuration(&self, path: &str) -> bool; - /// Whether `path` is a dotfile. - fn is_dot_file(&self, path: &str) -> bool; -} - -/// Generated-path / generated-content predicates. -pub trait GeneratedDetector { - /// Whether the path denotes generated code. - fn is_generated_path(&self, path: &str) -> bool; - /// Whether the content carries a generated-code marker. - fn is_generated_content(&self, content: &[u8]) -> bool; -} - -/// Categorizes files using an [`Enry`] backend and a [`GeneratedDetector`]. -/// -/// Construct with [`Classifier::new`] for the default backends, or -/// [`Classifier::with_backends`] to inject a faithful enry/pathfilter port. -pub struct Classifier<E: Enry, G: GeneratedDetector> { - enry: E, - generated: G, -} - -impl<E: Enry, G: GeneratedDetector> Classifier<E, G> { - /// Creates a classifier from explicit backends. - pub fn with_backends(enry: E, generated: G) -> Self { - Self { enry, generated } - } - - /// Returns the category for a file. - /// - /// `content` may be empty for path-only classification. Categories are - /// checked in priority order (first match wins): - /// - /// Binary > Image > Vendor > Generated(path) > Generated(content) > - /// Documentation > Configuration > `DotFile` > Source. - pub fn classify(&self, file_path: &str, content: &[u8]) -> Category { - if !content.is_empty() && self.enry.is_binary(content) { - return Category::Binary; - } - if self.enry.is_image(file_path) { - return Category::Image; - } - if self.enry.is_vendor(file_path) { - return Category::Vendor; - } - if self.generated.is_generated_path(file_path) { - return Category::Generated; - } - if !content.is_empty() && self.generated.is_generated_content(content) { - return Category::Generated; - } - if self.enry.is_documentation(file_path) { - return Category::Documentation; - } - if self.enry.is_configuration(file_path) { - return Category::Configuration; - } - if self.enry.is_dot_file(file_path) { - return Category::DotFile; - } - Category::Source - } -} - -/// Default enry backend pending the vendored data tables. -/// -/// Reproduces only the heuristic available without enry's data tables: content -/// containing a NUL byte is binary. All path predicates return `false`, so -/// [`Classifier::new`] classifies every non-binary file as [`Category::Source`]. -/// Replace by implementing [`Enry`] over the vendored enry tables. -#[derive(Debug, Default, Clone, Copy)] -pub struct PlaceholderEnry; - -impl Enry for PlaceholderEnry { - fn is_binary(&self, content: &[u8]) -> bool { - content.contains(&0) - } - fn is_image(&self, _path: &str) -> bool { - false - } - fn is_vendor(&self, _path: &str) -> bool { - false - } - fn is_documentation(&self, _path: &str) -> bool { - false - } - fn is_configuration(&self, _path: &str) -> bool { - false - } - fn is_dot_file(&self, _path: &str) -> bool { - false - } -} - -/// Default generated-content detector (always `false`). -/// -/// Replace by implementing [`GeneratedDetector`] over a pathfilter port. -#[derive(Debug, Default, Clone, Copy)] -pub struct PlaceholderGenerated; - -impl GeneratedDetector for PlaceholderGenerated { - fn is_generated_path(&self, _path: &str) -> bool { - false - } - fn is_generated_content(&self, _content: &[u8]) -> bool { - false - } -} - -impl Classifier<PlaceholderEnry, PlaceholderGenerated> { - /// Creates a `Classifier` with the default backends. - /// - /// Pending the vendored enry data tables, this only distinguishes - /// [`Category::Binary`] (NUL-containing content) from [`Category::Source`]. - /// - /// ``` - /// use cf_file_history::{Category, Classifier}; - /// - /// let c = Classifier::new(); - /// // NUL-containing content classifies as Binary, even for a source path. - /// assert_eq!(c.classify("main.go", b"hello\x00world"), Category::Binary); - /// // Plain text defaults to Source with the placeholder backend. - /// assert_eq!(c.classify("main.go", b"package main"), Category::Source); - /// ``` - #[must_use] - pub fn new() -> Self { - Self::with_backends(PlaceholderEnry, PlaceholderGenerated) - } -} - -impl Default for Classifier<PlaceholderEnry, PlaceholderGenerated> { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Cases that require real enry/pathfilter heuristics are gated behind - // #[ignore] until the data tables are vendored; the binary detection and - // priority-scaffold cases run against the default backend. - - #[test] - fn classify_binary_content() { - // NUL-containing content -> Binary. - let c = Classifier::new(); - let binary = b"hello\x00world"; - assert_eq!(c.classify("data.bin", binary), Category::Binary); - // Binary takes priority over other categories. - assert_eq!(c.classify("main.go", binary), Category::Binary); - } - - #[test] - fn classify_source_default() { - // Plain source files (default => Source). - let c = Classifier::new(); - assert_eq!(c.classify("main.go", b""), Category::Source); - assert_eq!( - c.classify("internal/server/handler.go", b""), - Category::Source - ); - assert_eq!(c.classify("src/index.ts", b""), Category::Source); - } - - #[test] - fn all_categories_contains_all() { - let expected = [ - Category::Source, - Category::Vendor, - Category::Generated, - Category::Documentation, - Category::Configuration, - Category::Image, - Category::DotFile, - Category::Binary, - ]; - assert_eq!(ALL_CATEGORIES.len(), expected.len()); - for cat in ALL_CATEGORIES { - assert!(expected.contains(&cat), "unexpected category {cat:?}"); - } - } - - #[test] - fn category_as_str_is_stable() { - assert_eq!(Category::Source.as_str(), "source"); - assert_eq!(Category::Vendor.as_str(), "vendor"); - assert_eq!(Category::Generated.as_str(), "generated"); - assert_eq!(Category::Documentation.as_str(), "documentation"); - assert_eq!(Category::Configuration.as_str(), "configuration"); - assert_eq!(Category::Image.as_str(), "image"); - assert_eq!(Category::DotFile.as_str(), "dotfile"); - assert_eq!(Category::Binary.as_str(), "binary"); - } - - #[test] - #[ignore = "requires vendored enry data tables (roadmap item)"] - fn classify_vendor_documentation_image_dotfile() { - // Covers the cases that need the real enry backend; build a classifier - // with the real enry implementation here once available. - } -} diff --git a/crates/cf-file-history/src/framework.rs b/crates/cf-file-history/src/framework.rs deleted file mode 100644 index 055b73fb..00000000 --- a/crates/cf-file-history/src/framework.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Framework integration sketch (NOT YET IMPLEMENTED). -//! -//! The streaming side of the analyzer — consume/fork/merge over the commit -//! walk, the aggregator and spill store, checkpointing, hibernation, and the -//! store writers/readers — depends on framework crates that are not yet stable -//! in this tree (`cf-analyze`, `cf-analyzers-plumbing`, `cf-spillstore`, -//! `cf-gitlib`, `cf-checkpoint`/`cf-persist`). -//! -//! Once those crates expose stable interfaces, the analyzer struct, aggregator, -//! checkpoint state and store writers can be completed here. The minimal -//! contracts that work will need are sketched below so downstream registration -//! code can reference them. - -use crate::metrics::FileHistory; - -/// Store record kinds written by the analyzer. -pub mod store_kinds { - /// Per-file `FileChurnData` records (sorted by churn score). - pub const FILE_CHURN: &str = "file_churn"; - /// Single `AggregateData` summary record. - pub const SUMMARY: &str = "summary"; - /// Per-tick `CompositionTimeSeriesEntry` records. - pub const COMPOSITION: &str = "composition"; -} - -/// Checkpoint basename. -pub const CHECKPOINT_BASENAME: &str = "file_history_state"; - -/// Estimated working-state bytes per commit. -pub const WORKING_STATE_SIZE: i64 = 2 * 1024; -/// Estimated TC payload bytes per commit. -pub const AVG_TC_SIZE: i64 = 10 * 1024; - -/// Merges two [`FileHistory`] values: sums per-author line stats and appends -/// hash lists. -/// -/// This is framework-adjacent but pure, so it is implemented here and unit -/// tested; the streaming aggregator will call it. -#[must_use] -pub fn merge_file_history(mut existing: FileHistory, incoming: FileHistory) -> FileHistory { - for (author, stats) in incoming.people { - let old = existing.people.entry(author).or_default(); - *old = *old + stats; - } - existing.hashes.extend(incoming.hashes); - existing -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tc::LineStats; - use std::collections::BTreeMap; - - #[test] - fn merge_sums_people_and_appends_hashes() { - let a = FileHistory { - people: BTreeMap::from([( - 1, - LineStats { - added: 10, - ..Default::default() - }, - )]), - hashes: vec!["h1".into()], - }; - let b = FileHistory { - people: BTreeMap::from([ - ( - 1, - LineStats { - added: 5, - removed: 2, - ..Default::default() - }, - ), - ( - 2, - LineStats { - changed: 7, - ..Default::default() - }, - ), - ]), - hashes: vec!["h2".into()], - }; - let merged = merge_file_history(a, b); - assert_eq!( - merged.people[&1], - LineStats { - added: 15, - removed: 2, - changed: 0 - } - ); - assert_eq!( - merged.people[&2], - LineStats { - added: 0, - removed: 0, - changed: 7 - } - ); - assert_eq!(merged.hashes, vec!["h1".to_string(), "h2".to_string()]); - } -} diff --git a/crates/cf-file-history/src/lib.rs b/crates/cf-file-history/src/lib.rs index eed08108..72109bb7 100644 --- a/crates/cf-file-history/src/lib.rs +++ b/crates/cf-file-history/src/lib.rs @@ -1,73 +1,35 @@ -//! File history analyzer (`history/file-history`). +//! File history analyzer (`history/file-history`) — the metric math. //! -//! Maps each file path to the list of commits which touch that file together -//! with the mapping from involved developers to their line statistics. From -//! that raw per-file history it derives churn, contributor, hotspot, aggregate -//! and file-composition metrics. +//! The history walk (in `cf-commands`) maps each file path to the commits +//! that touched it, following renames, together with the per-developer line +//! statistics, and classifies every changed file into a [`Category`]. From +//! that raw per-file history this crate derives churn, contributor, hotspot +//! and aggregate metrics ([`metrics`]) and the file-composition breakdown +//! (distinct changed files per category, overall and per tick), and renders +//! them as the report value ([`report`]) through [`cf_gojson`] — never raw +//! serde. //! -//! # Compatibility -//! -//! Output bytes are pinned against the reference implementation by -//! `tests/compat`. All machine-format report serialization is routed -//! through [`cf_gojson`] rather than raw serde. The pure metric computation in -//! [`metrics`] preserves the contract's sort orders (descending churn score; -//! risk then commit count); the byte rendering of [`metrics::ComputedMetrics`] -//! lives in [`report`]. -//! -//! # Nondeterminism note -//! -//! The `hashes` slice of a [`metrics::FileHistory`] has nondeterministic order -//! in the reference implementation (it is appended during a hash-map -//! traversal). Only its **length** (commit count) feeds the metrics, so the -//! derived metrics are deterministic; any report path that emits raw hashes -//! must be canonicalized (sorted) on both sides before diffing. -//! -//! # Scope of this crate -//! -//! The fully self-contained, byte-contract-critical pieces live here: the file -//! classifier scaffold ([`classify`]), the per-commit transport payload types -//! ([`tc`]), the metric computation ([`metrics`]) and its report rendering -//! ([`report`]). The streaming framework integration (aggregator, spill store, -//! checkpointing, hibernation and the commit-tree traversal that filters -//! deleted files) depends on framework crates that are not yet stable in this -//! tree and is tracked as a roadmap item; the corresponding interfaces are -//! sketched in [`framework`]. +//! Only the **length** of a [`metrics::FileHistory`]'s commit list feeds the +//! metrics, so the derived numbers do not depend on the order the hashes +//! were appended in. -// Deliberate parity casts (usize -> i64 lengths, i64 -> f64 averages) are part -// of the report contract; the lossy-cast lints would only add noise. +// Deliberate casts (usize -> i64 lengths, i64 -> f64 averages) are part of +// the report contract; the lossy-cast lints would only add noise. #![allow(clippy::cast_possible_wrap, clippy::cast_precision_loss)] -pub mod classify; -pub mod framework; +pub mod category; pub mod metrics; pub mod report; pub mod tc; -pub use classify::{all_categories, Category, Classifier, ALL_CATEGORIES}; +pub use category::{Category, ALL_CATEGORIES}; pub use metrics::{ compute_aggregate_with_options, compute_all_metrics, compute_all_metrics_with_options, compute_composition, compute_file_churn, compute_file_contributors, - compute_hotspots_with_options, AggregateData, CompositionData, CompositionTimeSeriesEntry, - ComputedMetrics, ContributorEntry, FileChurnData, FileContributorData, FileHistory, - HotspotData, MetricOptions, ReportData, HOTSPOT_THRESHOLD_CRITICAL, HOTSPOT_THRESHOLD_HIGH, - HOTSPOT_THRESHOLD_MEDIUM, + compute_hotspots_with_options, AggregateData, CompositionData, CompositionInput, + CompositionTimeSeriesEntry, ComputedMetrics, ContributorEntry, FileChurnData, + FileContributorData, FileHistory, HotspotData, MetricOptions, ReportData, TickBounds, + HOTSPOT_THRESHOLD_CRITICAL, HOTSPOT_THRESHOLD_HIGH, HOTSPOT_THRESHOLD_MEDIUM, }; -pub use report::{computed_metrics_to_go, to_compact_json, to_compact_json_string}; +pub use report::computed_metrics_to_go; pub use tc::{CategoryCounts, LineStats}; - -/// The analyzer identifier reported by [`metrics::ComputedMetrics::analyzer_name`]. -pub const ANALYZER_NAME: &str = "file_history"; - -/// Stable analyzer descriptor identifier. -pub const DESCRIPTOR_ID: &str = "history/file-history"; - -/// The CLI flag for the analyzer. -pub const FLAG: &str = "file-history"; - -/// The human-readable analyzer name. -pub const NAME: &str = "FileHistoryAnalysis"; - -/// The analyzer description shown in CLI help. -pub const DESCRIPTION: &str = - "Each file path is mapped to the list of commits which touch that file \ -and the mapping from involved developers to the corresponding line statistics."; diff --git a/crates/cf-file-history/src/metrics.rs b/crates/cf-file-history/src/metrics.rs index c10cc871..c49064c6 100644 --- a/crates/cf-file-history/src/metrics.rs +++ b/crates/cf-file-history/src/metrics.rs @@ -8,17 +8,18 @@ //! * [`HotspotData`] — high-churn files at/above the medium risk threshold, //! sorted by risk then commit count; //! * [`AggregateData`] — repository-wide summary statistics; -//! * [`CompositionData`] / [`CompositionTimeSeriesEntry`] — file category mix. +//! * [`CompositionData`] / [`CompositionTimeSeriesEntry`] — the category mix +//! of the DISTINCT files changed in the window, overall and per tick. //! -//! Sort orders and arithmetic are part of the report contract (pinned by the -//! differential gate) so the derived numeric values are byte-identical after -//! rendering through [`crate::report`]. +//! Sort orders and arithmetic are part of the report contract, so the +//! derived numeric values are stable after rendering through +//! [`crate::report`]. use std::collections::BTreeMap; use cf_metrics::{risk_priority, RiskLevel}; -use crate::classify::ALL_CATEGORIES; +use crate::category::ALL_CATEGORIES; use crate::tc::{CategoryCounts, LineStats}; /// Churn score divisor for normalization. @@ -34,11 +35,10 @@ pub const HOTSPOT_THRESHOLD_HIGH: i64 = 30; pub const HOTSPOT_THRESHOLD_MEDIUM: i64 = 15; /// The change history for a single file: a map from developer id to line -/// stats, plus the list of commit hashes that touched the file. +/// stats, plus the list of commit hashes that touched the file (following +/// renames). /// -/// Only the **length** of `hashes` (commit count) feeds the metrics, so the -/// hash order (nondeterministic in the reference implementation) does not -/// affect any derived numbers. +/// Only the **length** of `hashes` (commit count) feeds the metrics. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct FileHistory { /// Developer id -> aggregated line stats for this file. @@ -129,16 +129,19 @@ pub struct AggregateData { pub high_churn_files: i64, } -/// Aggregate file composition breakdown. +/// Aggregate file composition: the distinct files changed in the window by +/// category. #[derive(Debug, Clone, Default, PartialEq)] pub struct CompositionData { - /// Category name -> count (only non-zero categories present). + /// Category name -> number of distinct files (only non-zero categories + /// present). pub breakdown: BTreeMap<String, i64>, - /// Category name -> percentage of total. + /// Category name -> percentage of the distinct files changed. pub percentages: BTreeMap<String, f64>, } -/// File composition for a single tick. +/// File composition for a single tick: the distinct files changed in that +/// tick by category. #[derive(Debug, Clone, Default, PartialEq)] pub struct CompositionTimeSeriesEntry { /// Tick index. @@ -147,10 +150,27 @@ pub struct CompositionTimeSeriesEntry { pub start_time: String, /// Tick end time (RFC3339), empty when unknown (`omitempty`). pub end_time: String, - /// Category name -> count for this tick (only non-zero categories present). + /// Category name -> number of distinct files changed in this tick (only + /// non-zero categories present). pub breakdown: BTreeMap<String, i64>, } +/// Tick bounds (start/end RFC3339 strings) keyed by tick index. +pub type TickBounds = BTreeMap<i64, (String, String)>; + +/// The composition inputs the history walk accumulates: the distinct files +/// changed in the window by category, the same per tick, and the tick time +/// bounds. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct CompositionInput { + /// Distinct files changed in the window, by category. + pub files: CategoryCounts, + /// Distinct files changed per tick, by category. + pub ticks: BTreeMap<i64, CategoryCounts>, + /// Tick index -> `(start_time, end_time)` RFC3339 bounds. + pub tick_bounds: TickBounds, +} + /// All computed metric results. #[derive(Debug, Clone, Default, PartialEq)] pub struct ComputedMetrics { @@ -168,14 +188,6 @@ pub struct ComputedMetrics { pub composition_ts: Vec<CompositionTimeSeriesEntry>, } -impl ComputedMetrics { - /// Returns the analyzer identifier. - #[must_use] - pub fn analyzer_name(&self) -> &'static str { - crate::ANALYZER_NAME - } -} - /// Configurable hotspot thresholds. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MetricOptions { @@ -197,30 +209,25 @@ impl Default for MetricOptions { } } -/// Computes all metrics with default options. +/// Computes all metrics with default options and no composition inputs. #[must_use] pub fn compute_all_metrics(input: &ReportData) -> ComputedMetrics { - let empty: BTreeMap<i64, CategoryCounts> = BTreeMap::new(); - compute_all_metrics_with_options(input, MetricOptions::default(), &empty, None) + compute_all_metrics_with_options( + input, + MetricOptions::default(), + &CompositionInput::default(), + ) } -/// Tick bounds (start/end RFC3339 strings) keyed by tick index. -pub type TickBounds = BTreeMap<i64, (String, String)>; - -/// Computes all metrics with configurable thresholds and optional composition -/// inputs. -/// -/// `tick_composition` and `tick_bounds` correspond to the `tick_composition` -/// and `tick_bounds` report entries; pass an empty map / `None` when not -/// available. +/// Computes all metrics with configurable thresholds and the composition +/// inputs the walk accumulated. #[must_use] pub fn compute_all_metrics_with_options( input: &ReportData, opts: MetricOptions, - tick_composition: &BTreeMap<i64, CategoryCounts>, - tick_bounds: Option<&TickBounds>, + composition: &CompositionInput, ) -> ComputedMetrics { - let (composition, composition_ts) = compute_composition(tick_composition, tick_bounds); + let (composition, composition_ts) = compute_composition(composition); ComputedMetrics { file_churn: compute_file_churn(input), file_contributors: compute_file_contributors(input), @@ -410,62 +417,55 @@ pub fn compute_aggregate_with_options(input: &ReportData, opts: MetricOptions) - agg } -/// Computes the aggregate and per-tick composition. +/// Computes the aggregate and per-tick composition: the aggregate breakdown +/// and percentages come from the distinct files changed in the window, each +/// tick entry from the distinct files changed in that tick (ascending tick +/// order) with its time bounds when known. #[must_use] pub fn compute_composition( - tick_comp: &BTreeMap<i64, CategoryCounts>, - tick_bounds: Option<&TickBounds>, + input: &CompositionInput, ) -> (CompositionData, Vec<CompositionTimeSeriesEntry>) { - let mut comp = CompositionData::default(); - - if tick_comp.is_empty() { - return (comp, Vec::new()); - } - - // BTreeMap iterates ticks in ascending order (report contract). - let mut ts: Vec<CompositionTimeSeriesEntry> = Vec::with_capacity(tick_comp.len()); - let mut total = CategoryCounts::default(); - - for (&t, counts) in tick_comp { - total.add(counts); - - let mut breakdown = BTreeMap::new(); - for cat in ALL_CATEGORIES { - let v = counts.get(cat); - if v > 0 { - breakdown.insert(cat.as_str().to_string(), v); + let ts: Vec<CompositionTimeSeriesEntry> = input + .ticks + .iter() + .map(|(&tick, counts)| { + let (start_time, end_time) = input.tick_bounds.get(&tick).cloned().unwrap_or_default(); + CompositionTimeSeriesEntry { + tick, + start_time, + end_time, + breakdown: non_zero_breakdown(counts), } - } - - let mut entry = CompositionTimeSeriesEntry { - tick: t, - breakdown, - ..Default::default() - }; - if let Some(bounds) = tick_bounds.and_then(|tb| tb.get(&t)) { - entry.start_time.clone_from(&bounds.0); - entry.end_time.clone_from(&bounds.1); - } - ts.push(entry); - } + }) + .collect(); - let grand_total = total.total(); - for cat in ALL_CATEGORIES { - let v = total.get(cat); - if v > 0 { - comp.breakdown.insert(cat.as_str().to_string(), v); - } - if grand_total > 0 { + let mut comp = CompositionData { + breakdown: non_zero_breakdown(&input.files), + percentages: BTreeMap::new(), + }; + let total = input.files.total(); + if total > 0 { + for cat in ALL_CATEGORIES { comp.percentages.insert( cat.as_str().to_string(), - v as f64 / grand_total as f64 * PERCENT_MULTIPLIER, + input.files.get(cat) as f64 / total as f64 * PERCENT_MULTIPLIER, ); } } - (comp, ts) } +/// The non-zero categories of `counts`, keyed by report name. +fn non_zero_breakdown(counts: &CategoryCounts) -> BTreeMap<String, i64> { + ALL_CATEGORIES + .iter() + .filter_map(|&cat| { + let v = counts.get(cat); + (v > 0).then(|| (cat.as_str().to_string(), v)) + }) + .collect() +} + // --- helpers --- /// Sums per-author line stats for a file: `(added, removed, changed)`. @@ -786,11 +786,6 @@ mod tests { assert_eq!(r.aggregate.total_files, 0); } - #[test] - fn computed_metrics_analyzer_name() { - assert_eq!(ComputedMetrics::default().analyzer_name(), "file_history"); - } - #[test] fn compute_all_full() { let input = report(&[ @@ -843,25 +838,62 @@ mod tests { assert_eq!(r.aggregate.total_contributors, 2); } + /// The aggregate breakdown counts the distinct files of the window (one + /// source file edited in three ticks is ONE file), every tick lists its + /// own distinct files, and a tick carries its bounds when known. #[test] - fn composition_basic() { - let mut tick_comp = BTreeMap::new(); - tick_comp.insert( - 0, - CategoryCounts { - source: 3, - vendor: 1, + fn composition_counts_distinct_files_overall_and_per_tick() { + let one_source = CategoryCounts { + source: 1, + ..Default::default() + }; + let input = CompositionInput { + files: CategoryCounts { + source: 1, + binary: 1, ..Default::default() }, + ticks: BTreeMap::from([ + (0, one_source), + (1, one_source), + ( + 2, + CategoryCounts { + source: 1, + binary: 1, + ..Default::default() + }, + ), + ]), + tick_bounds: BTreeMap::from([( + 2, + ( + "2021-01-03T00:00:00Z".to_string(), + "2021-01-03T12:00:00Z".to_string(), + ), + )]), + }; + let (comp, ts) = compute_composition(&input); + assert_eq!( + comp.breakdown, + BTreeMap::from([("source".into(), 1), ("binary".into(), 1)]) ); - let (comp, ts) = compute_composition(&tick_comp, None); - assert_eq!(comp.breakdown.get("source"), Some(&3)); - assert_eq!(comp.breakdown.get("vendor"), Some(&1)); - // 3/4 * 100 = 75, 1/4 * 100 = 25. - assert!((comp.percentages["source"] - 75.0).abs() < DELTA); - assert!((comp.percentages["vendor"] - 25.0).abs() < DELTA); - assert_eq!(ts.len(), 1); + assert!((comp.percentages["source"] - 50.0).abs() < DELTA); + assert!((comp.percentages["binary"] - 50.0).abs() < DELTA); + assert!((comp.percentages["vendor"] - 0.0).abs() < DELTA); + assert_eq!(ts.len(), 3); assert_eq!(ts[0].tick, 0); - assert_eq!(ts[0].breakdown.get("source"), Some(&3)); + assert_eq!(ts[0].breakdown, BTreeMap::from([("source".into(), 1)])); + assert!(ts[0].start_time.is_empty() && ts[0].end_time.is_empty()); + assert_eq!(ts[2].breakdown.get("binary"), Some(&1)); + assert_eq!(ts[2].start_time, "2021-01-03T00:00:00Z"); + assert_eq!(ts[2].end_time, "2021-01-03T12:00:00Z"); + } + + #[test] + fn composition_empty_input_is_empty() { + let (comp, ts) = compute_composition(&CompositionInput::default()); + assert_eq!(comp, CompositionData::default()); + assert!(ts.is_empty()); } } diff --git a/crates/cf-file-history/src/report.rs b/crates/cf-file-history/src/report.rs index f8e8b4d6..8fc399af 100644 --- a/crates/cf-file-history/src/report.rs +++ b/crates/cf-file-history/src/report.rs @@ -1,23 +1,14 @@ //! Report-format rendering of the file-history metrics. //! -//! All machine-format report bytes are produced through [`cf_gojson`] (never -//! raw serde); the bytes are pinned against the reference binary by -//! `tests/compat`. -//! -//! The wrapper structs ([`ComputedMetrics`] and its fields) are emitted in +//! The report value is a [`cf_gojson::GoValue`] tree the command layer +//! serializes through `cf-gojson` / `cf-goyaml` (never raw serde). The +//! wrapper structs ([`ComputedMetrics`] and its fields) are emitted in //! **declaration order** (a struct-origin [`cf_gojson::GoMap`]). The //! composition `breakdown` / `percentages` maps and the per-tick composition -//! `breakdown` are emitted as map-origin `GoMap`s (byte-sorted keys), per the -//! report-format contract for string-keyed maps. Empty `start_time` / -//! `end_time` are omitted (`omitempty`). -//! -//! The compact form here ([`to_compact_json`]) is the `bin`/ndjson payload -//! shape (no indent, HTML-escape on, no trailing newline). The indented `json` -//! run/render form (two-space indent) is produced by the cross-format -//! conversion hub (`cf-analyze`) over the same [`GoValue`] tree returned by -//! [`computed_metrics_to_go`]. +//! `breakdown` are emitted as map-origin `GoMap`s (byte-sorted keys). Empty +//! `start_time` / `end_time` are omitted (`omitempty`). -use cf_gojson::{marshal, Encoder, GoMap, GoValue}; +use cf_gojson::{GoMap, GoValue}; use crate::metrics::{ AggregateData, CompositionData, CompositionTimeSeriesEntry, ComputedMetrics, ContributorEntry, @@ -50,8 +41,8 @@ pub fn computed_metrics_to_go(m: &ComputedMetrics) -> GoValue { ); obj.push("aggregate", aggregate_to_go(&m.aggregate)); obj.push("composition", composition_to_go(&m.composition)); - // Reference: `composition_ts` is a `var`-declared append target — an - // empty-walk run leaves it nil (JSON `null`, YAML `[]`), never `[]`. + // `composition_ts` renders as JSON `null` (YAML `[]`) for an empty walk, + // never `[]`. obj.push( "composition_ts", if m.composition_ts.is_empty() { @@ -159,34 +150,24 @@ fn composition_ts_to_go(e: &CompositionTimeSeriesEntry) -> GoValue { GoValue::Object(o) } -/// Renders [`ComputedMetrics`] to compact report-format JSON bytes (the -/// `bin` / ndjson payload form: no indent, HTML-escape on, no trailing -/// newline). -#[must_use] -pub fn to_compact_json(m: &ComputedMetrics) -> Vec<u8> { - marshal(&computed_metrics_to_go(m)) -} - -/// Renders [`ComputedMetrics`] to a compact report-format JSON string -/// (convenience over [`to_compact_json`], using the [`Encoder`] builder). -#[must_use] -pub fn to_compact_json_string(m: &ComputedMetrics) -> String { - Encoder::marshal().encode_to_string(&computed_metrics_to_go(m)) -} - #[cfg(test)] mod tests { use super::*; + use cf_gojson::Encoder; + + /// The compact JSON form of `m` (no indent, HTML-escape on). + fn compact(m: &ComputedMetrics) -> String { + Encoder::marshal().encode_to_string(&computed_metrics_to_go(m)) + } #[test] fn empty_metrics_compact_json_field_order() { // Field declaration order must be preserved in compact output, and // empty slices render as [] (report contract), empty maps as {}. - // `composition_ts` is the exception: the reference leaves it a nil - // append target on an empty walk, so it renders as `null` (see - // `computed_metrics_to_go`). + // `composition_ts` is the exception: it renders as `null` for an + // empty walk (see `computed_metrics_to_go`). let m = ComputedMetrics::default(); - let s = to_compact_json_string(&m); + let s = compact(&m); assert_eq!( s, "{\"file_churn\":[],\"file_contributors\":[],\"hotspots\":[],\ @@ -210,15 +191,9 @@ mod tests { }, ..Default::default() }; - let s = to_compact_json_string(&m); + let s = compact(&m); assert!(s.contains("\"avg_commits_per_file\":15"), "{s}"); assert!(s.contains("\"avg_contributors_per_file\":2"), "{s}"); assert!(!s.contains("15.0"), "{s}"); } - - #[test] - fn compact_bytes_match_string() { - let m = ComputedMetrics::default(); - assert_eq!(to_compact_json(&m), to_compact_json_string(&m).into_bytes()); - } } diff --git a/crates/cf-file-history/src/tc.rs b/crates/cf-file-history/src/tc.rs index 9e1584b5..6c3c4a80 100644 --- a/crates/cf-file-history/src/tc.rs +++ b/crates/cf-file-history/src/tc.rs @@ -1,14 +1,7 @@ -//! Per-commit transport-cell (TC) payload types. -//! -//! These types capture, for a single commit, the path actions -//! (insert/modify/delete/rename), the per-author line-stat deltas, and the file -//! category composition. The full TC plumbing (change routing, blob cache, -//! identity detection) belongs to the framework integration sketched in -//! [`crate::framework`]. The data shapes and the [`CategoryCounts`] arithmetic -//! are report-contract-relevant (the category names feed the composition -//! report) and live here. +//! The per-change value types the history walk accumulates: a developer's +//! line-stat delta on a file and the per-category file counts. -use crate::classify::Category; +use crate::category::Category; /// Line statistics for a single file/author change. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] diff --git a/crates/cf-framework/src/lib.rs b/crates/cf-framework/src/lib.rs index 146d6071..f6db05af 100644 --- a/crates/cf-framework/src/lib.rs +++ b/crates/cf-framework/src/lib.rs @@ -26,7 +26,7 @@ //! //! Compatibility: configuration defaults and parsing semantics feed analyzer //! output paths indirectly; output bytes are pinned against the reference -//! implementation by `tests/compat`. +//! implementation by the report e2e suites (`bins/codefang/tests`). //! //! # Example //! diff --git a/crates/cf-gitlib/Cargo.toml b/crates/cf-gitlib/Cargo.toml index e2e75e59..1ba5843a 100644 --- a/crates/cf-gitlib/Cargo.toml +++ b/crates/cf-gitlib/Cargo.toml @@ -14,6 +14,9 @@ description = "Git repository access layer over libgit2. Per-thread !Send/!Sync # pin source). Per-thread !Send/!Sync Repository, RAII Drop frees handles. # See DESIGN §3. git2 = { workspace = true } +# libgit2 options git2 does not wrap (the object-cache limit); the same +# libgit2-sys git2 builds on, so no second libgit2 is linked. +libgit2-sys = "0.17" # Byte text helpers (is_binary / count_lines) shared with the analyzers. cf-textutil = { path = "../cf-textutil" } # Overflow-checked numeric conversions. diff --git a/crates/cf-gitlib/README.md b/crates/cf-gitlib/README.md index 143f3b94..1d5d5703 100644 --- a/crates/cf-gitlib/README.md +++ b/crates/cf-gitlib/README.md @@ -11,10 +11,10 @@ reference binary byte-for-byte. ## Why The history analyzers (burndown, couples, devs, file_history, ...) need a single -git access layer whose output is reproducible and parity-stable. Pinning libgit2 -keeps diff line counts, tree-change ordering, and hash rendering identical to the -reference implementation; those surface into machine reports whose bytes are -pinned by `tests/compat`. +git access layer whose output is reproducible. Pinning libgit2 keeps diff line +counts, tree-change ordering, and hash rendering stable across machines; those +surface into machine reports whose bytes are pinned by the CLI-level end-to-end +tests. ## Model diff --git a/crates/cf-gitlib/src/changes.rs b/crates/cf-gitlib/src/changes.rs index 5b470364..cff8d78a 100644 --- a/crates/cf-gitlib/src/changes.rs +++ b/crates/cf-gitlib/src/changes.rs @@ -4,7 +4,7 @@ //! [`initial_tree_changes`] treats every file in a tree as an insertion (for a //! root commit); [`tree_files`] materializes all blob files in a tree. The //! change stream feeds the history analyzers, so its ordering and contents are -//! part of the report contract (pinned by `tests/compat`). +//! part of the report contract (pinned by the report e2e suites (`bins/codefang/tests`)). use crate::error::Result; use crate::file::File; diff --git a/crates/cf-gitlib/src/commit.rs b/crates/cf-gitlib/src/commit.rs index 2535e740..21c238d3 100644 --- a/crates/cf-gitlib/src/commit.rs +++ b/crates/cf-gitlib/src/commit.rs @@ -200,8 +200,13 @@ impl<'repo> Commit<'repo> { /// An iterator over commits from a revwalk. /// /// Looks up full commit objects lazily and honors an optional `since` -/// author-time filter: when set, iteration stops at the first commit whose -/// author time is strictly before `since`. +/// author-time filter: when set, every commit whose author time is strictly +/// before `since` is SKIPPED and the walk continues. The walk itself is +/// ordered by committer time, and author time can be arbitrarily skewed +/// against it (`git commit --date`, rebases, imported history), so the first +/// too-old commit is not a stop condition — stopping there dropped younger +/// commits that happened to sort below it. The price is a full walk when +/// `--since` is set. /// /// Implements [`cf_alg::PullIterator`]. pub struct CommitIter<'repo> { @@ -239,7 +244,8 @@ impl<'repo> CommitIter<'repo> { /// Returns the next commit, or `None` at end of iteration. /// - /// Frees the walk once exhausted or filtered out. + /// Commits older than the `since` cutoff (author time) are skipped; the + /// walk is freed once exhausted. #[must_use] pub fn next_commit(&mut self) -> Option<Commit<'repo>> { let walk = self.walk.as_mut()?; @@ -254,11 +260,8 @@ impl<'repo> CommitIter<'repo> { continue; }; - if let Some(since) = self.since { - if time_before(commit.author().when(), since) { - self.walk = None; - return None; - } + if is_before_since(&commit, self.since) { + continue; } return Some(Commit::new(commit, self.repo)); @@ -295,31 +298,32 @@ impl<'repo> CommitIter<'repo> { Ok(()) } - /// Advances by one commit without materializing a `Commit`. + /// Advances by one in-window commit without materializing a `Commit`. /// - /// Returns `true` if a commit was consumed, `false` at EOF / since-cutoff. + /// Returns `true` if a commit was consumed, `false` at EOF. fn skip1(&mut self) -> bool { - let Some(walk) = self.walk.as_mut() else { - return false; - }; + loop { + let Some(walk) = self.walk.as_mut() else { + return false; + }; - let Some(Ok(oid)) = walk.next() else { - self.walk = None; - return false; - }; + let Some(Ok(oid)) = walk.next() else { + self.walk = None; + return false; + }; - if let Some(since) = self.since { - // Look up the commit to inspect its author time. + if self.since.is_none() { + return true; + } + // Look up the commit to inspect its author time; a commit before + // the cutoff is skipped, like `next_commit` does. let Ok(commit) = self.repo.native().find_commit(oid) else { return false; }; - if time_before(commit.author().when(), since) { - self.walk = None; - return false; + if !is_before_since(&commit, self.since) { + return true; } } - - true } /// Releases the walk; idempotent. @@ -338,6 +342,12 @@ impl<'repo> PullIterator<Commit<'repo>> for CommitIter<'repo> { } } +/// Whether `commit`'s author time falls before the `since` cutoff (no cutoff +/// ⇒ never). +fn is_before_since(commit: &git2::Commit<'_>, since: Option<git2::Time>) -> bool { + since.is_some_and(|since| time_before(commit.author().when(), since)) +} + /// Reports whether `a` is strictly before `b`. /// /// Compares the absolute instant (seconds since epoch), independent of UTC diff --git a/crates/cf-gitlib/src/diff.rs b/crates/cf-gitlib/src/diff.rs index b8f566a1..d60372ca 100644 --- a/crates/cf-gitlib/src/diff.rs +++ b/crates/cf-gitlib/src/diff.rs @@ -29,7 +29,7 @@ pub enum LineOp { /// devs churn, …) flow straight into reports, and libgit2's Myers diff and /// `diffmatchpatch`'s line diff group changed-vs-added-vs-removed lines /// differently, so the two are not interchangeable. Pinned by the differential -/// gate in `tests/compat`. +/// report e2e suites (`bins/codefang/tests`). /// /// The op stream rules (reference-implementation behavior): /// * libgit2 default diff options (3 context lines) via diff --git a/crates/cf-gitlib/src/error.rs b/crates/cf-gitlib/src/error.rs index 8175b22b..369a2246 100644 --- a/crates/cf-gitlib/src/error.rs +++ b/crates/cf-gitlib/src/error.rs @@ -3,7 +3,7 @@ //! [`GitError`] is a flat set of sentinel errors plus `"<context>: <cause>"` //! wrappers over libgit2 errors. Every `Display` string is part of the CLI //! error-message contract and must stay byte-identical (pinned by the -//! differential gate in `tests/compat`). +//! the report e2e suites (`bins/codefang/tests`)). use std::fmt; diff --git a/crates/cf-gitlib/src/hash.rs b/crates/cf-gitlib/src/hash.rs index 058fc608..c7c727eb 100644 --- a/crates/cf-gitlib/src/hash.rs +++ b/crates/cf-gitlib/src/hash.rs @@ -9,7 +9,7 @@ //! //! [`Hash::to_hex`] renders a 40-character, lowercase, fixed-width hex string. //! Hashes surface into machine reports as these strings, so the rendering is -//! part of the report contract (pinned by `tests/compat`). +//! part of the report contract (pinned by the report e2e suites (`bins/codefang/tests`)). //! //! ``` //! use cf_gitlib::Hash; diff --git a/crates/cf-gitlib/src/lib.rs b/crates/cf-gitlib/src/lib.rs index 29edccdd..02c614dc 100644 --- a/crates/cf-gitlib/src/lib.rs +++ b/crates/cf-gitlib/src/lib.rs @@ -8,8 +8,8 @@ //! Repository handles are per-thread (`!Send`/`!Sync`) and rely on RAII `Drop` //! to free libgit2 objects. Everything that surfaces into machine reports //! (hash rendering, diff line counts, tree-change streams) reproduces the -//! reference implementation exactly; output bytes are pinned against the -//! reference binary by `tests/compat`. +//! reference implementation exactly; output bytes are pinned by the report +//! e2e suites (`bins/codefang/tests`). // Compile the README's usage example as a doctest (without injecting the README // into the rendered crate docs, which the `//!` block above already covers). @@ -41,7 +41,7 @@ pub use blob::Blob; pub use commit::{Commit, CommitIter}; pub use error::{GitCause, GitError, Result}; pub use hash::Hash; -pub use repository::{LogOptions, Repository}; +pub use repository::{limit_native_memory, LogOptions, NativeLimits, Repository}; pub use revwalk::RevWalk; pub use signature::Signature; diff --git a/crates/cf-gitlib/src/repository.rs b/crates/cf-gitlib/src/repository.rs index ee74713d..c7cb0738 100644 --- a/crates/cf-gitlib/src/repository.rs +++ b/crates/cf-gitlib/src/repository.rs @@ -6,6 +6,8 @@ //! and future-proof. RAII [`Drop`] frees the handle. use std::marker::PhantomData; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Once; use crate::blob::Blob; use crate::commit::{Commit, CommitIter}; @@ -45,17 +47,80 @@ pub fn time_from_unix_secs(secs: i64) -> git2::Time { /// output** (it only skips an integrity assertion, never changes which bytes are /// read or diffed). /// +/// **The memory limits** registered with [`limit_native_memory`]: libgit2 +/// maps pack files in windows and keeps up to 8 GiB of them mapped on 64-bit +/// hosts, every touched page resident (a one-worker `history/devs` walk over +/// 31,923 kubernetes commits held 383 MB of mapped pack pages); and its object +/// cache, shared by every repository handle of the process, defaults to +/// 256 MiB, which the parallel walks' handles overflow (the five non-UAST +/// history walks over those commits ran 183 s with it, 109 s with 512 MiB). +/// /// The options are libgit2 process-global state, so they are set once under a /// [`std::sync::Once`] on the first repository open. fn tune_libgit2() { - static TUNE: std::sync::Once = std::sync::Once::new(); TUNE.call_once(|| { // `git2::opts` calls `crate::init()` internally, so libgit2 is initialized // before the option is applied. git2::opts::strict_hash_verification(false); + let mapped_pack = MAPPED_PACK_LIMIT.load(Ordering::Acquire); + if mapped_pack > 0 { + // SAFETY: runs once, inside the `Once` the first repository open + // waits on; no repository exists yet, so no libgit2 operation + // reads the option while it changes. A refused option leaves + // libgit2's default in place. + let _ = unsafe { git2::opts::set_mwindow_mapped_limit(mapped_pack) }; + } + let object_cache = OBJECT_CACHE_LIMIT.load(Ordering::Acquire); + if let Ok(bytes) = isize::try_from(object_cache) { + if bytes > 0 { + // SAFETY: as above; `GIT_OPT_SET_CACHE_MAX_SIZE` takes one + // `ssize_t` and only stores it. + unsafe { + libgit2_sys::git_libgit2_opts( + libgit2_sys::GIT_OPT_SET_CACHE_MAX_SIZE as std::os::raw::c_int, + bytes, + ); + } + } + } }); } +/// Guards [`tune_libgit2`]. +static TUNE: Once = Once::new(); + +/// The mapped-pack limit the first repository open applies (`0` keeps +/// libgit2's default). +static MAPPED_PACK_LIMIT: AtomicUsize = AtomicUsize::new(0); + +/// The object-cache limit the first repository open applies (`0` keeps +/// libgit2's default). +static OBJECT_CACHE_LIMIT: AtomicUsize = AtomicUsize::new(0); + +/// Process-wide libgit2 memory limits, in bytes (`0` keeps a default). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct NativeLimits { + /// Pack-file memory kept mapped at once. + pub mapped_pack: usize, + /// Decoded objects (commits, trees) cached across every repository + /// handle. + pub object_cache: usize, +} + +/// Sets libgit2's process-wide memory limits. +/// +/// The limits take effect at the process's first [`Repository::open`]; +/// libgit2 options must not change while repositories are in use, so a call +/// after that open is refused. Returns whether the limits will take effect. +pub fn limit_native_memory(limits: NativeLimits) -> bool { + if TUNE.is_completed() { + return false; + } + MAPPED_PACK_LIMIT.store(limits.mapped_pack, Ordering::Release); + OBJECT_CACHE_LIMIT.store(limits.object_cache, Ordering::Release); + true +} + /// A libgit2 repository. /// /// **Not** `Send`/`Sync`: libgit2 repositories are single-threaded, so each @@ -221,10 +286,15 @@ impl Repository { Ok(count) } - /// Computes the diff between two trees. + /// Computes the diff between two trees, with rename detection. /// /// Either side may be `None` (an added/removed whole tree). Uses libgit2's - /// default diff options. + /// default diff options, then `git_diff_find_similar` with renames + /// enabled (libgit2's default 50 % similarity threshold, the `git diff -M` + /// behaviour) so a moved file surfaces as ONE [`git2::Delta::Renamed`] + /// delta instead of a Delete + Add pair. File identity across renames is + /// a documented contract of the history analyzers (burndown line + /// survival, couples coupling chains, file-history commit lists). /// /// # Errors /// @@ -234,10 +304,14 @@ impl Repository { old_tree: Option<&Tree<'_>>, new_tree: Option<&Tree<'_>>, ) -> Result<Diff<'_>> { - let diff = self + let mut diff = self .repo .diff_tree_to_tree(old_tree.map(Tree::native), new_tree.map(Tree::native), None) .map_err(GitError::DiffTrees)?; + let mut find = git2::DiffFindOptions::new(); + find.renames(true); + diff.find_similar(Some(&mut find)) + .map_err(GitError::DiffTrees)?; Ok(Diff::new(diff)) } diff --git a/crates/cf-gitlib/src/testutil.rs b/crates/cf-gitlib/src/testutil.rs index c017a6d1..d81df718 100644 --- a/crates/cf-gitlib/src/testutil.rs +++ b/crates/cf-gitlib/src/testutil.rs @@ -4,6 +4,12 @@ //! commits, used by this crate's integration tests and available to downstream //! crates' tests via the `testutil` feature. Commit timestamps are fixed so any //! golden built on these repos is reproducible (DESIGN §2.8). +//! +//! [`CommitBuilder`] is the general fixture primitive: it fixes the author, +//! the (optionally skewed) committer time, the parents (side lines and +//! merges) and the tree edits — writes, deletes and pure renames. The free +//! functions ([`commit_files`], [`commit_files_as`], [`delete_files`], +//! [`rename_file`]) are thin wrappers over it for the common one-liners. use crate::error::Result; use crate::hash::Hash; @@ -32,11 +38,10 @@ impl Drop for TestRepo { } } -/// A deterministic signature for test commits (fixed name/email/time). -fn fixed_sig<'a>(secs: i64) -> std::result::Result<git2::Signature<'a>, git2::Error> { - let when = git2::Time::new(secs, 0); - git2::Signature::new("Test Author", "test@example.com", &when) -} +/// Name of the default fixture author. +pub const DEFAULT_AUTHOR_NAME: &str = "Test Author"; +/// Email of the default fixture author. +pub const DEFAULT_AUTHOR_EMAIL: &str = "test@example.com"; /// Create an empty initialized repository in a fresh temp directory. /// @@ -55,6 +60,217 @@ pub fn init_repo() -> Result<TestRepo> { Ok(TestRepo { repo, dir }) } +/// A deterministic, fully specified commit under construction: author, +/// committer time, parents, and the tree edits (writes, deletes, renames) +/// applied on top of the base tree. +/// +/// Every field defaults to the plain [`commit_files`] behaviour: the default +/// author, committer time == author time, HEAD as the base/first parent, and +/// HEAD moved to the new commit. +pub struct CommitBuilder<'a> { + test: &'a TestRepo, + message: String, + author_name: String, + author_email: String, + author_time: i64, + committer_time: Option<i64>, + base: Option<Hash>, + extra_parents: Vec<Hash>, + update_head: bool, + writes: Vec<(String, Vec<u8>)>, + deletes: Vec<String>, + renames: Vec<(String, String)>, +} + +impl<'a> CommitBuilder<'a> { + /// Starts a commit with `message` at `time_secs` (author and committer + /// time) by the default fixture author, on top of HEAD. + #[must_use] + pub fn new(test: &'a TestRepo, message: &str, time_secs: i64) -> Self { + CommitBuilder { + test, + message: message.to_string(), + author_name: DEFAULT_AUTHOR_NAME.to_string(), + author_email: DEFAULT_AUTHOR_EMAIL.to_string(), + author_time: time_secs, + committer_time: None, + base: None, + extra_parents: Vec::new(), + update_head: true, + writes: Vec::new(), + deletes: Vec::new(), + renames: Vec::new(), + } + } + + /// Sets the author (and committer) name and email. + #[must_use] + pub fn author(mut self, name: &str, email: &str) -> Self { + self.author_name = name.to_string(); + self.author_email = email.to_string(); + self + } + + /// Sets a committer time different from the author time (skewed dates). + #[must_use] + pub fn committer_time(mut self, secs: i64) -> Self { + self.committer_time = Some(secs); + self + } + + /// Uses `base` (instead of HEAD) as the tree base and first parent. + #[must_use] + pub fn parent(mut self, base: Hash) -> Self { + self.base = Some(base); + self + } + + /// Adds a further parent, making this a merge commit. + #[must_use] + pub fn merge_parent(mut self, parent: Hash) -> Self { + self.extra_parents.push(parent); + self + } + + /// Leaves HEAD where it is (a commit on a side line). + #[must_use] + pub fn detached(mut self) -> Self { + self.update_head = false; + self + } + + /// Writes (or overwrites) `path` with `contents`. + #[must_use] + pub fn write(mut self, path: &str, contents: &[u8]) -> Self { + self.writes.push((path.to_string(), contents.to_vec())); + self + } + + /// Deletes `path` from the tree. + #[must_use] + pub fn delete(mut self, path: &str) -> Self { + self.deletes.push(path.to_string()); + self + } + + /// Moves `from` to `to`, keeping the blob (a pure `git mv`). + #[must_use] + pub fn rename(mut self, from: &str, to: &str) -> Self { + self.renames.push((from.to_string(), to.to_string())); + self + } + + /// Writes the commit and returns its hash. + /// + /// # Errors + /// Propagates libgit2 errors at any step, and a message error when a + /// rename source is absent from the base tree. + pub fn commit(self) -> Result<Hash> { + let repo = self.test.repo.native(); + let base_commit = match self.base { + Some(h) => Some( + repo.find_commit(h.to_oid()) + .map_err(|e| GitError::lib("find base commit", e))?, + ), + None => repo + .head() + .ok() + .and_then(|h| h.target()) + .and_then(|oid| repo.find_commit(oid).ok()), + }; + let base_tree = base_commit.as_ref().and_then(|c| c.tree().ok()); + + // Build the new tree via an in-memory index so nested paths (e.g. + // `dir/b.txt`) are materialized as subtrees; a flat treebuilder rejects + // entry names containing `/`. + let mut index = git2::Index::new().map_err(|e| GitError::lib("new index", e))?; + if let Some(tree) = base_tree.as_ref() { + index + .read_tree(tree) + .map_err(|e| GitError::lib("index read_tree", e))?; + } + for (from, to) in &self.renames { + let entry = base_tree + .as_ref() + .and_then(|t| t.get_path(std::path::Path::new(from)).ok()) + .ok_or_else(|| GitError::Message(format!("rename source {from} not in tree")))?; + add_blob_entry(&mut index, to, entry.id(), 0)?; + index + .remove_path(std::path::Path::new(from)) + .map_err(|e| GitError::lib("index remove_path", e))?; + } + for path in &self.deletes { + index + .remove_path(std::path::Path::new(path)) + .map_err(|e| GitError::lib("index remove_path", e))?; + } + for (path, contents) in &self.writes { + let oid = repo + .blob(contents) + .map_err(|e| GitError::lib("write blob", e))?; + add_blob_entry(&mut index, path, oid, contents.len())?; + } + + let tree_oid = index + .write_tree_to(repo) + .map_err(|e| GitError::lib("tree write", e))?; + let tree = repo + .find_tree(tree_oid) + .map_err(|e| GitError::lib("find tree", e))?; + let author = signature(&self.author_name, &self.author_email, self.author_time)?; + let committer = signature( + &self.author_name, + &self.author_email, + self.committer_time.unwrap_or(self.author_time), + )?; + let mut parents: Vec<git2::Commit> = base_commit.into_iter().collect(); + for extra in &self.extra_parents { + parents.push( + repo.find_commit(extra.to_oid()) + .map_err(|e| GitError::lib("find merge parent", e))?, + ); + } + let parent_refs: Vec<&git2::Commit> = parents.iter().collect(); + let update_ref = if self.update_head { Some("HEAD") } else { None }; + let commit_oid = repo + .commit( + update_ref, + &author, + &committer, + &self.message, + &tree, + &parent_refs, + ) + .map_err(|e| GitError::lib("commit", e))?; + Ok(Hash::from_oid(&commit_oid)) + } +} + +/// A fixed-time signature for test commits. +fn signature<'a>(name: &str, email: &str, secs: i64) -> Result<git2::Signature<'a>> { + let when = git2::Time::new(secs, 0); + git2::Signature::new(name, email, &when).map_err(|e| GitError::lib("signature", e)) +} + +/// Adds a regular-file blob entry to an in-memory index. +fn add_blob_entry(index: &mut git2::Index, path: &str, oid: git2::Oid, size: usize) -> Result<()> { + let entry = git2::IndexEntry { + ctime: git2::IndexTime::new(0, 0), + mtime: git2::IndexTime::new(0, 0), + dev: 0, + ino: 0, + mode: 0o100_644, + uid: 0, + gid: 0, + file_size: size as u32, + id: oid, + flags: 0, + flags_extended: 0, + path: path.as_bytes().to_vec(), + }; + index.add(&entry).map_err(|e| GitError::lib("index add", e)) +} + /// Append a commit to HEAD, writing `files` (path → contents) into a tree built /// on top of the current HEAD tree (or empty for the first commit). Returns the /// new commit's hash. `time_secs` fixes the timestamp for reproducibility. @@ -67,61 +283,67 @@ pub fn commit_files( time_secs: i64, files: &[(&str, &[u8])], ) -> Result<Hash> { - let repo = test.repo.native(); - - let parent_commit = repo - .head() - .ok() - .and_then(|h| h.target()) - .and_then(|oid| repo.find_commit(oid).ok()); - let base_tree = parent_commit.as_ref().and_then(|c| c.tree().ok()); - - // Build the new tree via an in-memory index so nested paths (e.g. - // `dir/b.txt`) are materialized as subtrees; a flat treebuilder rejects - // entry names containing `/`. - let mut index = git2::Index::new().map_err(|e| GitError::lib("new index", e))?; - if let Some(tree) = base_tree.as_ref() { - index - .read_tree(tree) - .map_err(|e| GitError::lib("index read_tree", e))?; - } + commit_files_as( + test, + DEFAULT_AUTHOR_NAME, + DEFAULT_AUTHOR_EMAIL, + message, + time_secs, + files, + ) +} +/// [`commit_files`] with an explicit author `name` / `email`. +/// +/// # Errors +/// Propagates libgit2 errors at any step. +pub fn commit_files_as( + test: &TestRepo, + name: &str, + email: &str, + message: &str, + time_secs: i64, + files: &[(&str, &[u8])], +) -> Result<Hash> { + let mut b = CommitBuilder::new(test, message, time_secs).author(name, email); for (path, contents) in files { - let oid = repo - .blob(contents) - .map_err(|e| GitError::lib("write blob", e))?; - let entry = git2::IndexEntry { - ctime: git2::IndexTime::new(0, 0), - mtime: git2::IndexTime::new(0, 0), - dev: 0, - ino: 0, - mode: 0o100_644, - uid: 0, - gid: 0, - file_size: contents.len() as u32, - id: oid, - flags: 0, - flags_extended: 0, - path: path.as_bytes().to_vec(), - }; - index - .add(&entry) - .map_err(|e| GitError::lib("index add", e))?; + b = b.write(path, contents); } + b.commit() +} - let tree_oid = index - .write_tree_to(repo) - .map_err(|e| GitError::lib("tree write", e))?; - let tree = repo - .find_tree(tree_oid) - .map_err(|e| GitError::lib("find tree", e))?; - let sig = fixed_sig(time_secs).map_err(|e| GitError::lib("signature", e))?; - let parents: Vec<&git2::Commit> = parent_commit.iter().collect(); - let commit_oid = repo - .commit(Some("HEAD"), &sig, &sig, message, &tree, &parents) - .map_err(|e| GitError::lib("commit", e))?; - - Ok(Hash::from_oid(&commit_oid)) +/// Append a commit to HEAD that deletes `paths`. +/// +/// # Errors +/// Propagates libgit2 errors at any step. +pub fn delete_files( + test: &TestRepo, + message: &str, + time_secs: i64, + paths: &[&str], +) -> Result<Hash> { + let mut b = CommitBuilder::new(test, message, time_secs); + for path in paths { + b = b.delete(path); + } + b.commit() +} + +/// Append a commit to HEAD that moves `from` to `to` without changing the blob +/// (a pure `git mv`). +/// +/// # Errors +/// Propagates libgit2 errors at any step. +pub fn rename_file( + test: &TestRepo, + message: &str, + time_secs: i64, + from: &str, + to: &str, +) -> Result<Hash> { + CommitBuilder::new(test, message, time_secs) + .rename(from, to) + .commit() } fn unique_temp_dir() -> std::path::PathBuf { @@ -143,6 +365,15 @@ mod tests { use crate::helpers::{load_commits, CommitLoadOptions}; use crate::repository::LogOptions; + /// A 100-line blob, large enough for libgit2's rename similarity to + /// recognise an unchanged copy under a new name. + fn hundred_lines() -> Vec<u8> { + (1..=100) + .map(|i| format!("line {i}\n")) + .collect::<String>() + .into_bytes() + } + #[test] fn build_and_walk_repo() { let test = init_repo().expect("init"); @@ -201,6 +432,146 @@ mod tests { assert_eq!(changes[0].to.name, "b.txt"); } + /// `delete_files` removes the path from the tree, and the diff reports it + /// as a Delete carrying the FROM side. + #[test] + fn diff_detects_deleted_file() { + let test = init_repo().expect("init"); + let c1 = commit_files(&test, "1", 1, &[("a.txt", b"a"), ("b.txt", b"b")]).unwrap(); + let c2 = delete_files(&test, "2", 2, &["b.txt"]).unwrap(); + + let t1 = test.repo.lookup_commit(c1).unwrap().tree().unwrap(); + let t2 = test.repo.lookup_commit(c2).unwrap().tree().unwrap(); + let changes = tree_diff(&test.repo, Some(&t1), Some(&t2)).unwrap(); + assert_eq!(changes.len(), 1); + assert_eq!(changes[0].action, ChangeAction::Delete); + assert_eq!(changes[0].from.name, "b.txt"); + assert!(changes[0].to.name.is_empty()); + } + + /// A pure `git mv` is ONE Modify whose from/to names differ and whose blob + /// hash is unchanged — not a Delete + Insert pair (rename detection). + #[test] + fn pure_rename_is_one_modify_with_different_names() { + let test = init_repo().expect("init"); + let body = hundred_lines(); + let c1 = commit_files(&test, "1", 1, &[("a.txt", &body)]).unwrap(); + let c2 = rename_file(&test, "mv", 2, "a.txt", "b.txt").unwrap(); + + let t1 = test.repo.lookup_commit(c1).unwrap().tree().unwrap(); + let t2 = test.repo.lookup_commit(c2).unwrap().tree().unwrap(); + let changes = tree_diff(&test.repo, Some(&t1), Some(&t2)).unwrap(); + assert_eq!( + changes.len(), + 1, + "a rename must be one change, got {changes:?}" + ); + let c = &changes[0]; + assert_eq!(c.action, ChangeAction::Modify); + assert_eq!(c.from.name, "a.txt"); + assert_eq!(c.to.name, "b.txt"); + assert_eq!(c.from.hash, c.to.hash, "a pure rename keeps the blob"); + } + + /// A rename whose content also changed (above the similarity threshold) + /// is still one Modify with differing names. + #[test] + fn edited_rename_is_still_one_modify() { + let test = init_repo().expect("init"); + let body = hundred_lines(); + let c1 = commit_files(&test, "1", 1, &[("a.txt", &body)]).unwrap(); + let mut edited = body.clone(); + edited.extend_from_slice(b"line 101\n"); + let c2 = CommitBuilder::new(&test, "mv+edit", 2) + .delete("a.txt") + .write("b.txt", &edited) + .commit() + .unwrap(); + + let t1 = test.repo.lookup_commit(c1).unwrap().tree().unwrap(); + let t2 = test.repo.lookup_commit(c2).unwrap().tree().unwrap(); + let changes = tree_diff(&test.repo, Some(&t1), Some(&t2)).unwrap(); + assert_eq!(changes.len(), 1, "got {changes:?}"); + assert_eq!(changes[0].action, ChangeAction::Modify); + assert_eq!(changes[0].from.name, "a.txt"); + assert_eq!(changes[0].to.name, "b.txt"); + assert_ne!(changes[0].from.hash, changes[0].to.hash); + } + + /// The builder records distinct authors and skewed committer times, and a + /// side line + merge yields a two-parent commit on HEAD. + #[test] + fn builder_authors_dates_and_merges() { + let test = init_repo().expect("init"); + let c1 = commit_files(&test, "1", 1_000, &[("a.txt", b"a")]).unwrap(); + let side = CommitBuilder::new(&test, "side", 2_000) + .author("Bob", "bob@example.com") + .committer_time(5_000) + .parent(c1) + .detached() + .write("b.txt", b"b") + .commit() + .unwrap(); + assert_eq!(test.repo.head().unwrap(), c1, "detached must not move HEAD"); + let side_commit = test.repo.lookup_commit(side).unwrap(); + assert_eq!(side_commit.author().name, "Bob"); + assert_eq!(side_commit.author().when.seconds(), 2_000); + assert_eq!(side_commit.committer().when.seconds(), 5_000); + + let merge = CommitBuilder::new(&test, "merge", 3_000) + .merge_parent(side) + .write("b.txt", b"b") + .commit() + .unwrap(); + let m = test.repo.lookup_commit(merge).unwrap(); + assert_eq!(m.num_parents(), 2); + assert_eq!(m.parent_hash(0), c1); + assert_eq!(m.parent_hash(1), side); + assert_eq!(test.repo.head().unwrap(), merge); + } + + /// `--since` is an AUTHOR-time filter and must be a SKIP filter: a commit + /// authored after the cutoff but committed before it (so it sorts below + /// younger commits in the committer-time walk) is still included, and a + /// commit authored before the cutoff but committed after it is dropped + /// without ending the walk. + #[test] + fn since_filter_skips_instead_of_stopping_on_skewed_dates() { + let test = init_repo().expect("init"); + const CUTOFF: i64 = 10_000; + // A: authored AFTER the cutoff, committed BEFORE it. + let a = CommitBuilder::new(&test, "a", CUTOFF + 100) + .committer_time(CUTOFF - 900) + .write("a.txt", b"a") + .commit() + .unwrap(); + // B: authored BEFORE the cutoff, committed AFTER it. + let _b = CommitBuilder::new(&test, "b", CUTOFF - 100) + .committer_time(CUTOFF + 100) + .write("b.txt", b"b") + .commit() + .unwrap(); + // C: both after. + let c = commit_files(&test, "c", CUTOFF + 500, &[("c.txt", b"c")]).unwrap(); + + let mut iter = test + .repo + .log(&LogOptions { + since: Some(crate::repository::time_from_unix_secs(CUTOFF)), + ..LogOptions::default() + }) + .unwrap(); + let mut got = Vec::new(); + while let Some(commit) = iter.next_commit() { + got.push(commit.hash()); + } + assert_eq!( + got, + vec![c, a], + "every commit with author time >= cutoff must be kept, newest-first" + ); + } + #[test] fn equal_trees_skip_diff() { let test = init_repo().expect("init"); diff --git a/crates/cf-gitlib/src/worker.rs b/crates/cf-gitlib/src/worker.rs index cc30869b..ec7c9034 100644 --- a/crates/cf-gitlib/src/worker.rs +++ b/crates/cf-gitlib/src/worker.rs @@ -7,7 +7,7 @@ //! the repository and executes each request synchronously by calling git2 //! directly. The numeric outputs (line counts, diff op runs) reproduce the //! reference batch algorithms byte-for-byte, which is what flows into reports -//! (pinned by `tests/compat`). +//! (pinned by the report e2e suites (`bins/codefang/tests`)). //! //! # Diff-op algorithm (reference batch-shim behavior) //! diff --git a/crates/cf-gitlib/tests/native_limits.rs b/crates/cf-gitlib/tests/native_limits.rs new file mode 100644 index 00000000..56a3b5de --- /dev/null +++ b/crates/cf-gitlib/tests/native_limits.rs @@ -0,0 +1,62 @@ +//! libgit2's memory limits are process-global state, so their contract is +//! tested in this file's own process. + +use std::os::raw::c_int; + +/// The mapped-pack limit the test registers: 64 MiB. +const MAPPED_PACK: usize = 64 * 1024 * 1024; +/// The object-cache limit the test registers: 96 MiB. +const OBJECT_CACHE: usize = 96 * 1024 * 1024; + +/// libgit2's current `(mapped-pack limit, object-cache limit)`. +fn read_limits() -> (usize, usize) { + // SAFETY: this test is the process's only thread using libgit2; both + // options only read globals into the out-parameters. + unsafe { + let mapped = git2::opts::get_mwindow_mapped_limit().expect("read the mapped limit"); + let (mut used, mut allowed): (isize, isize) = (0, 0); + let rc = libgit2_sys::git_libgit2_opts( + libgit2_sys::GIT_OPT_GET_CACHED_MEMORY as c_int, + &mut used as *mut isize, + &mut allowed as *mut isize, + ); + assert_eq!(rc, 0, "read the object-cache limit"); + ( + mapped, + usize::try_from(allowed).expect("non-negative limit"), + ) + } +} + +/// Limits registered before the process opens its first repository take +/// effect at that open; limits registered afterwards are refused and change +/// nothing (libgit2 options must not change under live repositories). +#[test] +fn the_native_limits_apply_at_the_first_open_and_only_then() { + let dir = std::env::temp_dir().join(format!("cf-gitlib-native-limits-{}", std::process::id())); + drop(git2::Repository::init(&dir).expect("init a repository")); + let limits = cf_gitlib::NativeLimits { + mapped_pack: MAPPED_PACK, + object_cache: OBJECT_CACHE, + }; + + assert!( + cf_gitlib::limit_native_memory(limits), + "registered before the first open" + ); + let repo = cf_gitlib::Repository::open(dir.to_str().expect("utf-8 path")).expect("open"); + assert_eq!(read_limits(), (MAPPED_PACK, OBJECT_CACHE)); + + let halved = cf_gitlib::NativeLimits { + mapped_pack: MAPPED_PACK / 2, + object_cache: OBJECT_CACHE / 2, + }; + assert!( + !cf_gitlib::limit_native_memory(halved), + "limits after the first open are refused" + ); + assert_eq!(read_limits(), (MAPPED_PACK, OBJECT_CACHE)); + + drop(repo); + std::fs::remove_dir_all(&dir).expect("remove the repository"); +} diff --git a/crates/cf-godiff/src/lib.rs b/crates/cf-godiff/src/lib.rs index 040d4f43..e23628dc 100644 --- a/crates/cf-godiff/src/lib.rs +++ b/crates/cf-godiff/src/lib.rs @@ -16,8 +16,8 @@ //! //! Compatibility: this is a frozen compatibility implementation — the segment //! stream must match the reference diff engine exactly, because the resulting -//! added/removed line counts flow into reports. Output bytes are pinned against -//! the reference binary by `tests/compat`; do not alter the algorithms. +//! added/removed line counts flow into reports. Output bytes are pinned by the +//! report e2e suites (`bins/codefang/tests`); do not alter the algorithms. //! //! # Pipeline notes //! diff --git a/crates/cf-gojson/oracle/go.mod b/crates/cf-gojson/oracle/go.mod deleted file mode 100644 index 6da96dda..00000000 --- a/crates/cf-gojson/oracle/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module cfgojson_oracle - -go 1.25.0 diff --git a/crates/cf-gojson/oracle/main.go b/crates/cf-gojson/oracle/main.go deleted file mode 100644 index 29173c57..00000000 --- a/crates/cf-gojson/oracle/main.go +++ /dev/null @@ -1,225 +0,0 @@ -// Command cfgojson_oracle emits Go-canonical golden fixtures that the Rust -// cf-gojson crate asserts byte-equality against. -// -// It has two modes: -// -// go run . floats -> tab-separated "<bits-hex>\t<g-format>" lines on stdout -// go run . json -> emits json.Marshal / Encoder fixtures as JSON records -// -// The float mode prints, for each f64 in a fixed adversarial corpus, the exact -// bytes of strconv.FormatFloat(f, 'g', -1, 64). The Rust test reconstructs the -// f64 from the bit pattern and compares its ftoa output. -// -// The json mode prints, for each named case, the exact bytes of json.Marshal -// (compact) and an indented json.Encoder (SetIndent(""," ")) so the Rust -// marshal / marshal_indent can be compared byte-for-byte. -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "math" - "os" - "strconv" -) - -func main() { - if len(os.Args) < 2 { - fmt.Fprintln(os.Stderr, "usage: oracle <floats|json>") - os.Exit(2) - } - switch os.Args[1] { - case "floats": - emitFloats() - case "jsonfloats": - emitJSONFloats() - case "json": - emitJSON() - default: - fmt.Fprintln(os.Stderr, "unknown mode:", os.Args[1]) - os.Exit(2) - } -} - -// floatCorpus returns the adversarial f64 set: every value where Go's 'g' -// rendering is known to diverge from Rust's Display, plus structural edges. -func floatCorpus() []float64 { - vals := []float64{ - 0.0, - math.Copysign(0, -1), // -0.0 - 1.0, -1.0, - 0.5, -0.5, - 3.14, -3.14, - 2.0, 10.0, 100.0, - // exponent threshold edges (>=21 -> 'e', <-4 -> 'e') - 1e20, 1e21, 1e22, -1e21, - 1.5e20, 1.5e21, - 1e-4, 1e-5, 1e-6, -1e-5, - 0.0001, 0.00001, - 1e5, 1e6, 1e7, 1e15, 1e16, 1e17, - 123456789012345680000.0, - 1.2345678901234568e+20, - // govader compound score range: x/sqrt(x*x+15) - 0.4404, -0.4404, 0.6249, -0.5719, 0.34, 0.8316, - // many small decimals - 0.1, 0.2, 0.3, 0.7, 0.123456789, - 1.0 / 3.0, 2.0 / 3.0, - // subnormals & extremes - math.SmallestNonzeroFloat64, - math.MaxFloat64, - -math.MaxFloat64, - 2.2250738585072014e-308, // min normal - // integers that are large - 9007199254740992.0, // 2^53 - 9007199254740993.0, // 2^53+1 (not representable -> rounds) - 18014398509481984.0, // 2^54 - // percentages * 100 forms - 33.33333333333333, 66.66666666666666, - 12.5, 87.5, 0.125, - // negatives near edges - -0.0001, -1e16, - // values producing trailing-zero stripping - 1.10, 1.20, 100.10, 0.250, - // stat outputs - 1.4142135623730951, 2.718281828459045, 3.141592653589793, - 1.959963984540054, // z 97.5% - // zscore sentinel - 100.0, -100.0, - } - // Add a deterministic sweep so the corpus is large without RNG seeds. - for i := -330; i <= 330; i++ { - vals = append(vals, math.Pow(10, float64(i)/7.0)) - } - for n := 1; n <= 2000; n++ { - // rational-ish values that stress shortest round-trip. - vals = append(vals, float64(n)/7.0) - vals = append(vals, float64(n)*1.0e-3) - vals = append(vals, float64(n)*1.0e6) - } - return vals -} - -func emitFloats() { - w := os.Stdout - for _, f := range floatCorpus() { - bits := math.Float64bits(f) - s := strconv.FormatFloat(f, 'g', -1, 64) - fmt.Fprintf(w, "%016x\t%s\n", bits, s) - } -} - -// emitJSONFloats prints, for every corpus value, the exact bytes that Go's -// encoding/json produces for that float as a top-level JSON number. This is the -// authoritative target for cf-gojson's GoValue::Float encoding — it differs -// from strconv 'g' in exponent rendering (json strips the leading exponent -// zero: e-05 -> e-5) and in the 'e'-vs-'f' threshold (json uses -// abs < 1e-6 || abs >= 1e21, computed on the decimal exponent). -func emitJSONFloats() { - w := os.Stdout - for _, f := range floatCorpus() { - // json.Marshal errors on NaN/Inf; the corpus contains neither. - b, err := json.Marshal(f) - if err != nil { - panic(err) - } - bits := math.Float64bits(f) - fmt.Fprintf(w, "%016x\t%s\n", bits, string(b)) - } -} - -// jsonCase is one logical value plus its Go-canonical encodings. -type jsonCase struct { - Name string `json:"name"` - Compact string `json:"compact"` // json.Marshal(value) - Indent string `json:"indent"` // Encoder with SetIndent(""," "), trailing \n trimmed - Value json.RawMessage `json:"-"` -} - -func encodeIndent(v any) string { - var buf bytes.Buffer - enc := json.NewEncoder(&buf) - enc.SetIndent("", " ") - if err := enc.Encode(v); err != nil { - panic(err) - } - // Encoder.Encode appends exactly one trailing newline; callers add \n - // themselves, so the Rust marshal_indent emits none. Trim it for parity. - out := buf.Bytes() - if len(out) > 0 && out[len(out)-1] == '\n' { - out = out[:len(out)-1] - } - return string(out) -} - -func marshal(v any) string { - b, err := json.Marshal(v) - if err != nil { - panic(err) - } - return string(b) -} - -func emitJSON() { - // ordered map type that preserves Go map encoding (keys sorted) — plain - // map[string]any already sorts keys in encoding/json. - cases := []struct { - name string - v any - }{ - {"null", nil}, - {"bool_true", true}, - {"bool_false", false}, - {"int", 42}, - {"int_neg", -7}, - {"int_zero", 0}, - {"float_int_valued", 2.0}, - {"float_frac", 3.14}, - {"float_exp_big", 1e21}, - {"float_exp_small", 1e-5}, - {"string_plain", "hello"}, - {"string_html", "a<b>c&d"}, - {"string_unicode_sep", "x
y
z"}, - {"string_quotes", "she said \"hi\"\tand\nleft"}, - {"string_backslash", "a\\b/c"}, - {"string_control", "\x00\x01\x1f\x7f"}, - {"string_emoji", "go🚀rust"}, - {"empty_array", []any{}}, - {"empty_object", map[string]any{}}, - {"array_mixed", []any{1, "two", 3.5, true, nil}}, - {"nested_array", []any{[]any{1, 2}, []any{}, []any{"a"}}}, - {"map_unsorted", map[string]any{"zebra": 1, "apple": 2, "Mango": 3, "_x": 4}}, - {"map_byte_order", map[string]any{"Z": 1, "a": 2, "A": 3, "z": 4, "[": 5}}, - {"map_html_keys", map[string]any{"<k>": "<v>", "a&b": "c&d"}}, - {"map_nested", map[string]any{ - "b": map[string]any{"y": 2, "x": 1}, - "a": []any{map[string]any{"k": "v"}}, - }}, - {"map_floats", map[string]any{"pi": 3.141592653589793, "big": 1e21, "tiny": 1e-5, "neg": -0.5}}, - {"deep", map[string]any{ - "score": 0.875, - "name": "test<x>", - "items": []any{}, - "counts": map[string]any{"go": 3, "rust": 1}, - "nested": map[string]any{"deep": map[string]any{"deeper": []any{1, 2, 3}}}, - "unicode": "line1
line2", - }}, - {"all_escapes", "\b\f\n\r\t\"\\<>&

"}, - {"string_with_slash_only", "https://example.com/path?x=1&y=2"}, - } - - out := make([]jsonCase, 0, len(cases)) - for _, c := range cases { - out = append(out, jsonCase{ - Name: c.name, - Compact: marshal(c.v), - Indent: encodeIndent(c.v), - }) - } - b, err := json.MarshalIndent(out, "", " ") - if err != nil { - panic(err) - } - os.Stdout.Write(b) - os.Stdout.Write([]byte("\n")) -} diff --git a/crates/cf-gojson/oracle/oracle_err.txt b/crates/cf-gojson/oracle/oracle_err.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/cf-gojson/src/lib.rs b/crates/cf-gojson/src/lib.rs index ae550dc5..c806167a 100644 --- a/crates/cf-gojson/src/lib.rs +++ b/crates/cf-gojson/src/lib.rs @@ -9,7 +9,7 @@ //! strategy. //! //! Compatibility: output bytes are pinned against the reference implementation -//! by the differential gate in `tests/compat`. +//! by the report e2e suites (`bins/codefang/tests`). //! //! # Modules //! diff --git a/crates/cf-gojson/src/marshal.rs b/crates/cf-gojson/src/marshal.rs index 361e10b8..1d506b79 100644 --- a/crates/cf-gojson/src/marshal.rs +++ b/crates/cf-gojson/src/marshal.rs @@ -2,7 +2,7 @@ //! //! This is the keystone the whole report layer routes through. Its encoding //! rules are a frozen byte contract, pinned against the reference -//! implementation by `tests/compat` (see `specs/rust-rewrite/DESIGN.md` +//! implementation by the report e2e suites (`bins/codefang/tests`) (see `specs/rust-rewrite/DESIGN.md` //! §2): //! //! * **HTML escaping ON** by default — `<`, `>`, `&` encode as `\u003c`, diff --git a/crates/cf-gojson/src/value.rs b/crates/cf-gojson/src/value.rs index 1bf61142..f5168e6d 100644 --- a/crates/cf-gojson/src/value.rs +++ b/crates/cf-gojson/src/value.rs @@ -8,7 +8,7 @@ //! # The dual-mode container is load-bearing //! //! The report format has two object-ordering rules, decided by the *origin* of -//! the JSON object (report-format contract; pinned by `tests/compat`): +//! the JSON object (report-format contract; pinned by the report e2e suites (`bins/codefang/tests`)): //! //! * a **struct-origin** object serializes its fields in **declaration //! order** (each field pushed once, in order; empty fields may be skipped by diff --git a/crates/cf-gojson/tests/oracle_data/_g_vs_json_diffs.txt b/crates/cf-gojson/tests/oracle_data/_g_vs_json_diffs.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/cf-gojson/tests/oracle_data/_zz_diffs.txt b/crates/cf-gojson/tests/oracle_data/_zz_diffs.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/cf-gojson/tests/oracle_data/floats.tsv b/crates/cf-gojson/tests/oracle_data/floats.tsv deleted file mode 100644 index d916c83b..00000000 --- a/crates/cf-gojson/tests/oracle_data/floats.tsv +++ /dev/null @@ -1,6729 +0,0 @@ -0000000000000000 0 -8000000000000000 -0 -3ff0000000000000 1 -bff0000000000000 -1 -3fe0000000000000 0.5 -bfe0000000000000 -0.5 -40091eb851eb851f 3.14 -c0091eb851eb851f -3.14 -4000000000000000 2 -4024000000000000 10 -4059000000000000 100 -4415af1d78b58c40 1e+20 -444b1ae4d6e2ef50 1e+21 -4480f0cf064dd592 1e+22 -c44b1ae4d6e2ef50 -1e+21 -442043561a882930 1.5e+20 -4454542ba12a337c 1.5e+21 -3f1a36e2eb1c432d 0.0001 -3ee4f8b588e368f1 1e-05 -3eb0c6f7a0b5ed8d 1e-06 -bee4f8b588e368f1 -1e-05 -3f1a36e2eb1c432d 0.0001 -3ee4f8b588e368f1 1e-05 -40f86a0000000000 100000 -412e848000000000 1e+06 -416312d000000000 1e+07 -430c6bf526340000 1e+15 -4341c37937e08000 1e+16 -4376345785d8a000 1e+17 -441ac53a7e04bcda 1.2345678901234568e+20 -441ac53a7e04bcda 1.2345678901234568e+20 -3fdc2f837b4a233a 0.4404 -bfdc2f837b4a233a -0.4404 -3fe3ff2e48e8a71e 0.6249 -bfe24d013a92a305 -0.5719 -3fd5c28f5c28f5c3 0.34 -3fea9c779a6b50b1 0.8316 -3fb999999999999a 0.1 -3fc999999999999a 0.2 -3fd3333333333333 0.3 -3fe6666666666666 0.7 -3fbf9add3739635f 0.123456789 -3fd5555555555555 0.3333333333333333 -3fe5555555555555 0.6666666666666666 -0000000000000001 5e-324 -7fefffffffffffff 1.7976931348623157e+308 -ffefffffffffffff -1.7976931348623157e+308 -0010000000000000 2.2250738585072014e-308 -4340000000000000 9.007199254740992e+15 -4340000000000000 9.007199254740992e+15 -4350000000000000 1.8014398509481984e+16 -4040aaaaaaaaaaaa 33.33333333333333 -4050aaaaaaaaaaaa 66.66666666666666 -4029000000000000 12.5 -4055e00000000000 87.5 -3fc0000000000000 0.125 -bf1a36e2eb1c432d -0.0001 -c341c37937e08000 -1e+16 -3ff199999999999a 1.1 -3ff3333333333333 1.2 -4059066666666666 100.1 -3fd0000000000000 0.25 -3ff6a09e667f3bcd 1.4142135623730951 -4005bf0a8b145769 2.718281828459045 -400921fb54442d18 3.141592653589793 -3fff5c0331eeff84 1.959963984540054 -4059000000000000 100 -c059000000000000 -100 -36250953db1a9192 7.196856730011471e-48 -362d3ae36d13bbce 1e-47 -36344eb847de6aa4 1.3894954943731472e-47 -363c379909a4cd2d 1.930697728883246e-47 -36439a940b805332 2.682695795279738e-47 -364b3d4abc5d06c6 3.727593720314922e-47 -3652ecadc9ff7bf5 5.179474679231223e-47 -365a4ba8d1e135f5 7.1968567300114695e-47 -366244ce242c5561 1e-46 -3669626659d6054c 1.389495494373147e-46 -3671a2bfa607003b 1.9306977288832456e-46 -367881390e6067ff 2.6826957952797384e-46 -3681064eb5ba243c 3.7275937203149224e-46 -3687a7d93c7f5af1 5.179474679231222e-46 -36906f49832cc1b9 7.19685673001147e-46 -3696d601ad376ab9 1e-45 -369fbafff04b86a1 1.3894954943731474e-45 -36a60b6f8f88c04a 1.9306977288832456e-45 -36aea18751f881ff 2.6826957952797385e-45 -36b547e26328ad4b 3.7275937203149224e-45 -36bd91cf8b9f31af 5.1794746792312233e-45 -36c48b1be3f7f227 7.196856730011469e-45 -36cc8b8218854567 1e-44 -36d3d4dff62f3425 1.3894954943731475e-44 -36db8e4b736af05d 1.9306977288832458e-44 -36e324f4933b513f 2.6826957952797383e-44 -36ea99dafbf2d89d 3.727593720314922e-44 -36f27b21b7437f0d 5.179474679231223e-44 -36f9ade2dcf5eeb1 7.196856730011469e-44 -3701d7314f534b61 1e-43 -3708ca17f3bb012d 1.3894954943731472e-43 -371138ef2822d639 1.9306977288832453e-43 -3717ee31b80a258f 2.6826957952797384e-43 -3720a028dd77c762 3.727593720314922e-43 -372719ea25145ed1 5.1794746792312235e-43 -37300cadca19b52f 7.19685673001147e-43 -37364cfda3281e38 9.999999999999999e-43 -373efc9df0a9c178 1.3894954943731472e-42 -3745872af22b8bc8 1.9306977288832456e-42 -374de9be260caef2 2.682695795279738e-42 -3754c83314d5b93b 3.727593720314922e-42 -375ce064ae597684 5.179474679231223e-42 -37640fd93ca0227b 7.19685673001147e-42 -376be03d0bf225c7 1e-41 -37735de2b66a18ec 1.3894954943731474e-41 -377ae8f5aeb66eba 1.9306977288832456e-41 -3782b216d7c7ed58 2.6826957952797385e-41 -3789fa3fda0b278b 3.727593720314923e-41 -37920c3eecf7ea13 5.179474679231223e-41 -379913cf8bc82b19 7.196856730011469e-41 -37a16c262777579c 1e-40 -37a8355b64049f27 1.3894954943731474e-40 -37b0d1998d320535 1.930697728883246e-40 -37b75e9c8db9e8ae 2.6826957952797385e-40 -37c03c67e846f8b6 3.727593720314922e-40 -37c68f4ea835e498 5.1794746792312235e-40 -37cf58c36eba35e0 7.19685673001147e-40 -37d5c72fb1552d83 1e-39 -37de42b23d05c6f0 1.3894954943731473e-39 -37e505fff07e8682 1.9306977288832458e-39 -37ed3643b12862d9 2.6826957952797384e-39 -37f44b81e258b6e4 3.7275937203149225e-39 -37fc332252435dbe 5.1794746792312235e-39 -3803977a253461ab 7.196856730011469e-39 -380b38fb9daa78e4 1e-38 -3812e9af66239c56 1.3894954943731473e-38 -381a477fec9e2821 1.9306977288832454e-38 -382241ea4eb93dc8 2.6826957952797386e-38 -38295e625aeee49d 3.7275937203149225e-38 -38319ff5736a1a96 5.179474679231223e-38 -38387d58ae817a16 7.196856730011469e-38 -3841039d428a8b8e 9.999999999999999e-38 -3847a41b3fac836c 1.3894954943731474e-37 -38506caff3e2d915 1.9306977288832456e-37 -3856d264e2678d3a 2.6826957952797386e-37 -385fb5faf1aa9dc4 3.7275937203149224e-37 -386607f2d044a13c 5.179474679231223e-37 -386e9caeda21d89b 7.1968567300114686e-37 -38754484932d2e72 1e-36 -387d8d220f97a447 1.3894954943731474e-36 -388487dbf0db8f5b 1.9306977288832458e-36 -388c86fe1b017087 2.682695795279738e-36 -3893d1bcd70aa29b 3.727593720314923e-36 -389b89ef8455c98b 5.179474679231223e-36 -38a321ed48552762 7.19685673001147e-36 -38aa95a5b7f87a0f 1e-35 -38b2783549bec6ac 1.3894954943731473e-35 -38b9a9d2ed127330 1.9306977288832453e-35 -38c1d45ed0e0e655 2.6826957952797384e-35 -38c8c62c0ccd4b40 3.727593720314922e-35 -38d13635b2b59df7 5.179474679231223e-35 -38d7ea689a6a713a 7.19685673001147e-35 -38e09d8792fb4c49 1e-34 -38e716429c2e7856 1.389495494373147e-34 -38f00a23d42b87ff 1.9306977288832458e-34 -38f6497685191fea 2.6826957952797383e-34 -38fef7b710009e11 3.7275937203149222e-34 -390583c31f630575 5.1794746792312234e-34 -390de502c1050d88 7.196856730011469e-34 -3914c4e977ba1f5b 9.999999999999999e-34 -391cdbd3433a166d 1.3894954943731473e-33 -39240cacc93669fe 1.9306977288832455e-33 -392bdbd4265f67e6 2.6826957952797388e-33 -39335ad26a0062cb 3.7275937203149225e-33 -393ae4b3e73bc6d1 5.1794746792312226e-33 -3942af21b8a32875 7.196856730011469e-33 -3949f623d5a8a732 9.999999999999999e-33 -395209640a044ddb 1.389495494373136e-32 -39590fd7fb84047e 1.9306977288832456e-32 -3961696497fba0c8 2.682695795279717e-32 -3968318704807bb5 3.7275937203149527e-32 -3970cef070855c44 5.179474679231224e-32 -39775aea26cbf2c8 7.196856730011528e-32 -398039d665896880 1e-31 -39868bbd0c856152 1.3894954943731361e-31 -398f53cdfa65059f 1.930697728883246e-31 -3995c3bdbdfa88fb 2.6826957952797175e-31 -399e3de8c5a09aa0 3.7275937203149517e-31 -39a502ac8ca6b354 5.179474679231223e-31 -39ad31a4b07eef7a 7.196856730011528e-31 -39b4484bfeebc29f 9.999999999999999e-31 -39bc2eac4fa6b9a6 1.389495494373136e-30 -39c39460bc7f2382 1.9306977288832455e-30 -39cb34ad2d792b38 2.682695795279717e-30 -39d2e6b17b8460a5 3.7275937203149524e-30 -39da4357afd06029 5.179474679231223e-30 -39e23f06ee4f55ac 7.196856730011528e-30 -39e95a5efea6b348 1.0000000000000001e-29 -39f19d2bb1c83408 1.389495494373136e-29 -39f87978eb9eec64 1.930697728883246e-29 -3a0100ec3c6bbb03 2.682695795279717e-29 -3a07a05dda6578cf 3.727593720314953e-29 -3a106a16cde23c1a 5.179474679231224e-29 -3a16cec8a9e32b17 7.196856730011528e-29 -3a1fb0f6be50601a 1.0000000000000001e-28 -3a2604769e3a410a 1.3894954943731361e-28 -3a2e97d72686a77d 1.930697728883246e-28 -3a3541274b86a9c4 2.682695795279717e-28 -3a3d887550fed701 3.727593720314952e-28 -3a44849c815acb20 5.179474679231223e-28 -3a4c827ad45bf5de 7.196856730011529e-28 -3a53ce9a36f23c10 1e-27 -3a5b859445c8d14c 1.389495494373136e-27 -3a631ee6781428ae 1.930697728883246e-27 -3a6a91711e685435 2.682695795279717e-27 -3a727549529f4661 3.727593720314952e-27 -3a79a5c3a1b17de9 5.179474679231224e-27 -3a81d18cc4b979aa 7.196856730011528e-27 -3a88c240c4aecb13 9.999999999999999e-27 -3a91337cab9d82d0 1.3894954943731362e-26 -3a97e6a0161932d9 1.9306977288832457e-26 -3aa09ae6b30134a1 2.682695795279717e-26 -3aa7129ba74717f9 3.727593720314952e-26 -3ab0079a450eeeb1 5.179474679231223e-26 -3ab645eff5e7d814 7.196856730011527e-26 -3abef2d0f5da7dd8 9.999999999999999e-26 -3ac5805bd684e384 1.3894954943731362e-25 -3acde0481b9f7f8e 1.9306977288832454e-25 -3ad4c1a05fc181ca 2.6826957952797173e-25 -3adcd7429118ddf7 3.727593720314952e-25 -3ae40980d652aa5d 5.179474679231223e-25 -3aebd76bf361ce1a 7.196856730011528e-25 -3af357c299a88ea8 1.0000000000000001e-24 -3afae072cc261c63 1.3894954943731358e-24 -3b02ac2d1143afba 1.930697728883246e-24 -3b09f20877b1e23c 2.682695795279717e-24 -3b1206899aaf8abb 3.7275937203149525e-24 -3b190be10be754f5 5.1794746792312235e-24 -3b2166a3781d20d0 7.196856730011528e-24 -3b282db34012b252 1.0000000000000001e-23 -3b30cc47bf97d1bf 1.389495494373136e-23 -3b37573855949ba8 1.9306977288832457e-23 -3b4037454acf2d66 2.6826957952797174e-23 -3b46882c015b6d6a 3.7275937203149527e-23 -3b4f4ed94ee12a31 5.179474679231223e-23 -3b55c04c56246905 7.196856730011529e-23 -3b5e392010175ee6 1e-22 -3b64ff59af7dc62e 1.389495494373136e-22 -3b6d2d066af9c291 1.9306977288832455e-22 -3b7445169d82f8bf 2.682695795279717e-22 -3b7c2a3701b248c5 3.727593720314953e-22 -3b839147d14cba5f 5.179474679231223e-22 -3b8b305f6bad8344 7.196856730011527e-22 -3b92e3b40a0e9b4f 1e-21 -3b9a3f301b5d37ba 1.389495494373136e-21 -3ba23c2402dc199b 1.9306977288832457e-21 -3ba9565c44e3b6ee 2.682695795279717e-21 -3bb19a62610f6d7a 3.727593720314952e-21 -3bb87599c59fe8f8 5.179474679231224e-21 -3bc0fe3ba34c720b 7.196856730011527e-21 -3bc79ca10c924223 1e-20 -3bd0677e111a42d4 1.389495494373136e-20 -3bd6cb2d03932003 1.930697728883246e-20 -3bdfabf3561ca4ab 2.6826957952797173e-20 -3be600faf95348d9 3.7275937203149524e-20 -3bee93003707e335 5.1794746792312235e-20 -3bf53dca8c1f8e8e 7.196856730011528e-20 -3bfd83c94fb6d2ac 1e-19 -3c04815d9560d389 1.389495494373136e-19 -3c0c7df84477e803 1.9306977288832458e-19 -3c13cb7815d1e6ea 2.682695795279717e-19 -3c1b8139b7a81b10 3.7275937203149527e-19 -3c231be02264ee01 5.179474679231223e-19 -3c2a8d3d2f277232 7.196856730011528e-19 -3c32725dd1d243ac 1e-18 -3c39a1b4fab9086c 1.3894954943731361e-18 -3c41cebb2acaf102 1.930697728883246e-18 -3c48be561b4660a5 2.682695795279717e-18 -3c5130c412c910ea 3.727593720314953e-18 -3c57e2d82afe2981 5.179474679231223e-18 -3c6098463d78a75f 7.196856730011528e-18 -3c670ef54646d497 1e-17 -3c7005111cb3a543 1.389495494373136e-17 -3c764269f57dad42 1.9306977288832457e-17 -3c7eedeba217f8cf 2.6826957952797173e-17 -3c857cf5177b5523 3.727593720314952e-17 -3c8ddb8e35bdb3e2 5.1794746792312236e-17 -3c94be57ccd6d137 7.196856730011528e-17 -3c9cd2b297d889bc 1e-16 -3ca4065563e08e94 1.389495494373136e-16 -3cabd30472dd18b2 1.9306977288832535e-16 -3cb354b3454efb96 2.6826957952797275e-16 -3cbadc325d5a2a4f 3.7275937203149376e-16 -3cc2a938e1969058 5.179474679231203e-16 -3cc9ededc00c8584 7.196856730011527e-16 -3cd203af9ee75616 1e-15 -3cd907eabcd8b239 1.389495494373136e-15 -3ce163e2c7ca2f70 1.9306977288832538e-15 -3ce829e016a2ba7b 2.6826957952797273e-15 -3cf0c99f7a585a71 3.727593720314937e-15 -3cf7538719fc346d 5.179474679231202e-15 -3d0034b49807d374 7.19685673001153e-15 -3d06849b86a12b9b 1e-14 -3d0f49e56c0edec8 1.3894954943731362e-14 -3d15bcdb79bcbb4c 1.9306977288832538e-14 -3d1e34581c4b691b 2.6826957952797277e-14 -3d24fc0758ee710e 3.727593720314938e-14 -3d2d2868e07b4189 5.1794746792312023e-14 -3d3441e1be09c84f 7.196856730011527e-14 -3d3c25c268497682 1e-13 -3d438e2f63894b3d 1.3894954943731362e-13 -3d4b2c12582bea1e 1.9306977288832536e-13 -3d52e0b711af21af 2.6826957952797267e-13 -3d5a3b092f2a0d50 3.727593720314937e-13 -3d6239418c4d08f6 5.179474679231203e-13 -3d69525a2d8c3a63 7.196856730011527e-13 -3d719799812dea11 1e-12 -3d7871bb3c6b9e0c 1.3894954943731361e-12 -3d80fb8b771b7253 1.9306977288832537e-12 -3d8798e4d61aea1d 2.6826957952797276e-12 -3d9064e5bd7a4853 3.727593720314938e-12 -3d96c791ef604b33 5.179474679231202e-12 -3d9fa6f0b8ef48fd 7.1968567300115285e-12 -3da5fd7fe1796495 1e-11 -3dae8e2a0b86858f 1.3894954943731361e-11 -3db53a6e54e24ee8 1.9306977288832537e-11 -3dbd7f1e0ba1a4a4 2.6826957952797275e-11 -3dc47e1f2cd8da67 3.7275937203149374e-11 -3dcc79766b385e00 5.1794746792312025e-11 -3dd3c85673958d9e 7.196856730011528e-11 -3ddb7cdfd9d7bdbb 1e-10 -3de318da47341379 1.389495494373136e-10 -3dea8909ea1ae2a1 1.9306977288832535e-10 -3df26f72c74506e7 2.682695795279728e-10 -3df99da6f80f1102 3.727593720314938e-10 -3e01cbea03033ac0 5.179474679231202e-10 -3e08ba6c107af104 7.196856730011527e-10 -3e112e0be826d695 1e-09 -3e17df10d9011858 1.3894954943731362e-09 -3e2095a63250cda5 1.9306977288832536e-09 -3e270b4f791648a0 2.6826957952797275e-09 -3e3002885b096aa1 3.727593720314938e-09 -3e363ee483c4096f 5.179474679231202e-09 -3e3ee9071499ad46 7.196856730011528e-09 -3e45798ee2308c3a 1e-08 -3e4dd6d50f415e7e 1.3894954943731388e-08 -3e54bb0fbee50103 1.93069772888325e-08 -3e5cce23575bdac8 2.6826957952797275e-08 -3e64032a71cbc549 3.7275937203149377e-08 -3e6bce9da4b50bda 5.179474679231212e-08 -3e7351a46ce00c42 7.196856730011515e-08 -3e7ad7f29abcaf48 1e-07 -3e82a6452988db0f 1.389495494373139e-07 -3e89e9d3ae9e4144 1.93069772888325e-07 -3e9200d6169968bc 2.682695795279727e-07 -3e9903f50e3eb69b 3.7275937203149375e-07 -3ea1612286f12768 5.179474679231211e-07 -3ea8260d88180f51 7.196856730011513e-07 -3eb0c6f7a0b5ed8d 1e-06 -3eb74fd673eb11d2 1.3894954943731387e-06 -3ec032244d22e8ca 1.9306977288832498e-06 -3ec6810b9c3fc2ed 2.682695795279728e-06 -3ecf44f251ce6443 3.727593720314938e-06 -3ed5b96b28ad7143 5.179474679231212e-06 -3ede2f90ea1e1326 7.196856730011514e-06 -3ee4f8b588e368f1 1e-05 -3eed23cc10e5d647 1.3894954943731388e-05 -3ef43ead606ba2fd 1.93069772888325e-05 -3efc214e834fb3a6 2.682695795279727e-05 -3f038b177320fea9 3.7275937203149375e-05 -3f0b27c5f2d8cd93 5.179474679231212e-05 -3f12ddba9252cbf8 7.196856730011514e-05 -3f1a36e2eb1c432d 0.0001 -3f22365f8a8fa5e8 0.00013894954943731376 -3f294e58b8868bbc 0.00019306977288832499 -3f3194d11211d043 0.00026826957952797245 -3f386ddd4fe93e5c 0.0003727593720314942 -3f40f8dbb7c7807c 0.0005179474679231212 -3f47952936e77efc 0.0007196856730011521 -3f50624dd2f1a9fc 0.001 -3f56c3f76d338f62 0.0013894954943731376 -3f5fa1eee6a82eab 0.0019306977288832499 -3f65fa0556964455 0.002682695795279725 -3f6e8954a3e38df3 0.003727593720314942 -3f753712a5b9609b 0.005179474679231212 -3f7d7a7384a15ebb 0.007196856730011521 -3f847ae147ae147b 0.01 -3f8c74f54880733a 0.013894954943731375 -3f93c53550291d2d 0.019306977288832506 -3f9b7886ac3bd56d 0.02682695795279726 -3fa315d4e66e38b3 0.03727593720314939 -3faa84d74f27b8bf 0.0517947467923121 -3fb26c8832e4db35 0.07196856730011521 -3fb999999999999a 0.1 -3fc1c9194d504805 0.13894954943731377 -3fc8b682a4336477 0.19306977288832503 -3fd12b542ba56565 0.26826957952797265 -3fd7db4a2009c6e1 0.37275937203149395 -3fe093069178d379 0.5179474679231212 -3fe707aa3f9e1203 0.7196856730011522 -3ff0000000000000 1 -3ff63b5fa0a45a05 1.3894954943731375 -3ffee4234d403d94 1.93069772888325 -40057629368ebebe 2.6826957952797263 -400dd21ca80c3899 3.7275937203149394 -4014b7c835d70856 5.179474679231211 -401cc994cf859681 7.196856730011519 -4024000000000000 10 -402bca3788cd7086 13.894954943731374 -40334e961048267e 19.306977288832506 -403ad3b384326e6e 26.826957952797265 -4042a351e907a360 37.2759372031494 -4049e5ba434cca6a 51.7947467923121 -4051fdfd01b37e12 71.96856730011521 -4059000000000000 100 -40615e62b5806654 138.94954943731375 -4068223b945a301b 193.069772888325 -4070c450329f8501 268.26957952797244 -40774c2663498c3b 372.75937203149414 -40802f946a0ffe84 517.9474679231212 -40867d7c42205d96 719.685673001152 -408f400000000000 1000 -4095b5fb62e07fe9 1389.4954943731375 -409e2aca7970bc22 1930.69772888325 -40a4f5643f476641 2682.6957952797243 -40ad1f2ffc1bef4b 3727.593720314942 -40b43b798493fe25 5179.474679231212 -40bc1cdb52a874fb 7196.85673001152 -40c3880000000000 10000 -40cb237a3b989fea 13894.954943731387 -40d2dabe8be67595 19306.9772888325 -40da32bd4f193fdb 26826.95795279728 -40e2337dfd91758a 37275.937203149384 -40e94a57e5b8fdae 51794.746792312115 -40f1920913a94919 71968.56730011514 -40f86a0000000000 100000 -4100f62c653f63f3 138949.5494373139 -4107916e2ee012fa 193069.77288832498 -41105fb6516fc7e8 268269.57952797273 -4116c05d7cf5d2eb 372759.37203149375 -411f9ceddf273d1a 517947.4679231212 -4125f68b58939b5f 719685.6730011514 -412e848000000000 1e+06 -413533b77e8f3cf0 1.389495494373139e+06 -413d75c9ba9817b9 1.93069772888325e+06 -414477a3e5cbb9e3 2.682695795279728e+06 -414c7074dc3347a7 3.727593720314938e+06 -4153c214ab788630 5.179474679231212e+06 -415b742e2eb88236 7.196856730011513e+06 -416312d000000000 1e+07 -416a80a55e330c2b 1.3894954943731388e+07 -4172699e149f0ed4 1.93069772888325e+07 -4179958cdf3ea85b 2.6826957952797275e+07 -4181c64909a00cc8 3.727593720314938e+07 -4188b299d656a7bc 5.1794746792312115e+07 -4191289cdd335162 7.196856730011514e+07 -4197d78400000000 1e+08 -41a090675adfe792 1.3894954943731362e+08 -41a7040599c6d295 1.9306977288832536e+08 -41affaf0170e5272 2.6826957952797276e+08 -41b637db4c080ffa 3.727593720314938e+08 -41bedf404bec519b 5.179474679231202e+08 -41c572c4148025c6 7.196856730011528e+08 -41cdcd6500000000 1e+09 -41d4b4813197e177 1.3894954943731363e+09 -41dcc5070038873a 1.9306977288832536e+09 -41e3fcd60e68f387 2.6826957952797275e+09 -41ebc5d21f0a13f8 3.7275937203149376e+09 -41f34b882f73b301 5.179474679231202e+09 -41facf7519a02f38 7.196856730011528e+09 -4202a05f20000000 1e+10 -4209e1a17dfdd9d4 1.3894954943731361e+10 -4211fb2460235484 1.9306977288832535e+10 -4218fc0b9203306a 2.682695795279728e+10 -42215ba353664c7b 3.7275937203149376e+10 -42281e6a3b509fc0 5.179474679231201e+10 -4230c1a930041d83 7.196856730011528e+10 -42374876e8000000 1e+11 -42402d04eebea824 1.389495494373136e+11 -424679ed782c29a5 1.9306977288832535e+11 -424f3b0e7683fc83 2.6826957952797275e+11 -4255b28c283fdf9a 3.727593720314938e+11 -425e2604ca24c7b1 5.179474679231202e+11 -4264f2137c0524e4 7.196856730011528e+11 -426d1a94a2000000 1e+12 -427438462a6e522e 1.3894954943731362e+12 -427c1868d637340e 1.9306977288832534e+12 -428384e90a127dd3 2.682695795279728e+12 -428b1f2f324fd782 3.7275937203149385e+12 -4292d7c2fe56fccf 5.179474679231202e+12 -429a2e985b066e1c 7.196856730011527e+12 -42a2309ce5400000 1e+13 -42a94657b509e6ba 1.3894954943731363e+13 -42b18f4185e28089 1.9306977288832535e+13 -42b866234c971d46 2.6826957952797273e+13 -42c0f37d7f71e6b0 3.7275937203149375e+13 -42c78db3bdecbc02 5.1794746792312016e+13 -42d05d1f38e404d2 7.196856730011528e+13 -42d6bcc41e900000 1e+14 -42df97eda24c6066 1.389495494373136e+14 -42e5f311e75b20ac 1.9306977288832538e+14 -42ee7fac1fbce499 2.6826957952797278e+14 -42f5305cdf4e605d 3.727593720314938e+14 -42fd7120ad67eb02 5.179474679231201e+14 -43047467071d0607 7.196856730011529e+14 -430c6bf526340000 1e+15 -4313bef4856fbc41 1.3894954943731362e+15 -431b6fd66131e8d6 1.9306977288832535e+15 -43230fcb93d60edf 2.6826957952797275e+15 -432a7c741721f874 3.727593720314938e+15 -433266b46c60f2e2 5.179474679231202e+15 -43399180c8e44788 7.196856730011528e+15 -4341c37937e08000 1e+16 -4348aeb1a6cbab51 1.3894954943731362e+16 -435125e5fcbf3172 1.9306977288832456e+16 -4357d3be78cb927d 2.6826957952797172e+16 -43608dc88e753b5a 3.727593720314952e+16 -4367006187792fb5 5.179474679231223e+16 -436ff5e0fb1d596a 7.196856730011528e+16 -4376345785d8a000 1e+17 -437eda5e107e9625 1.3894954943731362e+17 -43856f5f7beefdcf 1.9306977288832458e+17 -438dc8ae16fe771b 2.6826957952797168e+17 -4394b13ab2128a31 3.7275937203149523e+17 -439cc079e9577ba2 5.179474679231223e+17 -43a3f9ac9cf257e2 7.196856730011528e+17 -43abc16d674ec800 1e+18 -43b3487aca4f1dd7 1.3894954943731361e+18 -43bacb375aeabd42 1.9306977288832456e+18 -43c29d6cce5f0a71 2.682695795279717e+18 -43c9dd895e972cbd 3.727593720314952e+18 -43d1f84c31d6ad45 5.179474679231223e+18 -43d8f817c42eeddb 7.196856730011528e+18 -43e158e460913d00 1e+19 -43e81a997ce2e54d 1.3894954943731362e+19 -43f0bf0298d2b649 1.9306977288832455e+19 -43f744c801f6cd0e 2.682695795279717e+19 -44002a75db1e7bf6 3.727593720314952e+19 -4406765f3e4c5896 5.1794746792312226e+19 -440f361db53aa952 7.196856730011528e+19 -4415af1d78b58c40 1e+20 -441e213fdc1b9ea0 1.3894954943731361e+20 -4424eec33f0763db 1.9306977288832454e+20 -442d15fa02748051 2.682695795279717e+20 -4434351351e61af4 3.7275937203149524e+20 -443c13f70ddf6ebc 5.179474679231223e+20 -444381d29144a9d3 7.196856730011528e+20 -444b1ae4d6e2ef50 1e+21 -4452d4c7e9914325 1.3894954943731364e+21 -445a2a740ec93cd3 1.9306977288832457e+21 -44622dbc4188d032 2.6826957952797167e+21 -44694258265fa1b1 3.7275937203149524e+21 -44718c7a68aba536 5.179474679231223e+21 -447862473595d448 7.196856730011528e+21 -4480f0cf064dd592 1e+22 -448789f9e3f593ec 1.389495494373136e+22 -44905a88893dc604 1.9306977288832458e+22 -4496b92b51eb043f 2.682695795279717e+22 -449f92ee2ff78a1c 3.727593720314952e+22 -44a5ef9902d68e83 5.179474679231223e+22 -44ae7ad902fb495a 7.196856730011528e+22 -44b52d02c7e14af6 1e+23 -44bd6c785cf2f8e9 1.3894954943731363e+23 -44c4712aab8d3784 1.9306977288832454e+23 -44cc67762665c54f 2.682695795279717e+23 -44d3bbd4ddfab652 3.727593720314952e+23 -44db6b7f438c3223 5.1794746792312225e+23 -44e30cc7a1dd0dd9 7.19685673001153e+23 -44ea784379d99db4 1e+24 -44f263cb3a17db91 1.389495494373136e+24 -44f98d7556708566 1.9306977288832457e+24 -4501c0a9d7ff9b52 2.6826957952797173e+24 -4508aaca157963e6 3.727593720314952e+24 -4511232f8a379f57 5.179474679231224e+24 -4517cff98a54514e 7.196856730011528e+24 -45208b2a2c280291 1e+25 -4526fcbe089dd276 1.3894954943731363e+25 -452ff0d2ac0ca6c0 1.9306977288832458e+25 -453630d44dff8226 2.682695795279717e+25 -453ed57c9ad7bce0 3.727593720314952e+25 -45456bfb6cc5872c 5.179474679231223e+25 -454dc3f7ece965a2 7.196856730011528e+25 -4554adf4b7320335 1e+26 -455cbbed8ac54713 1.3894954943731362e+26 -4563f683ab87e837 1.9306977288832455e+26 -456bbd09617f62af 2.682695795279717e+26 -4573456de0c6d60c 3.727593720314952e+26 -457ac6fa47f6e8f7 5.179474679231223e+26 -45829a7af411df85 7.196856730011528e+26 -4589d971e4fe8402 1e+27 -4591f57476bb4c6b 1.389495494373136e+27 -4598f4249669e245 1.9306977288832455e+27 -45a15625dcef9dae 2.682695795279717e+27 -45a816c958f88b8f 3.727593720314952e+27 -45b0bc5c6cfa519a 5.179474679231223e+27 -45b74119b1165766 7.196856730011528e+27 -45c027e72f1f1281 1e+28 -45c672d1946a1f87 1.3894954943731362e+28 -45cf312dbc045ad7 1.9306977288832457e+28 -45d5abaf542b8518 2.6826957952797166e+28 -45de1c7baf36ae73 3.7275937203149523e+28 -45e4eb738838e600 5.179474679231222e+28 -45ed11601d5bed3f 7.196856730011527e+28 -45f431e0fae6d721 1e+29 -45fc0f85f984a769 1.3894954943731362e+29 -46037ebc9582b8c6 1.9306977288832456e+29 -460b169b2936665f 2.682695795279717e+29 -4612d1cd4d822d08 3.7275937203149524e+29 -461a26506a471f82 5.1794746792312236e+29 -46222adc12597448 7.196856730011528e+29 -46293e5939a08cea 1e+30 -463189b3bbf2e8a1 1.389495494373136e+30 -46385e6bbae366f8 1.9306977288832457e+30 -4640ee20f9c1fffc 2.6826957952797173e+30 -46478640a0e2b849 3.727593720314952e+30 -465057f2426c73b0 5.179474679231222e+30 -4656b59316efd15a 7.196856730011528e+30 -465f8def8808b024 1e+31 -4665ec20aaefa2ca 1.3894954943731362e+31 -466e7606a99c40b5 1.9306977288832455e+31 -467529a938327ffa 2.682695795279717e+31 -467d67d0c91b665d 3.7275937203149526e+31 -46846deed307909d 5.179474679231223e+31 -468c62f7dcabc5b1 7.196856730011529e+31 -4693b8b5b5056e17 1e+32 -469b6728d5ab8bbb 1.3894954943731475e+32 -46a309c42a01a872 1.9306977288832458e+32 -46aa7413863f2035 2.6826957952797387e+32 -46b260e27db11fd0 3.727593720314922e+32 -46b9896a87c974c5 5.179474679231224e+32 -46c1bddae9eb5b66 7.19685673001147e+32 -46c8a6e32246c99d 1.0000000000000001e+33 -46d12079858b3755 1.3894954943731475e+33 -46d7cc353482128e 1.9306977288832456e+33 -46e0888c33e77421 2.6826957952797386e+33 -46e6f91b1d1d67c5 3.727593720314923e+33 -46efebc529bbd1f6 5.1794746792312235e+33 -46f62d51a4663240 7.196856730011471e+33 -46fed09bead87c04 1.0000000000000001e+34 -47056897e6ee052a 1.3894954943731475e+34 -470dbf4281a29732 1.9306977288832458e+34 -4714aaaf40e1512a 2.682695795279739e+34 -471cb761e464c1b5 3.7275937203149222e+34 -4723f35b3a15633a 5.179474679231224e+34 -472bb8a60d7fbecf 7.19685673001147e+34 -4733426172c74d82 1e+35 -473ac2bde0a98674 1.3894954943731474e+35 -4742978991059e7f 1.9306977288832457e+35 -4749d55b1119a573 2.6826957952797384e+35 -4751f29d2ebef912 3.727593720314923e+35 -4758f032089abc07 5.179474679231223e+35 -47615367c86fd741 7.196856730011469e+35 -476812f9cf7920e3 1e+36 -4770b9b6ac69f409 1.3894954943731475e+36 -47773d6bf547061f 1.9306977288832457e+36 -47802558eab00768 2.6826957952797385e+36 -47866f447a6eb756 3.7275937203149226e+36 -478f2c3e8ac16b0a 5.1794746792312234e+36 -4795a841ba8bcd12 7.19685673001147e+36 -479e17b84357691c 1.0000000000000001e+37 -47a4e8245784710b 1.3894954943731475e+37 -47ad0cc6f298c7a7 1.9306977288832458e+37 -47b42eaf255c0942 2.6826957952797385e+37 -47bc0b15990a652b 3.7275937203149224e+37 -47c37ba716b8e2e7 5.179474679231224e+37 -47cb1252292ec057 7.196856730011471e+37 -47d2ced32a16a1b1 1e+38 -47da222d6d658d4e 1.3894954943731475e+38 -47e227fc579f7cc8 1.9306977288832457e+38 -47e93a5aeeb30b93 2.6826957952797386e+38 -47f186ed7fa67f3b 3.7275937203149225e+38 -47f85a90dc671b9f 5.179474679231223e+38 -4800eb7359bd3836 7.19685673001147e+38 -48078287f49c4a1e 1.0000000000000001e+39 -4810555c645f7850 1.3894954943731473e+39 -4816b1fb6d875bfa 1.9306977288832457e+39 -481f88f1aa5fce77 2.6826957952797384e+39 -4825e8a8df901f0a 3.7275937203149226e+39 -482e71351380e287 5.179474679231223e+39 -48352650302c8643 7.19685673001147e+39 -483d6329f1c35ca5 1e+40 -48446ab37d775665 1.3894954943731475e+40 -484c5e7a48e932f9 1.9306977288832458e+40 -4853b5970a7be10a 2.682695795279738e+40 -485b62d3177426cc 3.7275937203149224e+40 -486306c12c308d95 5.179474679231224e+40 -486a6fe43c37a7d4 7.19685673001147e+40 -48725dfa371a19e7 1e+41 -487985605cd52bfd 1.3894954943731473e+41 -4881bb0c6d91bfdc 1.930697728883246e+41 -4888a2fccd1ad94d 2.6826957952797384e+41 -48911dc3eea89840 3.727593720314923e+41 -4897c871773cb0fa 5.179474679231223e+41 -48a085eea5a2c8e5 7.19685673001147e+41 -48a6f578c4e0a061 1e+42 -48afe6b8740a76fd 1.3894954943731474e+42 -48b629cf88f62fd2 1.9306977288832456e+42 -48becbbc00618fa1 2.6826957952797387e+42 -48c56534ea52be50 3.727593720314923e+42 -48cdba8dd50bdd39 5.1794746792312236e+42 -48d4a76a4f0b7b1e 7.19685673001147e+42 -48dcb2d6f618c879 1e+43 -48e3f03348868a5e 1.3894954943731474e+43 -48ebb4436b33bbc7 1.9306977288832457e+43 -48f33f55803cf9c5 2.682695795279739e+43 -48fabe8224e76de4 3.7275937203149227e+43 -49029498a5276a43 5.179474679231223e+43 -4909d144e2ce59e5 7.19685673001147e+43 -4911efc659cf7d4c 1e+44 -4918ec401aa82cf6 1.3894954943731475e+44 -492150aa2300555c 1.9306977288832456e+44 -49280f2ae04c3835 2.6826957952797383e+44 -4930b7115710a4ae 3.727593720314922e+44 -493739bece7144d4 5.179474679231223e+44 -494022cb0dc0f82f 7.19685673001147e+44 -49466bb7f0435c9f 1.0000000000000001e+45 -494f275021523834 1.3894954943731476e+45 -4955a4d4abc06ab4 1.930697728883246e+45 -495e12f5985f4642 2.6826957952797383e+45 -4964e4d5acd4cdda 3.7275937203149227e+45 -496d082e820d960a 5.179474679231224e+45 -49742b7dd131363c 7.196856730011471e+45 -497c06a5ec5433c6 1e+46 -4983789214d36320 1.3894954943731474e+46 -498b0e09d6b08560 1.9306977288832457e+46 -4992cbd97f3b8bea 2.6826957952797386e+46 -499a1e0b180a0151 3.727593720314923e+46 -49a2251d11487dc5 5.179474679231223e+46 -49a9365d457d83ca 7.19685673001147e+46 -49b18427b3b4a05c 1e+47 -49b856b69a083be7 1.3894954943731472e+47 -3fc2492492492492 0.14285714285714285 -3f50624dd2f1a9fc 0.001 -412e848000000000 1e+06 -3fd2492492492492 0.2857142857142857 -3f60624dd2f1a9fc 0.002 -413e848000000000 2e+06 -3fdb6db6db6db6db 0.42857142857142855 -3f689374bc6a7efa 0.003 -4146e36000000000 3e+06 -3fe2492492492492 0.5714285714285714 -3f70624dd2f1a9fc 0.004 -414e848000000000 4e+06 -3fe6db6db6db6db7 0.7142857142857143 -3f747ae147ae147b 0.005 -415312d000000000 5e+06 -3feb6db6db6db6db 0.8571428571428571 -3f789374bc6a7efa 0.006 -4156e36000000000 6e+06 -3ff0000000000000 1 -3f7cac083126e979 0.007 -415ab3f000000000 7e+06 -3ff2492492492492 1.1428571428571428 -3f80624dd2f1a9fc 0.008 -415e848000000000 8e+06 -3ff4924924924925 1.2857142857142858 -3f826e978d4fdf3c 0.009000000000000001 -41612a8800000000 9e+06 -3ff6db6db6db6db7 1.4285714285714286 -3f847ae147ae147b 0.01 -416312d000000000 1e+07 -3ff9249249249249 1.5714285714285714 -3f86872b020c49ba 0.011 -4164fb1800000000 1.1e+07 -3ffb6db6db6db6db 1.7142857142857142 -3f889374bc6a7efa 0.012 -4166e36000000000 1.2e+07 -3ffdb6db6db6db6e 1.8571428571428572 -3f8a9fbe76c8b43a 0.013000000000000001 -4168cba800000000 1.3e+07 -4000000000000000 2 -3f8cac083126e979 0.014 -416ab3f000000000 1.4e+07 -4001249249249249 2.142857142857143 -3f8eb851eb851eb8 0.015 -416c9c3800000000 1.5e+07 -4002492492492492 2.2857142857142856 -3f90624dd2f1a9fc 0.016 -416e848000000000 1.6e+07 -40036db6db6db6db 2.4285714285714284 -3f916872b020c49c 0.017 -4170366400000000 1.7e+07 -4004924924924925 2.5714285714285716 -3f926e978d4fdf3c 0.018000000000000002 -41712a8800000000 1.8e+07 -4005b6db6db6db6e 2.7142857142857144 -3f9374bc6a7ef9db 0.019 -41721eac00000000 1.9e+07 -4006db6db6db6db7 2.857142857142857 -3f947ae147ae147b 0.02 -417312d000000000 2e+07 -4008000000000000 3 -3f95810624dd2f1b 0.021 -417406f400000000 2.1e+07 -4009249249249249 3.142857142857143 -3f96872b020c49ba 0.022 -4174fb1800000000 2.2e+07 -400a492492492492 3.2857142857142856 -3f978d4fdf3b645a 0.023 -4175ef3c00000000 2.3e+07 -400b6db6db6db6db 3.4285714285714284 -3f989374bc6a7efa 0.024 -4176e36000000000 2.4e+07 -400c924924924925 3.5714285714285716 -3f9999999999999a 0.025 -4177d78400000000 2.5e+07 -400db6db6db6db6e 3.7142857142857144 -3f9a9fbe76c8b43a 0.026000000000000002 -4178cba800000000 2.6e+07 -400edb6db6db6db7 3.857142857142857 -3f9ba5e353f7ced9 0.027 -4179bfcc00000000 2.7e+07 -4010000000000000 4 -3f9cac083126e979 0.028 -417ab3f000000000 2.8e+07 -4010924924924925 4.142857142857143 -3f9db22d0e560419 0.029 -417ba81400000000 2.9e+07 -4011249249249249 4.285714285714286 -3f9eb851eb851eb8 0.03 -417c9c3800000000 3e+07 -4011b6db6db6db6e 4.428571428571429 -3f9fbe76c8b43958 0.031 -417d905c00000000 3.1e+07 -4012492492492492 4.571428571428571 -3fa0624dd2f1a9fc 0.032 -417e848000000000 3.2e+07 -4012db6db6db6db7 4.714285714285714 -3fa0e5604189374c 0.033 -417f78a400000000 3.3e+07 -40136db6db6db6db 4.857142857142857 -3fa16872b020c49c 0.034 -4180366400000000 3.4e+07 -4014000000000000 5 -3fa1eb851eb851ec 0.035 -4180b07600000000 3.5e+07 -4014924924924925 5.142857142857143 -3fa26e978d4fdf3c 0.036000000000000004 -41812a8800000000 3.6e+07 -4015249249249249 5.285714285714286 -3fa2f1a9fbe76c8b 0.037 -4181a49a00000000 3.7e+07 -4015b6db6db6db6e 5.428571428571429 -3fa374bc6a7ef9db 0.038 -41821eac00000000 3.8e+07 -4016492492492492 5.571428571428571 -3fa3f7ced916872b 0.039 -418298be00000000 3.9e+07 -4016db6db6db6db7 5.714285714285714 -3fa47ae147ae147b 0.04 -418312d000000000 4e+07 -40176db6db6db6db 5.857142857142857 -3fa4fdf3b645a1cb 0.041 -41838ce200000000 4.1e+07 -4018000000000000 6 -3fa5810624dd2f1b 0.042 -418406f400000000 4.2e+07 -4018924924924925 6.142857142857143 -3fa604189374bc6b 0.043000000000000003 -4184810600000000 4.3e+07 -4019249249249249 6.285714285714286 -3fa6872b020c49ba 0.044 -4184fb1800000000 4.4e+07 -4019b6db6db6db6e 6.428571428571429 -3fa70a3d70a3d70a 0.045 -4185752a00000000 4.5e+07 -401a492492492492 6.571428571428571 -3fa78d4fdf3b645a 0.046 -4185ef3c00000000 4.6e+07 -401adb6db6db6db7 6.714285714285714 -3fa810624dd2f1aa 0.047 -4186694e00000000 4.7e+07 -401b6db6db6db6db 6.857142857142857 -3fa89374bc6a7efa 0.048 -4186e36000000000 4.8e+07 -401c000000000000 7 -3fa916872b020c4a 0.049 -41875d7200000000 4.9e+07 -401c924924924925 7.142857142857143 -3fa999999999999a 0.05 -4187d78400000000 5e+07 -401d249249249249 7.285714285714286 -3faa1cac083126ea 0.051000000000000004 -4188519600000000 5.1e+07 -401db6db6db6db6e 7.428571428571429 -3faa9fbe76c8b43a 0.052000000000000005 -4188cba800000000 5.2e+07 -401e492492492492 7.571428571428571 -3fab22d0e5604189 0.053 -418945ba00000000 5.3e+07 -401edb6db6db6db7 7.714285714285714 -3faba5e353f7ced9 0.054 -4189bfcc00000000 5.4e+07 -401f6db6db6db6db 7.857142857142857 -3fac28f5c28f5c29 0.055 -418a39de00000000 5.5e+07 -4020000000000000 8 -3facac083126e979 0.056 -418ab3f000000000 5.6e+07 -4020492492492492 8.142857142857142 -3fad2f1a9fbe76c9 0.057 -418b2e0200000000 5.7e+07 -4020924924924925 8.285714285714286 -3fadb22d0e560419 0.058 -418ba81400000000 5.8e+07 -4020db6db6db6db7 8.428571428571429 -3fae353f7ced9169 0.059000000000000004 -418c222600000000 5.9e+07 -4021249249249249 8.571428571428571 -3faeb851eb851eb8 0.06 -418c9c3800000000 6e+07 -40216db6db6db6db 8.714285714285714 -3faf3b645a1cac08 0.061 -418d164a00000000 6.1e+07 -4021b6db6db6db6e 8.857142857142858 -3fafbe76c8b43958 0.062 -418d905c00000000 6.2e+07 -4022000000000000 9 -3fb020c49ba5e354 0.063 -418e0a6e00000000 6.3e+07 -4022492492492492 9.142857142857142 -3fb0624dd2f1a9fc 0.064 -418e848000000000 6.4e+07 -4022924924924925 9.285714285714286 -3fb0a3d70a3d70a4 0.065 -418efe9200000000 6.5e+07 -4022db6db6db6db7 9.428571428571429 -3fb0e5604189374c 0.066 -418f78a400000000 6.6e+07 -4023249249249249 9.571428571428571 -3fb126e978d4fdf4 0.067 -418ff2b600000000 6.7e+07 -40236db6db6db6db 9.714285714285714 -3fb16872b020c49c 0.068 -4190366400000000 6.8e+07 -4023b6db6db6db6e 9.857142857142858 -3fb1a9fbe76c8b44 0.069 -4190736d00000000 6.9e+07 -4024000000000000 10 -3fb1eb851eb851ec 0.07 -4190b07600000000 7e+07 -4024492492492492 10.142857142857142 -3fb22d0e56041894 0.07100000000000001 -4190ed7f00000000 7.1e+07 -4024924924924925 10.285714285714286 -3fb26e978d4fdf3c 0.07200000000000001 -41912a8800000000 7.2e+07 -4024db6db6db6db7 10.428571428571429 -3fb2b020c49ba5e3 0.073 -4191679100000000 7.3e+07 -4025249249249249 10.571428571428571 -3fb2f1a9fbe76c8b 0.074 -4191a49a00000000 7.4e+07 -40256db6db6db6db 10.714285714285714 -3fb3333333333333 0.075 -4191e1a300000000 7.5e+07 -4025b6db6db6db6e 10.857142857142858 -3fb374bc6a7ef9db 0.076 -41921eac00000000 7.6e+07 -4026000000000000 11 -3fb3b645a1cac083 0.077 -41925bb500000000 7.7e+07 -4026492492492492 11.142857142857142 -3fb3f7ced916872b 0.078 -419298be00000000 7.8e+07 -4026924924924925 11.285714285714286 -3fb4395810624dd3 0.079 -4192d5c700000000 7.9e+07 -4026db6db6db6db7 11.428571428571429 -3fb47ae147ae147b 0.08 -419312d000000000 8e+07 -4027249249249249 11.571428571428571 -3fb4bc6a7ef9db23 0.081 -41934fd900000000 8.1e+07 -40276db6db6db6db 11.714285714285714 -3fb4fdf3b645a1cb 0.082 -41938ce200000000 8.2e+07 -4027b6db6db6db6e 11.857142857142858 -3fb53f7ced916873 0.083 -4193c9eb00000000 8.3e+07 -4028000000000000 12 -3fb5810624dd2f1b 0.084 -419406f400000000 8.4e+07 -4028492492492492 12.142857142857142 -3fb5c28f5c28f5c3 0.085 -419443fd00000000 8.5e+07 -4028924924924925 12.285714285714286 -3fb604189374bc6b 0.08600000000000001 -4194810600000000 8.6e+07 -4028db6db6db6db7 12.428571428571429 -3fb645a1cac08313 0.08700000000000001 -4194be0f00000000 8.7e+07 -4029249249249249 12.571428571428571 -3fb6872b020c49ba 0.088 -4194fb1800000000 8.8e+07 -40296db6db6db6db 12.714285714285714 -3fb6c8b439581062 0.089 -4195382100000000 8.9e+07 -4029b6db6db6db6e 12.857142857142858 -3fb70a3d70a3d70a 0.09 -4195752a00000000 9e+07 -402a000000000000 13 -3fb74bc6a7ef9db2 0.091 -4195b23300000000 9.1e+07 -402a492492492492 13.142857142857142 -3fb78d4fdf3b645a 0.092 -4195ef3c00000000 9.2e+07 -402a924924924925 13.285714285714286 -3fb7ced916872b02 0.093 -41962c4500000000 9.3e+07 -402adb6db6db6db7 13.428571428571429 -3fb810624dd2f1aa 0.094 -4196694e00000000 9.4e+07 -402b249249249249 13.571428571428571 -3fb851eb851eb852 0.095 -4196a65700000000 9.5e+07 -402b6db6db6db6db 13.714285714285714 -3fb89374bc6a7efa 0.096 -4196e36000000000 9.6e+07 -402bb6db6db6db6e 13.857142857142858 -3fb8d4fdf3b645a2 0.097 -4197206900000000 9.7e+07 -402c000000000000 14 -3fb916872b020c4a 0.098 -41975d7200000000 9.8e+07 -402c492492492492 14.142857142857142 -3fb95810624dd2f2 0.099 -41979a7b00000000 9.9e+07 -402c924924924925 14.285714285714286 -3fb999999999999a 0.1 -4197d78400000000 1e+08 -402cdb6db6db6db7 14.428571428571429 -3fb9db22d0e56042 0.101 -4198148d00000000 1.01e+08 -402d249249249249 14.571428571428571 -3fba1cac083126ea 0.10200000000000001 -4198519600000000 1.02e+08 -402d6db6db6db6db 14.714285714285714 -3fba5e353f7ced92 0.10300000000000001 -41988e9f00000000 1.03e+08 -402db6db6db6db6e 14.857142857142858 -3fba9fbe76c8b43a 0.10400000000000001 -4198cba800000000 1.04e+08 -402e000000000000 15 -3fbae147ae147ae1 0.105 -419908b100000000 1.05e+08 -402e492492492492 15.142857142857142 -3fbb22d0e5604189 0.106 -419945ba00000000 1.06e+08 -402e924924924925 15.285714285714286 -3fbb645a1cac0831 0.107 -419982c300000000 1.07e+08 -402edb6db6db6db7 15.428571428571429 -3fbba5e353f7ced9 0.108 -4199bfcc00000000 1.08e+08 -402f249249249249 15.571428571428571 -3fbbe76c8b439581 0.109 -4199fcd500000000 1.09e+08 -402f6db6db6db6db 15.714285714285714 -3fbc28f5c28f5c29 0.11 -419a39de00000000 1.1e+08 -402fb6db6db6db6e 15.857142857142858 -3fbc6a7ef9db22d1 0.111 -419a76e700000000 1.11e+08 -4030000000000000 16 -3fbcac083126e979 0.112 -419ab3f000000000 1.12e+08 -4030249249249249 16.142857142857142 -3fbced916872b021 0.113 -419af0f900000000 1.13e+08 -4030492492492492 16.285714285714285 -3fbd2f1a9fbe76c9 0.114 -419b2e0200000000 1.14e+08 -40306db6db6db6db 16.428571428571427 -3fbd70a3d70a3d71 0.115 -419b6b0b00000000 1.15e+08 -4030924924924925 16.571428571428573 -3fbdb22d0e560419 0.116 -419ba81400000000 1.16e+08 -4030b6db6db6db6e 16.714285714285715 -3fbdf3b645a1cac1 0.117 -419be51d00000000 1.17e+08 -4030db6db6db6db7 16.857142857142858 -3fbe353f7ced9169 0.11800000000000001 -419c222600000000 1.18e+08 -4031000000000000 17 -3fbe76c8b4395811 0.11900000000000001 -419c5f2f00000000 1.19e+08 -4031249249249249 17.142857142857142 -3fbeb851eb851eb8 0.12 -419c9c3800000000 1.2e+08 -4031492492492492 17.285714285714285 -3fbef9db22d0e560 0.121 -419cd94100000000 1.21e+08 -40316db6db6db6db 17.428571428571427 -3fbf3b645a1cac08 0.122 -419d164a00000000 1.22e+08 -4031924924924925 17.571428571428573 -3fbf7ced916872b0 0.123 -419d535300000000 1.23e+08 -4031b6db6db6db6e 17.714285714285715 -3fbfbe76c8b43958 0.124 -419d905c00000000 1.24e+08 -4031db6db6db6db7 17.857142857142858 -3fc0000000000000 0.125 -419dcd6500000000 1.25e+08 -4032000000000000 18 -3fc020c49ba5e354 0.126 -419e0a6e00000000 1.26e+08 -4032249249249249 18.142857142857142 -3fc04189374bc6a8 0.127 -419e477700000000 1.27e+08 -4032492492492492 18.285714285714285 -3fc0624dd2f1a9fc 0.128 -419e848000000000 1.28e+08 -40326db6db6db6db 18.428571428571427 -3fc083126e978d50 0.129 -419ec18900000000 1.29e+08 -4032924924924925 18.571428571428573 -3fc0a3d70a3d70a4 0.13 -419efe9200000000 1.3e+08 -4032b6db6db6db6e 18.714285714285715 -3fc0c49ba5e353f8 0.131 -419f3b9b00000000 1.31e+08 -4032db6db6db6db7 18.857142857142858 -3fc0e5604189374c 0.132 -419f78a400000000 1.32e+08 -4033000000000000 19 -3fc10624dd2f1aa0 0.133 -419fb5ad00000000 1.33e+08 -4033249249249249 19.142857142857142 -3fc126e978d4fdf4 0.134 -419ff2b600000000 1.34e+08 -4033492492492492 19.285714285714285 -3fc147ae147ae148 0.135 -41a017df80000000 1.35e+08 -40336db6db6db6db 19.428571428571427 -3fc16872b020c49c 0.136 -41a0366400000000 1.36e+08 -4033924924924925 19.571428571428573 -3fc189374bc6a7f0 0.137 -41a054e880000000 1.37e+08 -4033b6db6db6db6e 19.714285714285715 -3fc1a9fbe76c8b44 0.138 -41a0736d00000000 1.38e+08 -4033db6db6db6db7 19.857142857142858 -3fc1cac083126e98 0.139 -41a091f180000000 1.39e+08 -4034000000000000 20 -3fc1eb851eb851ec 0.14 -41a0b07600000000 1.4e+08 -4034249249249249 20.142857142857142 -3fc20c49ba5e3540 0.14100000000000001 -41a0cefa80000000 1.41e+08 -4034492492492492 20.285714285714285 -3fc22d0e56041894 0.14200000000000002 -41a0ed7f00000000 1.42e+08 -40346db6db6db6db 20.428571428571427 -3fc24dd2f1a9fbe8 0.14300000000000002 -41a10c0380000000 1.43e+08 -4034924924924925 20.571428571428573 -3fc26e978d4fdf3c 0.14400000000000002 -41a12a8800000000 1.44e+08 -4034b6db6db6db6e 20.714285714285715 -3fc28f5c28f5c28f 0.145 -41a1490c80000000 1.45e+08 -4034db6db6db6db7 20.857142857142858 -3fc2b020c49ba5e3 0.146 -41a1679100000000 1.46e+08 -4035000000000000 21 -3fc2d0e560418937 0.147 -41a1861580000000 1.47e+08 -4035249249249249 21.142857142857142 -3fc2f1a9fbe76c8b 0.148 -41a1a49a00000000 1.48e+08 -4035492492492492 21.285714285714285 -3fc3126e978d4fdf 0.149 -41a1c31e80000000 1.49e+08 -40356db6db6db6db 21.428571428571427 -3fc3333333333333 0.15 -41a1e1a300000000 1.5e+08 -4035924924924925 21.571428571428573 -3fc353f7ced91687 0.151 -41a2002780000000 1.51e+08 -4035b6db6db6db6e 21.714285714285715 -3fc374bc6a7ef9db 0.152 -41a21eac00000000 1.52e+08 -4035db6db6db6db7 21.857142857142858 -3fc395810624dd2f 0.153 -41a23d3080000000 1.53e+08 -4036000000000000 22 -3fc3b645a1cac083 0.154 -41a25bb500000000 1.54e+08 -4036249249249249 22.142857142857142 -3fc3d70a3d70a3d7 0.155 -41a27a3980000000 1.55e+08 -4036492492492492 22.285714285714285 -3fc3f7ced916872b 0.156 -41a298be00000000 1.56e+08 -40366db6db6db6db 22.428571428571427 -3fc4189374bc6a7f 0.157 -41a2b74280000000 1.57e+08 -4036924924924925 22.571428571428573 -3fc4395810624dd3 0.158 -41a2d5c700000000 1.58e+08 -4036b6db6db6db6e 22.714285714285715 -3fc45a1cac083127 0.159 -41a2f44b80000000 1.59e+08 -4036db6db6db6db7 22.857142857142858 -3fc47ae147ae147b 0.16 -41a312d000000000 1.6e+08 -4037000000000000 23 -3fc49ba5e353f7cf 0.161 -41a3315480000000 1.61e+08 -4037249249249249 23.142857142857142 -3fc4bc6a7ef9db23 0.162 -41a34fd900000000 1.62e+08 -4037492492492492 23.285714285714285 -3fc4dd2f1a9fbe77 0.163 -41a36e5d80000000 1.63e+08 -40376db6db6db6db 23.428571428571427 -3fc4fdf3b645a1cb 0.164 -41a38ce200000000 1.64e+08 -4037924924924925 23.571428571428573 -3fc51eb851eb851f 0.165 -41a3ab6680000000 1.65e+08 -4037b6db6db6db6e 23.714285714285715 -3fc53f7ced916873 0.166 -41a3c9eb00000000 1.66e+08 -4037db6db6db6db7 23.857142857142858 -3fc5604189374bc7 0.167 -41a3e86f80000000 1.67e+08 -4038000000000000 24 -3fc5810624dd2f1b 0.168 -41a406f400000000 1.68e+08 -4038249249249249 24.142857142857142 -3fc5a1cac083126f 0.169 -41a4257880000000 1.69e+08 -4038492492492492 24.285714285714285 -3fc5c28f5c28f5c3 0.17 -41a443fd00000000 1.7e+08 -40386db6db6db6db 24.428571428571427 -3fc5e353f7ced917 0.171 -41a4628180000000 1.71e+08 -4038924924924925 24.571428571428573 -3fc604189374bc6b 0.17200000000000001 -41a4810600000000 1.72e+08 -4038b6db6db6db6e 24.714285714285715 -3fc624dd2f1a9fbf 0.17300000000000001 -41a49f8a80000000 1.73e+08 -4038db6db6db6db7 24.857142857142858 -3fc645a1cac08313 0.17400000000000002 -41a4be0f00000000 1.74e+08 -4039000000000000 25 -3fc6666666666667 0.17500000000000002 -41a4dc9380000000 1.75e+08 -4039249249249249 25.142857142857142 -3fc6872b020c49ba 0.176 -41a4fb1800000000 1.76e+08 -4039492492492492 25.285714285714285 -3fc6a7ef9db22d0e 0.177 -41a5199c80000000 1.77e+08 -40396db6db6db6db 25.428571428571427 -3fc6c8b439581062 0.178 -41a5382100000000 1.78e+08 -4039924924924925 25.571428571428573 -3fc6e978d4fdf3b6 0.179 -41a556a580000000 1.79e+08 -4039b6db6db6db6e 25.714285714285715 -3fc70a3d70a3d70a 0.18 -41a5752a00000000 1.8e+08 -4039db6db6db6db7 25.857142857142858 -3fc72b020c49ba5e 0.181 -41a593ae80000000 1.81e+08 -403a000000000000 26 -3fc74bc6a7ef9db2 0.182 -41a5b23300000000 1.82e+08 -403a249249249249 26.142857142857142 -3fc76c8b43958106 0.183 -41a5d0b780000000 1.83e+08 -403a492492492492 26.285714285714285 -3fc78d4fdf3b645a 0.184 -41a5ef3c00000000 1.84e+08 -403a6db6db6db6db 26.428571428571427 -3fc7ae147ae147ae 0.185 -41a60dc080000000 1.85e+08 -403a924924924925 26.571428571428573 -3fc7ced916872b02 0.186 -41a62c4500000000 1.86e+08 -403ab6db6db6db6e 26.714285714285715 -3fc7ef9db22d0e56 0.187 -41a64ac980000000 1.87e+08 -403adb6db6db6db7 26.857142857142858 -3fc810624dd2f1aa 0.188 -41a6694e00000000 1.88e+08 -403b000000000000 27 -3fc83126e978d4fe 0.189 -41a687d280000000 1.89e+08 -403b249249249249 27.142857142857142 -3fc851eb851eb852 0.19 -41a6a65700000000 1.9e+08 -403b492492492492 27.285714285714285 -3fc872b020c49ba6 0.191 -41a6c4db80000000 1.91e+08 -403b6db6db6db6db 27.428571428571427 -3fc89374bc6a7efa 0.192 -41a6e36000000000 1.92e+08 -403b924924924925 27.571428571428573 -3fc8b4395810624e 0.193 -41a701e480000000 1.93e+08 -403bb6db6db6db6e 27.714285714285715 -3fc8d4fdf3b645a2 0.194 -41a7206900000000 1.94e+08 -403bdb6db6db6db7 27.857142857142858 -3fc8f5c28f5c28f6 0.195 -41a73eed80000000 1.95e+08 -403c000000000000 28 -3fc916872b020c4a 0.196 -41a75d7200000000 1.96e+08 -403c249249249249 28.142857142857142 -3fc9374bc6a7ef9e 0.197 -41a77bf680000000 1.97e+08 -403c492492492492 28.285714285714285 -3fc95810624dd2f2 0.198 -41a79a7b00000000 1.98e+08 -403c6db6db6db6db 28.428571428571427 -3fc978d4fdf3b646 0.199 -41a7b8ff80000000 1.99e+08 -403c924924924925 28.571428571428573 -3fc999999999999a 0.2 -41a7d78400000000 2e+08 -403cb6db6db6db6e 28.714285714285715 -3fc9ba5e353f7cee 0.201 -41a7f60880000000 2.01e+08 -403cdb6db6db6db7 28.857142857142858 -3fc9db22d0e56042 0.202 -41a8148d00000000 2.02e+08 -403d000000000000 29 -3fc9fbe76c8b4396 0.203 -41a8331180000000 2.03e+08 -403d249249249249 29.142857142857142 -3fca1cac083126ea 0.20400000000000001 -41a8519600000000 2.04e+08 -403d492492492492 29.285714285714285 -3fca3d70a3d70a3e 0.20500000000000002 -41a8701a80000000 2.05e+08 -403d6db6db6db6db 29.428571428571427 -3fca5e353f7ced92 0.20600000000000002 -41a88e9f00000000 2.06e+08 -403d924924924925 29.571428571428573 -3fca7ef9db22d0e6 0.20700000000000002 -41a8ad2380000000 2.07e+08 -403db6db6db6db6e 29.714285714285715 -3fca9fbe76c8b43a 0.20800000000000002 -41a8cba800000000 2.08e+08 -403ddb6db6db6db7 29.857142857142858 -3fcac083126e978d 0.209 -41a8ea2c80000000 2.09e+08 -403e000000000000 30 -3fcae147ae147ae1 0.21 -41a908b100000000 2.1e+08 -403e249249249249 30.142857142857142 -3fcb020c49ba5e35 0.211 -41a9273580000000 2.11e+08 -403e492492492492 30.285714285714285 -3fcb22d0e5604189 0.212 -41a945ba00000000 2.12e+08 -403e6db6db6db6db 30.428571428571427 -3fcb4395810624dd 0.213 -41a9643e80000000 2.13e+08 -403e924924924925 30.571428571428573 -3fcb645a1cac0831 0.214 -41a982c300000000 2.14e+08 -403eb6db6db6db6e 30.714285714285715 -3fcb851eb851eb85 0.215 -41a9a14780000000 2.15e+08 -403edb6db6db6db7 30.857142857142858 -3fcba5e353f7ced9 0.216 -41a9bfcc00000000 2.16e+08 -403f000000000000 31 -3fcbc6a7ef9db22d 0.217 -41a9de5080000000 2.17e+08 -403f249249249249 31.142857142857142 -3fcbe76c8b439581 0.218 -41a9fcd500000000 2.18e+08 -403f492492492492 31.285714285714285 -3fcc083126e978d5 0.219 -41aa1b5980000000 2.19e+08 -403f6db6db6db6db 31.428571428571427 -3fcc28f5c28f5c29 0.22 -41aa39de00000000 2.2e+08 -403f924924924925 31.571428571428573 -3fcc49ba5e353f7d 0.221 -41aa586280000000 2.21e+08 -403fb6db6db6db6e 31.714285714285715 -3fcc6a7ef9db22d1 0.222 -41aa76e700000000 2.22e+08 -403fdb6db6db6db7 31.857142857142858 -3fcc8b4395810625 0.223 -41aa956b80000000 2.23e+08 -4040000000000000 32 -3fccac083126e979 0.224 -41aab3f000000000 2.24e+08 -4040124924924925 32.142857142857146 -3fcccccccccccccd 0.225 -41aad27480000000 2.25e+08 -4040249249249249 32.285714285714285 -3fcced916872b021 0.226 -41aaf0f900000000 2.26e+08 -404036db6db6db6e 32.42857142857143 -3fcd0e5604189375 0.227 -41ab0f7d80000000 2.27e+08 -4040492492492492 32.57142857142857 -3fcd2f1a9fbe76c9 0.228 -41ab2e0200000000 2.28e+08 -40405b6db6db6db7 32.714285714285715 -3fcd4fdf3b645a1d 0.229 -41ab4c8680000000 2.29e+08 -40406db6db6db6db 32.857142857142854 -3fcd70a3d70a3d71 0.23 -41ab6b0b00000000 2.3e+08 -4040800000000000 33 -3fcd916872b020c5 0.231 -41ab898f80000000 2.31e+08 -4040924924924925 33.142857142857146 -3fcdb22d0e560419 0.232 -41aba81400000000 2.32e+08 -4040a49249249249 33.285714285714285 -3fcdd2f1a9fbe76d 0.233 -41abc69880000000 2.33e+08 -4040b6db6db6db6e 33.42857142857143 -3fcdf3b645a1cac1 0.234 -41abe51d00000000 2.34e+08 -4040c92492492492 33.57142857142857 -3fce147ae147ae15 0.23500000000000001 -41ac03a180000000 2.35e+08 -4040db6db6db6db7 33.714285714285715 -3fce353f7ced9169 0.23600000000000002 -41ac222600000000 2.36e+08 -4040edb6db6db6db 33.857142857142854 -3fce5604189374bd 0.23700000000000002 -41ac40aa80000000 2.37e+08 -4041000000000000 34 -3fce76c8b4395811 0.23800000000000002 -41ac5f2f00000000 2.38e+08 -4041124924924925 34.142857142857146 -3fce978d4fdf3b65 0.23900000000000002 -41ac7db380000000 2.39e+08 -4041249249249249 34.285714285714285 -3fceb851eb851eb8 0.24 -41ac9c3800000000 2.4e+08 -404136db6db6db6e 34.42857142857143 -3fced916872b020c 0.241 -41acbabc80000000 2.41e+08 -4041492492492492 34.57142857142857 -3fcef9db22d0e560 0.242 -41acd94100000000 2.42e+08 -40415b6db6db6db7 34.714285714285715 -3fcf1a9fbe76c8b4 0.243 -41acf7c580000000 2.43e+08 -40416db6db6db6db 34.857142857142854 -3fcf3b645a1cac08 0.244 -41ad164a00000000 2.44e+08 -4041800000000000 35 -3fcf5c28f5c28f5c 0.245 -41ad34ce80000000 2.45e+08 -4041924924924925 35.142857142857146 -3fcf7ced916872b0 0.246 -41ad535300000000 2.46e+08 -4041a49249249249 35.285714285714285 -3fcf9db22d0e5604 0.247 -41ad71d780000000 2.47e+08 -4041b6db6db6db6e 35.42857142857143 -3fcfbe76c8b43958 0.248 -41ad905c00000000 2.48e+08 -4041c92492492492 35.57142857142857 -3fcfdf3b645a1cac 0.249 -41adaee080000000 2.49e+08 -4041db6db6db6db7 35.714285714285715 -3fd0000000000000 0.25 -41adcd6500000000 2.5e+08 -4041edb6db6db6db 35.857142857142854 -3fd010624dd2f1aa 0.251 -41adebe980000000 2.51e+08 -4042000000000000 36 -3fd020c49ba5e354 0.252 -41ae0a6e00000000 2.52e+08 -4042124924924925 36.142857142857146 -3fd03126e978d4fe 0.253 -41ae28f280000000 2.53e+08 -4042249249249249 36.285714285714285 -3fd04189374bc6a8 0.254 -41ae477700000000 2.54e+08 -404236db6db6db6e 36.42857142857143 -3fd051eb851eb852 0.255 -41ae65fb80000000 2.55e+08 -4042492492492492 36.57142857142857 -3fd0624dd2f1a9fc 0.256 -41ae848000000000 2.56e+08 -40425b6db6db6db7 36.714285714285715 -3fd072b020c49ba6 0.257 -41aea30480000000 2.57e+08 -40426db6db6db6db 36.857142857142854 -3fd083126e978d50 0.258 -41aec18900000000 2.58e+08 -4042800000000000 37 -3fd09374bc6a7efa 0.259 -41aee00d80000000 2.59e+08 -4042924924924925 37.142857142857146 -3fd0a3d70a3d70a4 0.26 -41aefe9200000000 2.6e+08 -4042a49249249249 37.285714285714285 -3fd0b4395810624e 0.261 -41af1d1680000000 2.61e+08 -4042b6db6db6db6e 37.42857142857143 -3fd0c49ba5e353f8 0.262 -41af3b9b00000000 2.62e+08 -4042c92492492492 37.57142857142857 -3fd0d4fdf3b645a2 0.263 -41af5a1f80000000 2.63e+08 -4042db6db6db6db7 37.714285714285715 -3fd0e5604189374c 0.264 -41af78a400000000 2.64e+08 -4042edb6db6db6db 37.857142857142854 -3fd0f5c28f5c28f6 0.265 -41af972880000000 2.65e+08 -4043000000000000 38 -3fd10624dd2f1aa0 0.266 -41afb5ad00000000 2.66e+08 -4043124924924925 38.142857142857146 -3fd116872b020c4a 0.267 -41afd43180000000 2.67e+08 -4043249249249249 38.285714285714285 -3fd126e978d4fdf4 0.268 -41aff2b600000000 2.68e+08 -404336db6db6db6e 38.42857142857143 -3fd1374bc6a7ef9e 0.269 -41b0089d40000000 2.69e+08 -4043492492492492 38.57142857142857 -3fd147ae147ae148 0.27 -41b017df80000000 2.7e+08 -40435b6db6db6db7 38.714285714285715 -3fd15810624dd2f2 0.271 -41b02721c0000000 2.71e+08 -40436db6db6db6db 38.857142857142854 -3fd16872b020c49c 0.272 -41b0366400000000 2.72e+08 -4043800000000000 39 -3fd178d4fdf3b646 0.273 -41b045a640000000 2.73e+08 -4043924924924925 39.142857142857146 -3fd189374bc6a7f0 0.274 -41b054e880000000 2.74e+08 -4043a49249249249 39.285714285714285 -3fd199999999999a 0.275 -41b0642ac0000000 2.75e+08 -4043b6db6db6db6e 39.42857142857143 -3fd1a9fbe76c8b44 0.276 -41b0736d00000000 2.76e+08 -4043c92492492492 39.57142857142857 -3fd1ba5e353f7cee 0.277 -41b082af40000000 2.77e+08 -4043db6db6db6db7 39.714285714285715 -3fd1cac083126e98 0.278 -41b091f180000000 2.78e+08 -4043edb6db6db6db 39.857142857142854 -3fd1db22d0e56042 0.279 -41b0a133c0000000 2.79e+08 -4044000000000000 40 -3fd1eb851eb851ec 0.28 -41b0b07600000000 2.8e+08 -4044124924924925 40.142857142857146 -3fd1fbe76c8b4396 0.281 -41b0bfb840000000 2.81e+08 -4044249249249249 40.285714285714285 -3fd20c49ba5e3540 0.28200000000000003 -41b0cefa80000000 2.82e+08 -404436db6db6db6e 40.42857142857143 -3fd21cac083126ea 0.28300000000000003 -41b0de3cc0000000 2.83e+08 -4044492492492492 40.57142857142857 -3fd22d0e56041894 0.28400000000000003 -41b0ed7f00000000 2.84e+08 -40445b6db6db6db7 40.714285714285715 -3fd23d70a3d70a3e 0.28500000000000003 -41b0fcc140000000 2.85e+08 -40446db6db6db6db 40.857142857142854 -3fd24dd2f1a9fbe8 0.28600000000000003 -41b10c0380000000 2.86e+08 -4044800000000000 41 -3fd25e353f7ced92 0.28700000000000003 -41b11b45c0000000 2.87e+08 -4044924924924925 41.142857142857146 -3fd26e978d4fdf3c 0.28800000000000003 -41b12a8800000000 2.88e+08 -4044a49249249249 41.285714285714285 -3fd27ef9db22d0e5 0.289 -41b139ca40000000 2.89e+08 -4044b6db6db6db6e 41.42857142857143 -3fd28f5c28f5c28f 0.29 -41b1490c80000000 2.9e+08 -4044c92492492492 41.57142857142857 -3fd29fbe76c8b439 0.291 -41b1584ec0000000 2.91e+08 -4044db6db6db6db7 41.714285714285715 -3fd2b020c49ba5e3 0.292 -41b1679100000000 2.92e+08 -4044edb6db6db6db 41.857142857142854 -3fd2c083126e978d 0.293 -41b176d340000000 2.93e+08 -4045000000000000 42 -3fd2d0e560418937 0.294 -41b1861580000000 2.94e+08 -4045124924924925 42.142857142857146 -3fd2e147ae147ae1 0.295 -41b19557c0000000 2.95e+08 -4045249249249249 42.285714285714285 -3fd2f1a9fbe76c8b 0.296 -41b1a49a00000000 2.96e+08 -404536db6db6db6e 42.42857142857143 -3fd3020c49ba5e35 0.297 -41b1b3dc40000000 2.97e+08 -4045492492492492 42.57142857142857 -3fd3126e978d4fdf 0.298 -41b1c31e80000000 2.98e+08 -40455b6db6db6db7 42.714285714285715 -3fd322d0e5604189 0.299 -41b1d260c0000000 2.99e+08 -40456db6db6db6db 42.857142857142854 -3fd3333333333333 0.3 -41b1e1a300000000 3e+08 -4045800000000000 43 -3fd34395810624dd 0.301 -41b1f0e540000000 3.01e+08 -4045924924924925 43.142857142857146 -3fd353f7ced91687 0.302 -41b2002780000000 3.02e+08 -4045a49249249249 43.285714285714285 -3fd3645a1cac0831 0.303 -41b20f69c0000000 3.03e+08 -4045b6db6db6db6e 43.42857142857143 -3fd374bc6a7ef9db 0.304 -41b21eac00000000 3.04e+08 -4045c92492492492 43.57142857142857 -3fd3851eb851eb85 0.305 -41b22dee40000000 3.05e+08 -4045db6db6db6db7 43.714285714285715 -3fd395810624dd2f 0.306 -41b23d3080000000 3.06e+08 -4045edb6db6db6db 43.857142857142854 -3fd3a5e353f7ced9 0.307 -41b24c72c0000000 3.07e+08 -4046000000000000 44 -3fd3b645a1cac083 0.308 -41b25bb500000000 3.08e+08 -4046124924924925 44.142857142857146 -3fd3c6a7ef9db22d 0.309 -41b26af740000000 3.09e+08 -4046249249249249 44.285714285714285 -3fd3d70a3d70a3d7 0.31 -41b27a3980000000 3.1e+08 -404636db6db6db6e 44.42857142857143 -3fd3e76c8b439581 0.311 -41b2897bc0000000 3.11e+08 -4046492492492492 44.57142857142857 -3fd3f7ced916872b 0.312 -41b298be00000000 3.12e+08 -40465b6db6db6db7 44.714285714285715 -3fd4083126e978d5 0.313 -41b2a80040000000 3.13e+08 -40466db6db6db6db 44.857142857142854 -3fd4189374bc6a7f 0.314 -41b2b74280000000 3.14e+08 -4046800000000000 45 -3fd428f5c28f5c29 0.315 -41b2c684c0000000 3.15e+08 -4046924924924925 45.142857142857146 -3fd4395810624dd3 0.316 -41b2d5c700000000 3.16e+08 -4046a49249249249 45.285714285714285 -3fd449ba5e353f7d 0.317 -41b2e50940000000 3.17e+08 -4046b6db6db6db6e 45.42857142857143 -3fd45a1cac083127 0.318 -41b2f44b80000000 3.18e+08 -4046c92492492492 45.57142857142857 -3fd46a7ef9db22d1 0.319 -41b3038dc0000000 3.19e+08 -4046db6db6db6db7 45.714285714285715 -3fd47ae147ae147b 0.32 -41b312d000000000 3.2e+08 -4046edb6db6db6db 45.857142857142854 -3fd48b4395810625 0.321 -41b3221240000000 3.21e+08 -4047000000000000 46 -3fd49ba5e353f7cf 0.322 -41b3315480000000 3.22e+08 -4047124924924925 46.142857142857146 -3fd4ac083126e979 0.323 -41b34096c0000000 3.23e+08 -4047249249249249 46.285714285714285 -3fd4bc6a7ef9db23 0.324 -41b34fd900000000 3.24e+08 -404736db6db6db6e 46.42857142857143 -3fd4cccccccccccd 0.325 -41b35f1b40000000 3.25e+08 -4047492492492492 46.57142857142857 -3fd4dd2f1a9fbe77 0.326 -41b36e5d80000000 3.26e+08 -40475b6db6db6db7 46.714285714285715 -3fd4ed916872b021 0.327 -41b37d9fc0000000 3.27e+08 -40476db6db6db6db 46.857142857142854 -3fd4fdf3b645a1cb 0.328 -41b38ce200000000 3.28e+08 -4047800000000000 47 -3fd50e5604189375 0.329 -41b39c2440000000 3.29e+08 -4047924924924925 47.142857142857146 -3fd51eb851eb851f 0.33 -41b3ab6680000000 3.3e+08 -4047a49249249249 47.285714285714285 -3fd52f1a9fbe76c9 0.331 -41b3baa8c0000000 3.31e+08 -4047b6db6db6db6e 47.42857142857143 -3fd53f7ced916873 0.332 -41b3c9eb00000000 3.32e+08 -4047c92492492492 47.57142857142857 -3fd54fdf3b645a1d 0.333 -41b3d92d40000000 3.33e+08 -4047db6db6db6db7 47.714285714285715 -3fd5604189374bc7 0.334 -41b3e86f80000000 3.34e+08 -4047edb6db6db6db 47.857142857142854 -3fd570a3d70a3d71 0.335 -41b3f7b1c0000000 3.35e+08 -4048000000000000 48 -3fd5810624dd2f1b 0.336 -41b406f400000000 3.36e+08 -4048124924924925 48.142857142857146 -3fd5916872b020c5 0.337 -41b4163640000000 3.37e+08 -4048249249249249 48.285714285714285 -3fd5a1cac083126f 0.338 -41b4257880000000 3.38e+08 -404836db6db6db6e 48.42857142857143 -3fd5b22d0e560419 0.339 -41b434bac0000000 3.39e+08 -4048492492492492 48.57142857142857 -3fd5c28f5c28f5c3 0.34 -41b443fd00000000 3.4e+08 -40485b6db6db6db7 48.714285714285715 -3fd5d2f1a9fbe76d 0.341 -41b4533f40000000 3.41e+08 -40486db6db6db6db 48.857142857142854 -3fd5e353f7ced917 0.342 -41b4628180000000 3.42e+08 -4048800000000000 49 -3fd5f3b645a1cac1 0.343 -41b471c3c0000000 3.43e+08 -4048924924924925 49.142857142857146 -3fd604189374bc6b 0.34400000000000003 -41b4810600000000 3.44e+08 -4048a49249249249 49.285714285714285 -3fd6147ae147ae15 0.34500000000000003 -41b4904840000000 3.45e+08 -4048b6db6db6db6e 49.42857142857143 -3fd624dd2f1a9fbf 0.34600000000000003 -41b49f8a80000000 3.46e+08 -4048c92492492492 49.57142857142857 -3fd6353f7ced9169 0.34700000000000003 -41b4aeccc0000000 3.47e+08 -4048db6db6db6db7 49.714285714285715 -3fd645a1cac08313 0.34800000000000003 -41b4be0f00000000 3.48e+08 -4048edb6db6db6db 49.857142857142854 -3fd65604189374bd 0.34900000000000003 -41b4cd5140000000 3.49e+08 -4049000000000000 50 -3fd6666666666667 0.35000000000000003 -41b4dc9380000000 3.5e+08 -4049124924924925 50.142857142857146 -3fd676c8b4395811 0.35100000000000003 -41b4ebd5c0000000 3.51e+08 -4049249249249249 50.285714285714285 -3fd6872b020c49ba 0.352 -41b4fb1800000000 3.52e+08 -404936db6db6db6e 50.42857142857143 -3fd6978d4fdf3b64 0.353 -41b50a5a40000000 3.53e+08 -4049492492492492 50.57142857142857 -3fd6a7ef9db22d0e 0.354 -41b5199c80000000 3.54e+08 -40495b6db6db6db7 50.714285714285715 -3fd6b851eb851eb8 0.355 -41b528dec0000000 3.55e+08 -40496db6db6db6db 50.857142857142854 -3fd6c8b439581062 0.356 -41b5382100000000 3.56e+08 -4049800000000000 51 -3fd6d916872b020c 0.357 -41b5476340000000 3.57e+08 -4049924924924925 51.142857142857146 -3fd6e978d4fdf3b6 0.358 -41b556a580000000 3.58e+08 -4049a49249249249 51.285714285714285 -3fd6f9db22d0e560 0.359 -41b565e7c0000000 3.59e+08 -4049b6db6db6db6e 51.42857142857143 -3fd70a3d70a3d70a 0.36 -41b5752a00000000 3.6e+08 -4049c92492492492 51.57142857142857 -3fd71a9fbe76c8b4 0.361 -41b5846c40000000 3.61e+08 -4049db6db6db6db7 51.714285714285715 -3fd72b020c49ba5e 0.362 -41b593ae80000000 3.62e+08 -4049edb6db6db6db 51.857142857142854 -3fd73b645a1cac08 0.363 -41b5a2f0c0000000 3.63e+08 -404a000000000000 52 -3fd74bc6a7ef9db2 0.364 -41b5b23300000000 3.64e+08 -404a124924924925 52.142857142857146 -3fd75c28f5c28f5c 0.365 -41b5c17540000000 3.65e+08 -404a249249249249 52.285714285714285 -3fd76c8b43958106 0.366 -41b5d0b780000000 3.66e+08 -404a36db6db6db6e 52.42857142857143 -3fd77ced916872b0 0.367 -41b5dff9c0000000 3.67e+08 -404a492492492492 52.57142857142857 -3fd78d4fdf3b645a 0.368 -41b5ef3c00000000 3.68e+08 -404a5b6db6db6db7 52.714285714285715 -3fd79db22d0e5604 0.369 -41b5fe7e40000000 3.69e+08 -404a6db6db6db6db 52.857142857142854 -3fd7ae147ae147ae 0.37 -41b60dc080000000 3.7e+08 -404a800000000000 53 -3fd7be76c8b43958 0.371 -41b61d02c0000000 3.71e+08 -404a924924924925 53.142857142857146 -3fd7ced916872b02 0.372 -41b62c4500000000 3.72e+08 -404aa49249249249 53.285714285714285 -3fd7df3b645a1cac 0.373 -41b63b8740000000 3.73e+08 -404ab6db6db6db6e 53.42857142857143 -3fd7ef9db22d0e56 0.374 -41b64ac980000000 3.74e+08 -404ac92492492492 53.57142857142857 -3fd8000000000000 0.375 -41b65a0bc0000000 3.75e+08 -404adb6db6db6db7 53.714285714285715 -3fd810624dd2f1aa 0.376 -41b6694e00000000 3.76e+08 -404aedb6db6db6db 53.857142857142854 -3fd820c49ba5e354 0.377 -41b6789040000000 3.77e+08 -404b000000000000 54 -3fd83126e978d4fe 0.378 -41b687d280000000 3.78e+08 -404b124924924925 54.142857142857146 -3fd84189374bc6a8 0.379 -41b69714c0000000 3.79e+08 -404b249249249249 54.285714285714285 -3fd851eb851eb852 0.38 -41b6a65700000000 3.8e+08 -404b36db6db6db6e 54.42857142857143 -3fd8624dd2f1a9fc 0.381 -41b6b59940000000 3.81e+08 -404b492492492492 54.57142857142857 -3fd872b020c49ba6 0.382 -41b6c4db80000000 3.82e+08 -404b5b6db6db6db7 54.714285714285715 -3fd883126e978d50 0.383 -41b6d41dc0000000 3.83e+08 -404b6db6db6db6db 54.857142857142854 -3fd89374bc6a7efa 0.384 -41b6e36000000000 3.84e+08 -404b800000000000 55 -3fd8a3d70a3d70a4 0.385 -41b6f2a240000000 3.85e+08 -404b924924924925 55.142857142857146 -3fd8b4395810624e 0.386 -41b701e480000000 3.86e+08 -404ba49249249249 55.285714285714285 -3fd8c49ba5e353f8 0.387 -41b71126c0000000 3.87e+08 -404bb6db6db6db6e 55.42857142857143 -3fd8d4fdf3b645a2 0.388 -41b7206900000000 3.88e+08 -404bc92492492492 55.57142857142857 -3fd8e5604189374c 0.389 -41b72fab40000000 3.89e+08 -404bdb6db6db6db7 55.714285714285715 -3fd8f5c28f5c28f6 0.39 -41b73eed80000000 3.9e+08 -404bedb6db6db6db 55.857142857142854 -3fd90624dd2f1aa0 0.391 -41b74e2fc0000000 3.91e+08 -404c000000000000 56 -3fd916872b020c4a 0.392 -41b75d7200000000 3.92e+08 -404c124924924925 56.142857142857146 -3fd926e978d4fdf4 0.393 -41b76cb440000000 3.93e+08 -404c249249249249 56.285714285714285 -3fd9374bc6a7ef9e 0.394 -41b77bf680000000 3.94e+08 -404c36db6db6db6e 56.42857142857143 -3fd947ae147ae148 0.395 -41b78b38c0000000 3.95e+08 -404c492492492492 56.57142857142857 -3fd95810624dd2f2 0.396 -41b79a7b00000000 3.96e+08 -404c5b6db6db6db7 56.714285714285715 -3fd96872b020c49c 0.397 -41b7a9bd40000000 3.97e+08 -404c6db6db6db6db 56.857142857142854 -3fd978d4fdf3b646 0.398 -41b7b8ff80000000 3.98e+08 -404c800000000000 57 -3fd989374bc6a7f0 0.399 -41b7c841c0000000 3.99e+08 -404c924924924925 57.142857142857146 -3fd999999999999a 0.4 -41b7d78400000000 4e+08 -404ca49249249249 57.285714285714285 -3fd9a9fbe76c8b44 0.401 -41b7e6c640000000 4.01e+08 -404cb6db6db6db6e 57.42857142857143 -3fd9ba5e353f7cee 0.402 -41b7f60880000000 4.02e+08 -404cc92492492492 57.57142857142857 -3fd9cac083126e98 0.403 -41b8054ac0000000 4.03e+08 -404cdb6db6db6db7 57.714285714285715 -3fd9db22d0e56042 0.404 -41b8148d00000000 4.04e+08 -404cedb6db6db6db 57.857142857142854 -3fd9eb851eb851ec 0.405 -41b823cf40000000 4.05e+08 -404d000000000000 58 -3fd9fbe76c8b4396 0.406 -41b8331180000000 4.06e+08 -404d124924924925 58.142857142857146 -3fda0c49ba5e3540 0.40700000000000003 -41b84253c0000000 4.07e+08 -404d249249249249 58.285714285714285 -3fda1cac083126ea 0.40800000000000003 -41b8519600000000 4.08e+08 -404d36db6db6db6e 58.42857142857143 -3fda2d0e56041894 0.40900000000000003 -41b860d840000000 4.09e+08 -404d492492492492 58.57142857142857 -3fda3d70a3d70a3e 0.41000000000000003 -41b8701a80000000 4.1e+08 -404d5b6db6db6db7 58.714285714285715 -3fda4dd2f1a9fbe8 0.41100000000000003 -41b87f5cc0000000 4.11e+08 -404d6db6db6db6db 58.857142857142854 -3fda5e353f7ced92 0.41200000000000003 -41b88e9f00000000 4.12e+08 -404d800000000000 59 -3fda6e978d4fdf3c 0.41300000000000003 -41b89de140000000 4.13e+08 -404d924924924925 59.142857142857146 -3fda7ef9db22d0e6 0.41400000000000003 -41b8ad2380000000 4.14e+08 -404da49249249249 59.285714285714285 -3fda8f5c28f5c290 0.41500000000000004 -41b8bc65c0000000 4.15e+08 -404db6db6db6db6e 59.42857142857143 -3fda9fbe76c8b43a 0.41600000000000004 -41b8cba800000000 4.16e+08 -404dc92492492492 59.57142857142857 -3fdab020c49ba5e3 0.417 -41b8daea40000000 4.17e+08 -404ddb6db6db6db7 59.714285714285715 -3fdac083126e978d 0.418 -41b8ea2c80000000 4.18e+08 -404dedb6db6db6db 59.857142857142854 -3fdad0e560418937 0.419 -41b8f96ec0000000 4.19e+08 -404e000000000000 60 -3fdae147ae147ae1 0.42 -41b908b100000000 4.2e+08 -404e124924924925 60.142857142857146 -3fdaf1a9fbe76c8b 0.421 -41b917f340000000 4.21e+08 -404e249249249249 60.285714285714285 -3fdb020c49ba5e35 0.422 -41b9273580000000 4.22e+08 -404e36db6db6db6e 60.42857142857143 -3fdb126e978d4fdf 0.423 -41b93677c0000000 4.23e+08 -404e492492492492 60.57142857142857 -3fdb22d0e5604189 0.424 -41b945ba00000000 4.24e+08 -404e5b6db6db6db7 60.714285714285715 -3fdb333333333333 0.425 -41b954fc40000000 4.25e+08 -404e6db6db6db6db 60.857142857142854 -3fdb4395810624dd 0.426 -41b9643e80000000 4.26e+08 -404e800000000000 61 -3fdb53f7ced91687 0.427 -41b97380c0000000 4.27e+08 -404e924924924925 61.142857142857146 -3fdb645a1cac0831 0.428 -41b982c300000000 4.28e+08 -404ea49249249249 61.285714285714285 -3fdb74bc6a7ef9db 0.429 -41b9920540000000 4.29e+08 -404eb6db6db6db6e 61.42857142857143 -3fdb851eb851eb85 0.43 -41b9a14780000000 4.3e+08 -404ec92492492492 61.57142857142857 -3fdb95810624dd2f 0.431 -41b9b089c0000000 4.31e+08 -404edb6db6db6db7 61.714285714285715 -3fdba5e353f7ced9 0.432 -41b9bfcc00000000 4.32e+08 -404eedb6db6db6db 61.857142857142854 -3fdbb645a1cac083 0.433 -41b9cf0e40000000 4.33e+08 -404f000000000000 62 -3fdbc6a7ef9db22d 0.434 -41b9de5080000000 4.34e+08 -404f124924924925 62.142857142857146 -3fdbd70a3d70a3d7 0.435 -41b9ed92c0000000 4.35e+08 -404f249249249249 62.285714285714285 -3fdbe76c8b439581 0.436 -41b9fcd500000000 4.36e+08 -404f36db6db6db6e 62.42857142857143 -3fdbf7ced916872b 0.437 -41ba0c1740000000 4.37e+08 -404f492492492492 62.57142857142857 -3fdc083126e978d5 0.438 -41ba1b5980000000 4.38e+08 -404f5b6db6db6db7 62.714285714285715 -3fdc189374bc6a7f 0.439 -41ba2a9bc0000000 4.39e+08 -404f6db6db6db6db 62.857142857142854 -3fdc28f5c28f5c29 0.44 -41ba39de00000000 4.4e+08 -404f800000000000 63 -3fdc395810624dd3 0.441 -41ba492040000000 4.41e+08 -404f924924924925 63.142857142857146 -3fdc49ba5e353f7d 0.442 -41ba586280000000 4.42e+08 -404fa49249249249 63.285714285714285 -3fdc5a1cac083127 0.443 -41ba67a4c0000000 4.43e+08 -404fb6db6db6db6e 63.42857142857143 -3fdc6a7ef9db22d1 0.444 -41ba76e700000000 4.44e+08 -404fc92492492492 63.57142857142857 -3fdc7ae147ae147b 0.445 -41ba862940000000 4.45e+08 -404fdb6db6db6db7 63.714285714285715 -3fdc8b4395810625 0.446 -41ba956b80000000 4.46e+08 -404fedb6db6db6db 63.857142857142854 -3fdc9ba5e353f7cf 0.447 -41baa4adc0000000 4.47e+08 -4050000000000000 64 -3fdcac083126e979 0.448 -41bab3f000000000 4.48e+08 -4050092492492492 64.14285714285714 -3fdcbc6a7ef9db23 0.449 -41bac33240000000 4.49e+08 -4050124924924925 64.28571428571429 -3fdccccccccccccd 0.45 -41bad27480000000 4.5e+08 -40501b6db6db6db7 64.42857142857143 -3fdcdd2f1a9fbe77 0.451 -41bae1b6c0000000 4.51e+08 -4050249249249249 64.57142857142857 -3fdced916872b021 0.452 -41baf0f900000000 4.52e+08 -40502db6db6db6db 64.71428571428571 -3fdcfdf3b645a1cb 0.453 -41bb003b40000000 4.53e+08 -405036db6db6db6e 64.85714285714286 -3fdd0e5604189375 0.454 -41bb0f7d80000000 4.54e+08 -4050400000000000 65 -3fdd1eb851eb851f 0.455 -41bb1ebfc0000000 4.55e+08 -4050492492492492 65.14285714285714 -3fdd2f1a9fbe76c9 0.456 -41bb2e0200000000 4.56e+08 -4050524924924925 65.28571428571429 -3fdd3f7ced916873 0.457 -41bb3d4440000000 4.57e+08 -40505b6db6db6db7 65.42857142857143 -3fdd4fdf3b645a1d 0.458 -41bb4c8680000000 4.58e+08 -4050649249249249 65.57142857142857 -3fdd604189374bc7 0.459 -41bb5bc8c0000000 4.59e+08 -40506db6db6db6db 65.71428571428571 -3fdd70a3d70a3d71 0.46 -41bb6b0b00000000 4.6e+08 -405076db6db6db6e 65.85714285714286 -3fdd810624dd2f1b 0.461 -41bb7a4d40000000 4.61e+08 -4050800000000000 66 -3fdd916872b020c5 0.462 -41bb898f80000000 4.62e+08 -4050892492492492 66.14285714285714 -3fdda1cac083126f 0.463 -41bb98d1c0000000 4.63e+08 -4050924924924925 66.28571428571429 -3fddb22d0e560419 0.464 -41bba81400000000 4.64e+08 -40509b6db6db6db7 66.42857142857143 -3fddc28f5c28f5c3 0.465 -41bbb75640000000 4.65e+08 -4050a49249249249 66.57142857142857 -3fddd2f1a9fbe76d 0.466 -41bbc69880000000 4.66e+08 -4050adb6db6db6db 66.71428571428571 -3fdde353f7ced917 0.467 -41bbd5dac0000000 4.67e+08 -4050b6db6db6db6e 66.85714285714286 -3fddf3b645a1cac1 0.468 -41bbe51d00000000 4.68e+08 -4050c00000000000 67 -3fde04189374bc6b 0.46900000000000003 -41bbf45f40000000 4.69e+08 -4050c92492492492 67.14285714285714 -3fde147ae147ae15 0.47000000000000003 -41bc03a180000000 4.7e+08 -4050d24924924925 67.28571428571429 -3fde24dd2f1a9fbf 0.47100000000000003 -41bc12e3c0000000 4.71e+08 -4050db6db6db6db7 67.42857142857143 -3fde353f7ced9169 0.47200000000000003 -41bc222600000000 4.72e+08 -4050e49249249249 67.57142857142857 -3fde45a1cac08313 0.47300000000000003 -41bc316840000000 4.73e+08 -4050edb6db6db6db 67.71428571428571 -3fde5604189374bd 0.47400000000000003 -41bc40aa80000000 4.74e+08 -4050f6db6db6db6e 67.85714285714286 -3fde666666666667 0.47500000000000003 -41bc4fecc0000000 4.75e+08 -4051000000000000 68 -3fde76c8b4395811 0.47600000000000003 -41bc5f2f00000000 4.76e+08 -4051092492492492 68.14285714285714 -3fde872b020c49bb 0.47700000000000004 -41bc6e7140000000 4.77e+08 -4051124924924925 68.28571428571429 -3fde978d4fdf3b65 0.47800000000000004 -41bc7db380000000 4.78e+08 -40511b6db6db6db7 68.42857142857143 -3fdea7ef9db22d0f 0.47900000000000004 -41bc8cf5c0000000 4.79e+08 -4051249249249249 68.57142857142857 -3fdeb851eb851eb8 0.48 -41bc9c3800000000 4.8e+08 -40512db6db6db6db 68.71428571428571 -3fdec8b439581062 0.481 -41bcab7a40000000 4.81e+08 -405136db6db6db6e 68.85714285714286 -3fded916872b020c 0.482 -41bcbabc80000000 4.82e+08 -4051400000000000 69 -3fdee978d4fdf3b6 0.483 -41bcc9fec0000000 4.83e+08 -4051492492492492 69.14285714285714 -3fdef9db22d0e560 0.484 -41bcd94100000000 4.84e+08 -4051524924924925 69.28571428571429 -3fdf0a3d70a3d70a 0.485 -41bce88340000000 4.85e+08 -40515b6db6db6db7 69.42857142857143 -3fdf1a9fbe76c8b4 0.486 -41bcf7c580000000 4.86e+08 -4051649249249249 69.57142857142857 -3fdf2b020c49ba5e 0.487 -41bd0707c0000000 4.87e+08 -40516db6db6db6db 69.71428571428571 -3fdf3b645a1cac08 0.488 -41bd164a00000000 4.88e+08 -405176db6db6db6e 69.85714285714286 -3fdf4bc6a7ef9db2 0.489 -41bd258c40000000 4.89e+08 -4051800000000000 70 -3fdf5c28f5c28f5c 0.49 -41bd34ce80000000 4.9e+08 -4051892492492492 70.14285714285714 -3fdf6c8b43958106 0.491 -41bd4410c0000000 4.91e+08 -4051924924924925 70.28571428571429 -3fdf7ced916872b0 0.492 -41bd535300000000 4.92e+08 -40519b6db6db6db7 70.42857142857143 -3fdf8d4fdf3b645a 0.493 -41bd629540000000 4.93e+08 -4051a49249249249 70.57142857142857 -3fdf9db22d0e5604 0.494 -41bd71d780000000 4.94e+08 -4051adb6db6db6db 70.71428571428571 -3fdfae147ae147ae 0.495 -41bd8119c0000000 4.95e+08 -4051b6db6db6db6e 70.85714285714286 -3fdfbe76c8b43958 0.496 -41bd905c00000000 4.96e+08 -4051c00000000000 71 -3fdfced916872b02 0.497 -41bd9f9e40000000 4.97e+08 -4051c92492492492 71.14285714285714 -3fdfdf3b645a1cac 0.498 -41bdaee080000000 4.98e+08 -4051d24924924925 71.28571428571429 -3fdfef9db22d0e56 0.499 -41bdbe22c0000000 4.99e+08 -4051db6db6db6db7 71.42857142857143 -3fe0000000000000 0.5 -41bdcd6500000000 5e+08 -4051e49249249249 71.57142857142857 -3fe0083126e978d5 0.501 -41bddca740000000 5.01e+08 -4051edb6db6db6db 71.71428571428571 -3fe010624dd2f1aa 0.502 -41bdebe980000000 5.02e+08 -4051f6db6db6db6e 71.85714285714286 -3fe0189374bc6a7f 0.503 -41bdfb2bc0000000 5.03e+08 -4052000000000000 72 -3fe020c49ba5e354 0.504 -41be0a6e00000000 5.04e+08 -4052092492492492 72.14285714285714 -3fe028f5c28f5c29 0.505 -41be19b040000000 5.05e+08 -4052124924924925 72.28571428571429 -3fe03126e978d4fe 0.506 -41be28f280000000 5.06e+08 -40521b6db6db6db7 72.42857142857143 -3fe0395810624dd3 0.507 -41be3834c0000000 5.07e+08 -4052249249249249 72.57142857142857 -3fe04189374bc6a8 0.508 -41be477700000000 5.08e+08 -40522db6db6db6db 72.71428571428571 -3fe049ba5e353f7d 0.509 -41be56b940000000 5.09e+08 -405236db6db6db6e 72.85714285714286 -3fe051eb851eb852 0.51 -41be65fb80000000 5.1e+08 -4052400000000000 73 -3fe05a1cac083127 0.511 -41be753dc0000000 5.11e+08 -4052492492492492 73.14285714285714 -3fe0624dd2f1a9fc 0.512 -41be848000000000 5.12e+08 -4052524924924925 73.28571428571429 -3fe06a7ef9db22d1 0.513 -41be93c240000000 5.13e+08 -40525b6db6db6db7 73.42857142857143 -3fe072b020c49ba6 0.514 -41bea30480000000 5.14e+08 -4052649249249249 73.57142857142857 -3fe07ae147ae147b 0.515 -41beb246c0000000 5.15e+08 -40526db6db6db6db 73.71428571428571 -3fe083126e978d50 0.516 -41bec18900000000 5.16e+08 -405276db6db6db6e 73.85714285714286 -3fe08b4395810625 0.517 -41bed0cb40000000 5.17e+08 -4052800000000000 74 -3fe09374bc6a7efa 0.518 -41bee00d80000000 5.18e+08 -4052892492492492 74.14285714285714 -3fe09ba5e353f7cf 0.519 -41beef4fc0000000 5.19e+08 -4052924924924925 74.28571428571429 -3fe0a3d70a3d70a4 0.52 -41befe9200000000 5.2e+08 -40529b6db6db6db7 74.42857142857143 -3fe0ac083126e979 0.521 -41bf0dd440000000 5.21e+08 -4052a49249249249 74.57142857142857 -3fe0b4395810624e 0.522 -41bf1d1680000000 5.22e+08 -4052adb6db6db6db 74.71428571428571 -3fe0bc6a7ef9db23 0.523 -41bf2c58c0000000 5.23e+08 -4052b6db6db6db6e 74.85714285714286 -3fe0c49ba5e353f8 0.524 -41bf3b9b00000000 5.24e+08 -4052c00000000000 75 -3fe0cccccccccccd 0.525 -41bf4add40000000 5.25e+08 -4052c92492492492 75.14285714285714 -3fe0d4fdf3b645a2 0.526 -41bf5a1f80000000 5.26e+08 -4052d24924924925 75.28571428571429 -3fe0dd2f1a9fbe77 0.527 -41bf6961c0000000 5.27e+08 -4052db6db6db6db7 75.42857142857143 -3fe0e5604189374c 0.528 -41bf78a400000000 5.28e+08 -4052e49249249249 75.57142857142857 -3fe0ed916872b021 0.529 -41bf87e640000000 5.29e+08 -4052edb6db6db6db 75.71428571428571 -3fe0f5c28f5c28f6 0.53 -41bf972880000000 5.3e+08 -4052f6db6db6db6e 75.85714285714286 -3fe0fdf3b645a1cb 0.531 -41bfa66ac0000000 5.31e+08 -4053000000000000 76 -3fe10624dd2f1aa0 0.532 -41bfb5ad00000000 5.32e+08 -4053092492492492 76.14285714285714 -3fe10e5604189375 0.533 -41bfc4ef40000000 5.33e+08 -4053124924924925 76.28571428571429 -3fe116872b020c4a 0.534 -41bfd43180000000 5.34e+08 -40531b6db6db6db7 76.42857142857143 -3fe11eb851eb851f 0.535 -41bfe373c0000000 5.35e+08 -4053249249249249 76.57142857142857 -3fe126e978d4fdf4 0.536 -41bff2b600000000 5.36e+08 -40532db6db6db6db 76.71428571428571 -3fe12f1a9fbe76c9 0.537 -41c000fc20000000 5.37e+08 -405336db6db6db6e 76.85714285714286 -3fe1374bc6a7ef9e 0.538 -41c0089d40000000 5.38e+08 -4053400000000000 77 -3fe13f7ced916873 0.539 -41c0103e60000000 5.39e+08 -4053492492492492 77.14285714285714 -3fe147ae147ae148 0.54 -41c017df80000000 5.4e+08 -4053524924924925 77.28571428571429 -3fe14fdf3b645a1d 0.541 -41c01f80a0000000 5.41e+08 -40535b6db6db6db7 77.42857142857143 -3fe15810624dd2f2 0.542 -41c02721c0000000 5.42e+08 -4053649249249249 77.57142857142857 -3fe1604189374bc7 0.543 -41c02ec2e0000000 5.43e+08 -40536db6db6db6db 77.71428571428571 -3fe16872b020c49c 0.544 -41c0366400000000 5.44e+08 -405376db6db6db6e 77.85714285714286 -3fe170a3d70a3d71 0.545 -41c03e0520000000 5.45e+08 -4053800000000000 78 -3fe178d4fdf3b646 0.546 -41c045a640000000 5.46e+08 -4053892492492492 78.14285714285714 -3fe1810624dd2f1b 0.547 -41c04d4760000000 5.47e+08 -4053924924924925 78.28571428571429 -3fe189374bc6a7f0 0.548 -41c054e880000000 5.48e+08 -40539b6db6db6db7 78.42857142857143 -3fe1916872b020c5 0.549 -41c05c89a0000000 5.49e+08 -4053a49249249249 78.57142857142857 -3fe199999999999a 0.55 -41c0642ac0000000 5.5e+08 -4053adb6db6db6db 78.71428571428571 -3fe1a1cac083126f 0.551 -41c06bcbe0000000 5.51e+08 -4053b6db6db6db6e 78.85714285714286 -3fe1a9fbe76c8b44 0.552 -41c0736d00000000 5.52e+08 -4053c00000000000 79 -3fe1b22d0e560419 0.553 -41c07b0e20000000 5.53e+08 -4053c92492492492 79.14285714285714 -3fe1ba5e353f7cee 0.554 -41c082af40000000 5.54e+08 -4053d24924924925 79.28571428571429 -3fe1c28f5c28f5c3 0.555 -41c08a5060000000 5.55e+08 -4053db6db6db6db7 79.42857142857143 -3fe1cac083126e98 0.556 -41c091f180000000 5.56e+08 -4053e49249249249 79.57142857142857 -3fe1d2f1a9fbe76d 0.557 -41c09992a0000000 5.57e+08 -4053edb6db6db6db 79.71428571428571 -3fe1db22d0e56042 0.558 -41c0a133c0000000 5.58e+08 -4053f6db6db6db6e 79.85714285714286 -3fe1e353f7ced917 0.559 -41c0a8d4e0000000 5.59e+08 -4054000000000000 80 -3fe1eb851eb851ec 0.56 -41c0b07600000000 5.6e+08 -4054092492492492 80.14285714285714 -3fe1f3b645a1cac1 0.561 -41c0b81720000000 5.61e+08 -4054124924924925 80.28571428571429 -3fe1fbe76c8b4396 0.562 -41c0bfb840000000 5.62e+08 -40541b6db6db6db7 80.42857142857143 -3fe204189374bc6b 0.5630000000000001 -41c0c75960000000 5.63e+08 -4054249249249249 80.57142857142857 -3fe20c49ba5e3540 0.5640000000000001 -41c0cefa80000000 5.64e+08 -40542db6db6db6db 80.71428571428571 -3fe2147ae147ae15 0.5650000000000001 -41c0d69ba0000000 5.65e+08 -405436db6db6db6e 80.85714285714286 -3fe21cac083126ea 0.5660000000000001 -41c0de3cc0000000 5.66e+08 -4054400000000000 81 -3fe224dd2f1a9fbf 0.5670000000000001 -41c0e5dde0000000 5.67e+08 -4054492492492492 81.14285714285714 -3fe22d0e56041894 0.5680000000000001 -41c0ed7f00000000 5.68e+08 -4054524924924925 81.28571428571429 -3fe2353f7ced9169 0.5690000000000001 -41c0f52020000000 5.69e+08 -40545b6db6db6db7 81.42857142857143 -3fe23d70a3d70a3e 0.5700000000000001 -41c0fcc140000000 5.7e+08 -4054649249249249 81.57142857142857 -3fe245a1cac08313 0.5710000000000001 -41c1046260000000 5.71e+08 -40546db6db6db6db 81.71428571428571 -3fe24dd2f1a9fbe8 0.5720000000000001 -41c10c0380000000 5.72e+08 -405476db6db6db6e 81.85714285714286 -3fe25604189374bd 0.5730000000000001 -41c113a4a0000000 5.73e+08 -4054800000000000 82 -3fe25e353f7ced92 0.5740000000000001 -41c11b45c0000000 5.74e+08 -4054892492492492 82.14285714285714 -3fe2666666666667 0.5750000000000001 -41c122e6e0000000 5.75e+08 -4054924924924925 82.28571428571429 -3fe26e978d4fdf3c 0.5760000000000001 -41c12a8800000000 5.76e+08 -40549b6db6db6db7 82.42857142857143 -3fe276c8b4395810 0.577 -41c1322920000000 5.77e+08 -4054a49249249249 82.57142857142857 -3fe27ef9db22d0e5 0.578 -41c139ca40000000 5.78e+08 -4054adb6db6db6db 82.71428571428571 -3fe2872b020c49ba 0.579 -41c1416b60000000 5.79e+08 -4054b6db6db6db6e 82.85714285714286 -3fe28f5c28f5c28f 0.58 -41c1490c80000000 5.8e+08 -4054c00000000000 83 -3fe2978d4fdf3b64 0.581 -41c150ada0000000 5.81e+08 -4054c92492492492 83.14285714285714 -3fe29fbe76c8b439 0.582 -41c1584ec0000000 5.82e+08 -4054d24924924925 83.28571428571429 -3fe2a7ef9db22d0e 0.583 -41c15fefe0000000 5.83e+08 -4054db6db6db6db7 83.42857142857143 -3fe2b020c49ba5e3 0.584 -41c1679100000000 5.84e+08 -4054e49249249249 83.57142857142857 -3fe2b851eb851eb8 0.585 -41c16f3220000000 5.85e+08 -4054edb6db6db6db 83.71428571428571 -3fe2c083126e978d 0.586 -41c176d340000000 5.86e+08 -4054f6db6db6db6e 83.85714285714286 -3fe2c8b439581062 0.587 -41c17e7460000000 5.87e+08 -4055000000000000 84 -3fe2d0e560418937 0.588 -41c1861580000000 5.88e+08 -4055092492492492 84.14285714285714 -3fe2d916872b020c 0.589 -41c18db6a0000000 5.89e+08 -4055124924924925 84.28571428571429 -3fe2e147ae147ae1 0.59 -41c19557c0000000 5.9e+08 -40551b6db6db6db7 84.42857142857143 -3fe2e978d4fdf3b6 0.591 -41c19cf8e0000000 5.91e+08 -4055249249249249 84.57142857142857 -3fe2f1a9fbe76c8b 0.592 -41c1a49a00000000 5.92e+08 -40552db6db6db6db 84.71428571428571 -3fe2f9db22d0e560 0.593 -41c1ac3b20000000 5.93e+08 -405536db6db6db6e 84.85714285714286 -3fe3020c49ba5e35 0.594 -41c1b3dc40000000 5.94e+08 -4055400000000000 85 -3fe30a3d70a3d70a 0.595 -41c1bb7d60000000 5.95e+08 -4055492492492492 85.14285714285714 -3fe3126e978d4fdf 0.596 -41c1c31e80000000 5.96e+08 -4055524924924925 85.28571428571429 -3fe31a9fbe76c8b4 0.597 -41c1cabfa0000000 5.97e+08 -40555b6db6db6db7 85.42857142857143 -3fe322d0e5604189 0.598 -41c1d260c0000000 5.98e+08 -4055649249249249 85.57142857142857 -3fe32b020c49ba5e 0.599 -41c1da01e0000000 5.99e+08 -40556db6db6db6db 85.71428571428571 -3fe3333333333333 0.6 -41c1e1a300000000 6e+08 -405576db6db6db6e 85.85714285714286 -3fe33b645a1cac08 0.601 -41c1e94420000000 6.01e+08 -4055800000000000 86 -3fe34395810624dd 0.602 -41c1f0e540000000 6.02e+08 -4055892492492492 86.14285714285714 -3fe34bc6a7ef9db2 0.603 -41c1f88660000000 6.03e+08 -4055924924924925 86.28571428571429 -3fe353f7ced91687 0.604 -41c2002780000000 6.04e+08 -40559b6db6db6db7 86.42857142857143 -3fe35c28f5c28f5c 0.605 -41c207c8a0000000 6.05e+08 -4055a49249249249 86.57142857142857 -3fe3645a1cac0831 0.606 -41c20f69c0000000 6.06e+08 -4055adb6db6db6db 86.71428571428571 -3fe36c8b43958106 0.607 -41c2170ae0000000 6.07e+08 -4055b6db6db6db6e 86.85714285714286 -3fe374bc6a7ef9db 0.608 -41c21eac00000000 6.08e+08 -4055c00000000000 87 -3fe37ced916872b0 0.609 -41c2264d20000000 6.09e+08 -4055c92492492492 87.14285714285714 -3fe3851eb851eb85 0.61 -41c22dee40000000 6.1e+08 -4055d24924924925 87.28571428571429 -3fe38d4fdf3b645a 0.611 -41c2358f60000000 6.11e+08 -4055db6db6db6db7 87.42857142857143 -3fe395810624dd2f 0.612 -41c23d3080000000 6.12e+08 -4055e49249249249 87.57142857142857 -3fe39db22d0e5604 0.613 -41c244d1a0000000 6.13e+08 -4055edb6db6db6db 87.71428571428571 -3fe3a5e353f7ced9 0.614 -41c24c72c0000000 6.14e+08 -4055f6db6db6db6e 87.85714285714286 -3fe3ae147ae147ae 0.615 -41c25413e0000000 6.15e+08 -4056000000000000 88 -3fe3b645a1cac083 0.616 -41c25bb500000000 6.16e+08 -4056092492492492 88.14285714285714 -3fe3be76c8b43958 0.617 -41c2635620000000 6.17e+08 -4056124924924925 88.28571428571429 -3fe3c6a7ef9db22d 0.618 -41c26af740000000 6.18e+08 -40561b6db6db6db7 88.42857142857143 -3fe3ced916872b02 0.619 -41c2729860000000 6.19e+08 -4056249249249249 88.57142857142857 -3fe3d70a3d70a3d7 0.62 -41c27a3980000000 6.2e+08 -40562db6db6db6db 88.71428571428571 -3fe3df3b645a1cac 0.621 -41c281daa0000000 6.21e+08 -405636db6db6db6e 88.85714285714286 -3fe3e76c8b439581 0.622 -41c2897bc0000000 6.22e+08 -4056400000000000 89 -3fe3ef9db22d0e56 0.623 -41c2911ce0000000 6.23e+08 -4056492492492492 89.14285714285714 -3fe3f7ced916872b 0.624 -41c298be00000000 6.24e+08 -4056524924924925 89.28571428571429 -3fe4000000000000 0.625 -41c2a05f20000000 6.25e+08 -40565b6db6db6db7 89.42857142857143 -3fe4083126e978d5 0.626 -41c2a80040000000 6.26e+08 -4056649249249249 89.57142857142857 -3fe410624dd2f1aa 0.627 -41c2afa160000000 6.27e+08 -40566db6db6db6db 89.71428571428571 -3fe4189374bc6a7f 0.628 -41c2b74280000000 6.28e+08 -405676db6db6db6e 89.85714285714286 -3fe420c49ba5e354 0.629 -41c2bee3a0000000 6.29e+08 -4056800000000000 90 -3fe428f5c28f5c29 0.63 -41c2c684c0000000 6.3e+08 -4056892492492492 90.14285714285714 -3fe43126e978d4fe 0.631 -41c2ce25e0000000 6.31e+08 -4056924924924925 90.28571428571429 -3fe4395810624dd3 0.632 -41c2d5c700000000 6.32e+08 -40569b6db6db6db7 90.42857142857143 -3fe44189374bc6a8 0.633 -41c2dd6820000000 6.33e+08 -4056a49249249249 90.57142857142857 -3fe449ba5e353f7d 0.634 -41c2e50940000000 6.34e+08 -4056adb6db6db6db 90.71428571428571 -3fe451eb851eb852 0.635 -41c2ecaa60000000 6.35e+08 -4056b6db6db6db6e 90.85714285714286 -3fe45a1cac083127 0.636 -41c2f44b80000000 6.36e+08 -4056c00000000000 91 -3fe4624dd2f1a9fc 0.637 -41c2fbeca0000000 6.37e+08 -4056c92492492492 91.14285714285714 -3fe46a7ef9db22d1 0.638 -41c3038dc0000000 6.38e+08 -4056d24924924925 91.28571428571429 -3fe472b020c49ba6 0.639 -41c30b2ee0000000 6.39e+08 -4056db6db6db6db7 91.42857142857143 -3fe47ae147ae147b 0.64 -41c312d000000000 6.4e+08 -4056e49249249249 91.57142857142857 -3fe483126e978d50 0.641 -41c31a7120000000 6.41e+08 -4056edb6db6db6db 91.71428571428571 -3fe48b4395810625 0.642 -41c3221240000000 6.42e+08 -4056f6db6db6db6e 91.85714285714286 -3fe49374bc6a7efa 0.643 -41c329b360000000 6.43e+08 -4057000000000000 92 -3fe49ba5e353f7cf 0.644 -41c3315480000000 6.44e+08 -4057092492492492 92.14285714285714 -3fe4a3d70a3d70a4 0.645 -41c338f5a0000000 6.45e+08 -4057124924924925 92.28571428571429 -3fe4ac083126e979 0.646 -41c34096c0000000 6.46e+08 -40571b6db6db6db7 92.42857142857143 -3fe4b4395810624e 0.647 -41c34837e0000000 6.47e+08 -4057249249249249 92.57142857142857 -3fe4bc6a7ef9db23 0.648 -41c34fd900000000 6.48e+08 -40572db6db6db6db 92.71428571428571 -3fe4c49ba5e353f8 0.649 -41c3577a20000000 6.49e+08 -405736db6db6db6e 92.85714285714286 -3fe4cccccccccccd 0.65 -41c35f1b40000000 6.5e+08 -4057400000000000 93 -3fe4d4fdf3b645a2 0.651 -41c366bc60000000 6.51e+08 -4057492492492492 93.14285714285714 -3fe4dd2f1a9fbe77 0.652 -41c36e5d80000000 6.52e+08 -4057524924924925 93.28571428571429 -3fe4e5604189374c 0.653 -41c375fea0000000 6.53e+08 -40575b6db6db6db7 93.42857142857143 -3fe4ed916872b021 0.654 -41c37d9fc0000000 6.54e+08 -4057649249249249 93.57142857142857 -3fe4f5c28f5c28f6 0.655 -41c38540e0000000 6.55e+08 -40576db6db6db6db 93.71428571428571 -3fe4fdf3b645a1cb 0.656 -41c38ce200000000 6.56e+08 -405776db6db6db6e 93.85714285714286 -3fe50624dd2f1aa0 0.657 -41c3948320000000 6.57e+08 -4057800000000000 94 -3fe50e5604189375 0.658 -41c39c2440000000 6.58e+08 -4057892492492492 94.14285714285714 -3fe516872b020c4a 0.659 -41c3a3c560000000 6.59e+08 -4057924924924925 94.28571428571429 -3fe51eb851eb851f 0.66 -41c3ab6680000000 6.6e+08 -40579b6db6db6db7 94.42857142857143 -3fe526e978d4fdf4 0.661 -41c3b307a0000000 6.61e+08 -4057a49249249249 94.57142857142857 -3fe52f1a9fbe76c9 0.662 -41c3baa8c0000000 6.62e+08 -4057adb6db6db6db 94.71428571428571 -3fe5374bc6a7ef9e 0.663 -41c3c249e0000000 6.63e+08 -4057b6db6db6db6e 94.85714285714286 -3fe53f7ced916873 0.664 -41c3c9eb00000000 6.64e+08 -4057c00000000000 95 -3fe547ae147ae148 0.665 -41c3d18c20000000 6.65e+08 -4057c92492492492 95.14285714285714 -3fe54fdf3b645a1d 0.666 -41c3d92d40000000 6.66e+08 -4057d24924924925 95.28571428571429 -3fe55810624dd2f2 0.667 -41c3e0ce60000000 6.67e+08 -4057db6db6db6db7 95.42857142857143 -3fe5604189374bc7 0.668 -41c3e86f80000000 6.68e+08 -4057e49249249249 95.57142857142857 -3fe56872b020c49c 0.669 -41c3f010a0000000 6.69e+08 -4057edb6db6db6db 95.71428571428571 -3fe570a3d70a3d71 0.67 -41c3f7b1c0000000 6.7e+08 -4057f6db6db6db6e 95.85714285714286 -3fe578d4fdf3b646 0.671 -41c3ff52e0000000 6.71e+08 -4058000000000000 96 -3fe5810624dd2f1b 0.672 -41c406f400000000 6.72e+08 -4058092492492492 96.14285714285714 -3fe589374bc6a7f0 0.673 -41c40e9520000000 6.73e+08 -4058124924924925 96.28571428571429 -3fe5916872b020c5 0.674 -41c4163640000000 6.74e+08 -40581b6db6db6db7 96.42857142857143 -3fe599999999999a 0.675 -41c41dd760000000 6.75e+08 -4058249249249249 96.57142857142857 -3fe5a1cac083126f 0.676 -41c4257880000000 6.76e+08 -40582db6db6db6db 96.71428571428571 -3fe5a9fbe76c8b44 0.677 -41c42d19a0000000 6.77e+08 -405836db6db6db6e 96.85714285714286 -3fe5b22d0e560419 0.678 -41c434bac0000000 6.78e+08 -4058400000000000 97 -3fe5ba5e353f7cee 0.679 -41c43c5be0000000 6.79e+08 -4058492492492492 97.14285714285714 -3fe5c28f5c28f5c3 0.68 -41c443fd00000000 6.8e+08 -4058524924924925 97.28571428571429 -3fe5cac083126e98 0.681 -41c44b9e20000000 6.81e+08 -40585b6db6db6db7 97.42857142857143 -3fe5d2f1a9fbe76d 0.682 -41c4533f40000000 6.82e+08 -4058649249249249 97.57142857142857 -3fe5db22d0e56042 0.683 -41c45ae060000000 6.83e+08 -40586db6db6db6db 97.71428571428571 -3fe5e353f7ced917 0.684 -41c4628180000000 6.84e+08 -405876db6db6db6e 97.85714285714286 -3fe5eb851eb851ec 0.685 -41c46a22a0000000 6.85e+08 -4058800000000000 98 -3fe5f3b645a1cac1 0.686 -41c471c3c0000000 6.86e+08 -4058892492492492 98.14285714285714 -3fe5fbe76c8b4396 0.687 -41c47964e0000000 6.87e+08 -4058924924924925 98.28571428571429 -3fe604189374bc6b 0.6880000000000001 -41c4810600000000 6.88e+08 -40589b6db6db6db7 98.42857142857143 -3fe60c49ba5e3540 0.6890000000000001 -41c488a720000000 6.89e+08 -4058a49249249249 98.57142857142857 -3fe6147ae147ae15 0.6900000000000001 -41c4904840000000 6.9e+08 -4058adb6db6db6db 98.71428571428571 -3fe61cac083126ea 0.6910000000000001 -41c497e960000000 6.91e+08 -4058b6db6db6db6e 98.85714285714286 -3fe624dd2f1a9fbf 0.6920000000000001 -41c49f8a80000000 6.92e+08 -4058c00000000000 99 -3fe62d0e56041894 0.6930000000000001 -41c4a72ba0000000 6.93e+08 -4058c92492492492 99.14285714285714 -3fe6353f7ced9169 0.6940000000000001 -41c4aeccc0000000 6.94e+08 -4058d24924924925 99.28571428571429 -3fe63d70a3d70a3e 0.6950000000000001 -41c4b66de0000000 6.95e+08 -4058db6db6db6db7 99.42857142857143 -3fe645a1cac08313 0.6960000000000001 -41c4be0f00000000 6.96e+08 -4058e49249249249 99.57142857142857 -3fe64dd2f1a9fbe8 0.6970000000000001 -41c4c5b020000000 6.97e+08 -4058edb6db6db6db 99.71428571428571 -3fe65604189374bd 0.6980000000000001 -41c4cd5140000000 6.98e+08 -4058f6db6db6db6e 99.85714285714286 -3fe65e353f7ced92 0.6990000000000001 -41c4d4f260000000 6.99e+08 -4059000000000000 100 -3fe6666666666667 0.7000000000000001 -41c4dc9380000000 7e+08 -4059092492492492 100.14285714285714 -3fe66e978d4fdf3c 0.7010000000000001 -41c4e434a0000000 7.01e+08 -4059124924924925 100.28571428571429 -3fe676c8b4395811 0.7020000000000001 -41c4ebd5c0000000 7.02e+08 -40591b6db6db6db7 100.42857142857143 -3fe67ef9db22d0e6 0.7030000000000001 -41c4f376e0000000 7.03e+08 -4059249249249249 100.57142857142857 -3fe6872b020c49ba 0.704 -41c4fb1800000000 7.04e+08 -40592db6db6db6db 100.71428571428571 -3fe68f5c28f5c28f 0.705 -41c502b920000000 7.05e+08 -405936db6db6db6e 100.85714285714286 -3fe6978d4fdf3b64 0.706 -41c50a5a40000000 7.06e+08 -4059400000000000 101 -3fe69fbe76c8b439 0.707 -41c511fb60000000 7.07e+08 -4059492492492492 101.14285714285714 -3fe6a7ef9db22d0e 0.708 -41c5199c80000000 7.08e+08 -4059524924924925 101.28571428571429 -3fe6b020c49ba5e3 0.709 -41c5213da0000000 7.09e+08 -40595b6db6db6db7 101.42857142857143 -3fe6b851eb851eb8 0.71 -41c528dec0000000 7.1e+08 -4059649249249249 101.57142857142857 -3fe6c083126e978d 0.711 -41c5307fe0000000 7.11e+08 -40596db6db6db6db 101.71428571428571 -3fe6c8b439581062 0.712 -41c5382100000000 7.12e+08 -405976db6db6db6e 101.85714285714286 -3fe6d0e560418937 0.713 -41c53fc220000000 7.13e+08 -4059800000000000 102 -3fe6d916872b020c 0.714 -41c5476340000000 7.14e+08 -4059892492492492 102.14285714285714 -3fe6e147ae147ae1 0.715 -41c54f0460000000 7.15e+08 -4059924924924925 102.28571428571429 -3fe6e978d4fdf3b6 0.716 -41c556a580000000 7.16e+08 -40599b6db6db6db7 102.42857142857143 -3fe6f1a9fbe76c8b 0.717 -41c55e46a0000000 7.17e+08 -4059a49249249249 102.57142857142857 -3fe6f9db22d0e560 0.718 -41c565e7c0000000 7.18e+08 -4059adb6db6db6db 102.71428571428571 -3fe7020c49ba5e35 0.719 -41c56d88e0000000 7.19e+08 -4059b6db6db6db6e 102.85714285714286 -3fe70a3d70a3d70a 0.72 -41c5752a00000000 7.2e+08 -4059c00000000000 103 -3fe7126e978d4fdf 0.721 -41c57ccb20000000 7.21e+08 -4059c92492492492 103.14285714285714 -3fe71a9fbe76c8b4 0.722 -41c5846c40000000 7.22e+08 -4059d24924924925 103.28571428571429 -3fe722d0e5604189 0.723 -41c58c0d60000000 7.23e+08 -4059db6db6db6db7 103.42857142857143 -3fe72b020c49ba5e 0.724 -41c593ae80000000 7.24e+08 -4059e49249249249 103.57142857142857 -3fe7333333333333 0.725 -41c59b4fa0000000 7.25e+08 -4059edb6db6db6db 103.71428571428571 -3fe73b645a1cac08 0.726 -41c5a2f0c0000000 7.26e+08 -4059f6db6db6db6e 103.85714285714286 -3fe74395810624dd 0.727 -41c5aa91e0000000 7.27e+08 -405a000000000000 104 -3fe74bc6a7ef9db2 0.728 -41c5b23300000000 7.28e+08 -405a092492492492 104.14285714285714 -3fe753f7ced91687 0.729 -41c5b9d420000000 7.29e+08 -405a124924924925 104.28571428571429 -3fe75c28f5c28f5c 0.73 -41c5c17540000000 7.3e+08 -405a1b6db6db6db7 104.42857142857143 -3fe7645a1cac0831 0.731 -41c5c91660000000 7.31e+08 -405a249249249249 104.57142857142857 -3fe76c8b43958106 0.732 -41c5d0b780000000 7.32e+08 -405a2db6db6db6db 104.71428571428571 -3fe774bc6a7ef9db 0.733 -41c5d858a0000000 7.33e+08 -405a36db6db6db6e 104.85714285714286 -3fe77ced916872b0 0.734 -41c5dff9c0000000 7.34e+08 -405a400000000000 105 -3fe7851eb851eb85 0.735 -41c5e79ae0000000 7.35e+08 -405a492492492492 105.14285714285714 -3fe78d4fdf3b645a 0.736 -41c5ef3c00000000 7.36e+08 -405a524924924925 105.28571428571429 -3fe795810624dd2f 0.737 -41c5f6dd20000000 7.37e+08 -405a5b6db6db6db7 105.42857142857143 -3fe79db22d0e5604 0.738 -41c5fe7e40000000 7.38e+08 -405a649249249249 105.57142857142857 -3fe7a5e353f7ced9 0.739 -41c6061f60000000 7.39e+08 -405a6db6db6db6db 105.71428571428571 -3fe7ae147ae147ae 0.74 -41c60dc080000000 7.4e+08 -405a76db6db6db6e 105.85714285714286 -3fe7b645a1cac083 0.741 -41c61561a0000000 7.41e+08 -405a800000000000 106 -3fe7be76c8b43958 0.742 -41c61d02c0000000 7.42e+08 -405a892492492492 106.14285714285714 -3fe7c6a7ef9db22d 0.743 -41c624a3e0000000 7.43e+08 -405a924924924925 106.28571428571429 -3fe7ced916872b02 0.744 -41c62c4500000000 7.44e+08 -405a9b6db6db6db7 106.42857142857143 -3fe7d70a3d70a3d7 0.745 -41c633e620000000 7.45e+08 -405aa49249249249 106.57142857142857 -3fe7df3b645a1cac 0.746 -41c63b8740000000 7.46e+08 -405aadb6db6db6db 106.71428571428571 -3fe7e76c8b439581 0.747 -41c6432860000000 7.47e+08 -405ab6db6db6db6e 106.85714285714286 -3fe7ef9db22d0e56 0.748 -41c64ac980000000 7.48e+08 -405ac00000000000 107 -3fe7f7ced916872b 0.749 -41c6526aa0000000 7.49e+08 -405ac92492492492 107.14285714285714 -3fe8000000000000 0.75 -41c65a0bc0000000 7.5e+08 -405ad24924924925 107.28571428571429 -3fe8083126e978d5 0.751 -41c661ace0000000 7.51e+08 -405adb6db6db6db7 107.42857142857143 -3fe810624dd2f1aa 0.752 -41c6694e00000000 7.52e+08 -405ae49249249249 107.57142857142857 -3fe8189374bc6a7f 0.753 -41c670ef20000000 7.53e+08 -405aedb6db6db6db 107.71428571428571 -3fe820c49ba5e354 0.754 -41c6789040000000 7.54e+08 -405af6db6db6db6e 107.85714285714286 -3fe828f5c28f5c29 0.755 -41c6803160000000 7.55e+08 -405b000000000000 108 -3fe83126e978d4fe 0.756 -41c687d280000000 7.56e+08 -405b092492492492 108.14285714285714 -3fe8395810624dd3 0.757 -41c68f73a0000000 7.57e+08 -405b124924924925 108.28571428571429 -3fe84189374bc6a8 0.758 -41c69714c0000000 7.58e+08 -405b1b6db6db6db7 108.42857142857143 -3fe849ba5e353f7d 0.759 -41c69eb5e0000000 7.59e+08 -405b249249249249 108.57142857142857 -3fe851eb851eb852 0.76 -41c6a65700000000 7.6e+08 -405b2db6db6db6db 108.71428571428571 -3fe85a1cac083127 0.761 -41c6adf820000000 7.61e+08 -405b36db6db6db6e 108.85714285714286 -3fe8624dd2f1a9fc 0.762 -41c6b59940000000 7.62e+08 -405b400000000000 109 -3fe86a7ef9db22d1 0.763 -41c6bd3a60000000 7.63e+08 -405b492492492492 109.14285714285714 -3fe872b020c49ba6 0.764 -41c6c4db80000000 7.64e+08 -405b524924924925 109.28571428571429 -3fe87ae147ae147b 0.765 -41c6cc7ca0000000 7.65e+08 -405b5b6db6db6db7 109.42857142857143 -3fe883126e978d50 0.766 -41c6d41dc0000000 7.66e+08 -405b649249249249 109.57142857142857 -3fe88b4395810625 0.767 -41c6dbbee0000000 7.67e+08 -405b6db6db6db6db 109.71428571428571 -3fe89374bc6a7efa 0.768 -41c6e36000000000 7.68e+08 -405b76db6db6db6e 109.85714285714286 -3fe89ba5e353f7cf 0.769 -41c6eb0120000000 7.69e+08 -405b800000000000 110 -3fe8a3d70a3d70a4 0.77 -41c6f2a240000000 7.7e+08 -405b892492492492 110.14285714285714 -3fe8ac083126e979 0.771 -41c6fa4360000000 7.71e+08 -405b924924924925 110.28571428571429 -3fe8b4395810624e 0.772 -41c701e480000000 7.72e+08 -405b9b6db6db6db7 110.42857142857143 -3fe8bc6a7ef9db23 0.773 -41c70985a0000000 7.73e+08 -405ba49249249249 110.57142857142857 -3fe8c49ba5e353f8 0.774 -41c71126c0000000 7.74e+08 -405badb6db6db6db 110.71428571428571 -3fe8cccccccccccd 0.775 -41c718c7e0000000 7.75e+08 -405bb6db6db6db6e 110.85714285714286 -3fe8d4fdf3b645a2 0.776 -41c7206900000000 7.76e+08 -405bc00000000000 111 -3fe8dd2f1a9fbe77 0.777 -41c7280a20000000 7.77e+08 -405bc92492492492 111.14285714285714 -3fe8e5604189374c 0.778 -41c72fab40000000 7.78e+08 -405bd24924924925 111.28571428571429 -3fe8ed916872b021 0.779 -41c7374c60000000 7.79e+08 -405bdb6db6db6db7 111.42857142857143 -3fe8f5c28f5c28f6 0.78 -41c73eed80000000 7.8e+08 -405be49249249249 111.57142857142857 -3fe8fdf3b645a1cb 0.781 -41c7468ea0000000 7.81e+08 -405bedb6db6db6db 111.71428571428571 -3fe90624dd2f1aa0 0.782 -41c74e2fc0000000 7.82e+08 -405bf6db6db6db6e 111.85714285714286 -3fe90e5604189375 0.783 -41c755d0e0000000 7.83e+08 -405c000000000000 112 -3fe916872b020c4a 0.784 -41c75d7200000000 7.84e+08 -405c092492492492 112.14285714285714 -3fe91eb851eb851f 0.785 -41c7651320000000 7.85e+08 -405c124924924925 112.28571428571429 -3fe926e978d4fdf4 0.786 -41c76cb440000000 7.86e+08 -405c1b6db6db6db7 112.42857142857143 -3fe92f1a9fbe76c9 0.787 -41c7745560000000 7.87e+08 -405c249249249249 112.57142857142857 -3fe9374bc6a7ef9e 0.788 -41c77bf680000000 7.88e+08 -405c2db6db6db6db 112.71428571428571 -3fe93f7ced916873 0.789 -41c78397a0000000 7.89e+08 -405c36db6db6db6e 112.85714285714286 -3fe947ae147ae148 0.79 -41c78b38c0000000 7.9e+08 -405c400000000000 113 -3fe94fdf3b645a1d 0.791 -41c792d9e0000000 7.91e+08 -405c492492492492 113.14285714285714 -3fe95810624dd2f2 0.792 -41c79a7b00000000 7.92e+08 -405c524924924925 113.28571428571429 -3fe9604189374bc7 0.793 -41c7a21c20000000 7.93e+08 -405c5b6db6db6db7 113.42857142857143 -3fe96872b020c49c 0.794 -41c7a9bd40000000 7.94e+08 -405c649249249249 113.57142857142857 -3fe970a3d70a3d71 0.795 -41c7b15e60000000 7.95e+08 -405c6db6db6db6db 113.71428571428571 -3fe978d4fdf3b646 0.796 -41c7b8ff80000000 7.96e+08 -405c76db6db6db6e 113.85714285714286 -3fe9810624dd2f1b 0.797 -41c7c0a0a0000000 7.97e+08 -405c800000000000 114 -3fe989374bc6a7f0 0.798 -41c7c841c0000000 7.98e+08 -405c892492492492 114.14285714285714 -3fe9916872b020c5 0.799 -41c7cfe2e0000000 7.99e+08 -405c924924924925 114.28571428571429 -3fe999999999999a 0.8 -41c7d78400000000 8e+08 -405c9b6db6db6db7 114.42857142857143 -3fe9a1cac083126f 0.801 -41c7df2520000000 8.01e+08 -405ca49249249249 114.57142857142857 -3fe9a9fbe76c8b44 0.802 -41c7e6c640000000 8.02e+08 -405cadb6db6db6db 114.71428571428571 -3fe9b22d0e560419 0.803 -41c7ee6760000000 8.03e+08 -405cb6db6db6db6e 114.85714285714286 -3fe9ba5e353f7cee 0.804 -41c7f60880000000 8.04e+08 -405cc00000000000 115 -3fe9c28f5c28f5c3 0.805 -41c7fda9a0000000 8.05e+08 -405cc92492492492 115.14285714285714 -3fe9cac083126e98 0.806 -41c8054ac0000000 8.06e+08 -405cd24924924925 115.28571428571429 -3fe9d2f1a9fbe76d 0.807 -41c80cebe0000000 8.07e+08 -405cdb6db6db6db7 115.42857142857143 -3fe9db22d0e56042 0.808 -41c8148d00000000 8.08e+08 -405ce49249249249 115.57142857142857 -3fe9e353f7ced917 0.809 -41c81c2e20000000 8.09e+08 -405cedb6db6db6db 115.71428571428571 -3fe9eb851eb851ec 0.81 -41c823cf40000000 8.1e+08 -405cf6db6db6db6e 115.85714285714286 -3fe9f3b645a1cac1 0.811 -41c82b7060000000 8.11e+08 -405d000000000000 116 -3fe9fbe76c8b4396 0.812 -41c8331180000000 8.12e+08 -405d092492492492 116.14285714285714 -3fea04189374bc6b 0.8130000000000001 -41c83ab2a0000000 8.13e+08 -405d124924924925 116.28571428571429 -3fea0c49ba5e3540 0.8140000000000001 -41c84253c0000000 8.14e+08 -405d1b6db6db6db7 116.42857142857143 -3fea147ae147ae15 0.8150000000000001 -41c849f4e0000000 8.15e+08 -405d249249249249 116.57142857142857 -3fea1cac083126ea 0.8160000000000001 -41c8519600000000 8.16e+08 -405d2db6db6db6db 116.71428571428571 -3fea24dd2f1a9fbf 0.8170000000000001 -41c8593720000000 8.17e+08 -405d36db6db6db6e 116.85714285714286 -3fea2d0e56041894 0.8180000000000001 -41c860d840000000 8.18e+08 -405d400000000000 117 -3fea353f7ced9169 0.8190000000000001 -41c8687960000000 8.19e+08 -405d492492492492 117.14285714285714 -3fea3d70a3d70a3e 0.8200000000000001 -41c8701a80000000 8.2e+08 -405d524924924925 117.28571428571429 -3fea45a1cac08313 0.8210000000000001 -41c877bba0000000 8.21e+08 -405d5b6db6db6db7 117.42857142857143 -3fea4dd2f1a9fbe8 0.8220000000000001 -41c87f5cc0000000 8.22e+08 -405d649249249249 117.57142857142857 -3fea5604189374bd 0.8230000000000001 -41c886fde0000000 8.23e+08 -405d6db6db6db6db 117.71428571428571 -3fea5e353f7ced92 0.8240000000000001 -41c88e9f00000000 8.24e+08 -405d76db6db6db6e 117.85714285714286 -3fea666666666667 0.8250000000000001 -41c8964020000000 8.25e+08 -405d800000000000 118 -3fea6e978d4fdf3c 0.8260000000000001 -41c89de140000000 8.26e+08 -405d892492492492 118.14285714285714 -3fea76c8b4395811 0.8270000000000001 -41c8a58260000000 8.27e+08 -405d924924924925 118.28571428571429 -3fea7ef9db22d0e6 0.8280000000000001 -41c8ad2380000000 8.28e+08 -405d9b6db6db6db7 118.42857142857143 -3fea872b020c49bb 0.8290000000000001 -41c8b4c4a0000000 8.29e+08 -405da49249249249 118.57142857142857 -3fea8f5c28f5c290 0.8300000000000001 -41c8bc65c0000000 8.3e+08 -405dadb6db6db6db 118.71428571428571 -3fea978d4fdf3b65 0.8310000000000001 -41c8c406e0000000 8.31e+08 -405db6db6db6db6e 118.85714285714286 -3fea9fbe76c8b43a 0.8320000000000001 -41c8cba800000000 8.32e+08 -405dc00000000000 119 -3feaa7ef9db22d0e 0.833 -41c8d34920000000 8.33e+08 -405dc92492492492 119.14285714285714 -3feab020c49ba5e3 0.834 -41c8daea40000000 8.34e+08 -405dd24924924925 119.28571428571429 -3feab851eb851eb8 0.835 -41c8e28b60000000 8.35e+08 -405ddb6db6db6db7 119.42857142857143 -3feac083126e978d 0.836 -41c8ea2c80000000 8.36e+08 -405de49249249249 119.57142857142857 -3feac8b439581062 0.837 -41c8f1cda0000000 8.37e+08 -405dedb6db6db6db 119.71428571428571 -3fead0e560418937 0.838 -41c8f96ec0000000 8.38e+08 -405df6db6db6db6e 119.85714285714286 -3fead916872b020c 0.839 -41c9010fe0000000 8.39e+08 -405e000000000000 120 -3feae147ae147ae1 0.84 -41c908b100000000 8.4e+08 -405e092492492492 120.14285714285714 -3feae978d4fdf3b6 0.841 -41c9105220000000 8.41e+08 -405e124924924925 120.28571428571429 -3feaf1a9fbe76c8b 0.842 -41c917f340000000 8.42e+08 -405e1b6db6db6db7 120.42857142857143 -3feaf9db22d0e560 0.843 -41c91f9460000000 8.43e+08 -405e249249249249 120.57142857142857 -3feb020c49ba5e35 0.844 -41c9273580000000 8.44e+08 -405e2db6db6db6db 120.71428571428571 -3feb0a3d70a3d70a 0.845 -41c92ed6a0000000 8.45e+08 -405e36db6db6db6e 120.85714285714286 -3feb126e978d4fdf 0.846 -41c93677c0000000 8.46e+08 -405e400000000000 121 -3feb1a9fbe76c8b4 0.847 -41c93e18e0000000 8.47e+08 -405e492492492492 121.14285714285714 -3feb22d0e5604189 0.848 -41c945ba00000000 8.48e+08 -405e524924924925 121.28571428571429 -3feb2b020c49ba5e 0.849 -41c94d5b20000000 8.49e+08 -405e5b6db6db6db7 121.42857142857143 -3feb333333333333 0.85 -41c954fc40000000 8.5e+08 -405e649249249249 121.57142857142857 -3feb3b645a1cac08 0.851 -41c95c9d60000000 8.51e+08 -405e6db6db6db6db 121.71428571428571 -3feb4395810624dd 0.852 -41c9643e80000000 8.52e+08 -405e76db6db6db6e 121.85714285714286 -3feb4bc6a7ef9db2 0.853 -41c96bdfa0000000 8.53e+08 -405e800000000000 122 -3feb53f7ced91687 0.854 -41c97380c0000000 8.54e+08 -405e892492492492 122.14285714285714 -3feb5c28f5c28f5c 0.855 -41c97b21e0000000 8.55e+08 -405e924924924925 122.28571428571429 -3feb645a1cac0831 0.856 -41c982c300000000 8.56e+08 -405e9b6db6db6db7 122.42857142857143 -3feb6c8b43958106 0.857 -41c98a6420000000 8.57e+08 -405ea49249249249 122.57142857142857 -3feb74bc6a7ef9db 0.858 -41c9920540000000 8.58e+08 -405eadb6db6db6db 122.71428571428571 -3feb7ced916872b0 0.859 -41c999a660000000 8.59e+08 -405eb6db6db6db6e 122.85714285714286 -3feb851eb851eb85 0.86 -41c9a14780000000 8.6e+08 -405ec00000000000 123 -3feb8d4fdf3b645a 0.861 -41c9a8e8a0000000 8.61e+08 -405ec92492492492 123.14285714285714 -3feb95810624dd2f 0.862 -41c9b089c0000000 8.62e+08 -405ed24924924925 123.28571428571429 -3feb9db22d0e5604 0.863 -41c9b82ae0000000 8.63e+08 -405edb6db6db6db7 123.42857142857143 -3feba5e353f7ced9 0.864 -41c9bfcc00000000 8.64e+08 -405ee49249249249 123.57142857142857 -3febae147ae147ae 0.865 -41c9c76d20000000 8.65e+08 -405eedb6db6db6db 123.71428571428571 -3febb645a1cac083 0.866 -41c9cf0e40000000 8.66e+08 -405ef6db6db6db6e 123.85714285714286 -3febbe76c8b43958 0.867 -41c9d6af60000000 8.67e+08 -405f000000000000 124 -3febc6a7ef9db22d 0.868 -41c9de5080000000 8.68e+08 -405f092492492492 124.14285714285714 -3febced916872b02 0.869 -41c9e5f1a0000000 8.69e+08 -405f124924924925 124.28571428571429 -3febd70a3d70a3d7 0.87 -41c9ed92c0000000 8.7e+08 -405f1b6db6db6db7 124.42857142857143 -3febdf3b645a1cac 0.871 -41c9f533e0000000 8.71e+08 -405f249249249249 124.57142857142857 -3febe76c8b439581 0.872 -41c9fcd500000000 8.72e+08 -405f2db6db6db6db 124.71428571428571 -3febef9db22d0e56 0.873 -41ca047620000000 8.73e+08 -405f36db6db6db6e 124.85714285714286 -3febf7ced916872b 0.874 -41ca0c1740000000 8.74e+08 -405f400000000000 125 -3fec000000000000 0.875 -41ca13b860000000 8.75e+08 -405f492492492492 125.14285714285714 -3fec083126e978d5 0.876 -41ca1b5980000000 8.76e+08 -405f524924924925 125.28571428571429 -3fec10624dd2f1aa 0.877 -41ca22faa0000000 8.77e+08 -405f5b6db6db6db7 125.42857142857143 -3fec189374bc6a7f 0.878 -41ca2a9bc0000000 8.78e+08 -405f649249249249 125.57142857142857 -3fec20c49ba5e354 0.879 -41ca323ce0000000 8.79e+08 -405f6db6db6db6db 125.71428571428571 -3fec28f5c28f5c29 0.88 -41ca39de00000000 8.8e+08 -405f76db6db6db6e 125.85714285714286 -3fec3126e978d4fe 0.881 -41ca417f20000000 8.81e+08 -405f800000000000 126 -3fec395810624dd3 0.882 -41ca492040000000 8.82e+08 -405f892492492492 126.14285714285714 -3fec4189374bc6a8 0.883 -41ca50c160000000 8.83e+08 -405f924924924925 126.28571428571429 -3fec49ba5e353f7d 0.884 -41ca586280000000 8.84e+08 -405f9b6db6db6db7 126.42857142857143 -3fec51eb851eb852 0.885 -41ca6003a0000000 8.85e+08 -405fa49249249249 126.57142857142857 -3fec5a1cac083127 0.886 -41ca67a4c0000000 8.86e+08 -405fadb6db6db6db 126.71428571428571 -3fec624dd2f1a9fc 0.887 -41ca6f45e0000000 8.87e+08 -405fb6db6db6db6e 126.85714285714286 -3fec6a7ef9db22d1 0.888 -41ca76e700000000 8.88e+08 -405fc00000000000 127 -3fec72b020c49ba6 0.889 -41ca7e8820000000 8.89e+08 -405fc92492492492 127.14285714285714 -3fec7ae147ae147b 0.89 -41ca862940000000 8.9e+08 -405fd24924924925 127.28571428571429 -3fec83126e978d50 0.891 -41ca8dca60000000 8.91e+08 -405fdb6db6db6db7 127.42857142857143 -3fec8b4395810625 0.892 -41ca956b80000000 8.92e+08 -405fe49249249249 127.57142857142857 -3fec9374bc6a7efa 0.893 -41ca9d0ca0000000 8.93e+08 -405fedb6db6db6db 127.71428571428571 -3fec9ba5e353f7cf 0.894 -41caa4adc0000000 8.94e+08 -405ff6db6db6db6e 127.85714285714286 -3feca3d70a3d70a4 0.895 -41caac4ee0000000 8.95e+08 -4060000000000000 128 -3fecac083126e979 0.896 -41cab3f000000000 8.96e+08 -4060049249249249 128.14285714285714 -3fecb4395810624e 0.897 -41cabb9120000000 8.97e+08 -4060092492492492 128.28571428571428 -3fecbc6a7ef9db23 0.898 -41cac33240000000 8.98e+08 -40600db6db6db6db 128.42857142857142 -3fecc49ba5e353f8 0.899 -41cacad360000000 8.99e+08 -4060124924924925 128.57142857142858 -3feccccccccccccd 0.9 -41cad27480000000 9e+08 -406016db6db6db6e 128.71428571428572 -3fecd4fdf3b645a2 0.901 -41cada15a0000000 9.01e+08 -40601b6db6db6db7 128.85714285714286 -3fecdd2f1a9fbe77 0.902 -41cae1b6c0000000 9.02e+08 -4060200000000000 129 -3fece5604189374c 0.903 -41cae957e0000000 9.03e+08 -4060249249249249 129.14285714285714 -3feced916872b021 0.904 -41caf0f900000000 9.04e+08 -4060292492492492 129.28571428571428 -3fecf5c28f5c28f6 0.905 -41caf89a20000000 9.05e+08 -40602db6db6db6db 129.42857142857142 -3fecfdf3b645a1cb 0.906 -41cb003b40000000 9.06e+08 -4060324924924925 129.57142857142858 -3fed0624dd2f1aa0 0.907 -41cb07dc60000000 9.07e+08 -406036db6db6db6e 129.71428571428572 -3fed0e5604189375 0.908 -41cb0f7d80000000 9.08e+08 -40603b6db6db6db7 129.85714285714286 -3fed16872b020c4a 0.909 -41cb171ea0000000 9.09e+08 -4060400000000000 130 -3fed1eb851eb851f 0.91 -41cb1ebfc0000000 9.1e+08 -4060449249249249 130.14285714285714 -3fed26e978d4fdf4 0.911 -41cb2660e0000000 9.11e+08 -4060492492492492 130.28571428571428 -3fed2f1a9fbe76c9 0.912 -41cb2e0200000000 9.12e+08 -40604db6db6db6db 130.42857142857142 -3fed374bc6a7ef9e 0.913 -41cb35a320000000 9.13e+08 -4060524924924925 130.57142857142858 -3fed3f7ced916873 0.914 -41cb3d4440000000 9.14e+08 -406056db6db6db6e 130.71428571428572 -3fed47ae147ae148 0.915 -41cb44e560000000 9.15e+08 -40605b6db6db6db7 130.85714285714286 -3fed4fdf3b645a1d 0.916 -41cb4c8680000000 9.16e+08 -4060600000000000 131 -3fed5810624dd2f2 0.917 -41cb5427a0000000 9.17e+08 -4060649249249249 131.14285714285714 -3fed604189374bc7 0.918 -41cb5bc8c0000000 9.18e+08 -4060692492492492 131.28571428571428 -3fed6872b020c49c 0.919 -41cb6369e0000000 9.19e+08 -40606db6db6db6db 131.42857142857142 -3fed70a3d70a3d71 0.92 -41cb6b0b00000000 9.2e+08 -4060724924924925 131.57142857142858 -3fed78d4fdf3b646 0.921 -41cb72ac20000000 9.21e+08 -406076db6db6db6e 131.71428571428572 -3fed810624dd2f1b 0.922 -41cb7a4d40000000 9.22e+08 -40607b6db6db6db7 131.85714285714286 -3fed89374bc6a7f0 0.923 -41cb81ee60000000 9.23e+08 -4060800000000000 132 -3fed916872b020c5 0.924 -41cb898f80000000 9.24e+08 -4060849249249249 132.14285714285714 -3fed99999999999a 0.925 -41cb9130a0000000 9.25e+08 -4060892492492492 132.28571428571428 -3feda1cac083126f 0.926 -41cb98d1c0000000 9.26e+08 -40608db6db6db6db 132.42857142857142 -3feda9fbe76c8b44 0.927 -41cba072e0000000 9.27e+08 -4060924924924925 132.57142857142858 -3fedb22d0e560419 0.928 -41cba81400000000 9.28e+08 -406096db6db6db6e 132.71428571428572 -3fedba5e353f7cee 0.929 -41cbafb520000000 9.29e+08 -40609b6db6db6db7 132.85714285714286 -3fedc28f5c28f5c3 0.93 -41cbb75640000000 9.3e+08 -4060a00000000000 133 -3fedcac083126e98 0.931 -41cbbef760000000 9.31e+08 -4060a49249249249 133.14285714285714 -3fedd2f1a9fbe76d 0.932 -41cbc69880000000 9.32e+08 -4060a92492492492 133.28571428571428 -3feddb22d0e56042 0.933 -41cbce39a0000000 9.33e+08 -4060adb6db6db6db 133.42857142857142 -3fede353f7ced917 0.934 -41cbd5dac0000000 9.34e+08 -4060b24924924925 133.57142857142858 -3fedeb851eb851ec 0.935 -41cbdd7be0000000 9.35e+08 -4060b6db6db6db6e 133.71428571428572 -3fedf3b645a1cac1 0.936 -41cbe51d00000000 9.36e+08 -4060bb6db6db6db7 133.85714285714286 -3fedfbe76c8b4396 0.937 -41cbecbe20000000 9.37e+08 -4060c00000000000 134 -3fee04189374bc6b 0.9380000000000001 -41cbf45f40000000 9.38e+08 -4060c49249249249 134.14285714285714 -3fee0c49ba5e3540 0.9390000000000001 -41cbfc0060000000 9.39e+08 -4060c92492492492 134.28571428571428 -3fee147ae147ae15 0.9400000000000001 -41cc03a180000000 9.4e+08 -4060cdb6db6db6db 134.42857142857142 -3fee1cac083126ea 0.9410000000000001 -41cc0b42a0000000 9.41e+08 -4060d24924924925 134.57142857142858 -3fee24dd2f1a9fbf 0.9420000000000001 -41cc12e3c0000000 9.42e+08 -4060d6db6db6db6e 134.71428571428572 -3fee2d0e56041894 0.9430000000000001 -41cc1a84e0000000 9.43e+08 -4060db6db6db6db7 134.85714285714286 -3fee353f7ced9169 0.9440000000000001 -41cc222600000000 9.44e+08 -4060e00000000000 135 -3fee3d70a3d70a3e 0.9450000000000001 -41cc29c720000000 9.45e+08 -4060e49249249249 135.14285714285714 -3fee45a1cac08313 0.9460000000000001 -41cc316840000000 9.46e+08 -4060e92492492492 135.28571428571428 -3fee4dd2f1a9fbe8 0.9470000000000001 -41cc390960000000 9.47e+08 -4060edb6db6db6db 135.42857142857142 -3fee5604189374bd 0.9480000000000001 -41cc40aa80000000 9.48e+08 -4060f24924924925 135.57142857142858 -3fee5e353f7ced92 0.9490000000000001 -41cc484ba0000000 9.49e+08 -4060f6db6db6db6e 135.71428571428572 -3fee666666666667 0.9500000000000001 -41cc4fecc0000000 9.5e+08 -4060fb6db6db6db7 135.85714285714286 -3fee6e978d4fdf3c 0.9510000000000001 -41cc578de0000000 9.51e+08 -4061000000000000 136 -3fee76c8b4395811 0.9520000000000001 -41cc5f2f00000000 9.52e+08 -4061049249249249 136.14285714285714 -3fee7ef9db22d0e6 0.9530000000000001 -41cc66d020000000 9.53e+08 -4061092492492492 136.28571428571428 -3fee872b020c49bb 0.9540000000000001 -41cc6e7140000000 9.54e+08 -40610db6db6db6db 136.42857142857142 -3fee8f5c28f5c290 0.9550000000000001 -41cc761260000000 9.55e+08 -4061124924924925 136.57142857142858 -3fee978d4fdf3b65 0.9560000000000001 -41cc7db380000000 9.56e+08 -406116db6db6db6e 136.71428571428572 -3fee9fbe76c8b43a 0.9570000000000001 -41cc8554a0000000 9.57e+08 -40611b6db6db6db7 136.85714285714286 -3feea7ef9db22d0f 0.9580000000000001 -41cc8cf5c0000000 9.58e+08 -4061200000000000 137 -3feeb020c49ba5e4 0.9590000000000001 -41cc9496e0000000 9.59e+08 -4061249249249249 137.14285714285714 -3feeb851eb851eb8 0.96 -41cc9c3800000000 9.6e+08 -4061292492492492 137.28571428571428 -3feec083126e978d 0.961 -41cca3d920000000 9.61e+08 -40612db6db6db6db 137.42857142857142 -3feec8b439581062 0.962 -41ccab7a40000000 9.62e+08 -4061324924924925 137.57142857142858 -3feed0e560418937 0.963 -41ccb31b60000000 9.63e+08 -406136db6db6db6e 137.71428571428572 -3feed916872b020c 0.964 -41ccbabc80000000 9.64e+08 -40613b6db6db6db7 137.85714285714286 -3feee147ae147ae1 0.965 -41ccc25da0000000 9.65e+08 -4061400000000000 138 -3feee978d4fdf3b6 0.966 -41ccc9fec0000000 9.66e+08 -4061449249249249 138.14285714285714 -3feef1a9fbe76c8b 0.967 -41ccd19fe0000000 9.67e+08 -4061492492492492 138.28571428571428 -3feef9db22d0e560 0.968 -41ccd94100000000 9.68e+08 -40614db6db6db6db 138.42857142857142 -3fef020c49ba5e35 0.969 -41cce0e220000000 9.69e+08 -4061524924924925 138.57142857142858 -3fef0a3d70a3d70a 0.97 -41cce88340000000 9.7e+08 -406156db6db6db6e 138.71428571428572 -3fef126e978d4fdf 0.971 -41ccf02460000000 9.71e+08 -40615b6db6db6db7 138.85714285714286 -3fef1a9fbe76c8b4 0.972 -41ccf7c580000000 9.72e+08 -4061600000000000 139 -3fef22d0e5604189 0.973 -41ccff66a0000000 9.73e+08 -4061649249249249 139.14285714285714 -3fef2b020c49ba5e 0.974 -41cd0707c0000000 9.74e+08 -4061692492492492 139.28571428571428 -3fef333333333333 0.975 -41cd0ea8e0000000 9.75e+08 -40616db6db6db6db 139.42857142857142 -3fef3b645a1cac08 0.976 -41cd164a00000000 9.76e+08 -4061724924924925 139.57142857142858 -3fef4395810624dd 0.977 -41cd1deb20000000 9.77e+08 -406176db6db6db6e 139.71428571428572 -3fef4bc6a7ef9db2 0.978 -41cd258c40000000 9.78e+08 -40617b6db6db6db7 139.85714285714286 -3fef53f7ced91687 0.979 -41cd2d2d60000000 9.79e+08 -4061800000000000 140 -3fef5c28f5c28f5c 0.98 -41cd34ce80000000 9.8e+08 -4061849249249249 140.14285714285714 -3fef645a1cac0831 0.981 -41cd3c6fa0000000 9.81e+08 -4061892492492492 140.28571428571428 -3fef6c8b43958106 0.982 -41cd4410c0000000 9.82e+08 -40618db6db6db6db 140.42857142857142 -3fef74bc6a7ef9db 0.983 -41cd4bb1e0000000 9.83e+08 -4061924924924925 140.57142857142858 -3fef7ced916872b0 0.984 -41cd535300000000 9.84e+08 -406196db6db6db6e 140.71428571428572 -3fef851eb851eb85 0.985 -41cd5af420000000 9.85e+08 -40619b6db6db6db7 140.85714285714286 -3fef8d4fdf3b645a 0.986 -41cd629540000000 9.86e+08 -4061a00000000000 141 -3fef95810624dd2f 0.987 -41cd6a3660000000 9.87e+08 -4061a49249249249 141.14285714285714 -3fef9db22d0e5604 0.988 -41cd71d780000000 9.88e+08 -4061a92492492492 141.28571428571428 -3fefa5e353f7ced9 0.989 -41cd7978a0000000 9.89e+08 -4061adb6db6db6db 141.42857142857142 -3fefae147ae147ae 0.99 -41cd8119c0000000 9.9e+08 -4061b24924924925 141.57142857142858 -3fefb645a1cac083 0.991 -41cd88bae0000000 9.91e+08 -4061b6db6db6db6e 141.71428571428572 -3fefbe76c8b43958 0.992 -41cd905c00000000 9.92e+08 -4061bb6db6db6db7 141.85714285714286 -3fefc6a7ef9db22d 0.993 -41cd97fd20000000 9.93e+08 -4061c00000000000 142 -3fefced916872b02 0.994 -41cd9f9e40000000 9.94e+08 -4061c49249249249 142.14285714285714 -3fefd70a3d70a3d7 0.995 -41cda73f60000000 9.95e+08 -4061c92492492492 142.28571428571428 -3fefdf3b645a1cac 0.996 -41cdaee080000000 9.96e+08 -4061cdb6db6db6db 142.42857142857142 -3fefe76c8b439581 0.997 -41cdb681a0000000 9.97e+08 -4061d24924924925 142.57142857142858 -3fefef9db22d0e56 0.998 -41cdbe22c0000000 9.98e+08 -4061d6db6db6db6e 142.71428571428572 -3feff7ced916872b 0.999 -41cdc5c3e0000000 9.99e+08 -4061db6db6db6db7 142.85714285714286 -3ff0000000000000 1 -41cdcd6500000000 1e+09 -4061e00000000000 143 -3ff004189374bc6b 1.0010000000000001 -41cdd50620000000 1.001e+09 -4061e49249249249 143.14285714285714 -3ff0083126e978d5 1.002 -41cddca740000000 1.002e+09 -4061e92492492492 143.28571428571428 -3ff00c49ba5e3540 1.0030000000000001 -41cde44860000000 1.003e+09 -4061edb6db6db6db 143.42857142857142 -3ff010624dd2f1aa 1.004 -41cdebe980000000 1.004e+09 -4061f24924924925 143.57142857142858 -3ff0147ae147ae15 1.0050000000000001 -41cdf38aa0000000 1.005e+09 -4061f6db6db6db6e 143.71428571428572 -3ff0189374bc6a7f 1.006 -41cdfb2bc0000000 1.006e+09 -4061fb6db6db6db7 143.85714285714286 -3ff01cac083126ea 1.0070000000000001 -41ce02cce0000000 1.007e+09 -4062000000000000 144 -3ff020c49ba5e354 1.008 -41ce0a6e00000000 1.008e+09 -4062049249249249 144.14285714285714 -3ff024dd2f1a9fbf 1.0090000000000001 -41ce120f20000000 1.009e+09 -4062092492492492 144.28571428571428 -3ff028f5c28f5c29 1.01 -41ce19b040000000 1.01e+09 -40620db6db6db6db 144.42857142857142 -3ff02d0e56041894 1.0110000000000001 -41ce215160000000 1.011e+09 -4062124924924925 144.57142857142858 -3ff03126e978d4fe 1.012 -41ce28f280000000 1.012e+09 -406216db6db6db6e 144.71428571428572 -3ff0353f7ced9169 1.0130000000000001 -41ce3093a0000000 1.013e+09 -40621b6db6db6db7 144.85714285714286 -3ff0395810624dd3 1.014 -41ce3834c0000000 1.014e+09 -4062200000000000 145 -3ff03d70a3d70a3e 1.0150000000000001 -41ce3fd5e0000000 1.015e+09 -4062249249249249 145.14285714285714 -3ff04189374bc6a8 1.016 -41ce477700000000 1.016e+09 -4062292492492492 145.28571428571428 -3ff045a1cac08313 1.0170000000000001 -41ce4f1820000000 1.017e+09 -40622db6db6db6db 145.42857142857142 -3ff049ba5e353f7d 1.018 -41ce56b940000000 1.018e+09 -4062324924924925 145.57142857142858 -3ff04dd2f1a9fbe8 1.0190000000000001 -41ce5e5a60000000 1.019e+09 -406236db6db6db6e 145.71428571428572 -3ff051eb851eb852 1.02 -41ce65fb80000000 1.02e+09 -40623b6db6db6db7 145.85714285714286 -3ff05604189374bd 1.0210000000000001 -41ce6d9ca0000000 1.021e+09 -4062400000000000 146 -3ff05a1cac083127 1.022 -41ce753dc0000000 1.022e+09 -4062449249249249 146.14285714285714 -3ff05e353f7ced92 1.0230000000000001 -41ce7cdee0000000 1.023e+09 -4062492492492492 146.28571428571428 -3ff0624dd2f1a9fc 1.024 -41ce848000000000 1.024e+09 -40624db6db6db6db 146.42857142857142 -3ff0666666666666 1.025 -41ce8c2120000000 1.025e+09 -4062524924924925 146.57142857142858 -3ff06a7ef9db22d1 1.026 -41ce93c240000000 1.026e+09 -406256db6db6db6e 146.71428571428572 -3ff06e978d4fdf3b 1.027 -41ce9b6360000000 1.027e+09 -40625b6db6db6db7 146.85714285714286 -3ff072b020c49ba6 1.028 -41cea30480000000 1.028e+09 -4062600000000000 147 -3ff076c8b4395810 1.029 -41ceaaa5a0000000 1.029e+09 -4062649249249249 147.14285714285714 -3ff07ae147ae147b 1.03 -41ceb246c0000000 1.03e+09 -4062692492492492 147.28571428571428 -3ff07ef9db22d0e5 1.031 -41ceb9e7e0000000 1.031e+09 -40626db6db6db6db 147.42857142857142 -3ff083126e978d50 1.032 -41cec18900000000 1.032e+09 -4062724924924925 147.57142857142858 -3ff0872b020c49ba 1.033 -41cec92a20000000 1.033e+09 -406276db6db6db6e 147.71428571428572 -3ff08b4395810625 1.034 -41ced0cb40000000 1.034e+09 -40627b6db6db6db7 147.85714285714286 -3ff08f5c28f5c28f 1.035 -41ced86c60000000 1.035e+09 -4062800000000000 148 -3ff09374bc6a7efa 1.036 -41cee00d80000000 1.036e+09 -4062849249249249 148.14285714285714 -3ff0978d4fdf3b64 1.037 -41cee7aea0000000 1.037e+09 -4062892492492492 148.28571428571428 -3ff09ba5e353f7cf 1.038 -41ceef4fc0000000 1.038e+09 -40628db6db6db6db 148.42857142857142 -3ff09fbe76c8b439 1.039 -41cef6f0e0000000 1.039e+09 -4062924924924925 148.57142857142858 -3ff0a3d70a3d70a4 1.04 -41cefe9200000000 1.04e+09 -406296db6db6db6e 148.71428571428572 -3ff0a7ef9db22d0e 1.041 -41cf063320000000 1.041e+09 -40629b6db6db6db7 148.85714285714286 -3ff0ac083126e979 1.042 -41cf0dd440000000 1.042e+09 -4062a00000000000 149 -3ff0b020c49ba5e3 1.043 -41cf157560000000 1.043e+09 -4062a49249249249 149.14285714285714 -3ff0b4395810624e 1.044 -41cf1d1680000000 1.044e+09 -4062a92492492492 149.28571428571428 -3ff0b851eb851eb8 1.045 -41cf24b7a0000000 1.045e+09 -4062adb6db6db6db 149.42857142857142 -3ff0bc6a7ef9db23 1.046 -41cf2c58c0000000 1.046e+09 -4062b24924924925 149.57142857142858 -3ff0c083126e978d 1.047 -41cf33f9e0000000 1.047e+09 -4062b6db6db6db6e 149.71428571428572 -3ff0c49ba5e353f8 1.048 -41cf3b9b00000000 1.048e+09 -4062bb6db6db6db7 149.85714285714286 -3ff0c8b439581062 1.049 -41cf433c20000000 1.049e+09 -4062c00000000000 150 -3ff0cccccccccccd 1.05 -41cf4add40000000 1.05e+09 -4062c49249249249 150.14285714285714 -3ff0d0e560418937 1.051 -41cf527e60000000 1.051e+09 -4062c92492492492 150.28571428571428 -3ff0d4fdf3b645a2 1.052 -41cf5a1f80000000 1.052e+09 -4062cdb6db6db6db 150.42857142857142 -3ff0d916872b020c 1.053 -41cf61c0a0000000 1.053e+09 -4062d24924924925 150.57142857142858 -3ff0dd2f1a9fbe77 1.054 -41cf6961c0000000 1.054e+09 -4062d6db6db6db6e 150.71428571428572 -3ff0e147ae147ae1 1.055 -41cf7102e0000000 1.055e+09 -4062db6db6db6db7 150.85714285714286 -3ff0e5604189374c 1.056 -41cf78a400000000 1.056e+09 -4062e00000000000 151 -3ff0e978d4fdf3b6 1.057 -41cf804520000000 1.057e+09 -4062e49249249249 151.14285714285714 -3ff0ed916872b021 1.058 -41cf87e640000000 1.058e+09 -4062e92492492492 151.28571428571428 -3ff0f1a9fbe76c8b 1.059 -41cf8f8760000000 1.059e+09 -4062edb6db6db6db 151.42857142857142 -3ff0f5c28f5c28f6 1.06 -41cf972880000000 1.06e+09 -4062f24924924925 151.57142857142858 -3ff0f9db22d0e560 1.061 -41cf9ec9a0000000 1.061e+09 -4062f6db6db6db6e 151.71428571428572 -3ff0fdf3b645a1cb 1.062 -41cfa66ac0000000 1.062e+09 -4062fb6db6db6db7 151.85714285714286 -3ff1020c49ba5e35 1.063 -41cfae0be0000000 1.063e+09 -4063000000000000 152 -3ff10624dd2f1aa0 1.064 -41cfb5ad00000000 1.064e+09 -4063049249249249 152.14285714285714 -3ff10a3d70a3d70a 1.065 -41cfbd4e20000000 1.065e+09 -4063092492492492 152.28571428571428 -3ff10e5604189375 1.066 -41cfc4ef40000000 1.066e+09 -40630db6db6db6db 152.42857142857142 -3ff1126e978d4fdf 1.067 -41cfcc9060000000 1.067e+09 -4063124924924925 152.57142857142858 -3ff116872b020c4a 1.068 -41cfd43180000000 1.068e+09 -406316db6db6db6e 152.71428571428572 -3ff11a9fbe76c8b4 1.069 -41cfdbd2a0000000 1.069e+09 -40631b6db6db6db7 152.85714285714286 -3ff11eb851eb851f 1.07 -41cfe373c0000000 1.07e+09 -4063200000000000 153 -3ff122d0e5604189 1.071 -41cfeb14e0000000 1.071e+09 -4063249249249249 153.14285714285714 -3ff126e978d4fdf4 1.072 -41cff2b600000000 1.072e+09 -4063292492492492 153.28571428571428 -3ff12b020c49ba5e 1.073 -41cffa5720000000 1.073e+09 -40632db6db6db6db 153.42857142857142 -3ff12f1a9fbe76c9 1.074 -41d000fc20000000 1.074e+09 -4063324924924925 153.57142857142858 -3ff1333333333333 1.075 -41d004ccb0000000 1.075e+09 -406336db6db6db6e 153.71428571428572 -3ff1374bc6a7ef9e 1.076 -41d0089d40000000 1.076e+09 -40633b6db6db6db7 153.85714285714286 -3ff13b645a1cac08 1.077 -41d00c6dd0000000 1.077e+09 -4063400000000000 154 -3ff13f7ced916873 1.078 -41d0103e60000000 1.078e+09 -4063449249249249 154.14285714285714 -3ff14395810624dd 1.079 -41d0140ef0000000 1.079e+09 -4063492492492492 154.28571428571428 -3ff147ae147ae148 1.08 -41d017df80000000 1.08e+09 -40634db6db6db6db 154.42857142857142 -3ff14bc6a7ef9db2 1.081 -41d01bb010000000 1.081e+09 -4063524924924925 154.57142857142858 -3ff14fdf3b645a1d 1.082 -41d01f80a0000000 1.082e+09 -406356db6db6db6e 154.71428571428572 -3ff153f7ced91687 1.083 -41d0235130000000 1.083e+09 -40635b6db6db6db7 154.85714285714286 -3ff15810624dd2f2 1.084 -41d02721c0000000 1.084e+09 -4063600000000000 155 -3ff15c28f5c28f5c 1.085 -41d02af250000000 1.085e+09 -4063649249249249 155.14285714285714 -3ff1604189374bc7 1.086 -41d02ec2e0000000 1.086e+09 -4063692492492492 155.28571428571428 -3ff1645a1cac0831 1.087 -41d0329370000000 1.087e+09 -40636db6db6db6db 155.42857142857142 -3ff16872b020c49c 1.088 -41d0366400000000 1.088e+09 -4063724924924925 155.57142857142858 -3ff16c8b43958106 1.089 -41d03a3490000000 1.089e+09 -406376db6db6db6e 155.71428571428572 -3ff170a3d70a3d71 1.09 -41d03e0520000000 1.09e+09 -40637b6db6db6db7 155.85714285714286 -3ff174bc6a7ef9db 1.091 -41d041d5b0000000 1.091e+09 -4063800000000000 156 -3ff178d4fdf3b646 1.092 -41d045a640000000 1.092e+09 -4063849249249249 156.14285714285714 -3ff17ced916872b0 1.093 -41d04976d0000000 1.093e+09 -4063892492492492 156.28571428571428 -3ff1810624dd2f1b 1.094 -41d04d4760000000 1.094e+09 -40638db6db6db6db 156.42857142857142 -3ff1851eb851eb85 1.095 -41d05117f0000000 1.095e+09 -4063924924924925 156.57142857142858 -3ff189374bc6a7f0 1.096 -41d054e880000000 1.096e+09 -406396db6db6db6e 156.71428571428572 -3ff18d4fdf3b645a 1.097 -41d058b910000000 1.097e+09 -40639b6db6db6db7 156.85714285714286 -3ff1916872b020c5 1.098 -41d05c89a0000000 1.098e+09 -4063a00000000000 157 -3ff195810624dd2f 1.099 -41d0605a30000000 1.099e+09 -4063a49249249249 157.14285714285714 -3ff199999999999a 1.1 -41d0642ac0000000 1.1e+09 -4063a92492492492 157.28571428571428 -3ff19db22d0e5604 1.101 -41d067fb50000000 1.101e+09 -4063adb6db6db6db 157.42857142857142 -3ff1a1cac083126f 1.102 -41d06bcbe0000000 1.102e+09 -4063b24924924925 157.57142857142858 -3ff1a5e353f7ced9 1.103 -41d06f9c70000000 1.103e+09 -4063b6db6db6db6e 157.71428571428572 -3ff1a9fbe76c8b44 1.104 -41d0736d00000000 1.104e+09 -4063bb6db6db6db7 157.85714285714286 -3ff1ae147ae147ae 1.105 -41d0773d90000000 1.105e+09 -4063c00000000000 158 -3ff1b22d0e560419 1.106 -41d07b0e20000000 1.106e+09 -4063c49249249249 158.14285714285714 -3ff1b645a1cac083 1.107 -41d07edeb0000000 1.107e+09 -4063c92492492492 158.28571428571428 -3ff1ba5e353f7cee 1.108 -41d082af40000000 1.108e+09 -4063cdb6db6db6db 158.42857142857142 -3ff1be76c8b43958 1.109 -41d0867fd0000000 1.109e+09 -4063d24924924925 158.57142857142858 -3ff1c28f5c28f5c3 1.11 -41d08a5060000000 1.11e+09 -4063d6db6db6db6e 158.71428571428572 -3ff1c6a7ef9db22d 1.111 -41d08e20f0000000 1.111e+09 -4063db6db6db6db7 158.85714285714286 -3ff1cac083126e98 1.112 -41d091f180000000 1.112e+09 -4063e00000000000 159 -3ff1ced916872b02 1.113 -41d095c210000000 1.113e+09 -4063e49249249249 159.14285714285714 -3ff1d2f1a9fbe76d 1.114 -41d09992a0000000 1.114e+09 -4063e92492492492 159.28571428571428 -3ff1d70a3d70a3d7 1.115 -41d09d6330000000 1.115e+09 -4063edb6db6db6db 159.42857142857142 -3ff1db22d0e56042 1.116 -41d0a133c0000000 1.116e+09 -4063f24924924925 159.57142857142858 -3ff1df3b645a1cac 1.117 -41d0a50450000000 1.117e+09 -4063f6db6db6db6e 159.71428571428572 -3ff1e353f7ced917 1.118 -41d0a8d4e0000000 1.118e+09 -4063fb6db6db6db7 159.85714285714286 -3ff1e76c8b439581 1.119 -41d0aca570000000 1.119e+09 -4064000000000000 160 -3ff1eb851eb851ec 1.12 -41d0b07600000000 1.12e+09 -4064049249249249 160.14285714285714 -3ff1ef9db22d0e56 1.121 -41d0b44690000000 1.121e+09 -4064092492492492 160.28571428571428 -3ff1f3b645a1cac1 1.122 -41d0b81720000000 1.122e+09 -40640db6db6db6db 160.42857142857142 -3ff1f7ced916872b 1.123 -41d0bbe7b0000000 1.123e+09 -4064124924924925 160.57142857142858 -3ff1fbe76c8b4396 1.124 -41d0bfb840000000 1.124e+09 -406416db6db6db6e 160.71428571428572 -3ff2000000000000 1.125 -41d0c388d0000000 1.125e+09 -40641b6db6db6db7 160.85714285714286 -3ff204189374bc6b 1.1260000000000001 -41d0c75960000000 1.126e+09 -4064200000000000 161 -3ff2083126e978d5 1.127 -41d0cb29f0000000 1.127e+09 -4064249249249249 161.14285714285714 -3ff20c49ba5e3540 1.1280000000000001 -41d0cefa80000000 1.128e+09 -4064292492492492 161.28571428571428 -3ff210624dd2f1aa 1.129 -41d0d2cb10000000 1.129e+09 -40642db6db6db6db 161.42857142857142 -3ff2147ae147ae15 1.1300000000000001 -41d0d69ba0000000 1.13e+09 -4064324924924925 161.57142857142858 -3ff2189374bc6a7f 1.131 -41d0da6c30000000 1.131e+09 -406436db6db6db6e 161.71428571428572 -3ff21cac083126ea 1.1320000000000001 -41d0de3cc0000000 1.132e+09 -40643b6db6db6db7 161.85714285714286 -3ff220c49ba5e354 1.133 -41d0e20d50000000 1.133e+09 -4064400000000000 162 -3ff224dd2f1a9fbf 1.1340000000000001 -41d0e5dde0000000 1.134e+09 -4064449249249249 162.14285714285714 -3ff228f5c28f5c29 1.135 -41d0e9ae70000000 1.135e+09 -4064492492492492 162.28571428571428 -3ff22d0e56041894 1.1360000000000001 -41d0ed7f00000000 1.136e+09 -40644db6db6db6db 162.42857142857142 -3ff23126e978d4fe 1.137 -41d0f14f90000000 1.137e+09 -4064524924924925 162.57142857142858 -3ff2353f7ced9169 1.1380000000000001 -41d0f52020000000 1.138e+09 -406456db6db6db6e 162.71428571428572 -3ff2395810624dd3 1.139 -41d0f8f0b0000000 1.139e+09 -40645b6db6db6db7 162.85714285714286 -3ff23d70a3d70a3e 1.1400000000000001 -41d0fcc140000000 1.14e+09 -4064600000000000 163 -3ff24189374bc6a8 1.141 -41d10091d0000000 1.141e+09 -4064649249249249 163.14285714285714 -3ff245a1cac08313 1.1420000000000001 -41d1046260000000 1.142e+09 -4064692492492492 163.28571428571428 -3ff249ba5e353f7d 1.143 -41d10832f0000000 1.143e+09 -40646db6db6db6db 163.42857142857142 -3ff24dd2f1a9fbe8 1.1440000000000001 -41d10c0380000000 1.144e+09 -4064724924924925 163.57142857142858 -3ff251eb851eb852 1.145 -41d10fd410000000 1.145e+09 -406476db6db6db6e 163.71428571428572 -3ff25604189374bd 1.1460000000000001 -41d113a4a0000000 1.146e+09 -40647b6db6db6db7 163.85714285714286 -3ff25a1cac083127 1.147 -41d1177530000000 1.147e+09 -4064800000000000 164 -3ff25e353f7ced92 1.1480000000000001 -41d11b45c0000000 1.148e+09 -4064849249249249 164.14285714285714 -3ff2624dd2f1a9fc 1.149 -41d11f1650000000 1.149e+09 -4064892492492492 164.28571428571428 -3ff2666666666667 1.1500000000000001 -41d122e6e0000000 1.15e+09 -40648db6db6db6db 164.42857142857142 -3ff26a7ef9db22d1 1.151 -41d126b770000000 1.151e+09 -4064924924924925 164.57142857142858 -3ff26e978d4fdf3c 1.1520000000000001 -41d12a8800000000 1.152e+09 -406496db6db6db6e 164.71428571428572 -3ff272b020c49ba6 1.153 -41d12e5890000000 1.153e+09 -40649b6db6db6db7 164.85714285714286 -3ff276c8b4395810 1.154 -41d1322920000000 1.154e+09 -4064a00000000000 165 -3ff27ae147ae147b 1.155 -41d135f9b0000000 1.155e+09 -4064a49249249249 165.14285714285714 -3ff27ef9db22d0e5 1.156 -41d139ca40000000 1.156e+09 -4064a92492492492 165.28571428571428 -3ff283126e978d50 1.157 -41d13d9ad0000000 1.157e+09 -4064adb6db6db6db 165.42857142857142 -3ff2872b020c49ba 1.158 -41d1416b60000000 1.158e+09 -4064b24924924925 165.57142857142858 -3ff28b4395810625 1.159 -41d1453bf0000000 1.159e+09 -4064b6db6db6db6e 165.71428571428572 -3ff28f5c28f5c28f 1.16 -41d1490c80000000 1.16e+09 -4064bb6db6db6db7 165.85714285714286 -3ff29374bc6a7efa 1.161 -41d14cdd10000000 1.161e+09 -4064c00000000000 166 -3ff2978d4fdf3b64 1.162 -41d150ada0000000 1.162e+09 -4064c49249249249 166.14285714285714 -3ff29ba5e353f7cf 1.163 -41d1547e30000000 1.163e+09 -4064c92492492492 166.28571428571428 -3ff29fbe76c8b439 1.164 -41d1584ec0000000 1.164e+09 -4064cdb6db6db6db 166.42857142857142 -3ff2a3d70a3d70a4 1.165 -41d15c1f50000000 1.165e+09 -4064d24924924925 166.57142857142858 -3ff2a7ef9db22d0e 1.166 -41d15fefe0000000 1.166e+09 -4064d6db6db6db6e 166.71428571428572 -3ff2ac083126e979 1.167 -41d163c070000000 1.167e+09 -4064db6db6db6db7 166.85714285714286 -3ff2b020c49ba5e3 1.168 -41d1679100000000 1.168e+09 -4064e00000000000 167 -3ff2b4395810624e 1.169 -41d16b6190000000 1.169e+09 -4064e49249249249 167.14285714285714 -3ff2b851eb851eb8 1.17 -41d16f3220000000 1.17e+09 -4064e92492492492 167.28571428571428 -3ff2bc6a7ef9db23 1.171 -41d17302b0000000 1.171e+09 -4064edb6db6db6db 167.42857142857142 -3ff2c083126e978d 1.172 -41d176d340000000 1.172e+09 -4064f24924924925 167.57142857142858 -3ff2c49ba5e353f8 1.173 -41d17aa3d0000000 1.173e+09 -4064f6db6db6db6e 167.71428571428572 -3ff2c8b439581062 1.174 -41d17e7460000000 1.174e+09 -4064fb6db6db6db7 167.85714285714286 -3ff2cccccccccccd 1.175 -41d18244f0000000 1.175e+09 -4065000000000000 168 -3ff2d0e560418937 1.176 -41d1861580000000 1.176e+09 -4065049249249249 168.14285714285714 -3ff2d4fdf3b645a2 1.177 -41d189e610000000 1.177e+09 -4065092492492492 168.28571428571428 -3ff2d916872b020c 1.178 -41d18db6a0000000 1.178e+09 -40650db6db6db6db 168.42857142857142 -3ff2dd2f1a9fbe77 1.179 -41d1918730000000 1.179e+09 -4065124924924925 168.57142857142858 -3ff2e147ae147ae1 1.18 -41d19557c0000000 1.18e+09 -406516db6db6db6e 168.71428571428572 -3ff2e5604189374c 1.181 -41d1992850000000 1.181e+09 -40651b6db6db6db7 168.85714285714286 -3ff2e978d4fdf3b6 1.182 -41d19cf8e0000000 1.182e+09 -4065200000000000 169 -3ff2ed916872b021 1.183 -41d1a0c970000000 1.183e+09 -4065249249249249 169.14285714285714 -3ff2f1a9fbe76c8b 1.184 -41d1a49a00000000 1.184e+09 -4065292492492492 169.28571428571428 -3ff2f5c28f5c28f6 1.185 -41d1a86a90000000 1.185e+09 -40652db6db6db6db 169.42857142857142 -3ff2f9db22d0e560 1.186 -41d1ac3b20000000 1.186e+09 -4065324924924925 169.57142857142858 -3ff2fdf3b645a1cb 1.187 -41d1b00bb0000000 1.187e+09 -406536db6db6db6e 169.71428571428572 -3ff3020c49ba5e35 1.188 -41d1b3dc40000000 1.188e+09 -40653b6db6db6db7 169.85714285714286 -3ff30624dd2f1aa0 1.189 -41d1b7acd0000000 1.189e+09 -4065400000000000 170 -3ff30a3d70a3d70a 1.19 -41d1bb7d60000000 1.19e+09 -4065449249249249 170.14285714285714 -3ff30e5604189375 1.191 -41d1bf4df0000000 1.191e+09 -4065492492492492 170.28571428571428 -3ff3126e978d4fdf 1.192 -41d1c31e80000000 1.192e+09 -40654db6db6db6db 170.42857142857142 -3ff316872b020c4a 1.193 -41d1c6ef10000000 1.193e+09 -4065524924924925 170.57142857142858 -3ff31a9fbe76c8b4 1.194 -41d1cabfa0000000 1.194e+09 -406556db6db6db6e 170.71428571428572 -3ff31eb851eb851f 1.195 -41d1ce9030000000 1.195e+09 -40655b6db6db6db7 170.85714285714286 -3ff322d0e5604189 1.196 -41d1d260c0000000 1.196e+09 -4065600000000000 171 -3ff326e978d4fdf4 1.197 -41d1d63150000000 1.197e+09 -4065649249249249 171.14285714285714 -3ff32b020c49ba5e 1.198 -41d1da01e0000000 1.198e+09 -4065692492492492 171.28571428571428 -3ff32f1a9fbe76c9 1.199 -41d1ddd270000000 1.199e+09 -40656db6db6db6db 171.42857142857142 -3ff3333333333333 1.2 -41d1e1a300000000 1.2e+09 -4065724924924925 171.57142857142858 -3ff3374bc6a7ef9e 1.201 -41d1e57390000000 1.201e+09 -406576db6db6db6e 171.71428571428572 -3ff33b645a1cac08 1.202 -41d1e94420000000 1.202e+09 -40657b6db6db6db7 171.85714285714286 -3ff33f7ced916873 1.203 -41d1ed14b0000000 1.203e+09 -4065800000000000 172 -3ff34395810624dd 1.204 -41d1f0e540000000 1.204e+09 -4065849249249249 172.14285714285714 -3ff347ae147ae148 1.205 -41d1f4b5d0000000 1.205e+09 -4065892492492492 172.28571428571428 -3ff34bc6a7ef9db2 1.206 -41d1f88660000000 1.206e+09 -40658db6db6db6db 172.42857142857142 -3ff34fdf3b645a1d 1.207 -41d1fc56f0000000 1.207e+09 -4065924924924925 172.57142857142858 -3ff353f7ced91687 1.208 -41d2002780000000 1.208e+09 -406596db6db6db6e 172.71428571428572 -3ff35810624dd2f2 1.209 -41d203f810000000 1.209e+09 -40659b6db6db6db7 172.85714285714286 -3ff35c28f5c28f5c 1.21 -41d207c8a0000000 1.21e+09 -4065a00000000000 173 -3ff3604189374bc7 1.211 -41d20b9930000000 1.211e+09 -4065a49249249249 173.14285714285714 -3ff3645a1cac0831 1.212 -41d20f69c0000000 1.212e+09 -4065a92492492492 173.28571428571428 -3ff36872b020c49c 1.213 -41d2133a50000000 1.213e+09 -4065adb6db6db6db 173.42857142857142 -3ff36c8b43958106 1.214 -41d2170ae0000000 1.214e+09 -4065b24924924925 173.57142857142858 -3ff370a3d70a3d71 1.215 -41d21adb70000000 1.215e+09 -4065b6db6db6db6e 173.71428571428572 -3ff374bc6a7ef9db 1.216 -41d21eac00000000 1.216e+09 -4065bb6db6db6db7 173.85714285714286 -3ff378d4fdf3b646 1.217 -41d2227c90000000 1.217e+09 -4065c00000000000 174 -3ff37ced916872b0 1.218 -41d2264d20000000 1.218e+09 -4065c49249249249 174.14285714285714 -3ff3810624dd2f1b 1.219 -41d22a1db0000000 1.219e+09 -4065c92492492492 174.28571428571428 -3ff3851eb851eb85 1.22 -41d22dee40000000 1.22e+09 -4065cdb6db6db6db 174.42857142857142 -3ff389374bc6a7f0 1.221 -41d231bed0000000 1.221e+09 -4065d24924924925 174.57142857142858 -3ff38d4fdf3b645a 1.222 -41d2358f60000000 1.222e+09 -4065d6db6db6db6e 174.71428571428572 -3ff3916872b020c5 1.223 -41d2395ff0000000 1.223e+09 -4065db6db6db6db7 174.85714285714286 -3ff395810624dd2f 1.224 -41d23d3080000000 1.224e+09 -4065e00000000000 175 -3ff399999999999a 1.225 -41d2410110000000 1.225e+09 -4065e49249249249 175.14285714285714 -3ff39db22d0e5604 1.226 -41d244d1a0000000 1.226e+09 -4065e92492492492 175.28571428571428 -3ff3a1cac083126f 1.227 -41d248a230000000 1.227e+09 -4065edb6db6db6db 175.42857142857142 -3ff3a5e353f7ced9 1.228 -41d24c72c0000000 1.228e+09 -4065f24924924925 175.57142857142858 -3ff3a9fbe76c8b44 1.229 -41d2504350000000 1.229e+09 -4065f6db6db6db6e 175.71428571428572 -3ff3ae147ae147ae 1.23 -41d25413e0000000 1.23e+09 -4065fb6db6db6db7 175.85714285714286 -3ff3b22d0e560419 1.231 -41d257e470000000 1.231e+09 -4066000000000000 176 -3ff3b645a1cac083 1.232 -41d25bb500000000 1.232e+09 -4066049249249249 176.14285714285714 -3ff3ba5e353f7cee 1.233 -41d25f8590000000 1.233e+09 -4066092492492492 176.28571428571428 -3ff3be76c8b43958 1.234 -41d2635620000000 1.234e+09 -40660db6db6db6db 176.42857142857142 -3ff3c28f5c28f5c3 1.235 -41d26726b0000000 1.235e+09 -4066124924924925 176.57142857142858 -3ff3c6a7ef9db22d 1.236 -41d26af740000000 1.236e+09 -406616db6db6db6e 176.71428571428572 -3ff3cac083126e98 1.237 -41d26ec7d0000000 1.237e+09 -40661b6db6db6db7 176.85714285714286 -3ff3ced916872b02 1.238 -41d2729860000000 1.238e+09 -4066200000000000 177 -3ff3d2f1a9fbe76d 1.239 -41d27668f0000000 1.239e+09 -4066249249249249 177.14285714285714 -3ff3d70a3d70a3d7 1.24 -41d27a3980000000 1.24e+09 -4066292492492492 177.28571428571428 -3ff3db22d0e56042 1.241 -41d27e0a10000000 1.241e+09 -40662db6db6db6db 177.42857142857142 -3ff3df3b645a1cac 1.242 -41d281daa0000000 1.242e+09 -4066324924924925 177.57142857142858 -3ff3e353f7ced917 1.243 -41d285ab30000000 1.243e+09 -406636db6db6db6e 177.71428571428572 -3ff3e76c8b439581 1.244 -41d2897bc0000000 1.244e+09 -40663b6db6db6db7 177.85714285714286 -3ff3eb851eb851ec 1.245 -41d28d4c50000000 1.245e+09 -4066400000000000 178 -3ff3ef9db22d0e56 1.246 -41d2911ce0000000 1.246e+09 -4066449249249249 178.14285714285714 -3ff3f3b645a1cac1 1.247 -41d294ed70000000 1.247e+09 -4066492492492492 178.28571428571428 -3ff3f7ced916872b 1.248 -41d298be00000000 1.248e+09 -40664db6db6db6db 178.42857142857142 -3ff3fbe76c8b4396 1.249 -41d29c8e90000000 1.249e+09 -4066524924924925 178.57142857142858 -3ff4000000000000 1.25 -41d2a05f20000000 1.25e+09 -406656db6db6db6e 178.71428571428572 -3ff404189374bc6b 1.2510000000000001 -41d2a42fb0000000 1.251e+09 -40665b6db6db6db7 178.85714285714286 -3ff4083126e978d5 1.252 -41d2a80040000000 1.252e+09 -4066600000000000 179 -3ff40c49ba5e3540 1.2530000000000001 -41d2abd0d0000000 1.253e+09 -4066649249249249 179.14285714285714 -3ff410624dd2f1aa 1.254 -41d2afa160000000 1.254e+09 -4066692492492492 179.28571428571428 -3ff4147ae147ae15 1.2550000000000001 -41d2b371f0000000 1.255e+09 -40666db6db6db6db 179.42857142857142 -3ff4189374bc6a7f 1.256 -41d2b74280000000 1.256e+09 -4066724924924925 179.57142857142858 -3ff41cac083126ea 1.2570000000000001 -41d2bb1310000000 1.257e+09 -406676db6db6db6e 179.71428571428572 -3ff420c49ba5e354 1.258 -41d2bee3a0000000 1.258e+09 -40667b6db6db6db7 179.85714285714286 -3ff424dd2f1a9fbf 1.2590000000000001 -41d2c2b430000000 1.259e+09 -4066800000000000 180 -3ff428f5c28f5c29 1.26 -41d2c684c0000000 1.26e+09 -4066849249249249 180.14285714285714 -3ff42d0e56041894 1.2610000000000001 -41d2ca5550000000 1.261e+09 -4066892492492492 180.28571428571428 -3ff43126e978d4fe 1.262 -41d2ce25e0000000 1.262e+09 -40668db6db6db6db 180.42857142857142 -3ff4353f7ced9169 1.2630000000000001 -41d2d1f670000000 1.263e+09 -4066924924924925 180.57142857142858 -3ff4395810624dd3 1.264 -41d2d5c700000000 1.264e+09 -406696db6db6db6e 180.71428571428572 -3ff43d70a3d70a3e 1.2650000000000001 -41d2d99790000000 1.265e+09 -40669b6db6db6db7 180.85714285714286 -3ff44189374bc6a8 1.266 -41d2dd6820000000 1.266e+09 -4066a00000000000 181 -3ff445a1cac08313 1.2670000000000001 -41d2e138b0000000 1.267e+09 -4066a49249249249 181.14285714285714 -3ff449ba5e353f7d 1.268 -41d2e50940000000 1.268e+09 -4066a92492492492 181.28571428571428 -3ff44dd2f1a9fbe8 1.2690000000000001 -41d2e8d9d0000000 1.269e+09 -4066adb6db6db6db 181.42857142857142 -3ff451eb851eb852 1.27 -41d2ecaa60000000 1.27e+09 -4066b24924924925 181.57142857142858 -3ff45604189374bd 1.2710000000000001 -41d2f07af0000000 1.271e+09 -4066b6db6db6db6e 181.71428571428572 -3ff45a1cac083127 1.272 -41d2f44b80000000 1.272e+09 -4066bb6db6db6db7 181.85714285714286 -3ff45e353f7ced92 1.2730000000000001 -41d2f81c10000000 1.273e+09 -4066c00000000000 182 -3ff4624dd2f1a9fc 1.274 -41d2fbeca0000000 1.274e+09 -4066c49249249249 182.14285714285714 -3ff4666666666667 1.2750000000000001 -41d2ffbd30000000 1.275e+09 -4066c92492492492 182.28571428571428 -3ff46a7ef9db22d1 1.276 -41d3038dc0000000 1.276e+09 -4066cdb6db6db6db 182.42857142857142 -3ff46e978d4fdf3c 1.2770000000000001 -41d3075e50000000 1.277e+09 -4066d24924924925 182.57142857142858 -3ff472b020c49ba6 1.278 -41d30b2ee0000000 1.278e+09 -4066d6db6db6db6e 182.71428571428572 -3ff476c8b4395811 1.2790000000000001 -41d30eff70000000 1.279e+09 -4066db6db6db6db7 182.85714285714286 -3ff47ae147ae147b 1.28 -41d312d000000000 1.28e+09 -4066e00000000000 183 -3ff47ef9db22d0e5 1.281 -41d316a090000000 1.281e+09 -4066e49249249249 183.14285714285714 -3ff483126e978d50 1.282 -41d31a7120000000 1.282e+09 -4066e92492492492 183.28571428571428 -3ff4872b020c49ba 1.283 -41d31e41b0000000 1.283e+09 -4066edb6db6db6db 183.42857142857142 -3ff48b4395810625 1.284 -41d3221240000000 1.284e+09 -4066f24924924925 183.57142857142858 -3ff48f5c28f5c28f 1.285 -41d325e2d0000000 1.285e+09 -4066f6db6db6db6e 183.71428571428572 -3ff49374bc6a7efa 1.286 -41d329b360000000 1.286e+09 -4066fb6db6db6db7 183.85714285714286 -3ff4978d4fdf3b64 1.287 -41d32d83f0000000 1.287e+09 -4067000000000000 184 -3ff49ba5e353f7cf 1.288 -41d3315480000000 1.288e+09 -4067049249249249 184.14285714285714 -3ff49fbe76c8b439 1.289 -41d3352510000000 1.289e+09 -4067092492492492 184.28571428571428 -3ff4a3d70a3d70a4 1.29 -41d338f5a0000000 1.29e+09 -40670db6db6db6db 184.42857142857142 -3ff4a7ef9db22d0e 1.291 -41d33cc630000000 1.291e+09 -4067124924924925 184.57142857142858 -3ff4ac083126e979 1.292 -41d34096c0000000 1.292e+09 -406716db6db6db6e 184.71428571428572 -3ff4b020c49ba5e3 1.293 -41d3446750000000 1.293e+09 -40671b6db6db6db7 184.85714285714286 -3ff4b4395810624e 1.294 -41d34837e0000000 1.294e+09 -4067200000000000 185 -3ff4b851eb851eb8 1.295 -41d34c0870000000 1.295e+09 -4067249249249249 185.14285714285714 -3ff4bc6a7ef9db23 1.296 -41d34fd900000000 1.296e+09 -4067292492492492 185.28571428571428 -3ff4c083126e978d 1.297 -41d353a990000000 1.297e+09 -40672db6db6db6db 185.42857142857142 -3ff4c49ba5e353f8 1.298 -41d3577a20000000 1.298e+09 -4067324924924925 185.57142857142858 -3ff4c8b439581062 1.299 -41d35b4ab0000000 1.299e+09 -406736db6db6db6e 185.71428571428572 -3ff4cccccccccccd 1.3 -41d35f1b40000000 1.3e+09 -40673b6db6db6db7 185.85714285714286 -3ff4d0e560418937 1.301 -41d362ebd0000000 1.301e+09 -4067400000000000 186 -3ff4d4fdf3b645a2 1.302 -41d366bc60000000 1.302e+09 -4067449249249249 186.14285714285714 -3ff4d916872b020c 1.303 -41d36a8cf0000000 1.303e+09 -4067492492492492 186.28571428571428 -3ff4dd2f1a9fbe77 1.304 -41d36e5d80000000 1.304e+09 -40674db6db6db6db 186.42857142857142 -3ff4e147ae147ae1 1.305 -41d3722e10000000 1.305e+09 -4067524924924925 186.57142857142858 -3ff4e5604189374c 1.306 -41d375fea0000000 1.306e+09 -406756db6db6db6e 186.71428571428572 -3ff4e978d4fdf3b6 1.307 -41d379cf30000000 1.307e+09 -40675b6db6db6db7 186.85714285714286 -3ff4ed916872b021 1.308 -41d37d9fc0000000 1.308e+09 -4067600000000000 187 -3ff4f1a9fbe76c8b 1.309 -41d3817050000000 1.309e+09 -4067649249249249 187.14285714285714 -3ff4f5c28f5c28f6 1.31 -41d38540e0000000 1.31e+09 -4067692492492492 187.28571428571428 -3ff4f9db22d0e560 1.311 -41d3891170000000 1.311e+09 -40676db6db6db6db 187.42857142857142 -3ff4fdf3b645a1cb 1.312 -41d38ce200000000 1.312e+09 -4067724924924925 187.57142857142858 -3ff5020c49ba5e35 1.313 -41d390b290000000 1.313e+09 -406776db6db6db6e 187.71428571428572 -3ff50624dd2f1aa0 1.314 -41d3948320000000 1.314e+09 -40677b6db6db6db7 187.85714285714286 -3ff50a3d70a3d70a 1.315 -41d39853b0000000 1.315e+09 -4067800000000000 188 -3ff50e5604189375 1.316 -41d39c2440000000 1.316e+09 -4067849249249249 188.14285714285714 -3ff5126e978d4fdf 1.317 -41d39ff4d0000000 1.317e+09 -4067892492492492 188.28571428571428 -3ff516872b020c4a 1.318 -41d3a3c560000000 1.318e+09 -40678db6db6db6db 188.42857142857142 -3ff51a9fbe76c8b4 1.319 -41d3a795f0000000 1.319e+09 -4067924924924925 188.57142857142858 -3ff51eb851eb851f 1.32 -41d3ab6680000000 1.32e+09 -406796db6db6db6e 188.71428571428572 -3ff522d0e5604189 1.321 -41d3af3710000000 1.321e+09 -40679b6db6db6db7 188.85714285714286 -3ff526e978d4fdf4 1.322 -41d3b307a0000000 1.322e+09 -4067a00000000000 189 -3ff52b020c49ba5e 1.323 -41d3b6d830000000 1.323e+09 -4067a49249249249 189.14285714285714 -3ff52f1a9fbe76c9 1.324 -41d3baa8c0000000 1.324e+09 -4067a92492492492 189.28571428571428 -3ff5333333333333 1.325 -41d3be7950000000 1.325e+09 -4067adb6db6db6db 189.42857142857142 -3ff5374bc6a7ef9e 1.326 -41d3c249e0000000 1.326e+09 -4067b24924924925 189.57142857142858 -3ff53b645a1cac08 1.327 -41d3c61a70000000 1.327e+09 -4067b6db6db6db6e 189.71428571428572 -3ff53f7ced916873 1.328 -41d3c9eb00000000 1.328e+09 -4067bb6db6db6db7 189.85714285714286 -3ff54395810624dd 1.329 -41d3cdbb90000000 1.329e+09 -4067c00000000000 190 -3ff547ae147ae148 1.33 -41d3d18c20000000 1.33e+09 -4067c49249249249 190.14285714285714 -3ff54bc6a7ef9db2 1.331 -41d3d55cb0000000 1.331e+09 -4067c92492492492 190.28571428571428 -3ff54fdf3b645a1d 1.332 -41d3d92d40000000 1.332e+09 -4067cdb6db6db6db 190.42857142857142 -3ff553f7ced91687 1.333 -41d3dcfdd0000000 1.333e+09 -4067d24924924925 190.57142857142858 -3ff55810624dd2f2 1.334 -41d3e0ce60000000 1.334e+09 -4067d6db6db6db6e 190.71428571428572 -3ff55c28f5c28f5c 1.335 -41d3e49ef0000000 1.335e+09 -4067db6db6db6db7 190.85714285714286 -3ff5604189374bc7 1.336 -41d3e86f80000000 1.336e+09 -4067e00000000000 191 -3ff5645a1cac0831 1.337 -41d3ec4010000000 1.337e+09 -4067e49249249249 191.14285714285714 -3ff56872b020c49c 1.338 -41d3f010a0000000 1.338e+09 -4067e92492492492 191.28571428571428 -3ff56c8b43958106 1.339 -41d3f3e130000000 1.339e+09 -4067edb6db6db6db 191.42857142857142 -3ff570a3d70a3d71 1.34 -41d3f7b1c0000000 1.34e+09 -4067f24924924925 191.57142857142858 -3ff574bc6a7ef9db 1.341 -41d3fb8250000000 1.341e+09 -4067f6db6db6db6e 191.71428571428572 -3ff578d4fdf3b646 1.342 -41d3ff52e0000000 1.342e+09 -4067fb6db6db6db7 191.85714285714286 -3ff57ced916872b0 1.343 -41d4032370000000 1.343e+09 -4068000000000000 192 -3ff5810624dd2f1b 1.344 -41d406f400000000 1.344e+09 -4068049249249249 192.14285714285714 -3ff5851eb851eb85 1.345 -41d40ac490000000 1.345e+09 -4068092492492492 192.28571428571428 -3ff589374bc6a7f0 1.346 -41d40e9520000000 1.346e+09 -40680db6db6db6db 192.42857142857142 -3ff58d4fdf3b645a 1.347 -41d41265b0000000 1.347e+09 -4068124924924925 192.57142857142858 -3ff5916872b020c5 1.348 -41d4163640000000 1.348e+09 -406816db6db6db6e 192.71428571428572 -3ff595810624dd2f 1.349 -41d41a06d0000000 1.349e+09 -40681b6db6db6db7 192.85714285714286 -3ff599999999999a 1.35 -41d41dd760000000 1.35e+09 -4068200000000000 193 -3ff59db22d0e5604 1.351 -41d421a7f0000000 1.351e+09 -4068249249249249 193.14285714285714 -3ff5a1cac083126f 1.352 -41d4257880000000 1.352e+09 -4068292492492492 193.28571428571428 -3ff5a5e353f7ced9 1.353 -41d4294910000000 1.353e+09 -40682db6db6db6db 193.42857142857142 -3ff5a9fbe76c8b44 1.354 -41d42d19a0000000 1.354e+09 -4068324924924925 193.57142857142858 -3ff5ae147ae147ae 1.355 -41d430ea30000000 1.355e+09 -406836db6db6db6e 193.71428571428572 -3ff5b22d0e560419 1.356 -41d434bac0000000 1.356e+09 -40683b6db6db6db7 193.85714285714286 -3ff5b645a1cac083 1.357 -41d4388b50000000 1.357e+09 -4068400000000000 194 -3ff5ba5e353f7cee 1.358 -41d43c5be0000000 1.358e+09 -4068449249249249 194.14285714285714 -3ff5be76c8b43958 1.359 -41d4402c70000000 1.359e+09 -4068492492492492 194.28571428571428 -3ff5c28f5c28f5c3 1.36 -41d443fd00000000 1.36e+09 -40684db6db6db6db 194.42857142857142 -3ff5c6a7ef9db22d 1.361 -41d447cd90000000 1.361e+09 -4068524924924925 194.57142857142858 -3ff5cac083126e98 1.362 -41d44b9e20000000 1.362e+09 -406856db6db6db6e 194.71428571428572 -3ff5ced916872b02 1.363 -41d44f6eb0000000 1.363e+09 -40685b6db6db6db7 194.85714285714286 -3ff5d2f1a9fbe76d 1.364 -41d4533f40000000 1.364e+09 -4068600000000000 195 -3ff5d70a3d70a3d7 1.365 -41d4570fd0000000 1.365e+09 -4068649249249249 195.14285714285714 -3ff5db22d0e56042 1.366 -41d45ae060000000 1.366e+09 -4068692492492492 195.28571428571428 -3ff5df3b645a1cac 1.367 -41d45eb0f0000000 1.367e+09 -40686db6db6db6db 195.42857142857142 -3ff5e353f7ced917 1.368 -41d4628180000000 1.368e+09 -4068724924924925 195.57142857142858 -3ff5e76c8b439581 1.369 -41d4665210000000 1.369e+09 -406876db6db6db6e 195.71428571428572 -3ff5eb851eb851ec 1.37 -41d46a22a0000000 1.37e+09 -40687b6db6db6db7 195.85714285714286 -3ff5ef9db22d0e56 1.371 -41d46df330000000 1.371e+09 -4068800000000000 196 -3ff5f3b645a1cac1 1.372 -41d471c3c0000000 1.372e+09 -4068849249249249 196.14285714285714 -3ff5f7ced916872b 1.373 -41d4759450000000 1.373e+09 -4068892492492492 196.28571428571428 -3ff5fbe76c8b4396 1.374 -41d47964e0000000 1.374e+09 -40688db6db6db6db 196.42857142857142 -3ff6000000000000 1.375 -41d47d3570000000 1.375e+09 -4068924924924925 196.57142857142858 -3ff604189374bc6b 1.3760000000000001 -41d4810600000000 1.376e+09 -406896db6db6db6e 196.71428571428572 -3ff6083126e978d5 1.377 -41d484d690000000 1.377e+09 -40689b6db6db6db7 196.85714285714286 -3ff60c49ba5e3540 1.3780000000000001 -41d488a720000000 1.378e+09 -4068a00000000000 197 -3ff610624dd2f1aa 1.379 -41d48c77b0000000 1.379e+09 -4068a49249249249 197.14285714285714 -3ff6147ae147ae15 1.3800000000000001 -41d4904840000000 1.38e+09 -4068a92492492492 197.28571428571428 -3ff6189374bc6a7f 1.381 -41d49418d0000000 1.381e+09 -4068adb6db6db6db 197.42857142857142 -3ff61cac083126ea 1.3820000000000001 -41d497e960000000 1.382e+09 -4068b24924924925 197.57142857142858 -3ff620c49ba5e354 1.383 -41d49bb9f0000000 1.383e+09 -4068b6db6db6db6e 197.71428571428572 -3ff624dd2f1a9fbf 1.3840000000000001 -41d49f8a80000000 1.384e+09 -4068bb6db6db6db7 197.85714285714286 -3ff628f5c28f5c29 1.385 -41d4a35b10000000 1.385e+09 -4068c00000000000 198 -3ff62d0e56041894 1.3860000000000001 -41d4a72ba0000000 1.386e+09 -4068c49249249249 198.14285714285714 -3ff63126e978d4fe 1.387 -41d4aafc30000000 1.387e+09 -4068c92492492492 198.28571428571428 -3ff6353f7ced9169 1.3880000000000001 -41d4aeccc0000000 1.388e+09 -4068cdb6db6db6db 198.42857142857142 -3ff6395810624dd3 1.389 -41d4b29d50000000 1.389e+09 -4068d24924924925 198.57142857142858 -3ff63d70a3d70a3e 1.3900000000000001 -41d4b66de0000000 1.39e+09 -4068d6db6db6db6e 198.71428571428572 -3ff64189374bc6a8 1.391 -41d4ba3e70000000 1.391e+09 -4068db6db6db6db7 198.85714285714286 -3ff645a1cac08313 1.3920000000000001 -41d4be0f00000000 1.392e+09 -4068e00000000000 199 -3ff649ba5e353f7d 1.393 -41d4c1df90000000 1.393e+09 -4068e49249249249 199.14285714285714 -3ff64dd2f1a9fbe8 1.3940000000000001 -41d4c5b020000000 1.394e+09 -4068e92492492492 199.28571428571428 -3ff651eb851eb852 1.395 -41d4c980b0000000 1.395e+09 -4068edb6db6db6db 199.42857142857142 -3ff65604189374bd 1.3960000000000001 -41d4cd5140000000 1.396e+09 -4068f24924924925 199.57142857142858 -3ff65a1cac083127 1.397 -41d4d121d0000000 1.397e+09 -4068f6db6db6db6e 199.71428571428572 -3ff65e353f7ced92 1.3980000000000001 -41d4d4f260000000 1.398e+09 -4068fb6db6db6db7 199.85714285714286 -3ff6624dd2f1a9fc 1.399 -41d4d8c2f0000000 1.399e+09 -4069000000000000 200 -3ff6666666666667 1.4000000000000001 -41d4dc9380000000 1.4e+09 -4069049249249249 200.14285714285714 -3ff66a7ef9db22d1 1.401 -41d4e06410000000 1.401e+09 -4069092492492492 200.28571428571428 -3ff66e978d4fdf3c 1.4020000000000001 -41d4e434a0000000 1.402e+09 -40690db6db6db6db 200.42857142857142 -3ff672b020c49ba6 1.403 -41d4e80530000000 1.403e+09 -4069124924924925 200.57142857142858 -3ff676c8b4395811 1.4040000000000001 -41d4ebd5c0000000 1.404e+09 -406916db6db6db6e 200.71428571428572 -3ff67ae147ae147b 1.405 -41d4efa650000000 1.405e+09 -40691b6db6db6db7 200.85714285714286 -3ff67ef9db22d0e6 1.4060000000000001 -41d4f376e0000000 1.406e+09 -4069200000000000 201 -3ff683126e978d50 1.407 -41d4f74770000000 1.407e+09 -4069249249249249 201.14285714285714 -3ff6872b020c49ba 1.408 -41d4fb1800000000 1.408e+09 -4069292492492492 201.28571428571428 -3ff68b4395810625 1.409 -41d4fee890000000 1.409e+09 -40692db6db6db6db 201.42857142857142 -3ff68f5c28f5c28f 1.41 -41d502b920000000 1.41e+09 -4069324924924925 201.57142857142858 -3ff69374bc6a7efa 1.411 -41d50689b0000000 1.411e+09 -406936db6db6db6e 201.71428571428572 -3ff6978d4fdf3b64 1.412 -41d50a5a40000000 1.412e+09 -40693b6db6db6db7 201.85714285714286 -3ff69ba5e353f7cf 1.413 -41d50e2ad0000000 1.413e+09 -4069400000000000 202 -3ff69fbe76c8b439 1.414 -41d511fb60000000 1.414e+09 -4069449249249249 202.14285714285714 -3ff6a3d70a3d70a4 1.415 -41d515cbf0000000 1.415e+09 -4069492492492492 202.28571428571428 -3ff6a7ef9db22d0e 1.416 -41d5199c80000000 1.416e+09 -40694db6db6db6db 202.42857142857142 -3ff6ac083126e979 1.417 -41d51d6d10000000 1.417e+09 -4069524924924925 202.57142857142858 -3ff6b020c49ba5e3 1.418 -41d5213da0000000 1.418e+09 -406956db6db6db6e 202.71428571428572 -3ff6b4395810624e 1.419 -41d5250e30000000 1.419e+09 -40695b6db6db6db7 202.85714285714286 -3ff6b851eb851eb8 1.42 -41d528dec0000000 1.42e+09 -4069600000000000 203 -3ff6bc6a7ef9db23 1.421 -41d52caf50000000 1.421e+09 -4069649249249249 203.14285714285714 -3ff6c083126e978d 1.422 -41d5307fe0000000 1.422e+09 -4069692492492492 203.28571428571428 -3ff6c49ba5e353f8 1.423 -41d5345070000000 1.423e+09 -40696db6db6db6db 203.42857142857142 -3ff6c8b439581062 1.424 -41d5382100000000 1.424e+09 -4069724924924925 203.57142857142858 -3ff6cccccccccccd 1.425 -41d53bf190000000 1.425e+09 -406976db6db6db6e 203.71428571428572 -3ff6d0e560418937 1.426 -41d53fc220000000 1.426e+09 -40697b6db6db6db7 203.85714285714286 -3ff6d4fdf3b645a2 1.427 -41d54392b0000000 1.427e+09 -4069800000000000 204 -3ff6d916872b020c 1.428 -41d5476340000000 1.428e+09 -4069849249249249 204.14285714285714 -3ff6dd2f1a9fbe77 1.429 -41d54b33d0000000 1.429e+09 -4069892492492492 204.28571428571428 -3ff6e147ae147ae1 1.43 -41d54f0460000000 1.43e+09 -40698db6db6db6db 204.42857142857142 -3ff6e5604189374c 1.431 -41d552d4f0000000 1.431e+09 -4069924924924925 204.57142857142858 -3ff6e978d4fdf3b6 1.432 -41d556a580000000 1.432e+09 -406996db6db6db6e 204.71428571428572 -3ff6ed916872b021 1.433 -41d55a7610000000 1.433e+09 -40699b6db6db6db7 204.85714285714286 -3ff6f1a9fbe76c8b 1.434 -41d55e46a0000000 1.434e+09 -4069a00000000000 205 -3ff6f5c28f5c28f6 1.435 -41d5621730000000 1.435e+09 -4069a49249249249 205.14285714285714 -3ff6f9db22d0e560 1.436 -41d565e7c0000000 1.436e+09 -4069a92492492492 205.28571428571428 -3ff6fdf3b645a1cb 1.437 -41d569b850000000 1.437e+09 -4069adb6db6db6db 205.42857142857142 -3ff7020c49ba5e35 1.438 -41d56d88e0000000 1.438e+09 -4069b24924924925 205.57142857142858 -3ff70624dd2f1aa0 1.439 -41d5715970000000 1.439e+09 -4069b6db6db6db6e 205.71428571428572 -3ff70a3d70a3d70a 1.44 -41d5752a00000000 1.44e+09 -4069bb6db6db6db7 205.85714285714286 -3ff70e5604189375 1.441 -41d578fa90000000 1.441e+09 -4069c00000000000 206 -3ff7126e978d4fdf 1.442 -41d57ccb20000000 1.442e+09 -4069c49249249249 206.14285714285714 -3ff716872b020c4a 1.443 -41d5809bb0000000 1.443e+09 -4069c92492492492 206.28571428571428 -3ff71a9fbe76c8b4 1.444 -41d5846c40000000 1.444e+09 -4069cdb6db6db6db 206.42857142857142 -3ff71eb851eb851f 1.445 -41d5883cd0000000 1.445e+09 -4069d24924924925 206.57142857142858 -3ff722d0e5604189 1.446 -41d58c0d60000000 1.446e+09 -4069d6db6db6db6e 206.71428571428572 -3ff726e978d4fdf4 1.447 -41d58fddf0000000 1.447e+09 -4069db6db6db6db7 206.85714285714286 -3ff72b020c49ba5e 1.448 -41d593ae80000000 1.448e+09 -4069e00000000000 207 -3ff72f1a9fbe76c9 1.449 -41d5977f10000000 1.449e+09 -4069e49249249249 207.14285714285714 -3ff7333333333333 1.45 -41d59b4fa0000000 1.45e+09 -4069e92492492492 207.28571428571428 -3ff7374bc6a7ef9e 1.451 -41d59f2030000000 1.451e+09 -4069edb6db6db6db 207.42857142857142 -3ff73b645a1cac08 1.452 -41d5a2f0c0000000 1.452e+09 -4069f24924924925 207.57142857142858 -3ff73f7ced916873 1.453 -41d5a6c150000000 1.453e+09 -4069f6db6db6db6e 207.71428571428572 -3ff74395810624dd 1.454 -41d5aa91e0000000 1.454e+09 -4069fb6db6db6db7 207.85714285714286 -3ff747ae147ae148 1.455 -41d5ae6270000000 1.455e+09 -406a000000000000 208 -3ff74bc6a7ef9db2 1.456 -41d5b23300000000 1.456e+09 -406a049249249249 208.14285714285714 -3ff74fdf3b645a1d 1.457 -41d5b60390000000 1.457e+09 -406a092492492492 208.28571428571428 -3ff753f7ced91687 1.458 -41d5b9d420000000 1.458e+09 -406a0db6db6db6db 208.42857142857142 -3ff75810624dd2f2 1.459 -41d5bda4b0000000 1.459e+09 -406a124924924925 208.57142857142858 -3ff75c28f5c28f5c 1.46 -41d5c17540000000 1.46e+09 -406a16db6db6db6e 208.71428571428572 -3ff7604189374bc7 1.461 -41d5c545d0000000 1.461e+09 -406a1b6db6db6db7 208.85714285714286 -3ff7645a1cac0831 1.462 -41d5c91660000000 1.462e+09 -406a200000000000 209 -3ff76872b020c49c 1.463 -41d5cce6f0000000 1.463e+09 -406a249249249249 209.14285714285714 -3ff76c8b43958106 1.464 -41d5d0b780000000 1.464e+09 -406a292492492492 209.28571428571428 -3ff770a3d70a3d71 1.465 -41d5d48810000000 1.465e+09 -406a2db6db6db6db 209.42857142857142 -3ff774bc6a7ef9db 1.466 -41d5d858a0000000 1.466e+09 -406a324924924925 209.57142857142858 -3ff778d4fdf3b646 1.467 -41d5dc2930000000 1.467e+09 -406a36db6db6db6e 209.71428571428572 -3ff77ced916872b0 1.468 -41d5dff9c0000000 1.468e+09 -406a3b6db6db6db7 209.85714285714286 -3ff7810624dd2f1b 1.469 -41d5e3ca50000000 1.469e+09 -406a400000000000 210 -3ff7851eb851eb85 1.47 -41d5e79ae0000000 1.47e+09 -406a449249249249 210.14285714285714 -3ff789374bc6a7f0 1.471 -41d5eb6b70000000 1.471e+09 -406a492492492492 210.28571428571428 -3ff78d4fdf3b645a 1.472 -41d5ef3c00000000 1.472e+09 -406a4db6db6db6db 210.42857142857142 -3ff7916872b020c5 1.473 -41d5f30c90000000 1.473e+09 -406a524924924925 210.57142857142858 -3ff795810624dd2f 1.474 -41d5f6dd20000000 1.474e+09 -406a56db6db6db6e 210.71428571428572 -3ff799999999999a 1.475 -41d5faadb0000000 1.475e+09 -406a5b6db6db6db7 210.85714285714286 -3ff79db22d0e5604 1.476 -41d5fe7e40000000 1.476e+09 -406a600000000000 211 -3ff7a1cac083126f 1.477 -41d6024ed0000000 1.477e+09 -406a649249249249 211.14285714285714 -3ff7a5e353f7ced9 1.478 -41d6061f60000000 1.478e+09 -406a692492492492 211.28571428571428 -3ff7a9fbe76c8b44 1.479 -41d609eff0000000 1.479e+09 -406a6db6db6db6db 211.42857142857142 -3ff7ae147ae147ae 1.48 -41d60dc080000000 1.48e+09 -406a724924924925 211.57142857142858 -3ff7b22d0e560419 1.481 -41d6119110000000 1.481e+09 -406a76db6db6db6e 211.71428571428572 -3ff7b645a1cac083 1.482 -41d61561a0000000 1.482e+09 -406a7b6db6db6db7 211.85714285714286 -3ff7ba5e353f7cee 1.483 -41d6193230000000 1.483e+09 -406a800000000000 212 -3ff7be76c8b43958 1.484 -41d61d02c0000000 1.484e+09 -406a849249249249 212.14285714285714 -3ff7c28f5c28f5c3 1.485 -41d620d350000000 1.485e+09 -406a892492492492 212.28571428571428 -3ff7c6a7ef9db22d 1.486 -41d624a3e0000000 1.486e+09 -406a8db6db6db6db 212.42857142857142 -3ff7cac083126e98 1.487 -41d6287470000000 1.487e+09 -406a924924924925 212.57142857142858 -3ff7ced916872b02 1.488 -41d62c4500000000 1.488e+09 -406a96db6db6db6e 212.71428571428572 -3ff7d2f1a9fbe76d 1.489 -41d6301590000000 1.489e+09 -406a9b6db6db6db7 212.85714285714286 -3ff7d70a3d70a3d7 1.49 -41d633e620000000 1.49e+09 -406aa00000000000 213 -3ff7db22d0e56042 1.491 -41d637b6b0000000 1.491e+09 -406aa49249249249 213.14285714285714 -3ff7df3b645a1cac 1.492 -41d63b8740000000 1.492e+09 -406aa92492492492 213.28571428571428 -3ff7e353f7ced917 1.493 -41d63f57d0000000 1.493e+09 -406aadb6db6db6db 213.42857142857142 -3ff7e76c8b439581 1.494 -41d6432860000000 1.494e+09 -406ab24924924925 213.57142857142858 -3ff7eb851eb851ec 1.495 -41d646f8f0000000 1.495e+09 -406ab6db6db6db6e 213.71428571428572 -3ff7ef9db22d0e56 1.496 -41d64ac980000000 1.496e+09 -406abb6db6db6db7 213.85714285714286 -3ff7f3b645a1cac1 1.497 -41d64e9a10000000 1.497e+09 -406ac00000000000 214 -3ff7f7ced916872b 1.498 -41d6526aa0000000 1.498e+09 -406ac49249249249 214.14285714285714 -3ff7fbe76c8b4396 1.499 -41d6563b30000000 1.499e+09 -406ac92492492492 214.28571428571428 -3ff8000000000000 1.5 -41d65a0bc0000000 1.5e+09 -406acdb6db6db6db 214.42857142857142 -3ff804189374bc6b 1.5010000000000001 -41d65ddc50000000 1.501e+09 -406ad24924924925 214.57142857142858 -3ff8083126e978d5 1.502 -41d661ace0000000 1.502e+09 -406ad6db6db6db6e 214.71428571428572 -3ff80c49ba5e3540 1.5030000000000001 -41d6657d70000000 1.503e+09 -406adb6db6db6db7 214.85714285714286 -3ff810624dd2f1aa 1.504 -41d6694e00000000 1.504e+09 -406ae00000000000 215 -3ff8147ae147ae15 1.5050000000000001 -41d66d1e90000000 1.505e+09 -406ae49249249249 215.14285714285714 -3ff8189374bc6a7f 1.506 -41d670ef20000000 1.506e+09 -406ae92492492492 215.28571428571428 -3ff81cac083126ea 1.5070000000000001 -41d674bfb0000000 1.507e+09 -406aedb6db6db6db 215.42857142857142 -3ff820c49ba5e354 1.508 -41d6789040000000 1.508e+09 -406af24924924925 215.57142857142858 -3ff824dd2f1a9fbf 1.5090000000000001 -41d67c60d0000000 1.509e+09 -406af6db6db6db6e 215.71428571428572 -3ff828f5c28f5c29 1.51 -41d6803160000000 1.51e+09 -406afb6db6db6db7 215.85714285714286 -3ff82d0e56041894 1.5110000000000001 -41d68401f0000000 1.511e+09 -406b000000000000 216 -3ff83126e978d4fe 1.512 -41d687d280000000 1.512e+09 -406b049249249249 216.14285714285714 -3ff8353f7ced9169 1.5130000000000001 -41d68ba310000000 1.513e+09 -406b092492492492 216.28571428571428 -3ff8395810624dd3 1.514 -41d68f73a0000000 1.514e+09 -406b0db6db6db6db 216.42857142857142 -3ff83d70a3d70a3e 1.5150000000000001 -41d6934430000000 1.515e+09 -406b124924924925 216.57142857142858 -3ff84189374bc6a8 1.516 -41d69714c0000000 1.516e+09 -406b16db6db6db6e 216.71428571428572 -3ff845a1cac08313 1.5170000000000001 -41d69ae550000000 1.517e+09 -406b1b6db6db6db7 216.85714285714286 -3ff849ba5e353f7d 1.518 -41d69eb5e0000000 1.518e+09 -406b200000000000 217 -3ff84dd2f1a9fbe8 1.5190000000000001 -41d6a28670000000 1.519e+09 -406b249249249249 217.14285714285714 -3ff851eb851eb852 1.52 -41d6a65700000000 1.52e+09 -406b292492492492 217.28571428571428 -3ff85604189374bd 1.5210000000000001 -41d6aa2790000000 1.521e+09 -406b2db6db6db6db 217.42857142857142 -3ff85a1cac083127 1.522 -41d6adf820000000 1.522e+09 -406b324924924925 217.57142857142858 -3ff85e353f7ced92 1.5230000000000001 -41d6b1c8b0000000 1.523e+09 -406b36db6db6db6e 217.71428571428572 -3ff8624dd2f1a9fc 1.524 -41d6b59940000000 1.524e+09 -406b3b6db6db6db7 217.85714285714286 -3ff8666666666667 1.5250000000000001 -41d6b969d0000000 1.525e+09 -406b400000000000 218 -3ff86a7ef9db22d1 1.526 -41d6bd3a60000000 1.526e+09 -406b449249249249 218.14285714285714 -3ff86e978d4fdf3c 1.5270000000000001 -41d6c10af0000000 1.527e+09 -406b492492492492 218.28571428571428 -3ff872b020c49ba6 1.528 -41d6c4db80000000 1.528e+09 -406b4db6db6db6db 218.42857142857142 -3ff876c8b4395811 1.5290000000000001 -41d6c8ac10000000 1.529e+09 -406b524924924925 218.57142857142858 -3ff87ae147ae147b 1.53 -41d6cc7ca0000000 1.53e+09 -406b56db6db6db6e 218.71428571428572 -3ff87ef9db22d0e6 1.5310000000000001 -41d6d04d30000000 1.531e+09 -406b5b6db6db6db7 218.85714285714286 -3ff883126e978d50 1.532 -41d6d41dc0000000 1.532e+09 -406b600000000000 219 -3ff8872b020c49bb 1.5330000000000001 -41d6d7ee50000000 1.533e+09 -406b649249249249 219.14285714285714 -3ff88b4395810625 1.534 -41d6dbbee0000000 1.534e+09 -406b692492492492 219.28571428571428 -3ff88f5c28f5c290 1.5350000000000001 -41d6df8f70000000 1.535e+09 -406b6db6db6db6db 219.42857142857142 -3ff89374bc6a7efa 1.536 -41d6e36000000000 1.536e+09 -406b724924924925 219.57142857142858 -3ff8978d4fdf3b64 1.537 -41d6e73090000000 1.537e+09 -406b76db6db6db6e 219.71428571428572 -3ff89ba5e353f7cf 1.538 -41d6eb0120000000 1.538e+09 -406b7b6db6db6db7 219.85714285714286 -3ff89fbe76c8b439 1.539 -41d6eed1b0000000 1.539e+09 -406b800000000000 220 -3ff8a3d70a3d70a4 1.54 -41d6f2a240000000 1.54e+09 -406b849249249249 220.14285714285714 -3ff8a7ef9db22d0e 1.541 -41d6f672d0000000 1.541e+09 -406b892492492492 220.28571428571428 -3ff8ac083126e979 1.542 -41d6fa4360000000 1.542e+09 -406b8db6db6db6db 220.42857142857142 -3ff8b020c49ba5e3 1.543 -41d6fe13f0000000 1.543e+09 -406b924924924925 220.57142857142858 -3ff8b4395810624e 1.544 -41d701e480000000 1.544e+09 -406b96db6db6db6e 220.71428571428572 -3ff8b851eb851eb8 1.545 -41d705b510000000 1.545e+09 -406b9b6db6db6db7 220.85714285714286 -3ff8bc6a7ef9db23 1.546 -41d70985a0000000 1.546e+09 -406ba00000000000 221 -3ff8c083126e978d 1.547 -41d70d5630000000 1.547e+09 -406ba49249249249 221.14285714285714 -3ff8c49ba5e353f8 1.548 -41d71126c0000000 1.548e+09 -406ba92492492492 221.28571428571428 -3ff8c8b439581062 1.549 -41d714f750000000 1.549e+09 -406badb6db6db6db 221.42857142857142 -3ff8cccccccccccd 1.55 -41d718c7e0000000 1.55e+09 -406bb24924924925 221.57142857142858 -3ff8d0e560418937 1.551 -41d71c9870000000 1.551e+09 -406bb6db6db6db6e 221.71428571428572 -3ff8d4fdf3b645a2 1.552 -41d7206900000000 1.552e+09 -406bbb6db6db6db7 221.85714285714286 -3ff8d916872b020c 1.553 -41d7243990000000 1.553e+09 -406bc00000000000 222 -3ff8dd2f1a9fbe77 1.554 -41d7280a20000000 1.554e+09 -406bc49249249249 222.14285714285714 -3ff8e147ae147ae1 1.555 -41d72bdab0000000 1.555e+09 -406bc92492492492 222.28571428571428 -3ff8e5604189374c 1.556 -41d72fab40000000 1.556e+09 -406bcdb6db6db6db 222.42857142857142 -3ff8e978d4fdf3b6 1.557 -41d7337bd0000000 1.557e+09 -406bd24924924925 222.57142857142858 -3ff8ed916872b021 1.558 -41d7374c60000000 1.558e+09 -406bd6db6db6db6e 222.71428571428572 -3ff8f1a9fbe76c8b 1.559 -41d73b1cf0000000 1.559e+09 -406bdb6db6db6db7 222.85714285714286 -3ff8f5c28f5c28f6 1.56 -41d73eed80000000 1.56e+09 -406be00000000000 223 -3ff8f9db22d0e560 1.561 -41d742be10000000 1.561e+09 -406be49249249249 223.14285714285714 -3ff8fdf3b645a1cb 1.562 -41d7468ea0000000 1.562e+09 -406be92492492492 223.28571428571428 -3ff9020c49ba5e35 1.563 -41d74a5f30000000 1.563e+09 -406bedb6db6db6db 223.42857142857142 -3ff90624dd2f1aa0 1.564 -41d74e2fc0000000 1.564e+09 -406bf24924924925 223.57142857142858 -3ff90a3d70a3d70a 1.565 -41d7520050000000 1.565e+09 -406bf6db6db6db6e 223.71428571428572 -3ff90e5604189375 1.566 -41d755d0e0000000 1.566e+09 -406bfb6db6db6db7 223.85714285714286 -3ff9126e978d4fdf 1.567 -41d759a170000000 1.567e+09 -406c000000000000 224 -3ff916872b020c4a 1.568 -41d75d7200000000 1.568e+09 -406c049249249249 224.14285714285714 -3ff91a9fbe76c8b4 1.569 -41d7614290000000 1.569e+09 -406c092492492492 224.28571428571428 -3ff91eb851eb851f 1.57 -41d7651320000000 1.57e+09 -406c0db6db6db6db 224.42857142857142 -3ff922d0e5604189 1.571 -41d768e3b0000000 1.571e+09 -406c124924924925 224.57142857142858 -3ff926e978d4fdf4 1.572 -41d76cb440000000 1.572e+09 -406c16db6db6db6e 224.71428571428572 -3ff92b020c49ba5e 1.573 -41d77084d0000000 1.573e+09 -406c1b6db6db6db7 224.85714285714286 -3ff92f1a9fbe76c9 1.574 -41d7745560000000 1.574e+09 -406c200000000000 225 -3ff9333333333333 1.575 -41d77825f0000000 1.575e+09 -406c249249249249 225.14285714285714 -3ff9374bc6a7ef9e 1.576 -41d77bf680000000 1.576e+09 -406c292492492492 225.28571428571428 -3ff93b645a1cac08 1.577 -41d77fc710000000 1.577e+09 -406c2db6db6db6db 225.42857142857142 -3ff93f7ced916873 1.578 -41d78397a0000000 1.578e+09 -406c324924924925 225.57142857142858 -3ff94395810624dd 1.579 -41d7876830000000 1.579e+09 -406c36db6db6db6e 225.71428571428572 -3ff947ae147ae148 1.58 -41d78b38c0000000 1.58e+09 -406c3b6db6db6db7 225.85714285714286 -3ff94bc6a7ef9db2 1.581 -41d78f0950000000 1.581e+09 -406c400000000000 226 -3ff94fdf3b645a1d 1.582 -41d792d9e0000000 1.582e+09 -406c449249249249 226.14285714285714 -3ff953f7ced91687 1.583 -41d796aa70000000 1.583e+09 -406c492492492492 226.28571428571428 -3ff95810624dd2f2 1.584 -41d79a7b00000000 1.584e+09 -406c4db6db6db6db 226.42857142857142 -3ff95c28f5c28f5c 1.585 -41d79e4b90000000 1.585e+09 -406c524924924925 226.57142857142858 -3ff9604189374bc7 1.586 -41d7a21c20000000 1.586e+09 -406c56db6db6db6e 226.71428571428572 -3ff9645a1cac0831 1.587 -41d7a5ecb0000000 1.587e+09 -406c5b6db6db6db7 226.85714285714286 -3ff96872b020c49c 1.588 -41d7a9bd40000000 1.588e+09 -406c600000000000 227 -3ff96c8b43958106 1.589 -41d7ad8dd0000000 1.589e+09 -406c649249249249 227.14285714285714 -3ff970a3d70a3d71 1.59 -41d7b15e60000000 1.59e+09 -406c692492492492 227.28571428571428 -3ff974bc6a7ef9db 1.591 -41d7b52ef0000000 1.591e+09 -406c6db6db6db6db 227.42857142857142 -3ff978d4fdf3b646 1.592 -41d7b8ff80000000 1.592e+09 -406c724924924925 227.57142857142858 -3ff97ced916872b0 1.593 -41d7bcd010000000 1.593e+09 -406c76db6db6db6e 227.71428571428572 -3ff9810624dd2f1b 1.594 -41d7c0a0a0000000 1.594e+09 -406c7b6db6db6db7 227.85714285714286 -3ff9851eb851eb85 1.595 -41d7c47130000000 1.595e+09 -406c800000000000 228 -3ff989374bc6a7f0 1.596 -41d7c841c0000000 1.596e+09 -406c849249249249 228.14285714285714 -3ff98d4fdf3b645a 1.597 -41d7cc1250000000 1.597e+09 -406c892492492492 228.28571428571428 -3ff9916872b020c5 1.598 -41d7cfe2e0000000 1.598e+09 -406c8db6db6db6db 228.42857142857142 -3ff995810624dd2f 1.599 -41d7d3b370000000 1.599e+09 -406c924924924925 228.57142857142858 -3ff999999999999a 1.6 -41d7d78400000000 1.6e+09 -406c96db6db6db6e 228.71428571428572 -3ff99db22d0e5604 1.601 -41d7db5490000000 1.601e+09 -406c9b6db6db6db7 228.85714285714286 -3ff9a1cac083126f 1.602 -41d7df2520000000 1.602e+09 -406ca00000000000 229 -3ff9a5e353f7ced9 1.603 -41d7e2f5b0000000 1.603e+09 -406ca49249249249 229.14285714285714 -3ff9a9fbe76c8b44 1.604 -41d7e6c640000000 1.604e+09 -406ca92492492492 229.28571428571428 -3ff9ae147ae147ae 1.605 -41d7ea96d0000000 1.605e+09 -406cadb6db6db6db 229.42857142857142 -3ff9b22d0e560419 1.606 -41d7ee6760000000 1.606e+09 -406cb24924924925 229.57142857142858 -3ff9b645a1cac083 1.607 -41d7f237f0000000 1.607e+09 -406cb6db6db6db6e 229.71428571428572 -3ff9ba5e353f7cee 1.608 -41d7f60880000000 1.608e+09 -406cbb6db6db6db7 229.85714285714286 -3ff9be76c8b43958 1.609 -41d7f9d910000000 1.609e+09 -406cc00000000000 230 -3ff9c28f5c28f5c3 1.61 -41d7fda9a0000000 1.61e+09 -406cc49249249249 230.14285714285714 -3ff9c6a7ef9db22d 1.611 -41d8017a30000000 1.611e+09 -406cc92492492492 230.28571428571428 -3ff9cac083126e98 1.612 -41d8054ac0000000 1.612e+09 -406ccdb6db6db6db 230.42857142857142 -3ff9ced916872b02 1.613 -41d8091b50000000 1.613e+09 -406cd24924924925 230.57142857142858 -3ff9d2f1a9fbe76d 1.614 -41d80cebe0000000 1.614e+09 -406cd6db6db6db6e 230.71428571428572 -3ff9d70a3d70a3d7 1.615 -41d810bc70000000 1.615e+09 -406cdb6db6db6db7 230.85714285714286 -3ff9db22d0e56042 1.616 -41d8148d00000000 1.616e+09 -406ce00000000000 231 -3ff9df3b645a1cac 1.617 -41d8185d90000000 1.617e+09 -406ce49249249249 231.14285714285714 -3ff9e353f7ced917 1.618 -41d81c2e20000000 1.618e+09 -406ce92492492492 231.28571428571428 -3ff9e76c8b439581 1.619 -41d81ffeb0000000 1.619e+09 -406cedb6db6db6db 231.42857142857142 -3ff9eb851eb851ec 1.62 -41d823cf40000000 1.62e+09 -406cf24924924925 231.57142857142858 -3ff9ef9db22d0e56 1.621 -41d8279fd0000000 1.621e+09 -406cf6db6db6db6e 231.71428571428572 -3ff9f3b645a1cac1 1.622 -41d82b7060000000 1.622e+09 -406cfb6db6db6db7 231.85714285714286 -3ff9f7ced916872b 1.623 -41d82f40f0000000 1.623e+09 -406d000000000000 232 -3ff9fbe76c8b4396 1.624 -41d8331180000000 1.624e+09 -406d049249249249 232.14285714285714 -3ffa000000000000 1.625 -41d836e210000000 1.625e+09 -406d092492492492 232.28571428571428 -3ffa04189374bc6b 1.6260000000000001 -41d83ab2a0000000 1.626e+09 -406d0db6db6db6db 232.42857142857142 -3ffa083126e978d5 1.627 -41d83e8330000000 1.627e+09 -406d124924924925 232.57142857142858 -3ffa0c49ba5e3540 1.6280000000000001 -41d84253c0000000 1.628e+09 -406d16db6db6db6e 232.71428571428572 -3ffa10624dd2f1aa 1.629 -41d8462450000000 1.629e+09 -406d1b6db6db6db7 232.85714285714286 -3ffa147ae147ae15 1.6300000000000001 -41d849f4e0000000 1.63e+09 -406d200000000000 233 -3ffa189374bc6a7f 1.631 -41d84dc570000000 1.631e+09 -406d249249249249 233.14285714285714 -3ffa1cac083126ea 1.6320000000000001 -41d8519600000000 1.632e+09 -406d292492492492 233.28571428571428 -3ffa20c49ba5e354 1.633 -41d8556690000000 1.633e+09 -406d2db6db6db6db 233.42857142857142 -3ffa24dd2f1a9fbf 1.6340000000000001 -41d8593720000000 1.634e+09 -406d324924924925 233.57142857142858 -3ffa28f5c28f5c29 1.635 -41d85d07b0000000 1.635e+09 -406d36db6db6db6e 233.71428571428572 -3ffa2d0e56041894 1.6360000000000001 -41d860d840000000 1.636e+09 -406d3b6db6db6db7 233.85714285714286 -3ffa3126e978d4fe 1.637 -41d864a8d0000000 1.637e+09 -406d400000000000 234 -3ffa353f7ced9169 1.6380000000000001 -41d8687960000000 1.638e+09 -406d449249249249 234.14285714285714 -3ffa395810624dd3 1.639 -41d86c49f0000000 1.639e+09 -406d492492492492 234.28571428571428 -3ffa3d70a3d70a3e 1.6400000000000001 -41d8701a80000000 1.64e+09 -406d4db6db6db6db 234.42857142857142 -3ffa4189374bc6a8 1.641 -41d873eb10000000 1.641e+09 -406d524924924925 234.57142857142858 -3ffa45a1cac08313 1.6420000000000001 -41d877bba0000000 1.642e+09 -406d56db6db6db6e 234.71428571428572 -3ffa49ba5e353f7d 1.643 -41d87b8c30000000 1.643e+09 -406d5b6db6db6db7 234.85714285714286 -3ffa4dd2f1a9fbe8 1.6440000000000001 -41d87f5cc0000000 1.644e+09 -406d600000000000 235 -3ffa51eb851eb852 1.645 -41d8832d50000000 1.645e+09 -406d649249249249 235.14285714285714 -3ffa5604189374bd 1.6460000000000001 -41d886fde0000000 1.646e+09 -406d692492492492 235.28571428571428 -3ffa5a1cac083127 1.647 -41d88ace70000000 1.647e+09 -406d6db6db6db6db 235.42857142857142 -3ffa5e353f7ced92 1.6480000000000001 -41d88e9f00000000 1.648e+09 -406d724924924925 235.57142857142858 -3ffa624dd2f1a9fc 1.649 -41d8926f90000000 1.649e+09 -406d76db6db6db6e 235.71428571428572 -3ffa666666666667 1.6500000000000001 -41d8964020000000 1.65e+09 -406d7b6db6db6db7 235.85714285714286 -3ffa6a7ef9db22d1 1.651 -41d89a10b0000000 1.651e+09 -406d800000000000 236 -3ffa6e978d4fdf3c 1.6520000000000001 -41d89de140000000 1.652e+09 -406d849249249249 236.14285714285714 -3ffa72b020c49ba6 1.653 -41d8a1b1d0000000 1.653e+09 -406d892492492492 236.28571428571428 -3ffa76c8b4395811 1.6540000000000001 -41d8a58260000000 1.654e+09 -406d8db6db6db6db 236.42857142857142 -3ffa7ae147ae147b 1.655 -41d8a952f0000000 1.655e+09 -406d924924924925 236.57142857142858 -3ffa7ef9db22d0e6 1.6560000000000001 -41d8ad2380000000 1.656e+09 -406d96db6db6db6e 236.71428571428572 -3ffa83126e978d50 1.657 -41d8b0f410000000 1.657e+09 -406d9b6db6db6db7 236.85714285714286 -3ffa872b020c49bb 1.6580000000000001 -41d8b4c4a0000000 1.658e+09 -406da00000000000 237 -3ffa8b4395810625 1.659 -41d8b89530000000 1.659e+09 -406da49249249249 237.14285714285714 -3ffa8f5c28f5c290 1.6600000000000001 -41d8bc65c0000000 1.66e+09 -406da92492492492 237.28571428571428 -3ffa9374bc6a7efa 1.661 -41d8c03650000000 1.661e+09 -406dadb6db6db6db 237.42857142857142 -3ffa978d4fdf3b65 1.6620000000000001 -41d8c406e0000000 1.662e+09 -406db24924924925 237.57142857142858 -3ffa9ba5e353f7cf 1.663 -41d8c7d770000000 1.663e+09 -406db6db6db6db6e 237.71428571428572 -3ffa9fbe76c8b43a 1.6640000000000001 -41d8cba800000000 1.664e+09 -406dbb6db6db6db7 237.85714285714286 -3ffaa3d70a3d70a4 1.665 -41d8cf7890000000 1.665e+09 -406dc00000000000 238 -3ffaa7ef9db22d0e 1.666 -41d8d34920000000 1.666e+09 -406dc49249249249 238.14285714285714 -3ffaac083126e979 1.667 -41d8d719b0000000 1.667e+09 -406dc92492492492 238.28571428571428 -3ffab020c49ba5e3 1.668 -41d8daea40000000 1.668e+09 -406dcdb6db6db6db 238.42857142857142 -3ffab4395810624e 1.669 -41d8debad0000000 1.669e+09 -406dd24924924925 238.57142857142858 -3ffab851eb851eb8 1.67 -41d8e28b60000000 1.67e+09 -406dd6db6db6db6e 238.71428571428572 -3ffabc6a7ef9db23 1.671 -41d8e65bf0000000 1.671e+09 -406ddb6db6db6db7 238.85714285714286 -3ffac083126e978d 1.672 -41d8ea2c80000000 1.672e+09 -406de00000000000 239 -3ffac49ba5e353f8 1.673 -41d8edfd10000000 1.673e+09 -406de49249249249 239.14285714285714 -3ffac8b439581062 1.674 -41d8f1cda0000000 1.674e+09 -406de92492492492 239.28571428571428 -3ffacccccccccccd 1.675 -41d8f59e30000000 1.675e+09 -406dedb6db6db6db 239.42857142857142 -3ffad0e560418937 1.676 -41d8f96ec0000000 1.676e+09 -406df24924924925 239.57142857142858 -3ffad4fdf3b645a2 1.677 -41d8fd3f50000000 1.677e+09 -406df6db6db6db6e 239.71428571428572 -3ffad916872b020c 1.678 -41d9010fe0000000 1.678e+09 -406dfb6db6db6db7 239.85714285714286 -3ffadd2f1a9fbe77 1.679 -41d904e070000000 1.679e+09 -406e000000000000 240 -3ffae147ae147ae1 1.68 -41d908b100000000 1.68e+09 -406e049249249249 240.14285714285714 -3ffae5604189374c 1.681 -41d90c8190000000 1.681e+09 -406e092492492492 240.28571428571428 -3ffae978d4fdf3b6 1.682 -41d9105220000000 1.682e+09 -406e0db6db6db6db 240.42857142857142 -3ffaed916872b021 1.683 -41d91422b0000000 1.683e+09 -406e124924924925 240.57142857142858 -3ffaf1a9fbe76c8b 1.684 -41d917f340000000 1.684e+09 -406e16db6db6db6e 240.71428571428572 -3ffaf5c28f5c28f6 1.685 -41d91bc3d0000000 1.685e+09 -406e1b6db6db6db7 240.85714285714286 -3ffaf9db22d0e560 1.686 -41d91f9460000000 1.686e+09 -406e200000000000 241 -3ffafdf3b645a1cb 1.687 -41d92364f0000000 1.687e+09 -406e249249249249 241.14285714285714 -3ffb020c49ba5e35 1.688 -41d9273580000000 1.688e+09 -406e292492492492 241.28571428571428 -3ffb0624dd2f1aa0 1.689 -41d92b0610000000 1.689e+09 -406e2db6db6db6db 241.42857142857142 -3ffb0a3d70a3d70a 1.69 -41d92ed6a0000000 1.69e+09 -406e324924924925 241.57142857142858 -3ffb0e5604189375 1.691 -41d932a730000000 1.691e+09 -406e36db6db6db6e 241.71428571428572 -3ffb126e978d4fdf 1.692 -41d93677c0000000 1.692e+09 -406e3b6db6db6db7 241.85714285714286 -3ffb16872b020c4a 1.693 -41d93a4850000000 1.693e+09 -406e400000000000 242 -3ffb1a9fbe76c8b4 1.694 -41d93e18e0000000 1.694e+09 -406e449249249249 242.14285714285714 -3ffb1eb851eb851f 1.695 -41d941e970000000 1.695e+09 -406e492492492492 242.28571428571428 -3ffb22d0e5604189 1.696 -41d945ba00000000 1.696e+09 -406e4db6db6db6db 242.42857142857142 -3ffb26e978d4fdf4 1.697 -41d9498a90000000 1.697e+09 -406e524924924925 242.57142857142858 -3ffb2b020c49ba5e 1.698 -41d94d5b20000000 1.698e+09 -406e56db6db6db6e 242.71428571428572 -3ffb2f1a9fbe76c9 1.699 -41d9512bb0000000 1.699e+09 -406e5b6db6db6db7 242.85714285714286 -3ffb333333333333 1.7 -41d954fc40000000 1.7e+09 -406e600000000000 243 -3ffb374bc6a7ef9e 1.701 -41d958ccd0000000 1.701e+09 -406e649249249249 243.14285714285714 -3ffb3b645a1cac08 1.702 -41d95c9d60000000 1.702e+09 -406e692492492492 243.28571428571428 -3ffb3f7ced916873 1.703 -41d9606df0000000 1.703e+09 -406e6db6db6db6db 243.42857142857142 -3ffb4395810624dd 1.704 -41d9643e80000000 1.704e+09 -406e724924924925 243.57142857142858 -3ffb47ae147ae148 1.705 -41d9680f10000000 1.705e+09 -406e76db6db6db6e 243.71428571428572 -3ffb4bc6a7ef9db2 1.706 -41d96bdfa0000000 1.706e+09 -406e7b6db6db6db7 243.85714285714286 -3ffb4fdf3b645a1d 1.707 -41d96fb030000000 1.707e+09 -406e800000000000 244 -3ffb53f7ced91687 1.708 -41d97380c0000000 1.708e+09 -406e849249249249 244.14285714285714 -3ffb5810624dd2f2 1.709 -41d9775150000000 1.709e+09 -406e892492492492 244.28571428571428 -3ffb5c28f5c28f5c 1.71 -41d97b21e0000000 1.71e+09 -406e8db6db6db6db 244.42857142857142 -3ffb604189374bc7 1.711 -41d97ef270000000 1.711e+09 -406e924924924925 244.57142857142858 -3ffb645a1cac0831 1.712 -41d982c300000000 1.712e+09 -406e96db6db6db6e 244.71428571428572 -3ffb6872b020c49c 1.713 -41d9869390000000 1.713e+09 -406e9b6db6db6db7 244.85714285714286 -3ffb6c8b43958106 1.714 -41d98a6420000000 1.714e+09 -406ea00000000000 245 -3ffb70a3d70a3d71 1.715 -41d98e34b0000000 1.715e+09 -406ea49249249249 245.14285714285714 -3ffb74bc6a7ef9db 1.716 -41d9920540000000 1.716e+09 -406ea92492492492 245.28571428571428 -3ffb78d4fdf3b646 1.717 -41d995d5d0000000 1.717e+09 -406eadb6db6db6db 245.42857142857142 -3ffb7ced916872b0 1.718 -41d999a660000000 1.718e+09 -406eb24924924925 245.57142857142858 -3ffb810624dd2f1b 1.719 -41d99d76f0000000 1.719e+09 -406eb6db6db6db6e 245.71428571428572 -3ffb851eb851eb85 1.72 -41d9a14780000000 1.72e+09 -406ebb6db6db6db7 245.85714285714286 -3ffb89374bc6a7f0 1.721 -41d9a51810000000 1.721e+09 -406ec00000000000 246 -3ffb8d4fdf3b645a 1.722 -41d9a8e8a0000000 1.722e+09 -406ec49249249249 246.14285714285714 -3ffb916872b020c5 1.723 -41d9acb930000000 1.723e+09 -406ec92492492492 246.28571428571428 -3ffb95810624dd2f 1.724 -41d9b089c0000000 1.724e+09 -406ecdb6db6db6db 246.42857142857142 -3ffb99999999999a 1.725 -41d9b45a50000000 1.725e+09 -406ed24924924925 246.57142857142858 -3ffb9db22d0e5604 1.726 -41d9b82ae0000000 1.726e+09 -406ed6db6db6db6e 246.71428571428572 -3ffba1cac083126f 1.727 -41d9bbfb70000000 1.727e+09 -406edb6db6db6db7 246.85714285714286 -3ffba5e353f7ced9 1.728 -41d9bfcc00000000 1.728e+09 -406ee00000000000 247 -3ffba9fbe76c8b44 1.729 -41d9c39c90000000 1.729e+09 -406ee49249249249 247.14285714285714 -3ffbae147ae147ae 1.73 -41d9c76d20000000 1.73e+09 -406ee92492492492 247.28571428571428 -3ffbb22d0e560419 1.731 -41d9cb3db0000000 1.731e+09 -406eedb6db6db6db 247.42857142857142 -3ffbb645a1cac083 1.732 -41d9cf0e40000000 1.732e+09 -406ef24924924925 247.57142857142858 -3ffbba5e353f7cee 1.733 -41d9d2ded0000000 1.733e+09 -406ef6db6db6db6e 247.71428571428572 -3ffbbe76c8b43958 1.734 -41d9d6af60000000 1.734e+09 -406efb6db6db6db7 247.85714285714286 -3ffbc28f5c28f5c3 1.735 -41d9da7ff0000000 1.735e+09 -406f000000000000 248 -3ffbc6a7ef9db22d 1.736 -41d9de5080000000 1.736e+09 -406f049249249249 248.14285714285714 -3ffbcac083126e98 1.737 -41d9e22110000000 1.737e+09 -406f092492492492 248.28571428571428 -3ffbced916872b02 1.738 -41d9e5f1a0000000 1.738e+09 -406f0db6db6db6db 248.42857142857142 -3ffbd2f1a9fbe76d 1.739 -41d9e9c230000000 1.739e+09 -406f124924924925 248.57142857142858 -3ffbd70a3d70a3d7 1.74 -41d9ed92c0000000 1.74e+09 -406f16db6db6db6e 248.71428571428572 -3ffbdb22d0e56042 1.741 -41d9f16350000000 1.741e+09 -406f1b6db6db6db7 248.85714285714286 -3ffbdf3b645a1cac 1.742 -41d9f533e0000000 1.742e+09 -406f200000000000 249 -3ffbe353f7ced917 1.743 -41d9f90470000000 1.743e+09 -406f249249249249 249.14285714285714 -3ffbe76c8b439581 1.744 -41d9fcd500000000 1.744e+09 -406f292492492492 249.28571428571428 -3ffbeb851eb851ec 1.745 -41da00a590000000 1.745e+09 -406f2db6db6db6db 249.42857142857142 -3ffbef9db22d0e56 1.746 -41da047620000000 1.746e+09 -406f324924924925 249.57142857142858 -3ffbf3b645a1cac1 1.747 -41da0846b0000000 1.747e+09 -406f36db6db6db6e 249.71428571428572 -3ffbf7ced916872b 1.748 -41da0c1740000000 1.748e+09 -406f3b6db6db6db7 249.85714285714286 -3ffbfbe76c8b4396 1.749 -41da0fe7d0000000 1.749e+09 -406f400000000000 250 -3ffc000000000000 1.75 -41da13b860000000 1.75e+09 -406f449249249249 250.14285714285714 -3ffc04189374bc6b 1.7510000000000001 -41da1788f0000000 1.751e+09 -406f492492492492 250.28571428571428 -3ffc083126e978d5 1.752 -41da1b5980000000 1.752e+09 -406f4db6db6db6db 250.42857142857142 -3ffc0c49ba5e3540 1.7530000000000001 -41da1f2a10000000 1.753e+09 -406f524924924925 250.57142857142858 -3ffc10624dd2f1aa 1.754 -41da22faa0000000 1.754e+09 -406f56db6db6db6e 250.71428571428572 -3ffc147ae147ae15 1.7550000000000001 -41da26cb30000000 1.755e+09 -406f5b6db6db6db7 250.85714285714286 -3ffc189374bc6a7f 1.756 -41da2a9bc0000000 1.756e+09 -406f600000000000 251 -3ffc1cac083126ea 1.7570000000000001 -41da2e6c50000000 1.757e+09 -406f649249249249 251.14285714285714 -3ffc20c49ba5e354 1.758 -41da323ce0000000 1.758e+09 -406f692492492492 251.28571428571428 -3ffc24dd2f1a9fbf 1.7590000000000001 -41da360d70000000 1.759e+09 -406f6db6db6db6db 251.42857142857142 -3ffc28f5c28f5c29 1.76 -41da39de00000000 1.76e+09 -406f724924924925 251.57142857142858 -3ffc2d0e56041894 1.7610000000000001 -41da3dae90000000 1.761e+09 -406f76db6db6db6e 251.71428571428572 -3ffc3126e978d4fe 1.762 -41da417f20000000 1.762e+09 -406f7b6db6db6db7 251.85714285714286 -3ffc353f7ced9169 1.7630000000000001 -41da454fb0000000 1.763e+09 -406f800000000000 252 -3ffc395810624dd3 1.764 -41da492040000000 1.764e+09 -406f849249249249 252.14285714285714 -3ffc3d70a3d70a3e 1.7650000000000001 -41da4cf0d0000000 1.765e+09 -406f892492492492 252.28571428571428 -3ffc4189374bc6a8 1.766 -41da50c160000000 1.766e+09 -406f8db6db6db6db 252.42857142857142 -3ffc45a1cac08313 1.7670000000000001 -41da5491f0000000 1.767e+09 -406f924924924925 252.57142857142858 -3ffc49ba5e353f7d 1.768 -41da586280000000 1.768e+09 -406f96db6db6db6e 252.71428571428572 -3ffc4dd2f1a9fbe8 1.7690000000000001 -41da5c3310000000 1.769e+09 -406f9b6db6db6db7 252.85714285714286 -3ffc51eb851eb852 1.77 -41da6003a0000000 1.77e+09 -406fa00000000000 253 -3ffc5604189374bd 1.7710000000000001 -41da63d430000000 1.771e+09 -406fa49249249249 253.14285714285714 -3ffc5a1cac083127 1.772 -41da67a4c0000000 1.772e+09 -406fa92492492492 253.28571428571428 -3ffc5e353f7ced92 1.7730000000000001 -41da6b7550000000 1.773e+09 -406fadb6db6db6db 253.42857142857142 -3ffc624dd2f1a9fc 1.774 -41da6f45e0000000 1.774e+09 -406fb24924924925 253.57142857142858 -3ffc666666666667 1.7750000000000001 -41da731670000000 1.775e+09 -406fb6db6db6db6e 253.71428571428572 -3ffc6a7ef9db22d1 1.776 -41da76e700000000 1.776e+09 -406fbb6db6db6db7 253.85714285714286 -3ffc6e978d4fdf3c 1.7770000000000001 -41da7ab790000000 1.777e+09 -406fc00000000000 254 -3ffc72b020c49ba6 1.778 -41da7e8820000000 1.778e+09 -406fc49249249249 254.14285714285714 -3ffc76c8b4395811 1.7790000000000001 -41da8258b0000000 1.779e+09 -406fc92492492492 254.28571428571428 -3ffc7ae147ae147b 1.78 -41da862940000000 1.78e+09 -406fcdb6db6db6db 254.42857142857142 -3ffc7ef9db22d0e6 1.7810000000000001 -41da89f9d0000000 1.781e+09 -406fd24924924925 254.57142857142858 -3ffc83126e978d50 1.782 -41da8dca60000000 1.782e+09 -406fd6db6db6db6e 254.71428571428572 -3ffc872b020c49bb 1.7830000000000001 -41da919af0000000 1.783e+09 -406fdb6db6db6db7 254.85714285714286 -3ffc8b4395810625 1.784 -41da956b80000000 1.784e+09 -406fe00000000000 255 -3ffc8f5c28f5c290 1.7850000000000001 -41da993c10000000 1.785e+09 -406fe49249249249 255.14285714285714 -3ffc9374bc6a7efa 1.786 -41da9d0ca0000000 1.786e+09 -406fe92492492492 255.28571428571428 -3ffc978d4fdf3b65 1.7870000000000001 -41daa0dd30000000 1.787e+09 -406fedb6db6db6db 255.42857142857142 -3ffc9ba5e353f7cf 1.788 -41daa4adc0000000 1.788e+09 -406ff24924924925 255.57142857142858 -3ffc9fbe76c8b43a 1.7890000000000001 -41daa87e50000000 1.789e+09 -406ff6db6db6db6e 255.71428571428572 -3ffca3d70a3d70a4 1.79 -41daac4ee0000000 1.79e+09 -406ffb6db6db6db7 255.85714285714286 -3ffca7ef9db22d0f 1.7910000000000001 -41dab01f70000000 1.791e+09 -4070000000000000 256 -3ffcac083126e979 1.792 -41dab3f000000000 1.792e+09 -4070024924924925 256.14285714285717 -3ffcb020c49ba5e3 1.793 -41dab7c090000000 1.793e+09 -4070049249249249 256.2857142857143 -3ffcb4395810624e 1.794 -41dabb9120000000 1.794e+09 -407006db6db6db6e 256.42857142857144 -3ffcb851eb851eb8 1.795 -41dabf61b0000000 1.795e+09 -4070092492492492 256.57142857142856 -3ffcbc6a7ef9db23 1.796 -41dac33240000000 1.796e+09 -40700b6db6db6db7 256.7142857142857 -3ffcc083126e978d 1.797 -41dac702d0000000 1.797e+09 -40700db6db6db6db 256.85714285714283 -3ffcc49ba5e353f8 1.798 -41dacad360000000 1.798e+09 -4070100000000000 257 -3ffcc8b439581062 1.799 -41dacea3f0000000 1.799e+09 -4070124924924925 257.14285714285717 -3ffccccccccccccd 1.8 -41dad27480000000 1.8e+09 -4070149249249249 257.2857142857143 -3ffcd0e560418937 1.801 -41dad64510000000 1.801e+09 -407016db6db6db6e 257.42857142857144 -3ffcd4fdf3b645a2 1.802 -41dada15a0000000 1.802e+09 -4070192492492492 257.57142857142856 -3ffcd916872b020c 1.803 -41dadde630000000 1.803e+09 -40701b6db6db6db7 257.7142857142857 -3ffcdd2f1a9fbe77 1.804 -41dae1b6c0000000 1.804e+09 -40701db6db6db6db 257.85714285714283 -3ffce147ae147ae1 1.805 -41dae58750000000 1.805e+09 -4070200000000000 258 -3ffce5604189374c 1.806 -41dae957e0000000 1.806e+09 -4070224924924925 258.14285714285717 -3ffce978d4fdf3b6 1.807 -41daed2870000000 1.807e+09 -4070249249249249 258.2857142857143 -3ffced916872b021 1.808 -41daf0f900000000 1.808e+09 -407026db6db6db6e 258.42857142857144 -3ffcf1a9fbe76c8b 1.809 -41daf4c990000000 1.809e+09 -4070292492492492 258.57142857142856 -3ffcf5c28f5c28f6 1.81 -41daf89a20000000 1.81e+09 -40702b6db6db6db7 258.7142857142857 -3ffcf9db22d0e560 1.811 -41dafc6ab0000000 1.811e+09 -40702db6db6db6db 258.85714285714283 -3ffcfdf3b645a1cb 1.812 -41db003b40000000 1.812e+09 -4070300000000000 259 -3ffd020c49ba5e35 1.813 -41db040bd0000000 1.813e+09 -4070324924924925 259.14285714285717 -3ffd0624dd2f1aa0 1.814 -41db07dc60000000 1.814e+09 -4070349249249249 259.2857142857143 -3ffd0a3d70a3d70a 1.815 -41db0bacf0000000 1.815e+09 -407036db6db6db6e 259.42857142857144 -3ffd0e5604189375 1.816 -41db0f7d80000000 1.816e+09 -4070392492492492 259.57142857142856 -3ffd126e978d4fdf 1.817 -41db134e10000000 1.817e+09 -40703b6db6db6db7 259.7142857142857 -3ffd16872b020c4a 1.818 -41db171ea0000000 1.818e+09 -40703db6db6db6db 259.85714285714283 -3ffd1a9fbe76c8b4 1.819 -41db1aef30000000 1.819e+09 -4070400000000000 260 -3ffd1eb851eb851f 1.82 -41db1ebfc0000000 1.82e+09 -4070424924924925 260.14285714285717 -3ffd22d0e5604189 1.821 -41db229050000000 1.821e+09 -4070449249249249 260.2857142857143 -3ffd26e978d4fdf4 1.822 -41db2660e0000000 1.822e+09 -407046db6db6db6e 260.42857142857144 -3ffd2b020c49ba5e 1.823 -41db2a3170000000 1.823e+09 -4070492492492492 260.57142857142856 -3ffd2f1a9fbe76c9 1.824 -41db2e0200000000 1.824e+09 -40704b6db6db6db7 260.7142857142857 -3ffd333333333333 1.825 -41db31d290000000 1.825e+09 -40704db6db6db6db 260.85714285714283 -3ffd374bc6a7ef9e 1.826 -41db35a320000000 1.826e+09 -4070500000000000 261 -3ffd3b645a1cac08 1.827 -41db3973b0000000 1.827e+09 -4070524924924925 261.14285714285717 -3ffd3f7ced916873 1.828 -41db3d4440000000 1.828e+09 -4070549249249249 261.2857142857143 -3ffd4395810624dd 1.829 -41db4114d0000000 1.829e+09 -407056db6db6db6e 261.42857142857144 -3ffd47ae147ae148 1.83 -41db44e560000000 1.83e+09 -4070592492492492 261.57142857142856 -3ffd4bc6a7ef9db2 1.831 -41db48b5f0000000 1.831e+09 -40705b6db6db6db7 261.7142857142857 -3ffd4fdf3b645a1d 1.832 -41db4c8680000000 1.832e+09 -40705db6db6db6db 261.85714285714283 -3ffd53f7ced91687 1.833 -41db505710000000 1.833e+09 -4070600000000000 262 -3ffd5810624dd2f2 1.834 -41db5427a0000000 1.834e+09 -4070624924924925 262.14285714285717 -3ffd5c28f5c28f5c 1.835 -41db57f830000000 1.835e+09 -4070649249249249 262.2857142857143 -3ffd604189374bc7 1.836 -41db5bc8c0000000 1.836e+09 -407066db6db6db6e 262.42857142857144 -3ffd645a1cac0831 1.837 -41db5f9950000000 1.837e+09 -4070692492492492 262.57142857142856 -3ffd6872b020c49c 1.838 -41db6369e0000000 1.838e+09 -40706b6db6db6db7 262.7142857142857 -3ffd6c8b43958106 1.839 -41db673a70000000 1.839e+09 -40706db6db6db6db 262.85714285714283 -3ffd70a3d70a3d71 1.84 -41db6b0b00000000 1.84e+09 -4070700000000000 263 -3ffd74bc6a7ef9db 1.841 -41db6edb90000000 1.841e+09 -4070724924924925 263.14285714285717 -3ffd78d4fdf3b646 1.842 -41db72ac20000000 1.842e+09 -4070749249249249 263.2857142857143 -3ffd7ced916872b0 1.843 -41db767cb0000000 1.843e+09 -407076db6db6db6e 263.42857142857144 -3ffd810624dd2f1b 1.844 -41db7a4d40000000 1.844e+09 -4070792492492492 263.57142857142856 -3ffd851eb851eb85 1.845 -41db7e1dd0000000 1.845e+09 -40707b6db6db6db7 263.7142857142857 -3ffd89374bc6a7f0 1.846 -41db81ee60000000 1.846e+09 -40707db6db6db6db 263.85714285714283 -3ffd8d4fdf3b645a 1.847 -41db85bef0000000 1.847e+09 -4070800000000000 264 -3ffd916872b020c5 1.848 -41db898f80000000 1.848e+09 -4070824924924925 264.14285714285717 -3ffd95810624dd2f 1.849 -41db8d6010000000 1.849e+09 -4070849249249249 264.2857142857143 -3ffd99999999999a 1.85 -41db9130a0000000 1.85e+09 -407086db6db6db6e 264.42857142857144 -3ffd9db22d0e5604 1.851 -41db950130000000 1.851e+09 -4070892492492492 264.57142857142856 -3ffda1cac083126f 1.852 -41db98d1c0000000 1.852e+09 -40708b6db6db6db7 264.7142857142857 -3ffda5e353f7ced9 1.853 -41db9ca250000000 1.853e+09 -40708db6db6db6db 264.85714285714283 -3ffda9fbe76c8b44 1.854 -41dba072e0000000 1.854e+09 -4070900000000000 265 -3ffdae147ae147ae 1.855 -41dba44370000000 1.855e+09 -4070924924924925 265.14285714285717 -3ffdb22d0e560419 1.856 -41dba81400000000 1.856e+09 -4070949249249249 265.2857142857143 -3ffdb645a1cac083 1.857 -41dbabe490000000 1.857e+09 -407096db6db6db6e 265.42857142857144 -3ffdba5e353f7cee 1.858 -41dbafb520000000 1.858e+09 -4070992492492492 265.57142857142856 -3ffdbe76c8b43958 1.859 -41dbb385b0000000 1.859e+09 -40709b6db6db6db7 265.7142857142857 -3ffdc28f5c28f5c3 1.86 -41dbb75640000000 1.86e+09 -40709db6db6db6db 265.85714285714283 -3ffdc6a7ef9db22d 1.861 -41dbbb26d0000000 1.861e+09 -4070a00000000000 266 -3ffdcac083126e98 1.862 -41dbbef760000000 1.862e+09 -4070a24924924925 266.14285714285717 -3ffdced916872b02 1.863 -41dbc2c7f0000000 1.863e+09 -4070a49249249249 266.2857142857143 -3ffdd2f1a9fbe76d 1.864 -41dbc69880000000 1.864e+09 -4070a6db6db6db6e 266.42857142857144 -3ffdd70a3d70a3d7 1.865 -41dbca6910000000 1.865e+09 -4070a92492492492 266.57142857142856 -3ffddb22d0e56042 1.866 -41dbce39a0000000 1.866e+09 -4070ab6db6db6db7 266.7142857142857 -3ffddf3b645a1cac 1.867 -41dbd20a30000000 1.867e+09 -4070adb6db6db6db 266.85714285714283 -3ffde353f7ced917 1.868 -41dbd5dac0000000 1.868e+09 -4070b00000000000 267 -3ffde76c8b439581 1.869 -41dbd9ab50000000 1.869e+09 -4070b24924924925 267.14285714285717 -3ffdeb851eb851ec 1.87 -41dbdd7be0000000 1.87e+09 -4070b49249249249 267.2857142857143 -3ffdef9db22d0e56 1.871 -41dbe14c70000000 1.871e+09 -4070b6db6db6db6e 267.42857142857144 -3ffdf3b645a1cac1 1.872 -41dbe51d00000000 1.872e+09 -4070b92492492492 267.57142857142856 -3ffdf7ced916872b 1.873 -41dbe8ed90000000 1.873e+09 -4070bb6db6db6db7 267.7142857142857 -3ffdfbe76c8b4396 1.874 -41dbecbe20000000 1.874e+09 -4070bdb6db6db6db 267.85714285714283 -3ffe000000000000 1.875 -41dbf08eb0000000 1.875e+09 -4070c00000000000 268 -3ffe04189374bc6b 1.8760000000000001 -41dbf45f40000000 1.876e+09 -4070c24924924925 268.14285714285717 -3ffe083126e978d5 1.877 -41dbf82fd0000000 1.877e+09 -4070c49249249249 268.2857142857143 -3ffe0c49ba5e3540 1.8780000000000001 -41dbfc0060000000 1.878e+09 -4070c6db6db6db6e 268.42857142857144 -3ffe10624dd2f1aa 1.879 -41dbffd0f0000000 1.879e+09 -4070c92492492492 268.57142857142856 -3ffe147ae147ae15 1.8800000000000001 -41dc03a180000000 1.88e+09 -4070cb6db6db6db7 268.7142857142857 -3ffe189374bc6a7f 1.881 -41dc077210000000 1.881e+09 -4070cdb6db6db6db 268.85714285714283 -3ffe1cac083126ea 1.8820000000000001 -41dc0b42a0000000 1.882e+09 -4070d00000000000 269 -3ffe20c49ba5e354 1.883 -41dc0f1330000000 1.883e+09 -4070d24924924925 269.14285714285717 -3ffe24dd2f1a9fbf 1.8840000000000001 -41dc12e3c0000000 1.884e+09 -4070d49249249249 269.2857142857143 -3ffe28f5c28f5c29 1.885 -41dc16b450000000 1.885e+09 -4070d6db6db6db6e 269.42857142857144 -3ffe2d0e56041894 1.8860000000000001 -41dc1a84e0000000 1.886e+09 -4070d92492492492 269.57142857142856 -3ffe3126e978d4fe 1.887 -41dc1e5570000000 1.887e+09 -4070db6db6db6db7 269.7142857142857 -3ffe353f7ced9169 1.8880000000000001 -41dc222600000000 1.888e+09 -4070ddb6db6db6db 269.85714285714283 -3ffe395810624dd3 1.889 -41dc25f690000000 1.889e+09 -4070e00000000000 270 -3ffe3d70a3d70a3e 1.8900000000000001 -41dc29c720000000 1.89e+09 -4070e24924924925 270.14285714285717 -3ffe4189374bc6a8 1.891 -41dc2d97b0000000 1.891e+09 -4070e49249249249 270.2857142857143 -3ffe45a1cac08313 1.8920000000000001 -41dc316840000000 1.892e+09 -4070e6db6db6db6e 270.42857142857144 -3ffe49ba5e353f7d 1.893 -41dc3538d0000000 1.893e+09 -4070e92492492492 270.57142857142856 -3ffe4dd2f1a9fbe8 1.8940000000000001 -41dc390960000000 1.894e+09 -4070eb6db6db6db7 270.7142857142857 -3ffe51eb851eb852 1.895 -41dc3cd9f0000000 1.895e+09 -4070edb6db6db6db 270.85714285714283 -3ffe5604189374bd 1.8960000000000001 -41dc40aa80000000 1.896e+09 -4070f00000000000 271 -3ffe5a1cac083127 1.897 -41dc447b10000000 1.897e+09 -4070f24924924925 271.14285714285717 -3ffe5e353f7ced92 1.8980000000000001 -41dc484ba0000000 1.898e+09 -4070f49249249249 271.2857142857143 -3ffe624dd2f1a9fc 1.899 -41dc4c1c30000000 1.899e+09 -4070f6db6db6db6e 271.42857142857144 -3ffe666666666667 1.9000000000000001 -41dc4fecc0000000 1.9e+09 -4070f92492492492 271.57142857142856 -3ffe6a7ef9db22d1 1.901 -41dc53bd50000000 1.901e+09 -4070fb6db6db6db7 271.7142857142857 -3ffe6e978d4fdf3c 1.9020000000000001 -41dc578de0000000 1.902e+09 -4070fdb6db6db6db 271.85714285714283 -3ffe72b020c49ba6 1.903 -41dc5b5e70000000 1.903e+09 -4071000000000000 272 -3ffe76c8b4395811 1.9040000000000001 -41dc5f2f00000000 1.904e+09 -4071024924924925 272.14285714285717 -3ffe7ae147ae147b 1.905 -41dc62ff90000000 1.905e+09 -4071049249249249 272.2857142857143 -3ffe7ef9db22d0e6 1.9060000000000001 -41dc66d020000000 1.906e+09 -407106db6db6db6e 272.42857142857144 -3ffe83126e978d50 1.907 -41dc6aa0b0000000 1.907e+09 -4071092492492492 272.57142857142856 -3ffe872b020c49bb 1.9080000000000001 -41dc6e7140000000 1.908e+09 -40710b6db6db6db7 272.7142857142857 -3ffe8b4395810625 1.909 -41dc7241d0000000 1.909e+09 -40710db6db6db6db 272.85714285714283 -3ffe8f5c28f5c290 1.9100000000000001 -41dc761260000000 1.91e+09 -4071100000000000 273 -3ffe9374bc6a7efa 1.911 -41dc79e2f0000000 1.911e+09 -4071124924924925 273.14285714285717 -3ffe978d4fdf3b65 1.9120000000000001 -41dc7db380000000 1.912e+09 -4071149249249249 273.2857142857143 -3ffe9ba5e353f7cf 1.913 -41dc818410000000 1.913e+09 -407116db6db6db6e 273.42857142857144 -3ffe9fbe76c8b43a 1.9140000000000001 -41dc8554a0000000 1.914e+09 -4071192492492492 273.57142857142856 -3ffea3d70a3d70a4 1.915 -41dc892530000000 1.915e+09 -40711b6db6db6db7 273.7142857142857 -3ffea7ef9db22d0f 1.9160000000000001 -41dc8cf5c0000000 1.916e+09 -40711db6db6db6db 273.85714285714283 -3ffeac083126e979 1.917 -41dc90c650000000 1.917e+09 -4071200000000000 274 -3ffeb020c49ba5e4 1.9180000000000001 -41dc9496e0000000 1.918e+09 -4071224924924925 274.14285714285717 -3ffeb4395810624e 1.919 -41dc986770000000 1.919e+09 -4071249249249249 274.2857142857143 -3ffeb851eb851eb8 1.92 -41dc9c3800000000 1.92e+09 -407126db6db6db6e 274.42857142857144 -3ffebc6a7ef9db23 1.921 -41dca00890000000 1.921e+09 -4071292492492492 274.57142857142856 -3ffec083126e978d 1.922 -41dca3d920000000 1.922e+09 -40712b6db6db6db7 274.7142857142857 -3ffec49ba5e353f8 1.923 -41dca7a9b0000000 1.923e+09 -40712db6db6db6db 274.85714285714283 -3ffec8b439581062 1.924 -41dcab7a40000000 1.924e+09 -4071300000000000 275 -3ffecccccccccccd 1.925 -41dcaf4ad0000000 1.925e+09 -4071324924924925 275.14285714285717 -3ffed0e560418937 1.926 -41dcb31b60000000 1.926e+09 -4071349249249249 275.2857142857143 -3ffed4fdf3b645a2 1.927 -41dcb6ebf0000000 1.927e+09 -407136db6db6db6e 275.42857142857144 -3ffed916872b020c 1.928 -41dcbabc80000000 1.928e+09 -4071392492492492 275.57142857142856 -3ffedd2f1a9fbe77 1.929 -41dcbe8d10000000 1.929e+09 -40713b6db6db6db7 275.7142857142857 -3ffee147ae147ae1 1.93 -41dcc25da0000000 1.93e+09 -40713db6db6db6db 275.85714285714283 -3ffee5604189374c 1.931 -41dcc62e30000000 1.931e+09 -4071400000000000 276 -3ffee978d4fdf3b6 1.932 -41dcc9fec0000000 1.932e+09 -4071424924924925 276.14285714285717 -3ffeed916872b021 1.933 -41dccdcf50000000 1.933e+09 -4071449249249249 276.2857142857143 -3ffef1a9fbe76c8b 1.934 -41dcd19fe0000000 1.934e+09 -407146db6db6db6e 276.42857142857144 -3ffef5c28f5c28f6 1.935 -41dcd57070000000 1.935e+09 -4071492492492492 276.57142857142856 -3ffef9db22d0e560 1.936 -41dcd94100000000 1.936e+09 -40714b6db6db6db7 276.7142857142857 -3ffefdf3b645a1cb 1.937 -41dcdd1190000000 1.937e+09 -40714db6db6db6db 276.85714285714283 -3fff020c49ba5e35 1.938 -41dce0e220000000 1.938e+09 -4071500000000000 277 -3fff0624dd2f1aa0 1.939 -41dce4b2b0000000 1.939e+09 -4071524924924925 277.14285714285717 -3fff0a3d70a3d70a 1.94 -41dce88340000000 1.94e+09 -4071549249249249 277.2857142857143 -3fff0e5604189375 1.941 -41dcec53d0000000 1.941e+09 -407156db6db6db6e 277.42857142857144 -3fff126e978d4fdf 1.942 -41dcf02460000000 1.942e+09 -4071592492492492 277.57142857142856 -3fff16872b020c4a 1.943 -41dcf3f4f0000000 1.943e+09 -40715b6db6db6db7 277.7142857142857 -3fff1a9fbe76c8b4 1.944 -41dcf7c580000000 1.944e+09 -40715db6db6db6db 277.85714285714283 -3fff1eb851eb851f 1.945 -41dcfb9610000000 1.945e+09 -4071600000000000 278 -3fff22d0e5604189 1.946 -41dcff66a0000000 1.946e+09 -4071624924924925 278.14285714285717 -3fff26e978d4fdf4 1.947 -41dd033730000000 1.947e+09 -4071649249249249 278.2857142857143 -3fff2b020c49ba5e 1.948 -41dd0707c0000000 1.948e+09 -407166db6db6db6e 278.42857142857144 -3fff2f1a9fbe76c9 1.949 -41dd0ad850000000 1.949e+09 -4071692492492492 278.57142857142856 -3fff333333333333 1.95 -41dd0ea8e0000000 1.95e+09 -40716b6db6db6db7 278.7142857142857 -3fff374bc6a7ef9e 1.951 -41dd127970000000 1.951e+09 -40716db6db6db6db 278.85714285714283 -3fff3b645a1cac08 1.952 -41dd164a00000000 1.952e+09 -4071700000000000 279 -3fff3f7ced916873 1.953 -41dd1a1a90000000 1.953e+09 -4071724924924925 279.14285714285717 -3fff4395810624dd 1.954 -41dd1deb20000000 1.954e+09 -4071749249249249 279.2857142857143 -3fff47ae147ae148 1.955 -41dd21bbb0000000 1.955e+09 -407176db6db6db6e 279.42857142857144 -3fff4bc6a7ef9db2 1.956 -41dd258c40000000 1.956e+09 -4071792492492492 279.57142857142856 -3fff4fdf3b645a1d 1.957 -41dd295cd0000000 1.957e+09 -40717b6db6db6db7 279.7142857142857 -3fff53f7ced91687 1.958 -41dd2d2d60000000 1.958e+09 -40717db6db6db6db 279.85714285714283 -3fff5810624dd2f2 1.959 -41dd30fdf0000000 1.959e+09 -4071800000000000 280 -3fff5c28f5c28f5c 1.96 -41dd34ce80000000 1.96e+09 -4071824924924925 280.14285714285717 -3fff604189374bc7 1.961 -41dd389f10000000 1.961e+09 -4071849249249249 280.2857142857143 -3fff645a1cac0831 1.962 -41dd3c6fa0000000 1.962e+09 -407186db6db6db6e 280.42857142857144 -3fff6872b020c49c 1.963 -41dd404030000000 1.963e+09 -4071892492492492 280.57142857142856 -3fff6c8b43958106 1.964 -41dd4410c0000000 1.964e+09 -40718b6db6db6db7 280.7142857142857 -3fff70a3d70a3d71 1.965 -41dd47e150000000 1.965e+09 -40718db6db6db6db 280.85714285714283 -3fff74bc6a7ef9db 1.966 -41dd4bb1e0000000 1.966e+09 -4071900000000000 281 -3fff78d4fdf3b646 1.967 -41dd4f8270000000 1.967e+09 -4071924924924925 281.14285714285717 -3fff7ced916872b0 1.968 -41dd535300000000 1.968e+09 -4071949249249249 281.2857142857143 -3fff810624dd2f1b 1.969 -41dd572390000000 1.969e+09 -407196db6db6db6e 281.42857142857144 -3fff851eb851eb85 1.97 -41dd5af420000000 1.97e+09 -4071992492492492 281.57142857142856 -3fff89374bc6a7f0 1.971 -41dd5ec4b0000000 1.971e+09 -40719b6db6db6db7 281.7142857142857 -3fff8d4fdf3b645a 1.972 -41dd629540000000 1.972e+09 -40719db6db6db6db 281.85714285714283 -3fff916872b020c5 1.973 -41dd6665d0000000 1.973e+09 -4071a00000000000 282 -3fff95810624dd2f 1.974 -41dd6a3660000000 1.974e+09 -4071a24924924925 282.14285714285717 -3fff99999999999a 1.975 -41dd6e06f0000000 1.975e+09 -4071a49249249249 282.2857142857143 -3fff9db22d0e5604 1.976 -41dd71d780000000 1.976e+09 -4071a6db6db6db6e 282.42857142857144 -3fffa1cac083126f 1.977 -41dd75a810000000 1.977e+09 -4071a92492492492 282.57142857142856 -3fffa5e353f7ced9 1.978 -41dd7978a0000000 1.978e+09 -4071ab6db6db6db7 282.7142857142857 -3fffa9fbe76c8b44 1.979 -41dd7d4930000000 1.979e+09 -4071adb6db6db6db 282.85714285714283 -3fffae147ae147ae 1.98 -41dd8119c0000000 1.98e+09 -4071b00000000000 283 -3fffb22d0e560419 1.981 -41dd84ea50000000 1.981e+09 -4071b24924924925 283.14285714285717 -3fffb645a1cac083 1.982 -41dd88bae0000000 1.982e+09 -4071b49249249249 283.2857142857143 -3fffba5e353f7cee 1.983 -41dd8c8b70000000 1.983e+09 -4071b6db6db6db6e 283.42857142857144 -3fffbe76c8b43958 1.984 -41dd905c00000000 1.984e+09 -4071b92492492492 283.57142857142856 -3fffc28f5c28f5c3 1.985 -41dd942c90000000 1.985e+09 -4071bb6db6db6db7 283.7142857142857 -3fffc6a7ef9db22d 1.986 -41dd97fd20000000 1.986e+09 -4071bdb6db6db6db 283.85714285714283 -3fffcac083126e98 1.987 -41dd9bcdb0000000 1.987e+09 -4071c00000000000 284 -3fffced916872b02 1.988 -41dd9f9e40000000 1.988e+09 -4071c24924924925 284.14285714285717 -3fffd2f1a9fbe76d 1.989 -41dda36ed0000000 1.989e+09 -4071c49249249249 284.2857142857143 -3fffd70a3d70a3d7 1.99 -41dda73f60000000 1.99e+09 -4071c6db6db6db6e 284.42857142857144 -3fffdb22d0e56042 1.991 -41ddab0ff0000000 1.991e+09 -4071c92492492492 284.57142857142856 -3fffdf3b645a1cac 1.992 -41ddaee080000000 1.992e+09 -4071cb6db6db6db7 284.7142857142857 -3fffe353f7ced917 1.993 -41ddb2b110000000 1.993e+09 -4071cdb6db6db6db 284.85714285714283 -3fffe76c8b439581 1.994 -41ddb681a0000000 1.994e+09 -4071d00000000000 285 -3fffeb851eb851ec 1.995 -41ddba5230000000 1.995e+09 -4071d24924924925 285.14285714285717 -3fffef9db22d0e56 1.996 -41ddbe22c0000000 1.996e+09 -4071d49249249249 285.2857142857143 -3ffff3b645a1cac1 1.997 -41ddc1f350000000 1.997e+09 -4071d6db6db6db6e 285.42857142857144 -3ffff7ced916872b 1.998 -41ddc5c3e0000000 1.998e+09 -4071d92492492492 285.57142857142856 -3ffffbe76c8b4396 1.999 -41ddc99470000000 1.999e+09 -4071db6db6db6db7 285.7142857142857 -4000000000000000 2 -41ddcd6500000000 2e+09 diff --git a/crates/cf-gojson/tests/oracle_data/json_cases.json b/crates/cf-gojson/tests/oracle_data/json_cases.json deleted file mode 100644 index 1f8c1da4..00000000 --- a/crates/cf-gojson/tests/oracle_data/json_cases.json +++ /dev/null @@ -1,147 +0,0 @@ -[ - { - "name": "null", - "compact": "null", - "indent": "null" - }, - { - "name": "bool_true", - "compact": "true", - "indent": "true" - }, - { - "name": "bool_false", - "compact": "false", - "indent": "false" - }, - { - "name": "int", - "compact": "42", - "indent": "42" - }, - { - "name": "int_neg", - "compact": "-7", - "indent": "-7" - }, - { - "name": "int_zero", - "compact": "0", - "indent": "0" - }, - { - "name": "float_int_valued", - "compact": "2", - "indent": "2" - }, - { - "name": "float_frac", - "compact": "3.14", - "indent": "3.14" - }, - { - "name": "float_exp_big", - "compact": "1e+21", - "indent": "1e+21" - }, - { - "name": "float_exp_small", - "compact": "0.00001", - "indent": "0.00001" - }, - { - "name": "string_plain", - "compact": "\"hello\"", - "indent": "\"hello\"" - }, - { - "name": "string_html", - "compact": "\"a\\u003cb\\u003ec\\u0026d\"", - "indent": "\"a\\u003cb\\u003ec\\u0026d\"" - }, - { - "name": "string_unicode_sep", - "compact": "\"x\\u2028y\\u2029z\"", - "indent": "\"x\\u2028y\\u2029z\"" - }, - { - "name": "string_quotes", - "compact": "\"she said \\\"hi\\\"\\tand\\nleft\"", - "indent": "\"she said \\\"hi\\\"\\tand\\nleft\"" - }, - { - "name": "string_backslash", - "compact": "\"a\\\\b/c\"", - "indent": "\"a\\\\b/c\"" - }, - { - "name": "string_control", - "compact": "\"\\u0000\\u0001\\u001f\"", - "indent": "\"\\u0000\\u0001\\u001f\"" - }, - { - "name": "string_emoji", - "compact": "\"go🚀rust\"", - "indent": "\"go🚀rust\"" - }, - { - "name": "empty_array", - "compact": "[]", - "indent": "[]" - }, - { - "name": "empty_object", - "compact": "{}", - "indent": "{}" - }, - { - "name": "array_mixed", - "compact": "[1,\"two\",3.5,true,null]", - "indent": "[\n 1,\n \"two\",\n 3.5,\n true,\n null\n]" - }, - { - "name": "nested_array", - "compact": "[[1,2],[],[\"a\"]]", - "indent": "[\n [\n 1,\n 2\n ],\n [],\n [\n \"a\"\n ]\n]" - }, - { - "name": "map_unsorted", - "compact": "{\"Mango\":3,\"_x\":4,\"apple\":2,\"zebra\":1}", - "indent": "{\n \"Mango\": 3,\n \"_x\": 4,\n \"apple\": 2,\n \"zebra\": 1\n}" - }, - { - "name": "map_byte_order", - "compact": "{\"A\":3,\"Z\":1,\"[\":5,\"a\":2,\"z\":4}", - "indent": "{\n \"A\": 3,\n \"Z\": 1,\n \"[\": 5,\n \"a\": 2,\n \"z\": 4\n}" - }, - { - "name": "map_html_keys", - "compact": "{\"\\u003ck\\u003e\":\"\\u003cv\\u003e\",\"a\\u0026b\":\"c\\u0026d\"}", - "indent": "{\n \"\\u003ck\\u003e\": \"\\u003cv\\u003e\",\n \"a\\u0026b\": \"c\\u0026d\"\n}" - }, - { - "name": "map_nested", - "compact": "{\"a\":[{\"k\":\"v\"}],\"b\":{\"x\":1,\"y\":2}}", - "indent": "{\n \"a\": [\n {\n \"k\": \"v\"\n }\n ],\n \"b\": {\n \"x\": 1,\n \"y\": 2\n }\n}" - }, - { - "name": "map_floats", - "compact": "{\"big\":1e+21,\"neg\":-0.5,\"pi\":3.141592653589793,\"tiny\":0.00001}", - "indent": "{\n \"big\": 1e+21,\n \"neg\": -0.5,\n \"pi\": 3.141592653589793,\n \"tiny\": 0.00001\n}" - }, - { - "name": "deep", - "compact": "{\"counts\":{\"go\":3,\"rust\":1},\"items\":[],\"name\":\"test\\u003cx\\u003e\",\"nested\":{\"deep\":{\"deeper\":[1,2,3]}},\"score\":0.875,\"unicode\":\"line1\\u2028line2\"}", - "indent": "{\n \"counts\": {\n \"go\": 3,\n \"rust\": 1\n },\n \"items\": [],\n \"name\": \"test\\u003cx\\u003e\",\n \"nested\": {\n \"deep\": {\n \"deeper\": [\n 1,\n 2,\n 3\n ]\n }\n },\n \"score\": 0.875,\n \"unicode\": \"line1\\u2028line2\"\n}" - }, - { - "name": "all_escapes", - "compact": "\"\\b\\f\\n\\r\\t\\\"\\\\\\u003c\\u003e\\u0026\\u2028\\u2029\"", - "indent": "\"\\b\\f\\n\\r\\t\\\"\\\\\\u003c\\u003e\\u0026\\u2028\\u2029\"" - }, - { - "name": "string_with_slash_only", - "compact": "\"https://example.com/path?x=1\\u0026y=2\"", - "indent": "\"https://example.com/path?x=1\\u0026y=2\"" - } -] diff --git a/crates/cf-gojson/tests/oracle_data/json_floats.tsv b/crates/cf-gojson/tests/oracle_data/json_floats.tsv deleted file mode 100644 index 4fd53eeb..00000000 --- a/crates/cf-gojson/tests/oracle_data/json_floats.tsv +++ /dev/null @@ -1,6729 +0,0 @@ -0000000000000000 0 -8000000000000000 -0 -3ff0000000000000 1 -bff0000000000000 -1 -3fe0000000000000 0.5 -bfe0000000000000 -0.5 -40091eb851eb851f 3.14 -c0091eb851eb851f -3.14 -4000000000000000 2 -4024000000000000 10 -4059000000000000 100 -4415af1d78b58c40 100000000000000000000 -444b1ae4d6e2ef50 1e+21 -4480f0cf064dd592 1e+22 -c44b1ae4d6e2ef50 -1e+21 -442043561a882930 150000000000000000000 -4454542ba12a337c 1.5e+21 -3f1a36e2eb1c432d 0.0001 -3ee4f8b588e368f1 0.00001 -3eb0c6f7a0b5ed8d 0.000001 -bee4f8b588e368f1 -0.00001 -3f1a36e2eb1c432d 0.0001 -3ee4f8b588e368f1 0.00001 -40f86a0000000000 100000 -412e848000000000 1000000 -416312d000000000 10000000 -430c6bf526340000 1000000000000000 -4341c37937e08000 10000000000000000 -4376345785d8a000 100000000000000000 -441ac53a7e04bcda 123456789012345680000 -441ac53a7e04bcda 123456789012345680000 -3fdc2f837b4a233a 0.4404 -bfdc2f837b4a233a -0.4404 -3fe3ff2e48e8a71e 0.6249 -bfe24d013a92a305 -0.5719 -3fd5c28f5c28f5c3 0.34 -3fea9c779a6b50b1 0.8316 -3fb999999999999a 0.1 -3fc999999999999a 0.2 -3fd3333333333333 0.3 -3fe6666666666666 0.7 -3fbf9add3739635f 0.123456789 -3fd5555555555555 0.3333333333333333 -3fe5555555555555 0.6666666666666666 -0000000000000001 5e-324 -7fefffffffffffff 1.7976931348623157e+308 -ffefffffffffffff -1.7976931348623157e+308 -0010000000000000 2.2250738585072014e-308 -4340000000000000 9007199254740992 -4340000000000000 9007199254740992 -4350000000000000 18014398509481984 -4040aaaaaaaaaaaa 33.33333333333333 -4050aaaaaaaaaaaa 66.66666666666666 -4029000000000000 12.5 -4055e00000000000 87.5 -3fc0000000000000 0.125 -bf1a36e2eb1c432d -0.0001 -c341c37937e08000 -10000000000000000 -3ff199999999999a 1.1 -3ff3333333333333 1.2 -4059066666666666 100.1 -3fd0000000000000 0.25 -3ff6a09e667f3bcd 1.4142135623730951 -4005bf0a8b145769 2.718281828459045 -400921fb54442d18 3.141592653589793 -3fff5c0331eeff84 1.959963984540054 -4059000000000000 100 -c059000000000000 -100 -36250953db1a9192 7.196856730011471e-48 -362d3ae36d13bbce 1e-47 -36344eb847de6aa4 1.3894954943731472e-47 -363c379909a4cd2d 1.930697728883246e-47 -36439a940b805332 2.682695795279738e-47 -364b3d4abc5d06c6 3.727593720314922e-47 -3652ecadc9ff7bf5 5.179474679231223e-47 -365a4ba8d1e135f5 7.1968567300114695e-47 -366244ce242c5561 1e-46 -3669626659d6054c 1.389495494373147e-46 -3671a2bfa607003b 1.9306977288832456e-46 -367881390e6067ff 2.6826957952797384e-46 -3681064eb5ba243c 3.7275937203149224e-46 -3687a7d93c7f5af1 5.179474679231222e-46 -36906f49832cc1b9 7.19685673001147e-46 -3696d601ad376ab9 1e-45 -369fbafff04b86a1 1.3894954943731474e-45 -36a60b6f8f88c04a 1.9306977288832456e-45 -36aea18751f881ff 2.6826957952797385e-45 -36b547e26328ad4b 3.7275937203149224e-45 -36bd91cf8b9f31af 5.1794746792312233e-45 -36c48b1be3f7f227 7.196856730011469e-45 -36cc8b8218854567 1e-44 -36d3d4dff62f3425 1.3894954943731475e-44 -36db8e4b736af05d 1.9306977288832458e-44 -36e324f4933b513f 2.6826957952797383e-44 -36ea99dafbf2d89d 3.727593720314922e-44 -36f27b21b7437f0d 5.179474679231223e-44 -36f9ade2dcf5eeb1 7.196856730011469e-44 -3701d7314f534b61 1e-43 -3708ca17f3bb012d 1.3894954943731472e-43 -371138ef2822d639 1.9306977288832453e-43 -3717ee31b80a258f 2.6826957952797384e-43 -3720a028dd77c762 3.727593720314922e-43 -372719ea25145ed1 5.1794746792312235e-43 -37300cadca19b52f 7.19685673001147e-43 -37364cfda3281e38 9.999999999999999e-43 -373efc9df0a9c178 1.3894954943731472e-42 -3745872af22b8bc8 1.9306977288832456e-42 -374de9be260caef2 2.682695795279738e-42 -3754c83314d5b93b 3.727593720314922e-42 -375ce064ae597684 5.179474679231223e-42 -37640fd93ca0227b 7.19685673001147e-42 -376be03d0bf225c7 1e-41 -37735de2b66a18ec 1.3894954943731474e-41 -377ae8f5aeb66eba 1.9306977288832456e-41 -3782b216d7c7ed58 2.6826957952797385e-41 -3789fa3fda0b278b 3.727593720314923e-41 -37920c3eecf7ea13 5.179474679231223e-41 -379913cf8bc82b19 7.196856730011469e-41 -37a16c262777579c 1e-40 -37a8355b64049f27 1.3894954943731474e-40 -37b0d1998d320535 1.930697728883246e-40 -37b75e9c8db9e8ae 2.6826957952797385e-40 -37c03c67e846f8b6 3.727593720314922e-40 -37c68f4ea835e498 5.1794746792312235e-40 -37cf58c36eba35e0 7.19685673001147e-40 -37d5c72fb1552d83 1e-39 -37de42b23d05c6f0 1.3894954943731473e-39 -37e505fff07e8682 1.9306977288832458e-39 -37ed3643b12862d9 2.6826957952797384e-39 -37f44b81e258b6e4 3.7275937203149225e-39 -37fc332252435dbe 5.1794746792312235e-39 -3803977a253461ab 7.196856730011469e-39 -380b38fb9daa78e4 1e-38 -3812e9af66239c56 1.3894954943731473e-38 -381a477fec9e2821 1.9306977288832454e-38 -382241ea4eb93dc8 2.6826957952797386e-38 -38295e625aeee49d 3.7275937203149225e-38 -38319ff5736a1a96 5.179474679231223e-38 -38387d58ae817a16 7.196856730011469e-38 -3841039d428a8b8e 9.999999999999999e-38 -3847a41b3fac836c 1.3894954943731474e-37 -38506caff3e2d915 1.9306977288832456e-37 -3856d264e2678d3a 2.6826957952797386e-37 -385fb5faf1aa9dc4 3.7275937203149224e-37 -386607f2d044a13c 5.179474679231223e-37 -386e9caeda21d89b 7.1968567300114686e-37 -38754484932d2e72 1e-36 -387d8d220f97a447 1.3894954943731474e-36 -388487dbf0db8f5b 1.9306977288832458e-36 -388c86fe1b017087 2.682695795279738e-36 -3893d1bcd70aa29b 3.727593720314923e-36 -389b89ef8455c98b 5.179474679231223e-36 -38a321ed48552762 7.19685673001147e-36 -38aa95a5b7f87a0f 1e-35 -38b2783549bec6ac 1.3894954943731473e-35 -38b9a9d2ed127330 1.9306977288832453e-35 -38c1d45ed0e0e655 2.6826957952797384e-35 -38c8c62c0ccd4b40 3.727593720314922e-35 -38d13635b2b59df7 5.179474679231223e-35 -38d7ea689a6a713a 7.19685673001147e-35 -38e09d8792fb4c49 1e-34 -38e716429c2e7856 1.389495494373147e-34 -38f00a23d42b87ff 1.9306977288832458e-34 -38f6497685191fea 2.6826957952797383e-34 -38fef7b710009e11 3.7275937203149222e-34 -390583c31f630575 5.1794746792312234e-34 -390de502c1050d88 7.196856730011469e-34 -3914c4e977ba1f5b 9.999999999999999e-34 -391cdbd3433a166d 1.3894954943731473e-33 -39240cacc93669fe 1.9306977288832455e-33 -392bdbd4265f67e6 2.6826957952797388e-33 -39335ad26a0062cb 3.7275937203149225e-33 -393ae4b3e73bc6d1 5.1794746792312226e-33 -3942af21b8a32875 7.196856730011469e-33 -3949f623d5a8a732 9.999999999999999e-33 -395209640a044ddb 1.389495494373136e-32 -39590fd7fb84047e 1.9306977288832456e-32 -3961696497fba0c8 2.682695795279717e-32 -3968318704807bb5 3.7275937203149527e-32 -3970cef070855c44 5.179474679231224e-32 -39775aea26cbf2c8 7.196856730011528e-32 -398039d665896880 1e-31 -39868bbd0c856152 1.3894954943731361e-31 -398f53cdfa65059f 1.930697728883246e-31 -3995c3bdbdfa88fb 2.6826957952797175e-31 -399e3de8c5a09aa0 3.7275937203149517e-31 -39a502ac8ca6b354 5.179474679231223e-31 -39ad31a4b07eef7a 7.196856730011528e-31 -39b4484bfeebc29f 9.999999999999999e-31 -39bc2eac4fa6b9a6 1.389495494373136e-30 -39c39460bc7f2382 1.9306977288832455e-30 -39cb34ad2d792b38 2.682695795279717e-30 -39d2e6b17b8460a5 3.7275937203149524e-30 -39da4357afd06029 5.179474679231223e-30 -39e23f06ee4f55ac 7.196856730011528e-30 -39e95a5efea6b348 1.0000000000000001e-29 -39f19d2bb1c83408 1.389495494373136e-29 -39f87978eb9eec64 1.930697728883246e-29 -3a0100ec3c6bbb03 2.682695795279717e-29 -3a07a05dda6578cf 3.727593720314953e-29 -3a106a16cde23c1a 5.179474679231224e-29 -3a16cec8a9e32b17 7.196856730011528e-29 -3a1fb0f6be50601a 1.0000000000000001e-28 -3a2604769e3a410a 1.3894954943731361e-28 -3a2e97d72686a77d 1.930697728883246e-28 -3a3541274b86a9c4 2.682695795279717e-28 -3a3d887550fed701 3.727593720314952e-28 -3a44849c815acb20 5.179474679231223e-28 -3a4c827ad45bf5de 7.196856730011529e-28 -3a53ce9a36f23c10 1e-27 -3a5b859445c8d14c 1.389495494373136e-27 -3a631ee6781428ae 1.930697728883246e-27 -3a6a91711e685435 2.682695795279717e-27 -3a727549529f4661 3.727593720314952e-27 -3a79a5c3a1b17de9 5.179474679231224e-27 -3a81d18cc4b979aa 7.196856730011528e-27 -3a88c240c4aecb13 9.999999999999999e-27 -3a91337cab9d82d0 1.3894954943731362e-26 -3a97e6a0161932d9 1.9306977288832457e-26 -3aa09ae6b30134a1 2.682695795279717e-26 -3aa7129ba74717f9 3.727593720314952e-26 -3ab0079a450eeeb1 5.179474679231223e-26 -3ab645eff5e7d814 7.196856730011527e-26 -3abef2d0f5da7dd8 9.999999999999999e-26 -3ac5805bd684e384 1.3894954943731362e-25 -3acde0481b9f7f8e 1.9306977288832454e-25 -3ad4c1a05fc181ca 2.6826957952797173e-25 -3adcd7429118ddf7 3.727593720314952e-25 -3ae40980d652aa5d 5.179474679231223e-25 -3aebd76bf361ce1a 7.196856730011528e-25 -3af357c299a88ea8 1.0000000000000001e-24 -3afae072cc261c63 1.3894954943731358e-24 -3b02ac2d1143afba 1.930697728883246e-24 -3b09f20877b1e23c 2.682695795279717e-24 -3b1206899aaf8abb 3.7275937203149525e-24 -3b190be10be754f5 5.1794746792312235e-24 -3b2166a3781d20d0 7.196856730011528e-24 -3b282db34012b252 1.0000000000000001e-23 -3b30cc47bf97d1bf 1.389495494373136e-23 -3b37573855949ba8 1.9306977288832457e-23 -3b4037454acf2d66 2.6826957952797174e-23 -3b46882c015b6d6a 3.7275937203149527e-23 -3b4f4ed94ee12a31 5.179474679231223e-23 -3b55c04c56246905 7.196856730011529e-23 -3b5e392010175ee6 1e-22 -3b64ff59af7dc62e 1.389495494373136e-22 -3b6d2d066af9c291 1.9306977288832455e-22 -3b7445169d82f8bf 2.682695795279717e-22 -3b7c2a3701b248c5 3.727593720314953e-22 -3b839147d14cba5f 5.179474679231223e-22 -3b8b305f6bad8344 7.196856730011527e-22 -3b92e3b40a0e9b4f 1e-21 -3b9a3f301b5d37ba 1.389495494373136e-21 -3ba23c2402dc199b 1.9306977288832457e-21 -3ba9565c44e3b6ee 2.682695795279717e-21 -3bb19a62610f6d7a 3.727593720314952e-21 -3bb87599c59fe8f8 5.179474679231224e-21 -3bc0fe3ba34c720b 7.196856730011527e-21 -3bc79ca10c924223 1e-20 -3bd0677e111a42d4 1.389495494373136e-20 -3bd6cb2d03932003 1.930697728883246e-20 -3bdfabf3561ca4ab 2.6826957952797173e-20 -3be600faf95348d9 3.7275937203149524e-20 -3bee93003707e335 5.1794746792312235e-20 -3bf53dca8c1f8e8e 7.196856730011528e-20 -3bfd83c94fb6d2ac 1e-19 -3c04815d9560d389 1.389495494373136e-19 -3c0c7df84477e803 1.9306977288832458e-19 -3c13cb7815d1e6ea 2.682695795279717e-19 -3c1b8139b7a81b10 3.7275937203149527e-19 -3c231be02264ee01 5.179474679231223e-19 -3c2a8d3d2f277232 7.196856730011528e-19 -3c32725dd1d243ac 1e-18 -3c39a1b4fab9086c 1.3894954943731361e-18 -3c41cebb2acaf102 1.930697728883246e-18 -3c48be561b4660a5 2.682695795279717e-18 -3c5130c412c910ea 3.727593720314953e-18 -3c57e2d82afe2981 5.179474679231223e-18 -3c6098463d78a75f 7.196856730011528e-18 -3c670ef54646d497 1e-17 -3c7005111cb3a543 1.389495494373136e-17 -3c764269f57dad42 1.9306977288832457e-17 -3c7eedeba217f8cf 2.6826957952797173e-17 -3c857cf5177b5523 3.727593720314952e-17 -3c8ddb8e35bdb3e2 5.1794746792312236e-17 -3c94be57ccd6d137 7.196856730011528e-17 -3c9cd2b297d889bc 1e-16 -3ca4065563e08e94 1.389495494373136e-16 -3cabd30472dd18b2 1.9306977288832535e-16 -3cb354b3454efb96 2.6826957952797275e-16 -3cbadc325d5a2a4f 3.7275937203149376e-16 -3cc2a938e1969058 5.179474679231203e-16 -3cc9ededc00c8584 7.196856730011527e-16 -3cd203af9ee75616 1e-15 -3cd907eabcd8b239 1.389495494373136e-15 -3ce163e2c7ca2f70 1.9306977288832538e-15 -3ce829e016a2ba7b 2.6826957952797273e-15 -3cf0c99f7a585a71 3.727593720314937e-15 -3cf7538719fc346d 5.179474679231202e-15 -3d0034b49807d374 7.19685673001153e-15 -3d06849b86a12b9b 1e-14 -3d0f49e56c0edec8 1.3894954943731362e-14 -3d15bcdb79bcbb4c 1.9306977288832538e-14 -3d1e34581c4b691b 2.6826957952797277e-14 -3d24fc0758ee710e 3.727593720314938e-14 -3d2d2868e07b4189 5.1794746792312023e-14 -3d3441e1be09c84f 7.196856730011527e-14 -3d3c25c268497682 1e-13 -3d438e2f63894b3d 1.3894954943731362e-13 -3d4b2c12582bea1e 1.9306977288832536e-13 -3d52e0b711af21af 2.6826957952797267e-13 -3d5a3b092f2a0d50 3.727593720314937e-13 -3d6239418c4d08f6 5.179474679231203e-13 -3d69525a2d8c3a63 7.196856730011527e-13 -3d719799812dea11 1e-12 -3d7871bb3c6b9e0c 1.3894954943731361e-12 -3d80fb8b771b7253 1.9306977288832537e-12 -3d8798e4d61aea1d 2.6826957952797276e-12 -3d9064e5bd7a4853 3.727593720314938e-12 -3d96c791ef604b33 5.179474679231202e-12 -3d9fa6f0b8ef48fd 7.1968567300115285e-12 -3da5fd7fe1796495 1e-11 -3dae8e2a0b86858f 1.3894954943731361e-11 -3db53a6e54e24ee8 1.9306977288832537e-11 -3dbd7f1e0ba1a4a4 2.6826957952797275e-11 -3dc47e1f2cd8da67 3.7275937203149374e-11 -3dcc79766b385e00 5.1794746792312025e-11 -3dd3c85673958d9e 7.196856730011528e-11 -3ddb7cdfd9d7bdbb 1e-10 -3de318da47341379 1.389495494373136e-10 -3dea8909ea1ae2a1 1.9306977288832535e-10 -3df26f72c74506e7 2.682695795279728e-10 -3df99da6f80f1102 3.727593720314938e-10 -3e01cbea03033ac0 5.179474679231202e-10 -3e08ba6c107af104 7.196856730011527e-10 -3e112e0be826d695 1e-9 -3e17df10d9011858 1.3894954943731362e-9 -3e2095a63250cda5 1.9306977288832536e-9 -3e270b4f791648a0 2.6826957952797275e-9 -3e3002885b096aa1 3.727593720314938e-9 -3e363ee483c4096f 5.179474679231202e-9 -3e3ee9071499ad46 7.196856730011528e-9 -3e45798ee2308c3a 1e-8 -3e4dd6d50f415e7e 1.3894954943731388e-8 -3e54bb0fbee50103 1.93069772888325e-8 -3e5cce23575bdac8 2.6826957952797275e-8 -3e64032a71cbc549 3.7275937203149377e-8 -3e6bce9da4b50bda 5.179474679231212e-8 -3e7351a46ce00c42 7.196856730011515e-8 -3e7ad7f29abcaf48 1e-7 -3e82a6452988db0f 1.389495494373139e-7 -3e89e9d3ae9e4144 1.93069772888325e-7 -3e9200d6169968bc 2.682695795279727e-7 -3e9903f50e3eb69b 3.7275937203149375e-7 -3ea1612286f12768 5.179474679231211e-7 -3ea8260d88180f51 7.196856730011513e-7 -3eb0c6f7a0b5ed8d 0.000001 -3eb74fd673eb11d2 0.0000013894954943731387 -3ec032244d22e8ca 0.0000019306977288832498 -3ec6810b9c3fc2ed 0.000002682695795279728 -3ecf44f251ce6443 0.000003727593720314938 -3ed5b96b28ad7143 0.000005179474679231212 -3ede2f90ea1e1326 0.000007196856730011514 -3ee4f8b588e368f1 0.00001 -3eed23cc10e5d647 0.000013894954943731388 -3ef43ead606ba2fd 0.0000193069772888325 -3efc214e834fb3a6 0.00002682695795279727 -3f038b177320fea9 0.000037275937203149375 -3f0b27c5f2d8cd93 0.00005179474679231212 -3f12ddba9252cbf8 0.00007196856730011514 -3f1a36e2eb1c432d 0.0001 -3f22365f8a8fa5e8 0.00013894954943731376 -3f294e58b8868bbc 0.00019306977288832499 -3f3194d11211d043 0.00026826957952797245 -3f386ddd4fe93e5c 0.0003727593720314942 -3f40f8dbb7c7807c 0.0005179474679231212 -3f47952936e77efc 0.0007196856730011521 -3f50624dd2f1a9fc 0.001 -3f56c3f76d338f62 0.0013894954943731376 -3f5fa1eee6a82eab 0.0019306977288832499 -3f65fa0556964455 0.002682695795279725 -3f6e8954a3e38df3 0.003727593720314942 -3f753712a5b9609b 0.005179474679231212 -3f7d7a7384a15ebb 0.007196856730011521 -3f847ae147ae147b 0.01 -3f8c74f54880733a 0.013894954943731375 -3f93c53550291d2d 0.019306977288832506 -3f9b7886ac3bd56d 0.02682695795279726 -3fa315d4e66e38b3 0.03727593720314939 -3faa84d74f27b8bf 0.0517947467923121 -3fb26c8832e4db35 0.07196856730011521 -3fb999999999999a 0.1 -3fc1c9194d504805 0.13894954943731377 -3fc8b682a4336477 0.19306977288832503 -3fd12b542ba56565 0.26826957952797265 -3fd7db4a2009c6e1 0.37275937203149395 -3fe093069178d379 0.5179474679231212 -3fe707aa3f9e1203 0.7196856730011522 -3ff0000000000000 1 -3ff63b5fa0a45a05 1.3894954943731375 -3ffee4234d403d94 1.93069772888325 -40057629368ebebe 2.6826957952797263 -400dd21ca80c3899 3.7275937203149394 -4014b7c835d70856 5.179474679231211 -401cc994cf859681 7.196856730011519 -4024000000000000 10 -402bca3788cd7086 13.894954943731374 -40334e961048267e 19.306977288832506 -403ad3b384326e6e 26.826957952797265 -4042a351e907a360 37.2759372031494 -4049e5ba434cca6a 51.7947467923121 -4051fdfd01b37e12 71.96856730011521 -4059000000000000 100 -40615e62b5806654 138.94954943731375 -4068223b945a301b 193.069772888325 -4070c450329f8501 268.26957952797244 -40774c2663498c3b 372.75937203149414 -40802f946a0ffe84 517.9474679231212 -40867d7c42205d96 719.685673001152 -408f400000000000 1000 -4095b5fb62e07fe9 1389.4954943731375 -409e2aca7970bc22 1930.69772888325 -40a4f5643f476641 2682.6957952797243 -40ad1f2ffc1bef4b 3727.593720314942 -40b43b798493fe25 5179.474679231212 -40bc1cdb52a874fb 7196.85673001152 -40c3880000000000 10000 -40cb237a3b989fea 13894.954943731387 -40d2dabe8be67595 19306.9772888325 -40da32bd4f193fdb 26826.95795279728 -40e2337dfd91758a 37275.937203149384 -40e94a57e5b8fdae 51794.746792312115 -40f1920913a94919 71968.56730011514 -40f86a0000000000 100000 -4100f62c653f63f3 138949.5494373139 -4107916e2ee012fa 193069.77288832498 -41105fb6516fc7e8 268269.57952797273 -4116c05d7cf5d2eb 372759.37203149375 -411f9ceddf273d1a 517947.4679231212 -4125f68b58939b5f 719685.6730011514 -412e848000000000 1000000 -413533b77e8f3cf0 1389495.494373139 -413d75c9ba9817b9 1930697.72888325 -414477a3e5cbb9e3 2682695.795279728 -414c7074dc3347a7 3727593.720314938 -4153c214ab788630 5179474.679231212 -415b742e2eb88236 7196856.730011513 -416312d000000000 10000000 -416a80a55e330c2b 13894954.943731388 -4172699e149f0ed4 19306977.2888325 -4179958cdf3ea85b 26826957.952797275 -4181c64909a00cc8 37275937.20314938 -4188b299d656a7bc 51794746.792312115 -4191289cdd335162 71968567.30011514 -4197d78400000000 100000000 -41a090675adfe792 138949549.43731362 -41a7040599c6d295 193069772.88832536 -41affaf0170e5272 268269579.52797276 -41b637db4c080ffa 372759372.0314938 -41bedf404bec519b 517947467.9231202 -41c572c4148025c6 719685673.0011528 -41cdcd6500000000 1000000000 -41d4b4813197e177 1389495494.3731363 -41dcc5070038873a 1930697728.8832536 -41e3fcd60e68f387 2682695795.2797275 -41ebc5d21f0a13f8 3727593720.3149376 -41f34b882f73b301 5179474679.231202 -41facf7519a02f38 7196856730.011528 -4202a05f20000000 10000000000 -4209e1a17dfdd9d4 13894954943.731361 -4211fb2460235484 19306977288.832535 -4218fc0b9203306a 26826957952.79728 -42215ba353664c7b 37275937203.149376 -42281e6a3b509fc0 51794746792.31201 -4230c1a930041d83 71968567300.11528 -42374876e8000000 100000000000 -42402d04eebea824 138949549437.3136 -424679ed782c29a5 193069772888.32535 -424f3b0e7683fc83 268269579527.97275 -4255b28c283fdf9a 372759372031.4938 -425e2604ca24c7b1 517947467923.1202 -4264f2137c0524e4 719685673001.1528 -426d1a94a2000000 1000000000000 -427438462a6e522e 1389495494373.1362 -427c1868d637340e 1930697728883.2534 -428384e90a127dd3 2682695795279.728 -428b1f2f324fd782 3727593720314.9385 -4292d7c2fe56fccf 5179474679231.202 -429a2e985b066e1c 7196856730011.527 -42a2309ce5400000 10000000000000 -42a94657b509e6ba 13894954943731.363 -42b18f4185e28089 19306977288832.535 -42b866234c971d46 26826957952797.273 -42c0f37d7f71e6b0 37275937203149.375 -42c78db3bdecbc02 51794746792312.016 -42d05d1f38e404d2 71968567300115.28 -42d6bcc41e900000 100000000000000 -42df97eda24c6066 138949549437313.6 -42e5f311e75b20ac 193069772888325.38 -42ee7fac1fbce499 268269579527972.78 -42f5305cdf4e605d 372759372031493.8 -42fd7120ad67eb02 517947467923120.1 -43047467071d0607 719685673001152.9 -430c6bf526340000 1000000000000000 -4313bef4856fbc41 1389495494373136.2 -431b6fd66131e8d6 1930697728883253.5 -43230fcb93d60edf 2682695795279727.5 -432a7c741721f874 3727593720314938 -433266b46c60f2e2 5179474679231202 -43399180c8e44788 7196856730011528 -4341c37937e08000 10000000000000000 -4348aeb1a6cbab51 13894954943731362 -435125e5fcbf3172 19306977288832456 -4357d3be78cb927d 26826957952797172 -43608dc88e753b5a 37275937203149520 -4367006187792fb5 51794746792312230 -436ff5e0fb1d596a 71968567300115280 -4376345785d8a000 100000000000000000 -437eda5e107e9625 138949549437313620 -43856f5f7beefdcf 193069772888324580 -438dc8ae16fe771b 268269579527971680 -4394b13ab2128a31 372759372031495230 -439cc079e9577ba2 517947467923122300 -43a3f9ac9cf257e2 719685673001152800 -43abc16d674ec800 1000000000000000000 -43b3487aca4f1dd7 1389495494373136100 -43bacb375aeabd42 1930697728883245600 -43c29d6cce5f0a71 2682695795279717000 -43c9dd895e972cbd 3727593720314952000 -43d1f84c31d6ad45 5179474679231223000 -43d8f817c42eeddb 7196856730011528000 -43e158e460913d00 10000000000000000000 -43e81a997ce2e54d 13894954943731362000 -43f0bf0298d2b649 19306977288832455000 -43f744c801f6cd0e 26826957952797170000 -44002a75db1e7bf6 37275937203149520000 -4406765f3e4c5896 51794746792312226000 -440f361db53aa952 71968567300115280000 -4415af1d78b58c40 100000000000000000000 -441e213fdc1b9ea0 138949549437313610000 -4424eec33f0763db 193069772888324540000 -442d15fa02748051 268269579527971700000 -4434351351e61af4 372759372031495240000 -443c13f70ddf6ebc 517947467923122300000 -444381d29144a9d3 719685673001152800000 -444b1ae4d6e2ef50 1e+21 -4452d4c7e9914325 1.3894954943731364e+21 -445a2a740ec93cd3 1.9306977288832457e+21 -44622dbc4188d032 2.6826957952797167e+21 -44694258265fa1b1 3.7275937203149524e+21 -44718c7a68aba536 5.179474679231223e+21 -447862473595d448 7.196856730011528e+21 -4480f0cf064dd592 1e+22 -448789f9e3f593ec 1.389495494373136e+22 -44905a88893dc604 1.9306977288832458e+22 -4496b92b51eb043f 2.682695795279717e+22 -449f92ee2ff78a1c 3.727593720314952e+22 -44a5ef9902d68e83 5.179474679231223e+22 -44ae7ad902fb495a 7.196856730011528e+22 -44b52d02c7e14af6 1e+23 -44bd6c785cf2f8e9 1.3894954943731363e+23 -44c4712aab8d3784 1.9306977288832454e+23 -44cc67762665c54f 2.682695795279717e+23 -44d3bbd4ddfab652 3.727593720314952e+23 -44db6b7f438c3223 5.1794746792312225e+23 -44e30cc7a1dd0dd9 7.19685673001153e+23 -44ea784379d99db4 1e+24 -44f263cb3a17db91 1.389495494373136e+24 -44f98d7556708566 1.9306977288832457e+24 -4501c0a9d7ff9b52 2.6826957952797173e+24 -4508aaca157963e6 3.727593720314952e+24 -4511232f8a379f57 5.179474679231224e+24 -4517cff98a54514e 7.196856730011528e+24 -45208b2a2c280291 1e+25 -4526fcbe089dd276 1.3894954943731363e+25 -452ff0d2ac0ca6c0 1.9306977288832458e+25 -453630d44dff8226 2.682695795279717e+25 -453ed57c9ad7bce0 3.727593720314952e+25 -45456bfb6cc5872c 5.179474679231223e+25 -454dc3f7ece965a2 7.196856730011528e+25 -4554adf4b7320335 1e+26 -455cbbed8ac54713 1.3894954943731362e+26 -4563f683ab87e837 1.9306977288832455e+26 -456bbd09617f62af 2.682695795279717e+26 -4573456de0c6d60c 3.727593720314952e+26 -457ac6fa47f6e8f7 5.179474679231223e+26 -45829a7af411df85 7.196856730011528e+26 -4589d971e4fe8402 1e+27 -4591f57476bb4c6b 1.389495494373136e+27 -4598f4249669e245 1.9306977288832455e+27 -45a15625dcef9dae 2.682695795279717e+27 -45a816c958f88b8f 3.727593720314952e+27 -45b0bc5c6cfa519a 5.179474679231223e+27 -45b74119b1165766 7.196856730011528e+27 -45c027e72f1f1281 1e+28 -45c672d1946a1f87 1.3894954943731362e+28 -45cf312dbc045ad7 1.9306977288832457e+28 -45d5abaf542b8518 2.6826957952797166e+28 -45de1c7baf36ae73 3.7275937203149523e+28 -45e4eb738838e600 5.179474679231222e+28 -45ed11601d5bed3f 7.196856730011527e+28 -45f431e0fae6d721 1e+29 -45fc0f85f984a769 1.3894954943731362e+29 -46037ebc9582b8c6 1.9306977288832456e+29 -460b169b2936665f 2.682695795279717e+29 -4612d1cd4d822d08 3.7275937203149524e+29 -461a26506a471f82 5.1794746792312236e+29 -46222adc12597448 7.196856730011528e+29 -46293e5939a08cea 1e+30 -463189b3bbf2e8a1 1.389495494373136e+30 -46385e6bbae366f8 1.9306977288832457e+30 -4640ee20f9c1fffc 2.6826957952797173e+30 -46478640a0e2b849 3.727593720314952e+30 -465057f2426c73b0 5.179474679231222e+30 -4656b59316efd15a 7.196856730011528e+30 -465f8def8808b024 1e+31 -4665ec20aaefa2ca 1.3894954943731362e+31 -466e7606a99c40b5 1.9306977288832455e+31 -467529a938327ffa 2.682695795279717e+31 -467d67d0c91b665d 3.7275937203149526e+31 -46846deed307909d 5.179474679231223e+31 -468c62f7dcabc5b1 7.196856730011529e+31 -4693b8b5b5056e17 1e+32 -469b6728d5ab8bbb 1.3894954943731475e+32 -46a309c42a01a872 1.9306977288832458e+32 -46aa7413863f2035 2.6826957952797387e+32 -46b260e27db11fd0 3.727593720314922e+32 -46b9896a87c974c5 5.179474679231224e+32 -46c1bddae9eb5b66 7.19685673001147e+32 -46c8a6e32246c99d 1.0000000000000001e+33 -46d12079858b3755 1.3894954943731475e+33 -46d7cc353482128e 1.9306977288832456e+33 -46e0888c33e77421 2.6826957952797386e+33 -46e6f91b1d1d67c5 3.727593720314923e+33 -46efebc529bbd1f6 5.1794746792312235e+33 -46f62d51a4663240 7.196856730011471e+33 -46fed09bead87c04 1.0000000000000001e+34 -47056897e6ee052a 1.3894954943731475e+34 -470dbf4281a29732 1.9306977288832458e+34 -4714aaaf40e1512a 2.682695795279739e+34 -471cb761e464c1b5 3.7275937203149222e+34 -4723f35b3a15633a 5.179474679231224e+34 -472bb8a60d7fbecf 7.19685673001147e+34 -4733426172c74d82 1e+35 -473ac2bde0a98674 1.3894954943731474e+35 -4742978991059e7f 1.9306977288832457e+35 -4749d55b1119a573 2.6826957952797384e+35 -4751f29d2ebef912 3.727593720314923e+35 -4758f032089abc07 5.179474679231223e+35 -47615367c86fd741 7.196856730011469e+35 -476812f9cf7920e3 1e+36 -4770b9b6ac69f409 1.3894954943731475e+36 -47773d6bf547061f 1.9306977288832457e+36 -47802558eab00768 2.6826957952797385e+36 -47866f447a6eb756 3.7275937203149226e+36 -478f2c3e8ac16b0a 5.1794746792312234e+36 -4795a841ba8bcd12 7.19685673001147e+36 -479e17b84357691c 1.0000000000000001e+37 -47a4e8245784710b 1.3894954943731475e+37 -47ad0cc6f298c7a7 1.9306977288832458e+37 -47b42eaf255c0942 2.6826957952797385e+37 -47bc0b15990a652b 3.7275937203149224e+37 -47c37ba716b8e2e7 5.179474679231224e+37 -47cb1252292ec057 7.196856730011471e+37 -47d2ced32a16a1b1 1e+38 -47da222d6d658d4e 1.3894954943731475e+38 -47e227fc579f7cc8 1.9306977288832457e+38 -47e93a5aeeb30b93 2.6826957952797386e+38 -47f186ed7fa67f3b 3.7275937203149225e+38 -47f85a90dc671b9f 5.179474679231223e+38 -4800eb7359bd3836 7.19685673001147e+38 -48078287f49c4a1e 1.0000000000000001e+39 -4810555c645f7850 1.3894954943731473e+39 -4816b1fb6d875bfa 1.9306977288832457e+39 -481f88f1aa5fce77 2.6826957952797384e+39 -4825e8a8df901f0a 3.7275937203149226e+39 -482e71351380e287 5.179474679231223e+39 -48352650302c8643 7.19685673001147e+39 -483d6329f1c35ca5 1e+40 -48446ab37d775665 1.3894954943731475e+40 -484c5e7a48e932f9 1.9306977288832458e+40 -4853b5970a7be10a 2.682695795279738e+40 -485b62d3177426cc 3.7275937203149224e+40 -486306c12c308d95 5.179474679231224e+40 -486a6fe43c37a7d4 7.19685673001147e+40 -48725dfa371a19e7 1e+41 -487985605cd52bfd 1.3894954943731473e+41 -4881bb0c6d91bfdc 1.930697728883246e+41 -4888a2fccd1ad94d 2.6826957952797384e+41 -48911dc3eea89840 3.727593720314923e+41 -4897c871773cb0fa 5.179474679231223e+41 -48a085eea5a2c8e5 7.19685673001147e+41 -48a6f578c4e0a061 1e+42 -48afe6b8740a76fd 1.3894954943731474e+42 -48b629cf88f62fd2 1.9306977288832456e+42 -48becbbc00618fa1 2.6826957952797387e+42 -48c56534ea52be50 3.727593720314923e+42 -48cdba8dd50bdd39 5.1794746792312236e+42 -48d4a76a4f0b7b1e 7.19685673001147e+42 -48dcb2d6f618c879 1e+43 -48e3f03348868a5e 1.3894954943731474e+43 -48ebb4436b33bbc7 1.9306977288832457e+43 -48f33f55803cf9c5 2.682695795279739e+43 -48fabe8224e76de4 3.7275937203149227e+43 -49029498a5276a43 5.179474679231223e+43 -4909d144e2ce59e5 7.19685673001147e+43 -4911efc659cf7d4c 1e+44 -4918ec401aa82cf6 1.3894954943731475e+44 -492150aa2300555c 1.9306977288832456e+44 -49280f2ae04c3835 2.6826957952797383e+44 -4930b7115710a4ae 3.727593720314922e+44 -493739bece7144d4 5.179474679231223e+44 -494022cb0dc0f82f 7.19685673001147e+44 -49466bb7f0435c9f 1.0000000000000001e+45 -494f275021523834 1.3894954943731476e+45 -4955a4d4abc06ab4 1.930697728883246e+45 -495e12f5985f4642 2.6826957952797383e+45 -4964e4d5acd4cdda 3.7275937203149227e+45 -496d082e820d960a 5.179474679231224e+45 -49742b7dd131363c 7.196856730011471e+45 -497c06a5ec5433c6 1e+46 -4983789214d36320 1.3894954943731474e+46 -498b0e09d6b08560 1.9306977288832457e+46 -4992cbd97f3b8bea 2.6826957952797386e+46 -499a1e0b180a0151 3.727593720314923e+46 -49a2251d11487dc5 5.179474679231223e+46 -49a9365d457d83ca 7.19685673001147e+46 -49b18427b3b4a05c 1e+47 -49b856b69a083be7 1.3894954943731472e+47 -3fc2492492492492 0.14285714285714285 -3f50624dd2f1a9fc 0.001 -412e848000000000 1000000 -3fd2492492492492 0.2857142857142857 -3f60624dd2f1a9fc 0.002 -413e848000000000 2000000 -3fdb6db6db6db6db 0.42857142857142855 -3f689374bc6a7efa 0.003 -4146e36000000000 3000000 -3fe2492492492492 0.5714285714285714 -3f70624dd2f1a9fc 0.004 -414e848000000000 4000000 -3fe6db6db6db6db7 0.7142857142857143 -3f747ae147ae147b 0.005 -415312d000000000 5000000 -3feb6db6db6db6db 0.8571428571428571 -3f789374bc6a7efa 0.006 -4156e36000000000 6000000 -3ff0000000000000 1 -3f7cac083126e979 0.007 -415ab3f000000000 7000000 -3ff2492492492492 1.1428571428571428 -3f80624dd2f1a9fc 0.008 -415e848000000000 8000000 -3ff4924924924925 1.2857142857142858 -3f826e978d4fdf3c 0.009000000000000001 -41612a8800000000 9000000 -3ff6db6db6db6db7 1.4285714285714286 -3f847ae147ae147b 0.01 -416312d000000000 10000000 -3ff9249249249249 1.5714285714285714 -3f86872b020c49ba 0.011 -4164fb1800000000 11000000 -3ffb6db6db6db6db 1.7142857142857142 -3f889374bc6a7efa 0.012 -4166e36000000000 12000000 -3ffdb6db6db6db6e 1.8571428571428572 -3f8a9fbe76c8b43a 0.013000000000000001 -4168cba800000000 13000000 -4000000000000000 2 -3f8cac083126e979 0.014 -416ab3f000000000 14000000 -4001249249249249 2.142857142857143 -3f8eb851eb851eb8 0.015 -416c9c3800000000 15000000 -4002492492492492 2.2857142857142856 -3f90624dd2f1a9fc 0.016 -416e848000000000 16000000 -40036db6db6db6db 2.4285714285714284 -3f916872b020c49c 0.017 -4170366400000000 17000000 -4004924924924925 2.5714285714285716 -3f926e978d4fdf3c 0.018000000000000002 -41712a8800000000 18000000 -4005b6db6db6db6e 2.7142857142857144 -3f9374bc6a7ef9db 0.019 -41721eac00000000 19000000 -4006db6db6db6db7 2.857142857142857 -3f947ae147ae147b 0.02 -417312d000000000 20000000 -4008000000000000 3 -3f95810624dd2f1b 0.021 -417406f400000000 21000000 -4009249249249249 3.142857142857143 -3f96872b020c49ba 0.022 -4174fb1800000000 22000000 -400a492492492492 3.2857142857142856 -3f978d4fdf3b645a 0.023 -4175ef3c00000000 23000000 -400b6db6db6db6db 3.4285714285714284 -3f989374bc6a7efa 0.024 -4176e36000000000 24000000 -400c924924924925 3.5714285714285716 -3f9999999999999a 0.025 -4177d78400000000 25000000 -400db6db6db6db6e 3.7142857142857144 -3f9a9fbe76c8b43a 0.026000000000000002 -4178cba800000000 26000000 -400edb6db6db6db7 3.857142857142857 -3f9ba5e353f7ced9 0.027 -4179bfcc00000000 27000000 -4010000000000000 4 -3f9cac083126e979 0.028 -417ab3f000000000 28000000 -4010924924924925 4.142857142857143 -3f9db22d0e560419 0.029 -417ba81400000000 29000000 -4011249249249249 4.285714285714286 -3f9eb851eb851eb8 0.03 -417c9c3800000000 30000000 -4011b6db6db6db6e 4.428571428571429 -3f9fbe76c8b43958 0.031 -417d905c00000000 31000000 -4012492492492492 4.571428571428571 -3fa0624dd2f1a9fc 0.032 -417e848000000000 32000000 -4012db6db6db6db7 4.714285714285714 -3fa0e5604189374c 0.033 -417f78a400000000 33000000 -40136db6db6db6db 4.857142857142857 -3fa16872b020c49c 0.034 -4180366400000000 34000000 -4014000000000000 5 -3fa1eb851eb851ec 0.035 -4180b07600000000 35000000 -4014924924924925 5.142857142857143 -3fa26e978d4fdf3c 0.036000000000000004 -41812a8800000000 36000000 -4015249249249249 5.285714285714286 -3fa2f1a9fbe76c8b 0.037 -4181a49a00000000 37000000 -4015b6db6db6db6e 5.428571428571429 -3fa374bc6a7ef9db 0.038 -41821eac00000000 38000000 -4016492492492492 5.571428571428571 -3fa3f7ced916872b 0.039 -418298be00000000 39000000 -4016db6db6db6db7 5.714285714285714 -3fa47ae147ae147b 0.04 -418312d000000000 40000000 -40176db6db6db6db 5.857142857142857 -3fa4fdf3b645a1cb 0.041 -41838ce200000000 41000000 -4018000000000000 6 -3fa5810624dd2f1b 0.042 -418406f400000000 42000000 -4018924924924925 6.142857142857143 -3fa604189374bc6b 0.043000000000000003 -4184810600000000 43000000 -4019249249249249 6.285714285714286 -3fa6872b020c49ba 0.044 -4184fb1800000000 44000000 -4019b6db6db6db6e 6.428571428571429 -3fa70a3d70a3d70a 0.045 -4185752a00000000 45000000 -401a492492492492 6.571428571428571 -3fa78d4fdf3b645a 0.046 -4185ef3c00000000 46000000 -401adb6db6db6db7 6.714285714285714 -3fa810624dd2f1aa 0.047 -4186694e00000000 47000000 -401b6db6db6db6db 6.857142857142857 -3fa89374bc6a7efa 0.048 -4186e36000000000 48000000 -401c000000000000 7 -3fa916872b020c4a 0.049 -41875d7200000000 49000000 -401c924924924925 7.142857142857143 -3fa999999999999a 0.05 -4187d78400000000 50000000 -401d249249249249 7.285714285714286 -3faa1cac083126ea 0.051000000000000004 -4188519600000000 51000000 -401db6db6db6db6e 7.428571428571429 -3faa9fbe76c8b43a 0.052000000000000005 -4188cba800000000 52000000 -401e492492492492 7.571428571428571 -3fab22d0e5604189 0.053 -418945ba00000000 53000000 -401edb6db6db6db7 7.714285714285714 -3faba5e353f7ced9 0.054 -4189bfcc00000000 54000000 -401f6db6db6db6db 7.857142857142857 -3fac28f5c28f5c29 0.055 -418a39de00000000 55000000 -4020000000000000 8 -3facac083126e979 0.056 -418ab3f000000000 56000000 -4020492492492492 8.142857142857142 -3fad2f1a9fbe76c9 0.057 -418b2e0200000000 57000000 -4020924924924925 8.285714285714286 -3fadb22d0e560419 0.058 -418ba81400000000 58000000 -4020db6db6db6db7 8.428571428571429 -3fae353f7ced9169 0.059000000000000004 -418c222600000000 59000000 -4021249249249249 8.571428571428571 -3faeb851eb851eb8 0.06 -418c9c3800000000 60000000 -40216db6db6db6db 8.714285714285714 -3faf3b645a1cac08 0.061 -418d164a00000000 61000000 -4021b6db6db6db6e 8.857142857142858 -3fafbe76c8b43958 0.062 -418d905c00000000 62000000 -4022000000000000 9 -3fb020c49ba5e354 0.063 -418e0a6e00000000 63000000 -4022492492492492 9.142857142857142 -3fb0624dd2f1a9fc 0.064 -418e848000000000 64000000 -4022924924924925 9.285714285714286 -3fb0a3d70a3d70a4 0.065 -418efe9200000000 65000000 -4022db6db6db6db7 9.428571428571429 -3fb0e5604189374c 0.066 -418f78a400000000 66000000 -4023249249249249 9.571428571428571 -3fb126e978d4fdf4 0.067 -418ff2b600000000 67000000 -40236db6db6db6db 9.714285714285714 -3fb16872b020c49c 0.068 -4190366400000000 68000000 -4023b6db6db6db6e 9.857142857142858 -3fb1a9fbe76c8b44 0.069 -4190736d00000000 69000000 -4024000000000000 10 -3fb1eb851eb851ec 0.07 -4190b07600000000 70000000 -4024492492492492 10.142857142857142 -3fb22d0e56041894 0.07100000000000001 -4190ed7f00000000 71000000 -4024924924924925 10.285714285714286 -3fb26e978d4fdf3c 0.07200000000000001 -41912a8800000000 72000000 -4024db6db6db6db7 10.428571428571429 -3fb2b020c49ba5e3 0.073 -4191679100000000 73000000 -4025249249249249 10.571428571428571 -3fb2f1a9fbe76c8b 0.074 -4191a49a00000000 74000000 -40256db6db6db6db 10.714285714285714 -3fb3333333333333 0.075 -4191e1a300000000 75000000 -4025b6db6db6db6e 10.857142857142858 -3fb374bc6a7ef9db 0.076 -41921eac00000000 76000000 -4026000000000000 11 -3fb3b645a1cac083 0.077 -41925bb500000000 77000000 -4026492492492492 11.142857142857142 -3fb3f7ced916872b 0.078 -419298be00000000 78000000 -4026924924924925 11.285714285714286 -3fb4395810624dd3 0.079 -4192d5c700000000 79000000 -4026db6db6db6db7 11.428571428571429 -3fb47ae147ae147b 0.08 -419312d000000000 80000000 -4027249249249249 11.571428571428571 -3fb4bc6a7ef9db23 0.081 -41934fd900000000 81000000 -40276db6db6db6db 11.714285714285714 -3fb4fdf3b645a1cb 0.082 -41938ce200000000 82000000 -4027b6db6db6db6e 11.857142857142858 -3fb53f7ced916873 0.083 -4193c9eb00000000 83000000 -4028000000000000 12 -3fb5810624dd2f1b 0.084 -419406f400000000 84000000 -4028492492492492 12.142857142857142 -3fb5c28f5c28f5c3 0.085 -419443fd00000000 85000000 -4028924924924925 12.285714285714286 -3fb604189374bc6b 0.08600000000000001 -4194810600000000 86000000 -4028db6db6db6db7 12.428571428571429 -3fb645a1cac08313 0.08700000000000001 -4194be0f00000000 87000000 -4029249249249249 12.571428571428571 -3fb6872b020c49ba 0.088 -4194fb1800000000 88000000 -40296db6db6db6db 12.714285714285714 -3fb6c8b439581062 0.089 -4195382100000000 89000000 -4029b6db6db6db6e 12.857142857142858 -3fb70a3d70a3d70a 0.09 -4195752a00000000 90000000 -402a000000000000 13 -3fb74bc6a7ef9db2 0.091 -4195b23300000000 91000000 -402a492492492492 13.142857142857142 -3fb78d4fdf3b645a 0.092 -4195ef3c00000000 92000000 -402a924924924925 13.285714285714286 -3fb7ced916872b02 0.093 -41962c4500000000 93000000 -402adb6db6db6db7 13.428571428571429 -3fb810624dd2f1aa 0.094 -4196694e00000000 94000000 -402b249249249249 13.571428571428571 -3fb851eb851eb852 0.095 -4196a65700000000 95000000 -402b6db6db6db6db 13.714285714285714 -3fb89374bc6a7efa 0.096 -4196e36000000000 96000000 -402bb6db6db6db6e 13.857142857142858 -3fb8d4fdf3b645a2 0.097 -4197206900000000 97000000 -402c000000000000 14 -3fb916872b020c4a 0.098 -41975d7200000000 98000000 -402c492492492492 14.142857142857142 -3fb95810624dd2f2 0.099 -41979a7b00000000 99000000 -402c924924924925 14.285714285714286 -3fb999999999999a 0.1 -4197d78400000000 100000000 -402cdb6db6db6db7 14.428571428571429 -3fb9db22d0e56042 0.101 -4198148d00000000 101000000 -402d249249249249 14.571428571428571 -3fba1cac083126ea 0.10200000000000001 -4198519600000000 102000000 -402d6db6db6db6db 14.714285714285714 -3fba5e353f7ced92 0.10300000000000001 -41988e9f00000000 103000000 -402db6db6db6db6e 14.857142857142858 -3fba9fbe76c8b43a 0.10400000000000001 -4198cba800000000 104000000 -402e000000000000 15 -3fbae147ae147ae1 0.105 -419908b100000000 105000000 -402e492492492492 15.142857142857142 -3fbb22d0e5604189 0.106 -419945ba00000000 106000000 -402e924924924925 15.285714285714286 -3fbb645a1cac0831 0.107 -419982c300000000 107000000 -402edb6db6db6db7 15.428571428571429 -3fbba5e353f7ced9 0.108 -4199bfcc00000000 108000000 -402f249249249249 15.571428571428571 -3fbbe76c8b439581 0.109 -4199fcd500000000 109000000 -402f6db6db6db6db 15.714285714285714 -3fbc28f5c28f5c29 0.11 -419a39de00000000 110000000 -402fb6db6db6db6e 15.857142857142858 -3fbc6a7ef9db22d1 0.111 -419a76e700000000 111000000 -4030000000000000 16 -3fbcac083126e979 0.112 -419ab3f000000000 112000000 -4030249249249249 16.142857142857142 -3fbced916872b021 0.113 -419af0f900000000 113000000 -4030492492492492 16.285714285714285 -3fbd2f1a9fbe76c9 0.114 -419b2e0200000000 114000000 -40306db6db6db6db 16.428571428571427 -3fbd70a3d70a3d71 0.115 -419b6b0b00000000 115000000 -4030924924924925 16.571428571428573 -3fbdb22d0e560419 0.116 -419ba81400000000 116000000 -4030b6db6db6db6e 16.714285714285715 -3fbdf3b645a1cac1 0.117 -419be51d00000000 117000000 -4030db6db6db6db7 16.857142857142858 -3fbe353f7ced9169 0.11800000000000001 -419c222600000000 118000000 -4031000000000000 17 -3fbe76c8b4395811 0.11900000000000001 -419c5f2f00000000 119000000 -4031249249249249 17.142857142857142 -3fbeb851eb851eb8 0.12 -419c9c3800000000 120000000 -4031492492492492 17.285714285714285 -3fbef9db22d0e560 0.121 -419cd94100000000 121000000 -40316db6db6db6db 17.428571428571427 -3fbf3b645a1cac08 0.122 -419d164a00000000 122000000 -4031924924924925 17.571428571428573 -3fbf7ced916872b0 0.123 -419d535300000000 123000000 -4031b6db6db6db6e 17.714285714285715 -3fbfbe76c8b43958 0.124 -419d905c00000000 124000000 -4031db6db6db6db7 17.857142857142858 -3fc0000000000000 0.125 -419dcd6500000000 125000000 -4032000000000000 18 -3fc020c49ba5e354 0.126 -419e0a6e00000000 126000000 -4032249249249249 18.142857142857142 -3fc04189374bc6a8 0.127 -419e477700000000 127000000 -4032492492492492 18.285714285714285 -3fc0624dd2f1a9fc 0.128 -419e848000000000 128000000 -40326db6db6db6db 18.428571428571427 -3fc083126e978d50 0.129 -419ec18900000000 129000000 -4032924924924925 18.571428571428573 -3fc0a3d70a3d70a4 0.13 -419efe9200000000 130000000 -4032b6db6db6db6e 18.714285714285715 -3fc0c49ba5e353f8 0.131 -419f3b9b00000000 131000000 -4032db6db6db6db7 18.857142857142858 -3fc0e5604189374c 0.132 -419f78a400000000 132000000 -4033000000000000 19 -3fc10624dd2f1aa0 0.133 -419fb5ad00000000 133000000 -4033249249249249 19.142857142857142 -3fc126e978d4fdf4 0.134 -419ff2b600000000 134000000 -4033492492492492 19.285714285714285 -3fc147ae147ae148 0.135 -41a017df80000000 135000000 -40336db6db6db6db 19.428571428571427 -3fc16872b020c49c 0.136 -41a0366400000000 136000000 -4033924924924925 19.571428571428573 -3fc189374bc6a7f0 0.137 -41a054e880000000 137000000 -4033b6db6db6db6e 19.714285714285715 -3fc1a9fbe76c8b44 0.138 -41a0736d00000000 138000000 -4033db6db6db6db7 19.857142857142858 -3fc1cac083126e98 0.139 -41a091f180000000 139000000 -4034000000000000 20 -3fc1eb851eb851ec 0.14 -41a0b07600000000 140000000 -4034249249249249 20.142857142857142 -3fc20c49ba5e3540 0.14100000000000001 -41a0cefa80000000 141000000 -4034492492492492 20.285714285714285 -3fc22d0e56041894 0.14200000000000002 -41a0ed7f00000000 142000000 -40346db6db6db6db 20.428571428571427 -3fc24dd2f1a9fbe8 0.14300000000000002 -41a10c0380000000 143000000 -4034924924924925 20.571428571428573 -3fc26e978d4fdf3c 0.14400000000000002 -41a12a8800000000 144000000 -4034b6db6db6db6e 20.714285714285715 -3fc28f5c28f5c28f 0.145 -41a1490c80000000 145000000 -4034db6db6db6db7 20.857142857142858 -3fc2b020c49ba5e3 0.146 -41a1679100000000 146000000 -4035000000000000 21 -3fc2d0e560418937 0.147 -41a1861580000000 147000000 -4035249249249249 21.142857142857142 -3fc2f1a9fbe76c8b 0.148 -41a1a49a00000000 148000000 -4035492492492492 21.285714285714285 -3fc3126e978d4fdf 0.149 -41a1c31e80000000 149000000 -40356db6db6db6db 21.428571428571427 -3fc3333333333333 0.15 -41a1e1a300000000 150000000 -4035924924924925 21.571428571428573 -3fc353f7ced91687 0.151 -41a2002780000000 151000000 -4035b6db6db6db6e 21.714285714285715 -3fc374bc6a7ef9db 0.152 -41a21eac00000000 152000000 -4035db6db6db6db7 21.857142857142858 -3fc395810624dd2f 0.153 -41a23d3080000000 153000000 -4036000000000000 22 -3fc3b645a1cac083 0.154 -41a25bb500000000 154000000 -4036249249249249 22.142857142857142 -3fc3d70a3d70a3d7 0.155 -41a27a3980000000 155000000 -4036492492492492 22.285714285714285 -3fc3f7ced916872b 0.156 -41a298be00000000 156000000 -40366db6db6db6db 22.428571428571427 -3fc4189374bc6a7f 0.157 -41a2b74280000000 157000000 -4036924924924925 22.571428571428573 -3fc4395810624dd3 0.158 -41a2d5c700000000 158000000 -4036b6db6db6db6e 22.714285714285715 -3fc45a1cac083127 0.159 -41a2f44b80000000 159000000 -4036db6db6db6db7 22.857142857142858 -3fc47ae147ae147b 0.16 -41a312d000000000 160000000 -4037000000000000 23 -3fc49ba5e353f7cf 0.161 -41a3315480000000 161000000 -4037249249249249 23.142857142857142 -3fc4bc6a7ef9db23 0.162 -41a34fd900000000 162000000 -4037492492492492 23.285714285714285 -3fc4dd2f1a9fbe77 0.163 -41a36e5d80000000 163000000 -40376db6db6db6db 23.428571428571427 -3fc4fdf3b645a1cb 0.164 -41a38ce200000000 164000000 -4037924924924925 23.571428571428573 -3fc51eb851eb851f 0.165 -41a3ab6680000000 165000000 -4037b6db6db6db6e 23.714285714285715 -3fc53f7ced916873 0.166 -41a3c9eb00000000 166000000 -4037db6db6db6db7 23.857142857142858 -3fc5604189374bc7 0.167 -41a3e86f80000000 167000000 -4038000000000000 24 -3fc5810624dd2f1b 0.168 -41a406f400000000 168000000 -4038249249249249 24.142857142857142 -3fc5a1cac083126f 0.169 -41a4257880000000 169000000 -4038492492492492 24.285714285714285 -3fc5c28f5c28f5c3 0.17 -41a443fd00000000 170000000 -40386db6db6db6db 24.428571428571427 -3fc5e353f7ced917 0.171 -41a4628180000000 171000000 -4038924924924925 24.571428571428573 -3fc604189374bc6b 0.17200000000000001 -41a4810600000000 172000000 -4038b6db6db6db6e 24.714285714285715 -3fc624dd2f1a9fbf 0.17300000000000001 -41a49f8a80000000 173000000 -4038db6db6db6db7 24.857142857142858 -3fc645a1cac08313 0.17400000000000002 -41a4be0f00000000 174000000 -4039000000000000 25 -3fc6666666666667 0.17500000000000002 -41a4dc9380000000 175000000 -4039249249249249 25.142857142857142 -3fc6872b020c49ba 0.176 -41a4fb1800000000 176000000 -4039492492492492 25.285714285714285 -3fc6a7ef9db22d0e 0.177 -41a5199c80000000 177000000 -40396db6db6db6db 25.428571428571427 -3fc6c8b439581062 0.178 -41a5382100000000 178000000 -4039924924924925 25.571428571428573 -3fc6e978d4fdf3b6 0.179 -41a556a580000000 179000000 -4039b6db6db6db6e 25.714285714285715 -3fc70a3d70a3d70a 0.18 -41a5752a00000000 180000000 -4039db6db6db6db7 25.857142857142858 -3fc72b020c49ba5e 0.181 -41a593ae80000000 181000000 -403a000000000000 26 -3fc74bc6a7ef9db2 0.182 -41a5b23300000000 182000000 -403a249249249249 26.142857142857142 -3fc76c8b43958106 0.183 -41a5d0b780000000 183000000 -403a492492492492 26.285714285714285 -3fc78d4fdf3b645a 0.184 -41a5ef3c00000000 184000000 -403a6db6db6db6db 26.428571428571427 -3fc7ae147ae147ae 0.185 -41a60dc080000000 185000000 -403a924924924925 26.571428571428573 -3fc7ced916872b02 0.186 -41a62c4500000000 186000000 -403ab6db6db6db6e 26.714285714285715 -3fc7ef9db22d0e56 0.187 -41a64ac980000000 187000000 -403adb6db6db6db7 26.857142857142858 -3fc810624dd2f1aa 0.188 -41a6694e00000000 188000000 -403b000000000000 27 -3fc83126e978d4fe 0.189 -41a687d280000000 189000000 -403b249249249249 27.142857142857142 -3fc851eb851eb852 0.19 -41a6a65700000000 190000000 -403b492492492492 27.285714285714285 -3fc872b020c49ba6 0.191 -41a6c4db80000000 191000000 -403b6db6db6db6db 27.428571428571427 -3fc89374bc6a7efa 0.192 -41a6e36000000000 192000000 -403b924924924925 27.571428571428573 -3fc8b4395810624e 0.193 -41a701e480000000 193000000 -403bb6db6db6db6e 27.714285714285715 -3fc8d4fdf3b645a2 0.194 -41a7206900000000 194000000 -403bdb6db6db6db7 27.857142857142858 -3fc8f5c28f5c28f6 0.195 -41a73eed80000000 195000000 -403c000000000000 28 -3fc916872b020c4a 0.196 -41a75d7200000000 196000000 -403c249249249249 28.142857142857142 -3fc9374bc6a7ef9e 0.197 -41a77bf680000000 197000000 -403c492492492492 28.285714285714285 -3fc95810624dd2f2 0.198 -41a79a7b00000000 198000000 -403c6db6db6db6db 28.428571428571427 -3fc978d4fdf3b646 0.199 -41a7b8ff80000000 199000000 -403c924924924925 28.571428571428573 -3fc999999999999a 0.2 -41a7d78400000000 200000000 -403cb6db6db6db6e 28.714285714285715 -3fc9ba5e353f7cee 0.201 -41a7f60880000000 201000000 -403cdb6db6db6db7 28.857142857142858 -3fc9db22d0e56042 0.202 -41a8148d00000000 202000000 -403d000000000000 29 -3fc9fbe76c8b4396 0.203 -41a8331180000000 203000000 -403d249249249249 29.142857142857142 -3fca1cac083126ea 0.20400000000000001 -41a8519600000000 204000000 -403d492492492492 29.285714285714285 -3fca3d70a3d70a3e 0.20500000000000002 -41a8701a80000000 205000000 -403d6db6db6db6db 29.428571428571427 -3fca5e353f7ced92 0.20600000000000002 -41a88e9f00000000 206000000 -403d924924924925 29.571428571428573 -3fca7ef9db22d0e6 0.20700000000000002 -41a8ad2380000000 207000000 -403db6db6db6db6e 29.714285714285715 -3fca9fbe76c8b43a 0.20800000000000002 -41a8cba800000000 208000000 -403ddb6db6db6db7 29.857142857142858 -3fcac083126e978d 0.209 -41a8ea2c80000000 209000000 -403e000000000000 30 -3fcae147ae147ae1 0.21 -41a908b100000000 210000000 -403e249249249249 30.142857142857142 -3fcb020c49ba5e35 0.211 -41a9273580000000 211000000 -403e492492492492 30.285714285714285 -3fcb22d0e5604189 0.212 -41a945ba00000000 212000000 -403e6db6db6db6db 30.428571428571427 -3fcb4395810624dd 0.213 -41a9643e80000000 213000000 -403e924924924925 30.571428571428573 -3fcb645a1cac0831 0.214 -41a982c300000000 214000000 -403eb6db6db6db6e 30.714285714285715 -3fcb851eb851eb85 0.215 -41a9a14780000000 215000000 -403edb6db6db6db7 30.857142857142858 -3fcba5e353f7ced9 0.216 -41a9bfcc00000000 216000000 -403f000000000000 31 -3fcbc6a7ef9db22d 0.217 -41a9de5080000000 217000000 -403f249249249249 31.142857142857142 -3fcbe76c8b439581 0.218 -41a9fcd500000000 218000000 -403f492492492492 31.285714285714285 -3fcc083126e978d5 0.219 -41aa1b5980000000 219000000 -403f6db6db6db6db 31.428571428571427 -3fcc28f5c28f5c29 0.22 -41aa39de00000000 220000000 -403f924924924925 31.571428571428573 -3fcc49ba5e353f7d 0.221 -41aa586280000000 221000000 -403fb6db6db6db6e 31.714285714285715 -3fcc6a7ef9db22d1 0.222 -41aa76e700000000 222000000 -403fdb6db6db6db7 31.857142857142858 -3fcc8b4395810625 0.223 -41aa956b80000000 223000000 -4040000000000000 32 -3fccac083126e979 0.224 -41aab3f000000000 224000000 -4040124924924925 32.142857142857146 -3fcccccccccccccd 0.225 -41aad27480000000 225000000 -4040249249249249 32.285714285714285 -3fcced916872b021 0.226 -41aaf0f900000000 226000000 -404036db6db6db6e 32.42857142857143 -3fcd0e5604189375 0.227 -41ab0f7d80000000 227000000 -4040492492492492 32.57142857142857 -3fcd2f1a9fbe76c9 0.228 -41ab2e0200000000 228000000 -40405b6db6db6db7 32.714285714285715 -3fcd4fdf3b645a1d 0.229 -41ab4c8680000000 229000000 -40406db6db6db6db 32.857142857142854 -3fcd70a3d70a3d71 0.23 -41ab6b0b00000000 230000000 -4040800000000000 33 -3fcd916872b020c5 0.231 -41ab898f80000000 231000000 -4040924924924925 33.142857142857146 -3fcdb22d0e560419 0.232 -41aba81400000000 232000000 -4040a49249249249 33.285714285714285 -3fcdd2f1a9fbe76d 0.233 -41abc69880000000 233000000 -4040b6db6db6db6e 33.42857142857143 -3fcdf3b645a1cac1 0.234 -41abe51d00000000 234000000 -4040c92492492492 33.57142857142857 -3fce147ae147ae15 0.23500000000000001 -41ac03a180000000 235000000 -4040db6db6db6db7 33.714285714285715 -3fce353f7ced9169 0.23600000000000002 -41ac222600000000 236000000 -4040edb6db6db6db 33.857142857142854 -3fce5604189374bd 0.23700000000000002 -41ac40aa80000000 237000000 -4041000000000000 34 -3fce76c8b4395811 0.23800000000000002 -41ac5f2f00000000 238000000 -4041124924924925 34.142857142857146 -3fce978d4fdf3b65 0.23900000000000002 -41ac7db380000000 239000000 -4041249249249249 34.285714285714285 -3fceb851eb851eb8 0.24 -41ac9c3800000000 240000000 -404136db6db6db6e 34.42857142857143 -3fced916872b020c 0.241 -41acbabc80000000 241000000 -4041492492492492 34.57142857142857 -3fcef9db22d0e560 0.242 -41acd94100000000 242000000 -40415b6db6db6db7 34.714285714285715 -3fcf1a9fbe76c8b4 0.243 -41acf7c580000000 243000000 -40416db6db6db6db 34.857142857142854 -3fcf3b645a1cac08 0.244 -41ad164a00000000 244000000 -4041800000000000 35 -3fcf5c28f5c28f5c 0.245 -41ad34ce80000000 245000000 -4041924924924925 35.142857142857146 -3fcf7ced916872b0 0.246 -41ad535300000000 246000000 -4041a49249249249 35.285714285714285 -3fcf9db22d0e5604 0.247 -41ad71d780000000 247000000 -4041b6db6db6db6e 35.42857142857143 -3fcfbe76c8b43958 0.248 -41ad905c00000000 248000000 -4041c92492492492 35.57142857142857 -3fcfdf3b645a1cac 0.249 -41adaee080000000 249000000 -4041db6db6db6db7 35.714285714285715 -3fd0000000000000 0.25 -41adcd6500000000 250000000 -4041edb6db6db6db 35.857142857142854 -3fd010624dd2f1aa 0.251 -41adebe980000000 251000000 -4042000000000000 36 -3fd020c49ba5e354 0.252 -41ae0a6e00000000 252000000 -4042124924924925 36.142857142857146 -3fd03126e978d4fe 0.253 -41ae28f280000000 253000000 -4042249249249249 36.285714285714285 -3fd04189374bc6a8 0.254 -41ae477700000000 254000000 -404236db6db6db6e 36.42857142857143 -3fd051eb851eb852 0.255 -41ae65fb80000000 255000000 -4042492492492492 36.57142857142857 -3fd0624dd2f1a9fc 0.256 -41ae848000000000 256000000 -40425b6db6db6db7 36.714285714285715 -3fd072b020c49ba6 0.257 -41aea30480000000 257000000 -40426db6db6db6db 36.857142857142854 -3fd083126e978d50 0.258 -41aec18900000000 258000000 -4042800000000000 37 -3fd09374bc6a7efa 0.259 -41aee00d80000000 259000000 -4042924924924925 37.142857142857146 -3fd0a3d70a3d70a4 0.26 -41aefe9200000000 260000000 -4042a49249249249 37.285714285714285 -3fd0b4395810624e 0.261 -41af1d1680000000 261000000 -4042b6db6db6db6e 37.42857142857143 -3fd0c49ba5e353f8 0.262 -41af3b9b00000000 262000000 -4042c92492492492 37.57142857142857 -3fd0d4fdf3b645a2 0.263 -41af5a1f80000000 263000000 -4042db6db6db6db7 37.714285714285715 -3fd0e5604189374c 0.264 -41af78a400000000 264000000 -4042edb6db6db6db 37.857142857142854 -3fd0f5c28f5c28f6 0.265 -41af972880000000 265000000 -4043000000000000 38 -3fd10624dd2f1aa0 0.266 -41afb5ad00000000 266000000 -4043124924924925 38.142857142857146 -3fd116872b020c4a 0.267 -41afd43180000000 267000000 -4043249249249249 38.285714285714285 -3fd126e978d4fdf4 0.268 -41aff2b600000000 268000000 -404336db6db6db6e 38.42857142857143 -3fd1374bc6a7ef9e 0.269 -41b0089d40000000 269000000 -4043492492492492 38.57142857142857 -3fd147ae147ae148 0.27 -41b017df80000000 270000000 -40435b6db6db6db7 38.714285714285715 -3fd15810624dd2f2 0.271 -41b02721c0000000 271000000 -40436db6db6db6db 38.857142857142854 -3fd16872b020c49c 0.272 -41b0366400000000 272000000 -4043800000000000 39 -3fd178d4fdf3b646 0.273 -41b045a640000000 273000000 -4043924924924925 39.142857142857146 -3fd189374bc6a7f0 0.274 -41b054e880000000 274000000 -4043a49249249249 39.285714285714285 -3fd199999999999a 0.275 -41b0642ac0000000 275000000 -4043b6db6db6db6e 39.42857142857143 -3fd1a9fbe76c8b44 0.276 -41b0736d00000000 276000000 -4043c92492492492 39.57142857142857 -3fd1ba5e353f7cee 0.277 -41b082af40000000 277000000 -4043db6db6db6db7 39.714285714285715 -3fd1cac083126e98 0.278 -41b091f180000000 278000000 -4043edb6db6db6db 39.857142857142854 -3fd1db22d0e56042 0.279 -41b0a133c0000000 279000000 -4044000000000000 40 -3fd1eb851eb851ec 0.28 -41b0b07600000000 280000000 -4044124924924925 40.142857142857146 -3fd1fbe76c8b4396 0.281 -41b0bfb840000000 281000000 -4044249249249249 40.285714285714285 -3fd20c49ba5e3540 0.28200000000000003 -41b0cefa80000000 282000000 -404436db6db6db6e 40.42857142857143 -3fd21cac083126ea 0.28300000000000003 -41b0de3cc0000000 283000000 -4044492492492492 40.57142857142857 -3fd22d0e56041894 0.28400000000000003 -41b0ed7f00000000 284000000 -40445b6db6db6db7 40.714285714285715 -3fd23d70a3d70a3e 0.28500000000000003 -41b0fcc140000000 285000000 -40446db6db6db6db 40.857142857142854 -3fd24dd2f1a9fbe8 0.28600000000000003 -41b10c0380000000 286000000 -4044800000000000 41 -3fd25e353f7ced92 0.28700000000000003 -41b11b45c0000000 287000000 -4044924924924925 41.142857142857146 -3fd26e978d4fdf3c 0.28800000000000003 -41b12a8800000000 288000000 -4044a49249249249 41.285714285714285 -3fd27ef9db22d0e5 0.289 -41b139ca40000000 289000000 -4044b6db6db6db6e 41.42857142857143 -3fd28f5c28f5c28f 0.29 -41b1490c80000000 290000000 -4044c92492492492 41.57142857142857 -3fd29fbe76c8b439 0.291 -41b1584ec0000000 291000000 -4044db6db6db6db7 41.714285714285715 -3fd2b020c49ba5e3 0.292 -41b1679100000000 292000000 -4044edb6db6db6db 41.857142857142854 -3fd2c083126e978d 0.293 -41b176d340000000 293000000 -4045000000000000 42 -3fd2d0e560418937 0.294 -41b1861580000000 294000000 -4045124924924925 42.142857142857146 -3fd2e147ae147ae1 0.295 -41b19557c0000000 295000000 -4045249249249249 42.285714285714285 -3fd2f1a9fbe76c8b 0.296 -41b1a49a00000000 296000000 -404536db6db6db6e 42.42857142857143 -3fd3020c49ba5e35 0.297 -41b1b3dc40000000 297000000 -4045492492492492 42.57142857142857 -3fd3126e978d4fdf 0.298 -41b1c31e80000000 298000000 -40455b6db6db6db7 42.714285714285715 -3fd322d0e5604189 0.299 -41b1d260c0000000 299000000 -40456db6db6db6db 42.857142857142854 -3fd3333333333333 0.3 -41b1e1a300000000 300000000 -4045800000000000 43 -3fd34395810624dd 0.301 -41b1f0e540000000 301000000 -4045924924924925 43.142857142857146 -3fd353f7ced91687 0.302 -41b2002780000000 302000000 -4045a49249249249 43.285714285714285 -3fd3645a1cac0831 0.303 -41b20f69c0000000 303000000 -4045b6db6db6db6e 43.42857142857143 -3fd374bc6a7ef9db 0.304 -41b21eac00000000 304000000 -4045c92492492492 43.57142857142857 -3fd3851eb851eb85 0.305 -41b22dee40000000 305000000 -4045db6db6db6db7 43.714285714285715 -3fd395810624dd2f 0.306 -41b23d3080000000 306000000 -4045edb6db6db6db 43.857142857142854 -3fd3a5e353f7ced9 0.307 -41b24c72c0000000 307000000 -4046000000000000 44 -3fd3b645a1cac083 0.308 -41b25bb500000000 308000000 -4046124924924925 44.142857142857146 -3fd3c6a7ef9db22d 0.309 -41b26af740000000 309000000 -4046249249249249 44.285714285714285 -3fd3d70a3d70a3d7 0.31 -41b27a3980000000 310000000 -404636db6db6db6e 44.42857142857143 -3fd3e76c8b439581 0.311 -41b2897bc0000000 311000000 -4046492492492492 44.57142857142857 -3fd3f7ced916872b 0.312 -41b298be00000000 312000000 -40465b6db6db6db7 44.714285714285715 -3fd4083126e978d5 0.313 -41b2a80040000000 313000000 -40466db6db6db6db 44.857142857142854 -3fd4189374bc6a7f 0.314 -41b2b74280000000 314000000 -4046800000000000 45 -3fd428f5c28f5c29 0.315 -41b2c684c0000000 315000000 -4046924924924925 45.142857142857146 -3fd4395810624dd3 0.316 -41b2d5c700000000 316000000 -4046a49249249249 45.285714285714285 -3fd449ba5e353f7d 0.317 -41b2e50940000000 317000000 -4046b6db6db6db6e 45.42857142857143 -3fd45a1cac083127 0.318 -41b2f44b80000000 318000000 -4046c92492492492 45.57142857142857 -3fd46a7ef9db22d1 0.319 -41b3038dc0000000 319000000 -4046db6db6db6db7 45.714285714285715 -3fd47ae147ae147b 0.32 -41b312d000000000 320000000 -4046edb6db6db6db 45.857142857142854 -3fd48b4395810625 0.321 -41b3221240000000 321000000 -4047000000000000 46 -3fd49ba5e353f7cf 0.322 -41b3315480000000 322000000 -4047124924924925 46.142857142857146 -3fd4ac083126e979 0.323 -41b34096c0000000 323000000 -4047249249249249 46.285714285714285 -3fd4bc6a7ef9db23 0.324 -41b34fd900000000 324000000 -404736db6db6db6e 46.42857142857143 -3fd4cccccccccccd 0.325 -41b35f1b40000000 325000000 -4047492492492492 46.57142857142857 -3fd4dd2f1a9fbe77 0.326 -41b36e5d80000000 326000000 -40475b6db6db6db7 46.714285714285715 -3fd4ed916872b021 0.327 -41b37d9fc0000000 327000000 -40476db6db6db6db 46.857142857142854 -3fd4fdf3b645a1cb 0.328 -41b38ce200000000 328000000 -4047800000000000 47 -3fd50e5604189375 0.329 -41b39c2440000000 329000000 -4047924924924925 47.142857142857146 -3fd51eb851eb851f 0.33 -41b3ab6680000000 330000000 -4047a49249249249 47.285714285714285 -3fd52f1a9fbe76c9 0.331 -41b3baa8c0000000 331000000 -4047b6db6db6db6e 47.42857142857143 -3fd53f7ced916873 0.332 -41b3c9eb00000000 332000000 -4047c92492492492 47.57142857142857 -3fd54fdf3b645a1d 0.333 -41b3d92d40000000 333000000 -4047db6db6db6db7 47.714285714285715 -3fd5604189374bc7 0.334 -41b3e86f80000000 334000000 -4047edb6db6db6db 47.857142857142854 -3fd570a3d70a3d71 0.335 -41b3f7b1c0000000 335000000 -4048000000000000 48 -3fd5810624dd2f1b 0.336 -41b406f400000000 336000000 -4048124924924925 48.142857142857146 -3fd5916872b020c5 0.337 -41b4163640000000 337000000 -4048249249249249 48.285714285714285 -3fd5a1cac083126f 0.338 -41b4257880000000 338000000 -404836db6db6db6e 48.42857142857143 -3fd5b22d0e560419 0.339 -41b434bac0000000 339000000 -4048492492492492 48.57142857142857 -3fd5c28f5c28f5c3 0.34 -41b443fd00000000 340000000 -40485b6db6db6db7 48.714285714285715 -3fd5d2f1a9fbe76d 0.341 -41b4533f40000000 341000000 -40486db6db6db6db 48.857142857142854 -3fd5e353f7ced917 0.342 -41b4628180000000 342000000 -4048800000000000 49 -3fd5f3b645a1cac1 0.343 -41b471c3c0000000 343000000 -4048924924924925 49.142857142857146 -3fd604189374bc6b 0.34400000000000003 -41b4810600000000 344000000 -4048a49249249249 49.285714285714285 -3fd6147ae147ae15 0.34500000000000003 -41b4904840000000 345000000 -4048b6db6db6db6e 49.42857142857143 -3fd624dd2f1a9fbf 0.34600000000000003 -41b49f8a80000000 346000000 -4048c92492492492 49.57142857142857 -3fd6353f7ced9169 0.34700000000000003 -41b4aeccc0000000 347000000 -4048db6db6db6db7 49.714285714285715 -3fd645a1cac08313 0.34800000000000003 -41b4be0f00000000 348000000 -4048edb6db6db6db 49.857142857142854 -3fd65604189374bd 0.34900000000000003 -41b4cd5140000000 349000000 -4049000000000000 50 -3fd6666666666667 0.35000000000000003 -41b4dc9380000000 350000000 -4049124924924925 50.142857142857146 -3fd676c8b4395811 0.35100000000000003 -41b4ebd5c0000000 351000000 -4049249249249249 50.285714285714285 -3fd6872b020c49ba 0.352 -41b4fb1800000000 352000000 -404936db6db6db6e 50.42857142857143 -3fd6978d4fdf3b64 0.353 -41b50a5a40000000 353000000 -4049492492492492 50.57142857142857 -3fd6a7ef9db22d0e 0.354 -41b5199c80000000 354000000 -40495b6db6db6db7 50.714285714285715 -3fd6b851eb851eb8 0.355 -41b528dec0000000 355000000 -40496db6db6db6db 50.857142857142854 -3fd6c8b439581062 0.356 -41b5382100000000 356000000 -4049800000000000 51 -3fd6d916872b020c 0.357 -41b5476340000000 357000000 -4049924924924925 51.142857142857146 -3fd6e978d4fdf3b6 0.358 -41b556a580000000 358000000 -4049a49249249249 51.285714285714285 -3fd6f9db22d0e560 0.359 -41b565e7c0000000 359000000 -4049b6db6db6db6e 51.42857142857143 -3fd70a3d70a3d70a 0.36 -41b5752a00000000 360000000 -4049c92492492492 51.57142857142857 -3fd71a9fbe76c8b4 0.361 -41b5846c40000000 361000000 -4049db6db6db6db7 51.714285714285715 -3fd72b020c49ba5e 0.362 -41b593ae80000000 362000000 -4049edb6db6db6db 51.857142857142854 -3fd73b645a1cac08 0.363 -41b5a2f0c0000000 363000000 -404a000000000000 52 -3fd74bc6a7ef9db2 0.364 -41b5b23300000000 364000000 -404a124924924925 52.142857142857146 -3fd75c28f5c28f5c 0.365 -41b5c17540000000 365000000 -404a249249249249 52.285714285714285 -3fd76c8b43958106 0.366 -41b5d0b780000000 366000000 -404a36db6db6db6e 52.42857142857143 -3fd77ced916872b0 0.367 -41b5dff9c0000000 367000000 -404a492492492492 52.57142857142857 -3fd78d4fdf3b645a 0.368 -41b5ef3c00000000 368000000 -404a5b6db6db6db7 52.714285714285715 -3fd79db22d0e5604 0.369 -41b5fe7e40000000 369000000 -404a6db6db6db6db 52.857142857142854 -3fd7ae147ae147ae 0.37 -41b60dc080000000 370000000 -404a800000000000 53 -3fd7be76c8b43958 0.371 -41b61d02c0000000 371000000 -404a924924924925 53.142857142857146 -3fd7ced916872b02 0.372 -41b62c4500000000 372000000 -404aa49249249249 53.285714285714285 -3fd7df3b645a1cac 0.373 -41b63b8740000000 373000000 -404ab6db6db6db6e 53.42857142857143 -3fd7ef9db22d0e56 0.374 -41b64ac980000000 374000000 -404ac92492492492 53.57142857142857 -3fd8000000000000 0.375 -41b65a0bc0000000 375000000 -404adb6db6db6db7 53.714285714285715 -3fd810624dd2f1aa 0.376 -41b6694e00000000 376000000 -404aedb6db6db6db 53.857142857142854 -3fd820c49ba5e354 0.377 -41b6789040000000 377000000 -404b000000000000 54 -3fd83126e978d4fe 0.378 -41b687d280000000 378000000 -404b124924924925 54.142857142857146 -3fd84189374bc6a8 0.379 -41b69714c0000000 379000000 -404b249249249249 54.285714285714285 -3fd851eb851eb852 0.38 -41b6a65700000000 380000000 -404b36db6db6db6e 54.42857142857143 -3fd8624dd2f1a9fc 0.381 -41b6b59940000000 381000000 -404b492492492492 54.57142857142857 -3fd872b020c49ba6 0.382 -41b6c4db80000000 382000000 -404b5b6db6db6db7 54.714285714285715 -3fd883126e978d50 0.383 -41b6d41dc0000000 383000000 -404b6db6db6db6db 54.857142857142854 -3fd89374bc6a7efa 0.384 -41b6e36000000000 384000000 -404b800000000000 55 -3fd8a3d70a3d70a4 0.385 -41b6f2a240000000 385000000 -404b924924924925 55.142857142857146 -3fd8b4395810624e 0.386 -41b701e480000000 386000000 -404ba49249249249 55.285714285714285 -3fd8c49ba5e353f8 0.387 -41b71126c0000000 387000000 -404bb6db6db6db6e 55.42857142857143 -3fd8d4fdf3b645a2 0.388 -41b7206900000000 388000000 -404bc92492492492 55.57142857142857 -3fd8e5604189374c 0.389 -41b72fab40000000 389000000 -404bdb6db6db6db7 55.714285714285715 -3fd8f5c28f5c28f6 0.39 -41b73eed80000000 390000000 -404bedb6db6db6db 55.857142857142854 -3fd90624dd2f1aa0 0.391 -41b74e2fc0000000 391000000 -404c000000000000 56 -3fd916872b020c4a 0.392 -41b75d7200000000 392000000 -404c124924924925 56.142857142857146 -3fd926e978d4fdf4 0.393 -41b76cb440000000 393000000 -404c249249249249 56.285714285714285 -3fd9374bc6a7ef9e 0.394 -41b77bf680000000 394000000 -404c36db6db6db6e 56.42857142857143 -3fd947ae147ae148 0.395 -41b78b38c0000000 395000000 -404c492492492492 56.57142857142857 -3fd95810624dd2f2 0.396 -41b79a7b00000000 396000000 -404c5b6db6db6db7 56.714285714285715 -3fd96872b020c49c 0.397 -41b7a9bd40000000 397000000 -404c6db6db6db6db 56.857142857142854 -3fd978d4fdf3b646 0.398 -41b7b8ff80000000 398000000 -404c800000000000 57 -3fd989374bc6a7f0 0.399 -41b7c841c0000000 399000000 -404c924924924925 57.142857142857146 -3fd999999999999a 0.4 -41b7d78400000000 400000000 -404ca49249249249 57.285714285714285 -3fd9a9fbe76c8b44 0.401 -41b7e6c640000000 401000000 -404cb6db6db6db6e 57.42857142857143 -3fd9ba5e353f7cee 0.402 -41b7f60880000000 402000000 -404cc92492492492 57.57142857142857 -3fd9cac083126e98 0.403 -41b8054ac0000000 403000000 -404cdb6db6db6db7 57.714285714285715 -3fd9db22d0e56042 0.404 -41b8148d00000000 404000000 -404cedb6db6db6db 57.857142857142854 -3fd9eb851eb851ec 0.405 -41b823cf40000000 405000000 -404d000000000000 58 -3fd9fbe76c8b4396 0.406 -41b8331180000000 406000000 -404d124924924925 58.142857142857146 -3fda0c49ba5e3540 0.40700000000000003 -41b84253c0000000 407000000 -404d249249249249 58.285714285714285 -3fda1cac083126ea 0.40800000000000003 -41b8519600000000 408000000 -404d36db6db6db6e 58.42857142857143 -3fda2d0e56041894 0.40900000000000003 -41b860d840000000 409000000 -404d492492492492 58.57142857142857 -3fda3d70a3d70a3e 0.41000000000000003 -41b8701a80000000 410000000 -404d5b6db6db6db7 58.714285714285715 -3fda4dd2f1a9fbe8 0.41100000000000003 -41b87f5cc0000000 411000000 -404d6db6db6db6db 58.857142857142854 -3fda5e353f7ced92 0.41200000000000003 -41b88e9f00000000 412000000 -404d800000000000 59 -3fda6e978d4fdf3c 0.41300000000000003 -41b89de140000000 413000000 -404d924924924925 59.142857142857146 -3fda7ef9db22d0e6 0.41400000000000003 -41b8ad2380000000 414000000 -404da49249249249 59.285714285714285 -3fda8f5c28f5c290 0.41500000000000004 -41b8bc65c0000000 415000000 -404db6db6db6db6e 59.42857142857143 -3fda9fbe76c8b43a 0.41600000000000004 -41b8cba800000000 416000000 -404dc92492492492 59.57142857142857 -3fdab020c49ba5e3 0.417 -41b8daea40000000 417000000 -404ddb6db6db6db7 59.714285714285715 -3fdac083126e978d 0.418 -41b8ea2c80000000 418000000 -404dedb6db6db6db 59.857142857142854 -3fdad0e560418937 0.419 -41b8f96ec0000000 419000000 -404e000000000000 60 -3fdae147ae147ae1 0.42 -41b908b100000000 420000000 -404e124924924925 60.142857142857146 -3fdaf1a9fbe76c8b 0.421 -41b917f340000000 421000000 -404e249249249249 60.285714285714285 -3fdb020c49ba5e35 0.422 -41b9273580000000 422000000 -404e36db6db6db6e 60.42857142857143 -3fdb126e978d4fdf 0.423 -41b93677c0000000 423000000 -404e492492492492 60.57142857142857 -3fdb22d0e5604189 0.424 -41b945ba00000000 424000000 -404e5b6db6db6db7 60.714285714285715 -3fdb333333333333 0.425 -41b954fc40000000 425000000 -404e6db6db6db6db 60.857142857142854 -3fdb4395810624dd 0.426 -41b9643e80000000 426000000 -404e800000000000 61 -3fdb53f7ced91687 0.427 -41b97380c0000000 427000000 -404e924924924925 61.142857142857146 -3fdb645a1cac0831 0.428 -41b982c300000000 428000000 -404ea49249249249 61.285714285714285 -3fdb74bc6a7ef9db 0.429 -41b9920540000000 429000000 -404eb6db6db6db6e 61.42857142857143 -3fdb851eb851eb85 0.43 -41b9a14780000000 430000000 -404ec92492492492 61.57142857142857 -3fdb95810624dd2f 0.431 -41b9b089c0000000 431000000 -404edb6db6db6db7 61.714285714285715 -3fdba5e353f7ced9 0.432 -41b9bfcc00000000 432000000 -404eedb6db6db6db 61.857142857142854 -3fdbb645a1cac083 0.433 -41b9cf0e40000000 433000000 -404f000000000000 62 -3fdbc6a7ef9db22d 0.434 -41b9de5080000000 434000000 -404f124924924925 62.142857142857146 -3fdbd70a3d70a3d7 0.435 -41b9ed92c0000000 435000000 -404f249249249249 62.285714285714285 -3fdbe76c8b439581 0.436 -41b9fcd500000000 436000000 -404f36db6db6db6e 62.42857142857143 -3fdbf7ced916872b 0.437 -41ba0c1740000000 437000000 -404f492492492492 62.57142857142857 -3fdc083126e978d5 0.438 -41ba1b5980000000 438000000 -404f5b6db6db6db7 62.714285714285715 -3fdc189374bc6a7f 0.439 -41ba2a9bc0000000 439000000 -404f6db6db6db6db 62.857142857142854 -3fdc28f5c28f5c29 0.44 -41ba39de00000000 440000000 -404f800000000000 63 -3fdc395810624dd3 0.441 -41ba492040000000 441000000 -404f924924924925 63.142857142857146 -3fdc49ba5e353f7d 0.442 -41ba586280000000 442000000 -404fa49249249249 63.285714285714285 -3fdc5a1cac083127 0.443 -41ba67a4c0000000 443000000 -404fb6db6db6db6e 63.42857142857143 -3fdc6a7ef9db22d1 0.444 -41ba76e700000000 444000000 -404fc92492492492 63.57142857142857 -3fdc7ae147ae147b 0.445 -41ba862940000000 445000000 -404fdb6db6db6db7 63.714285714285715 -3fdc8b4395810625 0.446 -41ba956b80000000 446000000 -404fedb6db6db6db 63.857142857142854 -3fdc9ba5e353f7cf 0.447 -41baa4adc0000000 447000000 -4050000000000000 64 -3fdcac083126e979 0.448 -41bab3f000000000 448000000 -4050092492492492 64.14285714285714 -3fdcbc6a7ef9db23 0.449 -41bac33240000000 449000000 -4050124924924925 64.28571428571429 -3fdccccccccccccd 0.45 -41bad27480000000 450000000 -40501b6db6db6db7 64.42857142857143 -3fdcdd2f1a9fbe77 0.451 -41bae1b6c0000000 451000000 -4050249249249249 64.57142857142857 -3fdced916872b021 0.452 -41baf0f900000000 452000000 -40502db6db6db6db 64.71428571428571 -3fdcfdf3b645a1cb 0.453 -41bb003b40000000 453000000 -405036db6db6db6e 64.85714285714286 -3fdd0e5604189375 0.454 -41bb0f7d80000000 454000000 -4050400000000000 65 -3fdd1eb851eb851f 0.455 -41bb1ebfc0000000 455000000 -4050492492492492 65.14285714285714 -3fdd2f1a9fbe76c9 0.456 -41bb2e0200000000 456000000 -4050524924924925 65.28571428571429 -3fdd3f7ced916873 0.457 -41bb3d4440000000 457000000 -40505b6db6db6db7 65.42857142857143 -3fdd4fdf3b645a1d 0.458 -41bb4c8680000000 458000000 -4050649249249249 65.57142857142857 -3fdd604189374bc7 0.459 -41bb5bc8c0000000 459000000 -40506db6db6db6db 65.71428571428571 -3fdd70a3d70a3d71 0.46 -41bb6b0b00000000 460000000 -405076db6db6db6e 65.85714285714286 -3fdd810624dd2f1b 0.461 -41bb7a4d40000000 461000000 -4050800000000000 66 -3fdd916872b020c5 0.462 -41bb898f80000000 462000000 -4050892492492492 66.14285714285714 -3fdda1cac083126f 0.463 -41bb98d1c0000000 463000000 -4050924924924925 66.28571428571429 -3fddb22d0e560419 0.464 -41bba81400000000 464000000 -40509b6db6db6db7 66.42857142857143 -3fddc28f5c28f5c3 0.465 -41bbb75640000000 465000000 -4050a49249249249 66.57142857142857 -3fddd2f1a9fbe76d 0.466 -41bbc69880000000 466000000 -4050adb6db6db6db 66.71428571428571 -3fdde353f7ced917 0.467 -41bbd5dac0000000 467000000 -4050b6db6db6db6e 66.85714285714286 -3fddf3b645a1cac1 0.468 -41bbe51d00000000 468000000 -4050c00000000000 67 -3fde04189374bc6b 0.46900000000000003 -41bbf45f40000000 469000000 -4050c92492492492 67.14285714285714 -3fde147ae147ae15 0.47000000000000003 -41bc03a180000000 470000000 -4050d24924924925 67.28571428571429 -3fde24dd2f1a9fbf 0.47100000000000003 -41bc12e3c0000000 471000000 -4050db6db6db6db7 67.42857142857143 -3fde353f7ced9169 0.47200000000000003 -41bc222600000000 472000000 -4050e49249249249 67.57142857142857 -3fde45a1cac08313 0.47300000000000003 -41bc316840000000 473000000 -4050edb6db6db6db 67.71428571428571 -3fde5604189374bd 0.47400000000000003 -41bc40aa80000000 474000000 -4050f6db6db6db6e 67.85714285714286 -3fde666666666667 0.47500000000000003 -41bc4fecc0000000 475000000 -4051000000000000 68 -3fde76c8b4395811 0.47600000000000003 -41bc5f2f00000000 476000000 -4051092492492492 68.14285714285714 -3fde872b020c49bb 0.47700000000000004 -41bc6e7140000000 477000000 -4051124924924925 68.28571428571429 -3fde978d4fdf3b65 0.47800000000000004 -41bc7db380000000 478000000 -40511b6db6db6db7 68.42857142857143 -3fdea7ef9db22d0f 0.47900000000000004 -41bc8cf5c0000000 479000000 -4051249249249249 68.57142857142857 -3fdeb851eb851eb8 0.48 -41bc9c3800000000 480000000 -40512db6db6db6db 68.71428571428571 -3fdec8b439581062 0.481 -41bcab7a40000000 481000000 -405136db6db6db6e 68.85714285714286 -3fded916872b020c 0.482 -41bcbabc80000000 482000000 -4051400000000000 69 -3fdee978d4fdf3b6 0.483 -41bcc9fec0000000 483000000 -4051492492492492 69.14285714285714 -3fdef9db22d0e560 0.484 -41bcd94100000000 484000000 -4051524924924925 69.28571428571429 -3fdf0a3d70a3d70a 0.485 -41bce88340000000 485000000 -40515b6db6db6db7 69.42857142857143 -3fdf1a9fbe76c8b4 0.486 -41bcf7c580000000 486000000 -4051649249249249 69.57142857142857 -3fdf2b020c49ba5e 0.487 -41bd0707c0000000 487000000 -40516db6db6db6db 69.71428571428571 -3fdf3b645a1cac08 0.488 -41bd164a00000000 488000000 -405176db6db6db6e 69.85714285714286 -3fdf4bc6a7ef9db2 0.489 -41bd258c40000000 489000000 -4051800000000000 70 -3fdf5c28f5c28f5c 0.49 -41bd34ce80000000 490000000 -4051892492492492 70.14285714285714 -3fdf6c8b43958106 0.491 -41bd4410c0000000 491000000 -4051924924924925 70.28571428571429 -3fdf7ced916872b0 0.492 -41bd535300000000 492000000 -40519b6db6db6db7 70.42857142857143 -3fdf8d4fdf3b645a 0.493 -41bd629540000000 493000000 -4051a49249249249 70.57142857142857 -3fdf9db22d0e5604 0.494 -41bd71d780000000 494000000 -4051adb6db6db6db 70.71428571428571 -3fdfae147ae147ae 0.495 -41bd8119c0000000 495000000 -4051b6db6db6db6e 70.85714285714286 -3fdfbe76c8b43958 0.496 -41bd905c00000000 496000000 -4051c00000000000 71 -3fdfced916872b02 0.497 -41bd9f9e40000000 497000000 -4051c92492492492 71.14285714285714 -3fdfdf3b645a1cac 0.498 -41bdaee080000000 498000000 -4051d24924924925 71.28571428571429 -3fdfef9db22d0e56 0.499 -41bdbe22c0000000 499000000 -4051db6db6db6db7 71.42857142857143 -3fe0000000000000 0.5 -41bdcd6500000000 500000000 -4051e49249249249 71.57142857142857 -3fe0083126e978d5 0.501 -41bddca740000000 501000000 -4051edb6db6db6db 71.71428571428571 -3fe010624dd2f1aa 0.502 -41bdebe980000000 502000000 -4051f6db6db6db6e 71.85714285714286 -3fe0189374bc6a7f 0.503 -41bdfb2bc0000000 503000000 -4052000000000000 72 -3fe020c49ba5e354 0.504 -41be0a6e00000000 504000000 -4052092492492492 72.14285714285714 -3fe028f5c28f5c29 0.505 -41be19b040000000 505000000 -4052124924924925 72.28571428571429 -3fe03126e978d4fe 0.506 -41be28f280000000 506000000 -40521b6db6db6db7 72.42857142857143 -3fe0395810624dd3 0.507 -41be3834c0000000 507000000 -4052249249249249 72.57142857142857 -3fe04189374bc6a8 0.508 -41be477700000000 508000000 -40522db6db6db6db 72.71428571428571 -3fe049ba5e353f7d 0.509 -41be56b940000000 509000000 -405236db6db6db6e 72.85714285714286 -3fe051eb851eb852 0.51 -41be65fb80000000 510000000 -4052400000000000 73 -3fe05a1cac083127 0.511 -41be753dc0000000 511000000 -4052492492492492 73.14285714285714 -3fe0624dd2f1a9fc 0.512 -41be848000000000 512000000 -4052524924924925 73.28571428571429 -3fe06a7ef9db22d1 0.513 -41be93c240000000 513000000 -40525b6db6db6db7 73.42857142857143 -3fe072b020c49ba6 0.514 -41bea30480000000 514000000 -4052649249249249 73.57142857142857 -3fe07ae147ae147b 0.515 -41beb246c0000000 515000000 -40526db6db6db6db 73.71428571428571 -3fe083126e978d50 0.516 -41bec18900000000 516000000 -405276db6db6db6e 73.85714285714286 -3fe08b4395810625 0.517 -41bed0cb40000000 517000000 -4052800000000000 74 -3fe09374bc6a7efa 0.518 -41bee00d80000000 518000000 -4052892492492492 74.14285714285714 -3fe09ba5e353f7cf 0.519 -41beef4fc0000000 519000000 -4052924924924925 74.28571428571429 -3fe0a3d70a3d70a4 0.52 -41befe9200000000 520000000 -40529b6db6db6db7 74.42857142857143 -3fe0ac083126e979 0.521 -41bf0dd440000000 521000000 -4052a49249249249 74.57142857142857 -3fe0b4395810624e 0.522 -41bf1d1680000000 522000000 -4052adb6db6db6db 74.71428571428571 -3fe0bc6a7ef9db23 0.523 -41bf2c58c0000000 523000000 -4052b6db6db6db6e 74.85714285714286 -3fe0c49ba5e353f8 0.524 -41bf3b9b00000000 524000000 -4052c00000000000 75 -3fe0cccccccccccd 0.525 -41bf4add40000000 525000000 -4052c92492492492 75.14285714285714 -3fe0d4fdf3b645a2 0.526 -41bf5a1f80000000 526000000 -4052d24924924925 75.28571428571429 -3fe0dd2f1a9fbe77 0.527 -41bf6961c0000000 527000000 -4052db6db6db6db7 75.42857142857143 -3fe0e5604189374c 0.528 -41bf78a400000000 528000000 -4052e49249249249 75.57142857142857 -3fe0ed916872b021 0.529 -41bf87e640000000 529000000 -4052edb6db6db6db 75.71428571428571 -3fe0f5c28f5c28f6 0.53 -41bf972880000000 530000000 -4052f6db6db6db6e 75.85714285714286 -3fe0fdf3b645a1cb 0.531 -41bfa66ac0000000 531000000 -4053000000000000 76 -3fe10624dd2f1aa0 0.532 -41bfb5ad00000000 532000000 -4053092492492492 76.14285714285714 -3fe10e5604189375 0.533 -41bfc4ef40000000 533000000 -4053124924924925 76.28571428571429 -3fe116872b020c4a 0.534 -41bfd43180000000 534000000 -40531b6db6db6db7 76.42857142857143 -3fe11eb851eb851f 0.535 -41bfe373c0000000 535000000 -4053249249249249 76.57142857142857 -3fe126e978d4fdf4 0.536 -41bff2b600000000 536000000 -40532db6db6db6db 76.71428571428571 -3fe12f1a9fbe76c9 0.537 -41c000fc20000000 537000000 -405336db6db6db6e 76.85714285714286 -3fe1374bc6a7ef9e 0.538 -41c0089d40000000 538000000 -4053400000000000 77 -3fe13f7ced916873 0.539 -41c0103e60000000 539000000 -4053492492492492 77.14285714285714 -3fe147ae147ae148 0.54 -41c017df80000000 540000000 -4053524924924925 77.28571428571429 -3fe14fdf3b645a1d 0.541 -41c01f80a0000000 541000000 -40535b6db6db6db7 77.42857142857143 -3fe15810624dd2f2 0.542 -41c02721c0000000 542000000 -4053649249249249 77.57142857142857 -3fe1604189374bc7 0.543 -41c02ec2e0000000 543000000 -40536db6db6db6db 77.71428571428571 -3fe16872b020c49c 0.544 -41c0366400000000 544000000 -405376db6db6db6e 77.85714285714286 -3fe170a3d70a3d71 0.545 -41c03e0520000000 545000000 -4053800000000000 78 -3fe178d4fdf3b646 0.546 -41c045a640000000 546000000 -4053892492492492 78.14285714285714 -3fe1810624dd2f1b 0.547 -41c04d4760000000 547000000 -4053924924924925 78.28571428571429 -3fe189374bc6a7f0 0.548 -41c054e880000000 548000000 -40539b6db6db6db7 78.42857142857143 -3fe1916872b020c5 0.549 -41c05c89a0000000 549000000 -4053a49249249249 78.57142857142857 -3fe199999999999a 0.55 -41c0642ac0000000 550000000 -4053adb6db6db6db 78.71428571428571 -3fe1a1cac083126f 0.551 -41c06bcbe0000000 551000000 -4053b6db6db6db6e 78.85714285714286 -3fe1a9fbe76c8b44 0.552 -41c0736d00000000 552000000 -4053c00000000000 79 -3fe1b22d0e560419 0.553 -41c07b0e20000000 553000000 -4053c92492492492 79.14285714285714 -3fe1ba5e353f7cee 0.554 -41c082af40000000 554000000 -4053d24924924925 79.28571428571429 -3fe1c28f5c28f5c3 0.555 -41c08a5060000000 555000000 -4053db6db6db6db7 79.42857142857143 -3fe1cac083126e98 0.556 -41c091f180000000 556000000 -4053e49249249249 79.57142857142857 -3fe1d2f1a9fbe76d 0.557 -41c09992a0000000 557000000 -4053edb6db6db6db 79.71428571428571 -3fe1db22d0e56042 0.558 -41c0a133c0000000 558000000 -4053f6db6db6db6e 79.85714285714286 -3fe1e353f7ced917 0.559 -41c0a8d4e0000000 559000000 -4054000000000000 80 -3fe1eb851eb851ec 0.56 -41c0b07600000000 560000000 -4054092492492492 80.14285714285714 -3fe1f3b645a1cac1 0.561 -41c0b81720000000 561000000 -4054124924924925 80.28571428571429 -3fe1fbe76c8b4396 0.562 -41c0bfb840000000 562000000 -40541b6db6db6db7 80.42857142857143 -3fe204189374bc6b 0.5630000000000001 -41c0c75960000000 563000000 -4054249249249249 80.57142857142857 -3fe20c49ba5e3540 0.5640000000000001 -41c0cefa80000000 564000000 -40542db6db6db6db 80.71428571428571 -3fe2147ae147ae15 0.5650000000000001 -41c0d69ba0000000 565000000 -405436db6db6db6e 80.85714285714286 -3fe21cac083126ea 0.5660000000000001 -41c0de3cc0000000 566000000 -4054400000000000 81 -3fe224dd2f1a9fbf 0.5670000000000001 -41c0e5dde0000000 567000000 -4054492492492492 81.14285714285714 -3fe22d0e56041894 0.5680000000000001 -41c0ed7f00000000 568000000 -4054524924924925 81.28571428571429 -3fe2353f7ced9169 0.5690000000000001 -41c0f52020000000 569000000 -40545b6db6db6db7 81.42857142857143 -3fe23d70a3d70a3e 0.5700000000000001 -41c0fcc140000000 570000000 -4054649249249249 81.57142857142857 -3fe245a1cac08313 0.5710000000000001 -41c1046260000000 571000000 -40546db6db6db6db 81.71428571428571 -3fe24dd2f1a9fbe8 0.5720000000000001 -41c10c0380000000 572000000 -405476db6db6db6e 81.85714285714286 -3fe25604189374bd 0.5730000000000001 -41c113a4a0000000 573000000 -4054800000000000 82 -3fe25e353f7ced92 0.5740000000000001 -41c11b45c0000000 574000000 -4054892492492492 82.14285714285714 -3fe2666666666667 0.5750000000000001 -41c122e6e0000000 575000000 -4054924924924925 82.28571428571429 -3fe26e978d4fdf3c 0.5760000000000001 -41c12a8800000000 576000000 -40549b6db6db6db7 82.42857142857143 -3fe276c8b4395810 0.577 -41c1322920000000 577000000 -4054a49249249249 82.57142857142857 -3fe27ef9db22d0e5 0.578 -41c139ca40000000 578000000 -4054adb6db6db6db 82.71428571428571 -3fe2872b020c49ba 0.579 -41c1416b60000000 579000000 -4054b6db6db6db6e 82.85714285714286 -3fe28f5c28f5c28f 0.58 -41c1490c80000000 580000000 -4054c00000000000 83 -3fe2978d4fdf3b64 0.581 -41c150ada0000000 581000000 -4054c92492492492 83.14285714285714 -3fe29fbe76c8b439 0.582 -41c1584ec0000000 582000000 -4054d24924924925 83.28571428571429 -3fe2a7ef9db22d0e 0.583 -41c15fefe0000000 583000000 -4054db6db6db6db7 83.42857142857143 -3fe2b020c49ba5e3 0.584 -41c1679100000000 584000000 -4054e49249249249 83.57142857142857 -3fe2b851eb851eb8 0.585 -41c16f3220000000 585000000 -4054edb6db6db6db 83.71428571428571 -3fe2c083126e978d 0.586 -41c176d340000000 586000000 -4054f6db6db6db6e 83.85714285714286 -3fe2c8b439581062 0.587 -41c17e7460000000 587000000 -4055000000000000 84 -3fe2d0e560418937 0.588 -41c1861580000000 588000000 -4055092492492492 84.14285714285714 -3fe2d916872b020c 0.589 -41c18db6a0000000 589000000 -4055124924924925 84.28571428571429 -3fe2e147ae147ae1 0.59 -41c19557c0000000 590000000 -40551b6db6db6db7 84.42857142857143 -3fe2e978d4fdf3b6 0.591 -41c19cf8e0000000 591000000 -4055249249249249 84.57142857142857 -3fe2f1a9fbe76c8b 0.592 -41c1a49a00000000 592000000 -40552db6db6db6db 84.71428571428571 -3fe2f9db22d0e560 0.593 -41c1ac3b20000000 593000000 -405536db6db6db6e 84.85714285714286 -3fe3020c49ba5e35 0.594 -41c1b3dc40000000 594000000 -4055400000000000 85 -3fe30a3d70a3d70a 0.595 -41c1bb7d60000000 595000000 -4055492492492492 85.14285714285714 -3fe3126e978d4fdf 0.596 -41c1c31e80000000 596000000 -4055524924924925 85.28571428571429 -3fe31a9fbe76c8b4 0.597 -41c1cabfa0000000 597000000 -40555b6db6db6db7 85.42857142857143 -3fe322d0e5604189 0.598 -41c1d260c0000000 598000000 -4055649249249249 85.57142857142857 -3fe32b020c49ba5e 0.599 -41c1da01e0000000 599000000 -40556db6db6db6db 85.71428571428571 -3fe3333333333333 0.6 -41c1e1a300000000 600000000 -405576db6db6db6e 85.85714285714286 -3fe33b645a1cac08 0.601 -41c1e94420000000 601000000 -4055800000000000 86 -3fe34395810624dd 0.602 -41c1f0e540000000 602000000 -4055892492492492 86.14285714285714 -3fe34bc6a7ef9db2 0.603 -41c1f88660000000 603000000 -4055924924924925 86.28571428571429 -3fe353f7ced91687 0.604 -41c2002780000000 604000000 -40559b6db6db6db7 86.42857142857143 -3fe35c28f5c28f5c 0.605 -41c207c8a0000000 605000000 -4055a49249249249 86.57142857142857 -3fe3645a1cac0831 0.606 -41c20f69c0000000 606000000 -4055adb6db6db6db 86.71428571428571 -3fe36c8b43958106 0.607 -41c2170ae0000000 607000000 -4055b6db6db6db6e 86.85714285714286 -3fe374bc6a7ef9db 0.608 -41c21eac00000000 608000000 -4055c00000000000 87 -3fe37ced916872b0 0.609 -41c2264d20000000 609000000 -4055c92492492492 87.14285714285714 -3fe3851eb851eb85 0.61 -41c22dee40000000 610000000 -4055d24924924925 87.28571428571429 -3fe38d4fdf3b645a 0.611 -41c2358f60000000 611000000 -4055db6db6db6db7 87.42857142857143 -3fe395810624dd2f 0.612 -41c23d3080000000 612000000 -4055e49249249249 87.57142857142857 -3fe39db22d0e5604 0.613 -41c244d1a0000000 613000000 -4055edb6db6db6db 87.71428571428571 -3fe3a5e353f7ced9 0.614 -41c24c72c0000000 614000000 -4055f6db6db6db6e 87.85714285714286 -3fe3ae147ae147ae 0.615 -41c25413e0000000 615000000 -4056000000000000 88 -3fe3b645a1cac083 0.616 -41c25bb500000000 616000000 -4056092492492492 88.14285714285714 -3fe3be76c8b43958 0.617 -41c2635620000000 617000000 -4056124924924925 88.28571428571429 -3fe3c6a7ef9db22d 0.618 -41c26af740000000 618000000 -40561b6db6db6db7 88.42857142857143 -3fe3ced916872b02 0.619 -41c2729860000000 619000000 -4056249249249249 88.57142857142857 -3fe3d70a3d70a3d7 0.62 -41c27a3980000000 620000000 -40562db6db6db6db 88.71428571428571 -3fe3df3b645a1cac 0.621 -41c281daa0000000 621000000 -405636db6db6db6e 88.85714285714286 -3fe3e76c8b439581 0.622 -41c2897bc0000000 622000000 -4056400000000000 89 -3fe3ef9db22d0e56 0.623 -41c2911ce0000000 623000000 -4056492492492492 89.14285714285714 -3fe3f7ced916872b 0.624 -41c298be00000000 624000000 -4056524924924925 89.28571428571429 -3fe4000000000000 0.625 -41c2a05f20000000 625000000 -40565b6db6db6db7 89.42857142857143 -3fe4083126e978d5 0.626 -41c2a80040000000 626000000 -4056649249249249 89.57142857142857 -3fe410624dd2f1aa 0.627 -41c2afa160000000 627000000 -40566db6db6db6db 89.71428571428571 -3fe4189374bc6a7f 0.628 -41c2b74280000000 628000000 -405676db6db6db6e 89.85714285714286 -3fe420c49ba5e354 0.629 -41c2bee3a0000000 629000000 -4056800000000000 90 -3fe428f5c28f5c29 0.63 -41c2c684c0000000 630000000 -4056892492492492 90.14285714285714 -3fe43126e978d4fe 0.631 -41c2ce25e0000000 631000000 -4056924924924925 90.28571428571429 -3fe4395810624dd3 0.632 -41c2d5c700000000 632000000 -40569b6db6db6db7 90.42857142857143 -3fe44189374bc6a8 0.633 -41c2dd6820000000 633000000 -4056a49249249249 90.57142857142857 -3fe449ba5e353f7d 0.634 -41c2e50940000000 634000000 -4056adb6db6db6db 90.71428571428571 -3fe451eb851eb852 0.635 -41c2ecaa60000000 635000000 -4056b6db6db6db6e 90.85714285714286 -3fe45a1cac083127 0.636 -41c2f44b80000000 636000000 -4056c00000000000 91 -3fe4624dd2f1a9fc 0.637 -41c2fbeca0000000 637000000 -4056c92492492492 91.14285714285714 -3fe46a7ef9db22d1 0.638 -41c3038dc0000000 638000000 -4056d24924924925 91.28571428571429 -3fe472b020c49ba6 0.639 -41c30b2ee0000000 639000000 -4056db6db6db6db7 91.42857142857143 -3fe47ae147ae147b 0.64 -41c312d000000000 640000000 -4056e49249249249 91.57142857142857 -3fe483126e978d50 0.641 -41c31a7120000000 641000000 -4056edb6db6db6db 91.71428571428571 -3fe48b4395810625 0.642 -41c3221240000000 642000000 -4056f6db6db6db6e 91.85714285714286 -3fe49374bc6a7efa 0.643 -41c329b360000000 643000000 -4057000000000000 92 -3fe49ba5e353f7cf 0.644 -41c3315480000000 644000000 -4057092492492492 92.14285714285714 -3fe4a3d70a3d70a4 0.645 -41c338f5a0000000 645000000 -4057124924924925 92.28571428571429 -3fe4ac083126e979 0.646 -41c34096c0000000 646000000 -40571b6db6db6db7 92.42857142857143 -3fe4b4395810624e 0.647 -41c34837e0000000 647000000 -4057249249249249 92.57142857142857 -3fe4bc6a7ef9db23 0.648 -41c34fd900000000 648000000 -40572db6db6db6db 92.71428571428571 -3fe4c49ba5e353f8 0.649 -41c3577a20000000 649000000 -405736db6db6db6e 92.85714285714286 -3fe4cccccccccccd 0.65 -41c35f1b40000000 650000000 -4057400000000000 93 -3fe4d4fdf3b645a2 0.651 -41c366bc60000000 651000000 -4057492492492492 93.14285714285714 -3fe4dd2f1a9fbe77 0.652 -41c36e5d80000000 652000000 -4057524924924925 93.28571428571429 -3fe4e5604189374c 0.653 -41c375fea0000000 653000000 -40575b6db6db6db7 93.42857142857143 -3fe4ed916872b021 0.654 -41c37d9fc0000000 654000000 -4057649249249249 93.57142857142857 -3fe4f5c28f5c28f6 0.655 -41c38540e0000000 655000000 -40576db6db6db6db 93.71428571428571 -3fe4fdf3b645a1cb 0.656 -41c38ce200000000 656000000 -405776db6db6db6e 93.85714285714286 -3fe50624dd2f1aa0 0.657 -41c3948320000000 657000000 -4057800000000000 94 -3fe50e5604189375 0.658 -41c39c2440000000 658000000 -4057892492492492 94.14285714285714 -3fe516872b020c4a 0.659 -41c3a3c560000000 659000000 -4057924924924925 94.28571428571429 -3fe51eb851eb851f 0.66 -41c3ab6680000000 660000000 -40579b6db6db6db7 94.42857142857143 -3fe526e978d4fdf4 0.661 -41c3b307a0000000 661000000 -4057a49249249249 94.57142857142857 -3fe52f1a9fbe76c9 0.662 -41c3baa8c0000000 662000000 -4057adb6db6db6db 94.71428571428571 -3fe5374bc6a7ef9e 0.663 -41c3c249e0000000 663000000 -4057b6db6db6db6e 94.85714285714286 -3fe53f7ced916873 0.664 -41c3c9eb00000000 664000000 -4057c00000000000 95 -3fe547ae147ae148 0.665 -41c3d18c20000000 665000000 -4057c92492492492 95.14285714285714 -3fe54fdf3b645a1d 0.666 -41c3d92d40000000 666000000 -4057d24924924925 95.28571428571429 -3fe55810624dd2f2 0.667 -41c3e0ce60000000 667000000 -4057db6db6db6db7 95.42857142857143 -3fe5604189374bc7 0.668 -41c3e86f80000000 668000000 -4057e49249249249 95.57142857142857 -3fe56872b020c49c 0.669 -41c3f010a0000000 669000000 -4057edb6db6db6db 95.71428571428571 -3fe570a3d70a3d71 0.67 -41c3f7b1c0000000 670000000 -4057f6db6db6db6e 95.85714285714286 -3fe578d4fdf3b646 0.671 -41c3ff52e0000000 671000000 -4058000000000000 96 -3fe5810624dd2f1b 0.672 -41c406f400000000 672000000 -4058092492492492 96.14285714285714 -3fe589374bc6a7f0 0.673 -41c40e9520000000 673000000 -4058124924924925 96.28571428571429 -3fe5916872b020c5 0.674 -41c4163640000000 674000000 -40581b6db6db6db7 96.42857142857143 -3fe599999999999a 0.675 -41c41dd760000000 675000000 -4058249249249249 96.57142857142857 -3fe5a1cac083126f 0.676 -41c4257880000000 676000000 -40582db6db6db6db 96.71428571428571 -3fe5a9fbe76c8b44 0.677 -41c42d19a0000000 677000000 -405836db6db6db6e 96.85714285714286 -3fe5b22d0e560419 0.678 -41c434bac0000000 678000000 -4058400000000000 97 -3fe5ba5e353f7cee 0.679 -41c43c5be0000000 679000000 -4058492492492492 97.14285714285714 -3fe5c28f5c28f5c3 0.68 -41c443fd00000000 680000000 -4058524924924925 97.28571428571429 -3fe5cac083126e98 0.681 -41c44b9e20000000 681000000 -40585b6db6db6db7 97.42857142857143 -3fe5d2f1a9fbe76d 0.682 -41c4533f40000000 682000000 -4058649249249249 97.57142857142857 -3fe5db22d0e56042 0.683 -41c45ae060000000 683000000 -40586db6db6db6db 97.71428571428571 -3fe5e353f7ced917 0.684 -41c4628180000000 684000000 -405876db6db6db6e 97.85714285714286 -3fe5eb851eb851ec 0.685 -41c46a22a0000000 685000000 -4058800000000000 98 -3fe5f3b645a1cac1 0.686 -41c471c3c0000000 686000000 -4058892492492492 98.14285714285714 -3fe5fbe76c8b4396 0.687 -41c47964e0000000 687000000 -4058924924924925 98.28571428571429 -3fe604189374bc6b 0.6880000000000001 -41c4810600000000 688000000 -40589b6db6db6db7 98.42857142857143 -3fe60c49ba5e3540 0.6890000000000001 -41c488a720000000 689000000 -4058a49249249249 98.57142857142857 -3fe6147ae147ae15 0.6900000000000001 -41c4904840000000 690000000 -4058adb6db6db6db 98.71428571428571 -3fe61cac083126ea 0.6910000000000001 -41c497e960000000 691000000 -4058b6db6db6db6e 98.85714285714286 -3fe624dd2f1a9fbf 0.6920000000000001 -41c49f8a80000000 692000000 -4058c00000000000 99 -3fe62d0e56041894 0.6930000000000001 -41c4a72ba0000000 693000000 -4058c92492492492 99.14285714285714 -3fe6353f7ced9169 0.6940000000000001 -41c4aeccc0000000 694000000 -4058d24924924925 99.28571428571429 -3fe63d70a3d70a3e 0.6950000000000001 -41c4b66de0000000 695000000 -4058db6db6db6db7 99.42857142857143 -3fe645a1cac08313 0.6960000000000001 -41c4be0f00000000 696000000 -4058e49249249249 99.57142857142857 -3fe64dd2f1a9fbe8 0.6970000000000001 -41c4c5b020000000 697000000 -4058edb6db6db6db 99.71428571428571 -3fe65604189374bd 0.6980000000000001 -41c4cd5140000000 698000000 -4058f6db6db6db6e 99.85714285714286 -3fe65e353f7ced92 0.6990000000000001 -41c4d4f260000000 699000000 -4059000000000000 100 -3fe6666666666667 0.7000000000000001 -41c4dc9380000000 700000000 -4059092492492492 100.14285714285714 -3fe66e978d4fdf3c 0.7010000000000001 -41c4e434a0000000 701000000 -4059124924924925 100.28571428571429 -3fe676c8b4395811 0.7020000000000001 -41c4ebd5c0000000 702000000 -40591b6db6db6db7 100.42857142857143 -3fe67ef9db22d0e6 0.7030000000000001 -41c4f376e0000000 703000000 -4059249249249249 100.57142857142857 -3fe6872b020c49ba 0.704 -41c4fb1800000000 704000000 -40592db6db6db6db 100.71428571428571 -3fe68f5c28f5c28f 0.705 -41c502b920000000 705000000 -405936db6db6db6e 100.85714285714286 -3fe6978d4fdf3b64 0.706 -41c50a5a40000000 706000000 -4059400000000000 101 -3fe69fbe76c8b439 0.707 -41c511fb60000000 707000000 -4059492492492492 101.14285714285714 -3fe6a7ef9db22d0e 0.708 -41c5199c80000000 708000000 -4059524924924925 101.28571428571429 -3fe6b020c49ba5e3 0.709 -41c5213da0000000 709000000 -40595b6db6db6db7 101.42857142857143 -3fe6b851eb851eb8 0.71 -41c528dec0000000 710000000 -4059649249249249 101.57142857142857 -3fe6c083126e978d 0.711 -41c5307fe0000000 711000000 -40596db6db6db6db 101.71428571428571 -3fe6c8b439581062 0.712 -41c5382100000000 712000000 -405976db6db6db6e 101.85714285714286 -3fe6d0e560418937 0.713 -41c53fc220000000 713000000 -4059800000000000 102 -3fe6d916872b020c 0.714 -41c5476340000000 714000000 -4059892492492492 102.14285714285714 -3fe6e147ae147ae1 0.715 -41c54f0460000000 715000000 -4059924924924925 102.28571428571429 -3fe6e978d4fdf3b6 0.716 -41c556a580000000 716000000 -40599b6db6db6db7 102.42857142857143 -3fe6f1a9fbe76c8b 0.717 -41c55e46a0000000 717000000 -4059a49249249249 102.57142857142857 -3fe6f9db22d0e560 0.718 -41c565e7c0000000 718000000 -4059adb6db6db6db 102.71428571428571 -3fe7020c49ba5e35 0.719 -41c56d88e0000000 719000000 -4059b6db6db6db6e 102.85714285714286 -3fe70a3d70a3d70a 0.72 -41c5752a00000000 720000000 -4059c00000000000 103 -3fe7126e978d4fdf 0.721 -41c57ccb20000000 721000000 -4059c92492492492 103.14285714285714 -3fe71a9fbe76c8b4 0.722 -41c5846c40000000 722000000 -4059d24924924925 103.28571428571429 -3fe722d0e5604189 0.723 -41c58c0d60000000 723000000 -4059db6db6db6db7 103.42857142857143 -3fe72b020c49ba5e 0.724 -41c593ae80000000 724000000 -4059e49249249249 103.57142857142857 -3fe7333333333333 0.725 -41c59b4fa0000000 725000000 -4059edb6db6db6db 103.71428571428571 -3fe73b645a1cac08 0.726 -41c5a2f0c0000000 726000000 -4059f6db6db6db6e 103.85714285714286 -3fe74395810624dd 0.727 -41c5aa91e0000000 727000000 -405a000000000000 104 -3fe74bc6a7ef9db2 0.728 -41c5b23300000000 728000000 -405a092492492492 104.14285714285714 -3fe753f7ced91687 0.729 -41c5b9d420000000 729000000 -405a124924924925 104.28571428571429 -3fe75c28f5c28f5c 0.73 -41c5c17540000000 730000000 -405a1b6db6db6db7 104.42857142857143 -3fe7645a1cac0831 0.731 -41c5c91660000000 731000000 -405a249249249249 104.57142857142857 -3fe76c8b43958106 0.732 -41c5d0b780000000 732000000 -405a2db6db6db6db 104.71428571428571 -3fe774bc6a7ef9db 0.733 -41c5d858a0000000 733000000 -405a36db6db6db6e 104.85714285714286 -3fe77ced916872b0 0.734 -41c5dff9c0000000 734000000 -405a400000000000 105 -3fe7851eb851eb85 0.735 -41c5e79ae0000000 735000000 -405a492492492492 105.14285714285714 -3fe78d4fdf3b645a 0.736 -41c5ef3c00000000 736000000 -405a524924924925 105.28571428571429 -3fe795810624dd2f 0.737 -41c5f6dd20000000 737000000 -405a5b6db6db6db7 105.42857142857143 -3fe79db22d0e5604 0.738 -41c5fe7e40000000 738000000 -405a649249249249 105.57142857142857 -3fe7a5e353f7ced9 0.739 -41c6061f60000000 739000000 -405a6db6db6db6db 105.71428571428571 -3fe7ae147ae147ae 0.74 -41c60dc080000000 740000000 -405a76db6db6db6e 105.85714285714286 -3fe7b645a1cac083 0.741 -41c61561a0000000 741000000 -405a800000000000 106 -3fe7be76c8b43958 0.742 -41c61d02c0000000 742000000 -405a892492492492 106.14285714285714 -3fe7c6a7ef9db22d 0.743 -41c624a3e0000000 743000000 -405a924924924925 106.28571428571429 -3fe7ced916872b02 0.744 -41c62c4500000000 744000000 -405a9b6db6db6db7 106.42857142857143 -3fe7d70a3d70a3d7 0.745 -41c633e620000000 745000000 -405aa49249249249 106.57142857142857 -3fe7df3b645a1cac 0.746 -41c63b8740000000 746000000 -405aadb6db6db6db 106.71428571428571 -3fe7e76c8b439581 0.747 -41c6432860000000 747000000 -405ab6db6db6db6e 106.85714285714286 -3fe7ef9db22d0e56 0.748 -41c64ac980000000 748000000 -405ac00000000000 107 -3fe7f7ced916872b 0.749 -41c6526aa0000000 749000000 -405ac92492492492 107.14285714285714 -3fe8000000000000 0.75 -41c65a0bc0000000 750000000 -405ad24924924925 107.28571428571429 -3fe8083126e978d5 0.751 -41c661ace0000000 751000000 -405adb6db6db6db7 107.42857142857143 -3fe810624dd2f1aa 0.752 -41c6694e00000000 752000000 -405ae49249249249 107.57142857142857 -3fe8189374bc6a7f 0.753 -41c670ef20000000 753000000 -405aedb6db6db6db 107.71428571428571 -3fe820c49ba5e354 0.754 -41c6789040000000 754000000 -405af6db6db6db6e 107.85714285714286 -3fe828f5c28f5c29 0.755 -41c6803160000000 755000000 -405b000000000000 108 -3fe83126e978d4fe 0.756 -41c687d280000000 756000000 -405b092492492492 108.14285714285714 -3fe8395810624dd3 0.757 -41c68f73a0000000 757000000 -405b124924924925 108.28571428571429 -3fe84189374bc6a8 0.758 -41c69714c0000000 758000000 -405b1b6db6db6db7 108.42857142857143 -3fe849ba5e353f7d 0.759 -41c69eb5e0000000 759000000 -405b249249249249 108.57142857142857 -3fe851eb851eb852 0.76 -41c6a65700000000 760000000 -405b2db6db6db6db 108.71428571428571 -3fe85a1cac083127 0.761 -41c6adf820000000 761000000 -405b36db6db6db6e 108.85714285714286 -3fe8624dd2f1a9fc 0.762 -41c6b59940000000 762000000 -405b400000000000 109 -3fe86a7ef9db22d1 0.763 -41c6bd3a60000000 763000000 -405b492492492492 109.14285714285714 -3fe872b020c49ba6 0.764 -41c6c4db80000000 764000000 -405b524924924925 109.28571428571429 -3fe87ae147ae147b 0.765 -41c6cc7ca0000000 765000000 -405b5b6db6db6db7 109.42857142857143 -3fe883126e978d50 0.766 -41c6d41dc0000000 766000000 -405b649249249249 109.57142857142857 -3fe88b4395810625 0.767 -41c6dbbee0000000 767000000 -405b6db6db6db6db 109.71428571428571 -3fe89374bc6a7efa 0.768 -41c6e36000000000 768000000 -405b76db6db6db6e 109.85714285714286 -3fe89ba5e353f7cf 0.769 -41c6eb0120000000 769000000 -405b800000000000 110 -3fe8a3d70a3d70a4 0.77 -41c6f2a240000000 770000000 -405b892492492492 110.14285714285714 -3fe8ac083126e979 0.771 -41c6fa4360000000 771000000 -405b924924924925 110.28571428571429 -3fe8b4395810624e 0.772 -41c701e480000000 772000000 -405b9b6db6db6db7 110.42857142857143 -3fe8bc6a7ef9db23 0.773 -41c70985a0000000 773000000 -405ba49249249249 110.57142857142857 -3fe8c49ba5e353f8 0.774 -41c71126c0000000 774000000 -405badb6db6db6db 110.71428571428571 -3fe8cccccccccccd 0.775 -41c718c7e0000000 775000000 -405bb6db6db6db6e 110.85714285714286 -3fe8d4fdf3b645a2 0.776 -41c7206900000000 776000000 -405bc00000000000 111 -3fe8dd2f1a9fbe77 0.777 -41c7280a20000000 777000000 -405bc92492492492 111.14285714285714 -3fe8e5604189374c 0.778 -41c72fab40000000 778000000 -405bd24924924925 111.28571428571429 -3fe8ed916872b021 0.779 -41c7374c60000000 779000000 -405bdb6db6db6db7 111.42857142857143 -3fe8f5c28f5c28f6 0.78 -41c73eed80000000 780000000 -405be49249249249 111.57142857142857 -3fe8fdf3b645a1cb 0.781 -41c7468ea0000000 781000000 -405bedb6db6db6db 111.71428571428571 -3fe90624dd2f1aa0 0.782 -41c74e2fc0000000 782000000 -405bf6db6db6db6e 111.85714285714286 -3fe90e5604189375 0.783 -41c755d0e0000000 783000000 -405c000000000000 112 -3fe916872b020c4a 0.784 -41c75d7200000000 784000000 -405c092492492492 112.14285714285714 -3fe91eb851eb851f 0.785 -41c7651320000000 785000000 -405c124924924925 112.28571428571429 -3fe926e978d4fdf4 0.786 -41c76cb440000000 786000000 -405c1b6db6db6db7 112.42857142857143 -3fe92f1a9fbe76c9 0.787 -41c7745560000000 787000000 -405c249249249249 112.57142857142857 -3fe9374bc6a7ef9e 0.788 -41c77bf680000000 788000000 -405c2db6db6db6db 112.71428571428571 -3fe93f7ced916873 0.789 -41c78397a0000000 789000000 -405c36db6db6db6e 112.85714285714286 -3fe947ae147ae148 0.79 -41c78b38c0000000 790000000 -405c400000000000 113 -3fe94fdf3b645a1d 0.791 -41c792d9e0000000 791000000 -405c492492492492 113.14285714285714 -3fe95810624dd2f2 0.792 -41c79a7b00000000 792000000 -405c524924924925 113.28571428571429 -3fe9604189374bc7 0.793 -41c7a21c20000000 793000000 -405c5b6db6db6db7 113.42857142857143 -3fe96872b020c49c 0.794 -41c7a9bd40000000 794000000 -405c649249249249 113.57142857142857 -3fe970a3d70a3d71 0.795 -41c7b15e60000000 795000000 -405c6db6db6db6db 113.71428571428571 -3fe978d4fdf3b646 0.796 -41c7b8ff80000000 796000000 -405c76db6db6db6e 113.85714285714286 -3fe9810624dd2f1b 0.797 -41c7c0a0a0000000 797000000 -405c800000000000 114 -3fe989374bc6a7f0 0.798 -41c7c841c0000000 798000000 -405c892492492492 114.14285714285714 -3fe9916872b020c5 0.799 -41c7cfe2e0000000 799000000 -405c924924924925 114.28571428571429 -3fe999999999999a 0.8 -41c7d78400000000 800000000 -405c9b6db6db6db7 114.42857142857143 -3fe9a1cac083126f 0.801 -41c7df2520000000 801000000 -405ca49249249249 114.57142857142857 -3fe9a9fbe76c8b44 0.802 -41c7e6c640000000 802000000 -405cadb6db6db6db 114.71428571428571 -3fe9b22d0e560419 0.803 -41c7ee6760000000 803000000 -405cb6db6db6db6e 114.85714285714286 -3fe9ba5e353f7cee 0.804 -41c7f60880000000 804000000 -405cc00000000000 115 -3fe9c28f5c28f5c3 0.805 -41c7fda9a0000000 805000000 -405cc92492492492 115.14285714285714 -3fe9cac083126e98 0.806 -41c8054ac0000000 806000000 -405cd24924924925 115.28571428571429 -3fe9d2f1a9fbe76d 0.807 -41c80cebe0000000 807000000 -405cdb6db6db6db7 115.42857142857143 -3fe9db22d0e56042 0.808 -41c8148d00000000 808000000 -405ce49249249249 115.57142857142857 -3fe9e353f7ced917 0.809 -41c81c2e20000000 809000000 -405cedb6db6db6db 115.71428571428571 -3fe9eb851eb851ec 0.81 -41c823cf40000000 810000000 -405cf6db6db6db6e 115.85714285714286 -3fe9f3b645a1cac1 0.811 -41c82b7060000000 811000000 -405d000000000000 116 -3fe9fbe76c8b4396 0.812 -41c8331180000000 812000000 -405d092492492492 116.14285714285714 -3fea04189374bc6b 0.8130000000000001 -41c83ab2a0000000 813000000 -405d124924924925 116.28571428571429 -3fea0c49ba5e3540 0.8140000000000001 -41c84253c0000000 814000000 -405d1b6db6db6db7 116.42857142857143 -3fea147ae147ae15 0.8150000000000001 -41c849f4e0000000 815000000 -405d249249249249 116.57142857142857 -3fea1cac083126ea 0.8160000000000001 -41c8519600000000 816000000 -405d2db6db6db6db 116.71428571428571 -3fea24dd2f1a9fbf 0.8170000000000001 -41c8593720000000 817000000 -405d36db6db6db6e 116.85714285714286 -3fea2d0e56041894 0.8180000000000001 -41c860d840000000 818000000 -405d400000000000 117 -3fea353f7ced9169 0.8190000000000001 -41c8687960000000 819000000 -405d492492492492 117.14285714285714 -3fea3d70a3d70a3e 0.8200000000000001 -41c8701a80000000 820000000 -405d524924924925 117.28571428571429 -3fea45a1cac08313 0.8210000000000001 -41c877bba0000000 821000000 -405d5b6db6db6db7 117.42857142857143 -3fea4dd2f1a9fbe8 0.8220000000000001 -41c87f5cc0000000 822000000 -405d649249249249 117.57142857142857 -3fea5604189374bd 0.8230000000000001 -41c886fde0000000 823000000 -405d6db6db6db6db 117.71428571428571 -3fea5e353f7ced92 0.8240000000000001 -41c88e9f00000000 824000000 -405d76db6db6db6e 117.85714285714286 -3fea666666666667 0.8250000000000001 -41c8964020000000 825000000 -405d800000000000 118 -3fea6e978d4fdf3c 0.8260000000000001 -41c89de140000000 826000000 -405d892492492492 118.14285714285714 -3fea76c8b4395811 0.8270000000000001 -41c8a58260000000 827000000 -405d924924924925 118.28571428571429 -3fea7ef9db22d0e6 0.8280000000000001 -41c8ad2380000000 828000000 -405d9b6db6db6db7 118.42857142857143 -3fea872b020c49bb 0.8290000000000001 -41c8b4c4a0000000 829000000 -405da49249249249 118.57142857142857 -3fea8f5c28f5c290 0.8300000000000001 -41c8bc65c0000000 830000000 -405dadb6db6db6db 118.71428571428571 -3fea978d4fdf3b65 0.8310000000000001 -41c8c406e0000000 831000000 -405db6db6db6db6e 118.85714285714286 -3fea9fbe76c8b43a 0.8320000000000001 -41c8cba800000000 832000000 -405dc00000000000 119 -3feaa7ef9db22d0e 0.833 -41c8d34920000000 833000000 -405dc92492492492 119.14285714285714 -3feab020c49ba5e3 0.834 -41c8daea40000000 834000000 -405dd24924924925 119.28571428571429 -3feab851eb851eb8 0.835 -41c8e28b60000000 835000000 -405ddb6db6db6db7 119.42857142857143 -3feac083126e978d 0.836 -41c8ea2c80000000 836000000 -405de49249249249 119.57142857142857 -3feac8b439581062 0.837 -41c8f1cda0000000 837000000 -405dedb6db6db6db 119.71428571428571 -3fead0e560418937 0.838 -41c8f96ec0000000 838000000 -405df6db6db6db6e 119.85714285714286 -3fead916872b020c 0.839 -41c9010fe0000000 839000000 -405e000000000000 120 -3feae147ae147ae1 0.84 -41c908b100000000 840000000 -405e092492492492 120.14285714285714 -3feae978d4fdf3b6 0.841 -41c9105220000000 841000000 -405e124924924925 120.28571428571429 -3feaf1a9fbe76c8b 0.842 -41c917f340000000 842000000 -405e1b6db6db6db7 120.42857142857143 -3feaf9db22d0e560 0.843 -41c91f9460000000 843000000 -405e249249249249 120.57142857142857 -3feb020c49ba5e35 0.844 -41c9273580000000 844000000 -405e2db6db6db6db 120.71428571428571 -3feb0a3d70a3d70a 0.845 -41c92ed6a0000000 845000000 -405e36db6db6db6e 120.85714285714286 -3feb126e978d4fdf 0.846 -41c93677c0000000 846000000 -405e400000000000 121 -3feb1a9fbe76c8b4 0.847 -41c93e18e0000000 847000000 -405e492492492492 121.14285714285714 -3feb22d0e5604189 0.848 -41c945ba00000000 848000000 -405e524924924925 121.28571428571429 -3feb2b020c49ba5e 0.849 -41c94d5b20000000 849000000 -405e5b6db6db6db7 121.42857142857143 -3feb333333333333 0.85 -41c954fc40000000 850000000 -405e649249249249 121.57142857142857 -3feb3b645a1cac08 0.851 -41c95c9d60000000 851000000 -405e6db6db6db6db 121.71428571428571 -3feb4395810624dd 0.852 -41c9643e80000000 852000000 -405e76db6db6db6e 121.85714285714286 -3feb4bc6a7ef9db2 0.853 -41c96bdfa0000000 853000000 -405e800000000000 122 -3feb53f7ced91687 0.854 -41c97380c0000000 854000000 -405e892492492492 122.14285714285714 -3feb5c28f5c28f5c 0.855 -41c97b21e0000000 855000000 -405e924924924925 122.28571428571429 -3feb645a1cac0831 0.856 -41c982c300000000 856000000 -405e9b6db6db6db7 122.42857142857143 -3feb6c8b43958106 0.857 -41c98a6420000000 857000000 -405ea49249249249 122.57142857142857 -3feb74bc6a7ef9db 0.858 -41c9920540000000 858000000 -405eadb6db6db6db 122.71428571428571 -3feb7ced916872b0 0.859 -41c999a660000000 859000000 -405eb6db6db6db6e 122.85714285714286 -3feb851eb851eb85 0.86 -41c9a14780000000 860000000 -405ec00000000000 123 -3feb8d4fdf3b645a 0.861 -41c9a8e8a0000000 861000000 -405ec92492492492 123.14285714285714 -3feb95810624dd2f 0.862 -41c9b089c0000000 862000000 -405ed24924924925 123.28571428571429 -3feb9db22d0e5604 0.863 -41c9b82ae0000000 863000000 -405edb6db6db6db7 123.42857142857143 -3feba5e353f7ced9 0.864 -41c9bfcc00000000 864000000 -405ee49249249249 123.57142857142857 -3febae147ae147ae 0.865 -41c9c76d20000000 865000000 -405eedb6db6db6db 123.71428571428571 -3febb645a1cac083 0.866 -41c9cf0e40000000 866000000 -405ef6db6db6db6e 123.85714285714286 -3febbe76c8b43958 0.867 -41c9d6af60000000 867000000 -405f000000000000 124 -3febc6a7ef9db22d 0.868 -41c9de5080000000 868000000 -405f092492492492 124.14285714285714 -3febced916872b02 0.869 -41c9e5f1a0000000 869000000 -405f124924924925 124.28571428571429 -3febd70a3d70a3d7 0.87 -41c9ed92c0000000 870000000 -405f1b6db6db6db7 124.42857142857143 -3febdf3b645a1cac 0.871 -41c9f533e0000000 871000000 -405f249249249249 124.57142857142857 -3febe76c8b439581 0.872 -41c9fcd500000000 872000000 -405f2db6db6db6db 124.71428571428571 -3febef9db22d0e56 0.873 -41ca047620000000 873000000 -405f36db6db6db6e 124.85714285714286 -3febf7ced916872b 0.874 -41ca0c1740000000 874000000 -405f400000000000 125 -3fec000000000000 0.875 -41ca13b860000000 875000000 -405f492492492492 125.14285714285714 -3fec083126e978d5 0.876 -41ca1b5980000000 876000000 -405f524924924925 125.28571428571429 -3fec10624dd2f1aa 0.877 -41ca22faa0000000 877000000 -405f5b6db6db6db7 125.42857142857143 -3fec189374bc6a7f 0.878 -41ca2a9bc0000000 878000000 -405f649249249249 125.57142857142857 -3fec20c49ba5e354 0.879 -41ca323ce0000000 879000000 -405f6db6db6db6db 125.71428571428571 -3fec28f5c28f5c29 0.88 -41ca39de00000000 880000000 -405f76db6db6db6e 125.85714285714286 -3fec3126e978d4fe 0.881 -41ca417f20000000 881000000 -405f800000000000 126 -3fec395810624dd3 0.882 -41ca492040000000 882000000 -405f892492492492 126.14285714285714 -3fec4189374bc6a8 0.883 -41ca50c160000000 883000000 -405f924924924925 126.28571428571429 -3fec49ba5e353f7d 0.884 -41ca586280000000 884000000 -405f9b6db6db6db7 126.42857142857143 -3fec51eb851eb852 0.885 -41ca6003a0000000 885000000 -405fa49249249249 126.57142857142857 -3fec5a1cac083127 0.886 -41ca67a4c0000000 886000000 -405fadb6db6db6db 126.71428571428571 -3fec624dd2f1a9fc 0.887 -41ca6f45e0000000 887000000 -405fb6db6db6db6e 126.85714285714286 -3fec6a7ef9db22d1 0.888 -41ca76e700000000 888000000 -405fc00000000000 127 -3fec72b020c49ba6 0.889 -41ca7e8820000000 889000000 -405fc92492492492 127.14285714285714 -3fec7ae147ae147b 0.89 -41ca862940000000 890000000 -405fd24924924925 127.28571428571429 -3fec83126e978d50 0.891 -41ca8dca60000000 891000000 -405fdb6db6db6db7 127.42857142857143 -3fec8b4395810625 0.892 -41ca956b80000000 892000000 -405fe49249249249 127.57142857142857 -3fec9374bc6a7efa 0.893 -41ca9d0ca0000000 893000000 -405fedb6db6db6db 127.71428571428571 -3fec9ba5e353f7cf 0.894 -41caa4adc0000000 894000000 -405ff6db6db6db6e 127.85714285714286 -3feca3d70a3d70a4 0.895 -41caac4ee0000000 895000000 -4060000000000000 128 -3fecac083126e979 0.896 -41cab3f000000000 896000000 -4060049249249249 128.14285714285714 -3fecb4395810624e 0.897 -41cabb9120000000 897000000 -4060092492492492 128.28571428571428 -3fecbc6a7ef9db23 0.898 -41cac33240000000 898000000 -40600db6db6db6db 128.42857142857142 -3fecc49ba5e353f8 0.899 -41cacad360000000 899000000 -4060124924924925 128.57142857142858 -3feccccccccccccd 0.9 -41cad27480000000 900000000 -406016db6db6db6e 128.71428571428572 -3fecd4fdf3b645a2 0.901 -41cada15a0000000 901000000 -40601b6db6db6db7 128.85714285714286 -3fecdd2f1a9fbe77 0.902 -41cae1b6c0000000 902000000 -4060200000000000 129 -3fece5604189374c 0.903 -41cae957e0000000 903000000 -4060249249249249 129.14285714285714 -3feced916872b021 0.904 -41caf0f900000000 904000000 -4060292492492492 129.28571428571428 -3fecf5c28f5c28f6 0.905 -41caf89a20000000 905000000 -40602db6db6db6db 129.42857142857142 -3fecfdf3b645a1cb 0.906 -41cb003b40000000 906000000 -4060324924924925 129.57142857142858 -3fed0624dd2f1aa0 0.907 -41cb07dc60000000 907000000 -406036db6db6db6e 129.71428571428572 -3fed0e5604189375 0.908 -41cb0f7d80000000 908000000 -40603b6db6db6db7 129.85714285714286 -3fed16872b020c4a 0.909 -41cb171ea0000000 909000000 -4060400000000000 130 -3fed1eb851eb851f 0.91 -41cb1ebfc0000000 910000000 -4060449249249249 130.14285714285714 -3fed26e978d4fdf4 0.911 -41cb2660e0000000 911000000 -4060492492492492 130.28571428571428 -3fed2f1a9fbe76c9 0.912 -41cb2e0200000000 912000000 -40604db6db6db6db 130.42857142857142 -3fed374bc6a7ef9e 0.913 -41cb35a320000000 913000000 -4060524924924925 130.57142857142858 -3fed3f7ced916873 0.914 -41cb3d4440000000 914000000 -406056db6db6db6e 130.71428571428572 -3fed47ae147ae148 0.915 -41cb44e560000000 915000000 -40605b6db6db6db7 130.85714285714286 -3fed4fdf3b645a1d 0.916 -41cb4c8680000000 916000000 -4060600000000000 131 -3fed5810624dd2f2 0.917 -41cb5427a0000000 917000000 -4060649249249249 131.14285714285714 -3fed604189374bc7 0.918 -41cb5bc8c0000000 918000000 -4060692492492492 131.28571428571428 -3fed6872b020c49c 0.919 -41cb6369e0000000 919000000 -40606db6db6db6db 131.42857142857142 -3fed70a3d70a3d71 0.92 -41cb6b0b00000000 920000000 -4060724924924925 131.57142857142858 -3fed78d4fdf3b646 0.921 -41cb72ac20000000 921000000 -406076db6db6db6e 131.71428571428572 -3fed810624dd2f1b 0.922 -41cb7a4d40000000 922000000 -40607b6db6db6db7 131.85714285714286 -3fed89374bc6a7f0 0.923 -41cb81ee60000000 923000000 -4060800000000000 132 -3fed916872b020c5 0.924 -41cb898f80000000 924000000 -4060849249249249 132.14285714285714 -3fed99999999999a 0.925 -41cb9130a0000000 925000000 -4060892492492492 132.28571428571428 -3feda1cac083126f 0.926 -41cb98d1c0000000 926000000 -40608db6db6db6db 132.42857142857142 -3feda9fbe76c8b44 0.927 -41cba072e0000000 927000000 -4060924924924925 132.57142857142858 -3fedb22d0e560419 0.928 -41cba81400000000 928000000 -406096db6db6db6e 132.71428571428572 -3fedba5e353f7cee 0.929 -41cbafb520000000 929000000 -40609b6db6db6db7 132.85714285714286 -3fedc28f5c28f5c3 0.93 -41cbb75640000000 930000000 -4060a00000000000 133 -3fedcac083126e98 0.931 -41cbbef760000000 931000000 -4060a49249249249 133.14285714285714 -3fedd2f1a9fbe76d 0.932 -41cbc69880000000 932000000 -4060a92492492492 133.28571428571428 -3feddb22d0e56042 0.933 -41cbce39a0000000 933000000 -4060adb6db6db6db 133.42857142857142 -3fede353f7ced917 0.934 -41cbd5dac0000000 934000000 -4060b24924924925 133.57142857142858 -3fedeb851eb851ec 0.935 -41cbdd7be0000000 935000000 -4060b6db6db6db6e 133.71428571428572 -3fedf3b645a1cac1 0.936 -41cbe51d00000000 936000000 -4060bb6db6db6db7 133.85714285714286 -3fedfbe76c8b4396 0.937 -41cbecbe20000000 937000000 -4060c00000000000 134 -3fee04189374bc6b 0.9380000000000001 -41cbf45f40000000 938000000 -4060c49249249249 134.14285714285714 -3fee0c49ba5e3540 0.9390000000000001 -41cbfc0060000000 939000000 -4060c92492492492 134.28571428571428 -3fee147ae147ae15 0.9400000000000001 -41cc03a180000000 940000000 -4060cdb6db6db6db 134.42857142857142 -3fee1cac083126ea 0.9410000000000001 -41cc0b42a0000000 941000000 -4060d24924924925 134.57142857142858 -3fee24dd2f1a9fbf 0.9420000000000001 -41cc12e3c0000000 942000000 -4060d6db6db6db6e 134.71428571428572 -3fee2d0e56041894 0.9430000000000001 -41cc1a84e0000000 943000000 -4060db6db6db6db7 134.85714285714286 -3fee353f7ced9169 0.9440000000000001 -41cc222600000000 944000000 -4060e00000000000 135 -3fee3d70a3d70a3e 0.9450000000000001 -41cc29c720000000 945000000 -4060e49249249249 135.14285714285714 -3fee45a1cac08313 0.9460000000000001 -41cc316840000000 946000000 -4060e92492492492 135.28571428571428 -3fee4dd2f1a9fbe8 0.9470000000000001 -41cc390960000000 947000000 -4060edb6db6db6db 135.42857142857142 -3fee5604189374bd 0.9480000000000001 -41cc40aa80000000 948000000 -4060f24924924925 135.57142857142858 -3fee5e353f7ced92 0.9490000000000001 -41cc484ba0000000 949000000 -4060f6db6db6db6e 135.71428571428572 -3fee666666666667 0.9500000000000001 -41cc4fecc0000000 950000000 -4060fb6db6db6db7 135.85714285714286 -3fee6e978d4fdf3c 0.9510000000000001 -41cc578de0000000 951000000 -4061000000000000 136 -3fee76c8b4395811 0.9520000000000001 -41cc5f2f00000000 952000000 -4061049249249249 136.14285714285714 -3fee7ef9db22d0e6 0.9530000000000001 -41cc66d020000000 953000000 -4061092492492492 136.28571428571428 -3fee872b020c49bb 0.9540000000000001 -41cc6e7140000000 954000000 -40610db6db6db6db 136.42857142857142 -3fee8f5c28f5c290 0.9550000000000001 -41cc761260000000 955000000 -4061124924924925 136.57142857142858 -3fee978d4fdf3b65 0.9560000000000001 -41cc7db380000000 956000000 -406116db6db6db6e 136.71428571428572 -3fee9fbe76c8b43a 0.9570000000000001 -41cc8554a0000000 957000000 -40611b6db6db6db7 136.85714285714286 -3feea7ef9db22d0f 0.9580000000000001 -41cc8cf5c0000000 958000000 -4061200000000000 137 -3feeb020c49ba5e4 0.9590000000000001 -41cc9496e0000000 959000000 -4061249249249249 137.14285714285714 -3feeb851eb851eb8 0.96 -41cc9c3800000000 960000000 -4061292492492492 137.28571428571428 -3feec083126e978d 0.961 -41cca3d920000000 961000000 -40612db6db6db6db 137.42857142857142 -3feec8b439581062 0.962 -41ccab7a40000000 962000000 -4061324924924925 137.57142857142858 -3feed0e560418937 0.963 -41ccb31b60000000 963000000 -406136db6db6db6e 137.71428571428572 -3feed916872b020c 0.964 -41ccbabc80000000 964000000 -40613b6db6db6db7 137.85714285714286 -3feee147ae147ae1 0.965 -41ccc25da0000000 965000000 -4061400000000000 138 -3feee978d4fdf3b6 0.966 -41ccc9fec0000000 966000000 -4061449249249249 138.14285714285714 -3feef1a9fbe76c8b 0.967 -41ccd19fe0000000 967000000 -4061492492492492 138.28571428571428 -3feef9db22d0e560 0.968 -41ccd94100000000 968000000 -40614db6db6db6db 138.42857142857142 -3fef020c49ba5e35 0.969 -41cce0e220000000 969000000 -4061524924924925 138.57142857142858 -3fef0a3d70a3d70a 0.97 -41cce88340000000 970000000 -406156db6db6db6e 138.71428571428572 -3fef126e978d4fdf 0.971 -41ccf02460000000 971000000 -40615b6db6db6db7 138.85714285714286 -3fef1a9fbe76c8b4 0.972 -41ccf7c580000000 972000000 -4061600000000000 139 -3fef22d0e5604189 0.973 -41ccff66a0000000 973000000 -4061649249249249 139.14285714285714 -3fef2b020c49ba5e 0.974 -41cd0707c0000000 974000000 -4061692492492492 139.28571428571428 -3fef333333333333 0.975 -41cd0ea8e0000000 975000000 -40616db6db6db6db 139.42857142857142 -3fef3b645a1cac08 0.976 -41cd164a00000000 976000000 -4061724924924925 139.57142857142858 -3fef4395810624dd 0.977 -41cd1deb20000000 977000000 -406176db6db6db6e 139.71428571428572 -3fef4bc6a7ef9db2 0.978 -41cd258c40000000 978000000 -40617b6db6db6db7 139.85714285714286 -3fef53f7ced91687 0.979 -41cd2d2d60000000 979000000 -4061800000000000 140 -3fef5c28f5c28f5c 0.98 -41cd34ce80000000 980000000 -4061849249249249 140.14285714285714 -3fef645a1cac0831 0.981 -41cd3c6fa0000000 981000000 -4061892492492492 140.28571428571428 -3fef6c8b43958106 0.982 -41cd4410c0000000 982000000 -40618db6db6db6db 140.42857142857142 -3fef74bc6a7ef9db 0.983 -41cd4bb1e0000000 983000000 -4061924924924925 140.57142857142858 -3fef7ced916872b0 0.984 -41cd535300000000 984000000 -406196db6db6db6e 140.71428571428572 -3fef851eb851eb85 0.985 -41cd5af420000000 985000000 -40619b6db6db6db7 140.85714285714286 -3fef8d4fdf3b645a 0.986 -41cd629540000000 986000000 -4061a00000000000 141 -3fef95810624dd2f 0.987 -41cd6a3660000000 987000000 -4061a49249249249 141.14285714285714 -3fef9db22d0e5604 0.988 -41cd71d780000000 988000000 -4061a92492492492 141.28571428571428 -3fefa5e353f7ced9 0.989 -41cd7978a0000000 989000000 -4061adb6db6db6db 141.42857142857142 -3fefae147ae147ae 0.99 -41cd8119c0000000 990000000 -4061b24924924925 141.57142857142858 -3fefb645a1cac083 0.991 -41cd88bae0000000 991000000 -4061b6db6db6db6e 141.71428571428572 -3fefbe76c8b43958 0.992 -41cd905c00000000 992000000 -4061bb6db6db6db7 141.85714285714286 -3fefc6a7ef9db22d 0.993 -41cd97fd20000000 993000000 -4061c00000000000 142 -3fefced916872b02 0.994 -41cd9f9e40000000 994000000 -4061c49249249249 142.14285714285714 -3fefd70a3d70a3d7 0.995 -41cda73f60000000 995000000 -4061c92492492492 142.28571428571428 -3fefdf3b645a1cac 0.996 -41cdaee080000000 996000000 -4061cdb6db6db6db 142.42857142857142 -3fefe76c8b439581 0.997 -41cdb681a0000000 997000000 -4061d24924924925 142.57142857142858 -3fefef9db22d0e56 0.998 -41cdbe22c0000000 998000000 -4061d6db6db6db6e 142.71428571428572 -3feff7ced916872b 0.999 -41cdc5c3e0000000 999000000 -4061db6db6db6db7 142.85714285714286 -3ff0000000000000 1 -41cdcd6500000000 1000000000 -4061e00000000000 143 -3ff004189374bc6b 1.0010000000000001 -41cdd50620000000 1001000000 -4061e49249249249 143.14285714285714 -3ff0083126e978d5 1.002 -41cddca740000000 1002000000 -4061e92492492492 143.28571428571428 -3ff00c49ba5e3540 1.0030000000000001 -41cde44860000000 1003000000 -4061edb6db6db6db 143.42857142857142 -3ff010624dd2f1aa 1.004 -41cdebe980000000 1004000000 -4061f24924924925 143.57142857142858 -3ff0147ae147ae15 1.0050000000000001 -41cdf38aa0000000 1005000000 -4061f6db6db6db6e 143.71428571428572 -3ff0189374bc6a7f 1.006 -41cdfb2bc0000000 1006000000 -4061fb6db6db6db7 143.85714285714286 -3ff01cac083126ea 1.0070000000000001 -41ce02cce0000000 1007000000 -4062000000000000 144 -3ff020c49ba5e354 1.008 -41ce0a6e00000000 1008000000 -4062049249249249 144.14285714285714 -3ff024dd2f1a9fbf 1.0090000000000001 -41ce120f20000000 1009000000 -4062092492492492 144.28571428571428 -3ff028f5c28f5c29 1.01 -41ce19b040000000 1010000000 -40620db6db6db6db 144.42857142857142 -3ff02d0e56041894 1.0110000000000001 -41ce215160000000 1011000000 -4062124924924925 144.57142857142858 -3ff03126e978d4fe 1.012 -41ce28f280000000 1012000000 -406216db6db6db6e 144.71428571428572 -3ff0353f7ced9169 1.0130000000000001 -41ce3093a0000000 1013000000 -40621b6db6db6db7 144.85714285714286 -3ff0395810624dd3 1.014 -41ce3834c0000000 1014000000 -4062200000000000 145 -3ff03d70a3d70a3e 1.0150000000000001 -41ce3fd5e0000000 1015000000 -4062249249249249 145.14285714285714 -3ff04189374bc6a8 1.016 -41ce477700000000 1016000000 -4062292492492492 145.28571428571428 -3ff045a1cac08313 1.0170000000000001 -41ce4f1820000000 1017000000 -40622db6db6db6db 145.42857142857142 -3ff049ba5e353f7d 1.018 -41ce56b940000000 1018000000 -4062324924924925 145.57142857142858 -3ff04dd2f1a9fbe8 1.0190000000000001 -41ce5e5a60000000 1019000000 -406236db6db6db6e 145.71428571428572 -3ff051eb851eb852 1.02 -41ce65fb80000000 1020000000 -40623b6db6db6db7 145.85714285714286 -3ff05604189374bd 1.0210000000000001 -41ce6d9ca0000000 1021000000 -4062400000000000 146 -3ff05a1cac083127 1.022 -41ce753dc0000000 1022000000 -4062449249249249 146.14285714285714 -3ff05e353f7ced92 1.0230000000000001 -41ce7cdee0000000 1023000000 -4062492492492492 146.28571428571428 -3ff0624dd2f1a9fc 1.024 -41ce848000000000 1024000000 -40624db6db6db6db 146.42857142857142 -3ff0666666666666 1.025 -41ce8c2120000000 1025000000 -4062524924924925 146.57142857142858 -3ff06a7ef9db22d1 1.026 -41ce93c240000000 1026000000 -406256db6db6db6e 146.71428571428572 -3ff06e978d4fdf3b 1.027 -41ce9b6360000000 1027000000 -40625b6db6db6db7 146.85714285714286 -3ff072b020c49ba6 1.028 -41cea30480000000 1028000000 -4062600000000000 147 -3ff076c8b4395810 1.029 -41ceaaa5a0000000 1029000000 -4062649249249249 147.14285714285714 -3ff07ae147ae147b 1.03 -41ceb246c0000000 1030000000 -4062692492492492 147.28571428571428 -3ff07ef9db22d0e5 1.031 -41ceb9e7e0000000 1031000000 -40626db6db6db6db 147.42857142857142 -3ff083126e978d50 1.032 -41cec18900000000 1032000000 -4062724924924925 147.57142857142858 -3ff0872b020c49ba 1.033 -41cec92a20000000 1033000000 -406276db6db6db6e 147.71428571428572 -3ff08b4395810625 1.034 -41ced0cb40000000 1034000000 -40627b6db6db6db7 147.85714285714286 -3ff08f5c28f5c28f 1.035 -41ced86c60000000 1035000000 -4062800000000000 148 -3ff09374bc6a7efa 1.036 -41cee00d80000000 1036000000 -4062849249249249 148.14285714285714 -3ff0978d4fdf3b64 1.037 -41cee7aea0000000 1037000000 -4062892492492492 148.28571428571428 -3ff09ba5e353f7cf 1.038 -41ceef4fc0000000 1038000000 -40628db6db6db6db 148.42857142857142 -3ff09fbe76c8b439 1.039 -41cef6f0e0000000 1039000000 -4062924924924925 148.57142857142858 -3ff0a3d70a3d70a4 1.04 -41cefe9200000000 1040000000 -406296db6db6db6e 148.71428571428572 -3ff0a7ef9db22d0e 1.041 -41cf063320000000 1041000000 -40629b6db6db6db7 148.85714285714286 -3ff0ac083126e979 1.042 -41cf0dd440000000 1042000000 -4062a00000000000 149 -3ff0b020c49ba5e3 1.043 -41cf157560000000 1043000000 -4062a49249249249 149.14285714285714 -3ff0b4395810624e 1.044 -41cf1d1680000000 1044000000 -4062a92492492492 149.28571428571428 -3ff0b851eb851eb8 1.045 -41cf24b7a0000000 1045000000 -4062adb6db6db6db 149.42857142857142 -3ff0bc6a7ef9db23 1.046 -41cf2c58c0000000 1046000000 -4062b24924924925 149.57142857142858 -3ff0c083126e978d 1.047 -41cf33f9e0000000 1047000000 -4062b6db6db6db6e 149.71428571428572 -3ff0c49ba5e353f8 1.048 -41cf3b9b00000000 1048000000 -4062bb6db6db6db7 149.85714285714286 -3ff0c8b439581062 1.049 -41cf433c20000000 1049000000 -4062c00000000000 150 -3ff0cccccccccccd 1.05 -41cf4add40000000 1050000000 -4062c49249249249 150.14285714285714 -3ff0d0e560418937 1.051 -41cf527e60000000 1051000000 -4062c92492492492 150.28571428571428 -3ff0d4fdf3b645a2 1.052 -41cf5a1f80000000 1052000000 -4062cdb6db6db6db 150.42857142857142 -3ff0d916872b020c 1.053 -41cf61c0a0000000 1053000000 -4062d24924924925 150.57142857142858 -3ff0dd2f1a9fbe77 1.054 -41cf6961c0000000 1054000000 -4062d6db6db6db6e 150.71428571428572 -3ff0e147ae147ae1 1.055 -41cf7102e0000000 1055000000 -4062db6db6db6db7 150.85714285714286 -3ff0e5604189374c 1.056 -41cf78a400000000 1056000000 -4062e00000000000 151 -3ff0e978d4fdf3b6 1.057 -41cf804520000000 1057000000 -4062e49249249249 151.14285714285714 -3ff0ed916872b021 1.058 -41cf87e640000000 1058000000 -4062e92492492492 151.28571428571428 -3ff0f1a9fbe76c8b 1.059 -41cf8f8760000000 1059000000 -4062edb6db6db6db 151.42857142857142 -3ff0f5c28f5c28f6 1.06 -41cf972880000000 1060000000 -4062f24924924925 151.57142857142858 -3ff0f9db22d0e560 1.061 -41cf9ec9a0000000 1061000000 -4062f6db6db6db6e 151.71428571428572 -3ff0fdf3b645a1cb 1.062 -41cfa66ac0000000 1062000000 -4062fb6db6db6db7 151.85714285714286 -3ff1020c49ba5e35 1.063 -41cfae0be0000000 1063000000 -4063000000000000 152 -3ff10624dd2f1aa0 1.064 -41cfb5ad00000000 1064000000 -4063049249249249 152.14285714285714 -3ff10a3d70a3d70a 1.065 -41cfbd4e20000000 1065000000 -4063092492492492 152.28571428571428 -3ff10e5604189375 1.066 -41cfc4ef40000000 1066000000 -40630db6db6db6db 152.42857142857142 -3ff1126e978d4fdf 1.067 -41cfcc9060000000 1067000000 -4063124924924925 152.57142857142858 -3ff116872b020c4a 1.068 -41cfd43180000000 1068000000 -406316db6db6db6e 152.71428571428572 -3ff11a9fbe76c8b4 1.069 -41cfdbd2a0000000 1069000000 -40631b6db6db6db7 152.85714285714286 -3ff11eb851eb851f 1.07 -41cfe373c0000000 1070000000 -4063200000000000 153 -3ff122d0e5604189 1.071 -41cfeb14e0000000 1071000000 -4063249249249249 153.14285714285714 -3ff126e978d4fdf4 1.072 -41cff2b600000000 1072000000 -4063292492492492 153.28571428571428 -3ff12b020c49ba5e 1.073 -41cffa5720000000 1073000000 -40632db6db6db6db 153.42857142857142 -3ff12f1a9fbe76c9 1.074 -41d000fc20000000 1074000000 -4063324924924925 153.57142857142858 -3ff1333333333333 1.075 -41d004ccb0000000 1075000000 -406336db6db6db6e 153.71428571428572 -3ff1374bc6a7ef9e 1.076 -41d0089d40000000 1076000000 -40633b6db6db6db7 153.85714285714286 -3ff13b645a1cac08 1.077 -41d00c6dd0000000 1077000000 -4063400000000000 154 -3ff13f7ced916873 1.078 -41d0103e60000000 1078000000 -4063449249249249 154.14285714285714 -3ff14395810624dd 1.079 -41d0140ef0000000 1079000000 -4063492492492492 154.28571428571428 -3ff147ae147ae148 1.08 -41d017df80000000 1080000000 -40634db6db6db6db 154.42857142857142 -3ff14bc6a7ef9db2 1.081 -41d01bb010000000 1081000000 -4063524924924925 154.57142857142858 -3ff14fdf3b645a1d 1.082 -41d01f80a0000000 1082000000 -406356db6db6db6e 154.71428571428572 -3ff153f7ced91687 1.083 -41d0235130000000 1083000000 -40635b6db6db6db7 154.85714285714286 -3ff15810624dd2f2 1.084 -41d02721c0000000 1084000000 -4063600000000000 155 -3ff15c28f5c28f5c 1.085 -41d02af250000000 1085000000 -4063649249249249 155.14285714285714 -3ff1604189374bc7 1.086 -41d02ec2e0000000 1086000000 -4063692492492492 155.28571428571428 -3ff1645a1cac0831 1.087 -41d0329370000000 1087000000 -40636db6db6db6db 155.42857142857142 -3ff16872b020c49c 1.088 -41d0366400000000 1088000000 -4063724924924925 155.57142857142858 -3ff16c8b43958106 1.089 -41d03a3490000000 1089000000 -406376db6db6db6e 155.71428571428572 -3ff170a3d70a3d71 1.09 -41d03e0520000000 1090000000 -40637b6db6db6db7 155.85714285714286 -3ff174bc6a7ef9db 1.091 -41d041d5b0000000 1091000000 -4063800000000000 156 -3ff178d4fdf3b646 1.092 -41d045a640000000 1092000000 -4063849249249249 156.14285714285714 -3ff17ced916872b0 1.093 -41d04976d0000000 1093000000 -4063892492492492 156.28571428571428 -3ff1810624dd2f1b 1.094 -41d04d4760000000 1094000000 -40638db6db6db6db 156.42857142857142 -3ff1851eb851eb85 1.095 -41d05117f0000000 1095000000 -4063924924924925 156.57142857142858 -3ff189374bc6a7f0 1.096 -41d054e880000000 1096000000 -406396db6db6db6e 156.71428571428572 -3ff18d4fdf3b645a 1.097 -41d058b910000000 1097000000 -40639b6db6db6db7 156.85714285714286 -3ff1916872b020c5 1.098 -41d05c89a0000000 1098000000 -4063a00000000000 157 -3ff195810624dd2f 1.099 -41d0605a30000000 1099000000 -4063a49249249249 157.14285714285714 -3ff199999999999a 1.1 -41d0642ac0000000 1100000000 -4063a92492492492 157.28571428571428 -3ff19db22d0e5604 1.101 -41d067fb50000000 1101000000 -4063adb6db6db6db 157.42857142857142 -3ff1a1cac083126f 1.102 -41d06bcbe0000000 1102000000 -4063b24924924925 157.57142857142858 -3ff1a5e353f7ced9 1.103 -41d06f9c70000000 1103000000 -4063b6db6db6db6e 157.71428571428572 -3ff1a9fbe76c8b44 1.104 -41d0736d00000000 1104000000 -4063bb6db6db6db7 157.85714285714286 -3ff1ae147ae147ae 1.105 -41d0773d90000000 1105000000 -4063c00000000000 158 -3ff1b22d0e560419 1.106 -41d07b0e20000000 1106000000 -4063c49249249249 158.14285714285714 -3ff1b645a1cac083 1.107 -41d07edeb0000000 1107000000 -4063c92492492492 158.28571428571428 -3ff1ba5e353f7cee 1.108 -41d082af40000000 1108000000 -4063cdb6db6db6db 158.42857142857142 -3ff1be76c8b43958 1.109 -41d0867fd0000000 1109000000 -4063d24924924925 158.57142857142858 -3ff1c28f5c28f5c3 1.11 -41d08a5060000000 1110000000 -4063d6db6db6db6e 158.71428571428572 -3ff1c6a7ef9db22d 1.111 -41d08e20f0000000 1111000000 -4063db6db6db6db7 158.85714285714286 -3ff1cac083126e98 1.112 -41d091f180000000 1112000000 -4063e00000000000 159 -3ff1ced916872b02 1.113 -41d095c210000000 1113000000 -4063e49249249249 159.14285714285714 -3ff1d2f1a9fbe76d 1.114 -41d09992a0000000 1114000000 -4063e92492492492 159.28571428571428 -3ff1d70a3d70a3d7 1.115 -41d09d6330000000 1115000000 -4063edb6db6db6db 159.42857142857142 -3ff1db22d0e56042 1.116 -41d0a133c0000000 1116000000 -4063f24924924925 159.57142857142858 -3ff1df3b645a1cac 1.117 -41d0a50450000000 1117000000 -4063f6db6db6db6e 159.71428571428572 -3ff1e353f7ced917 1.118 -41d0a8d4e0000000 1118000000 -4063fb6db6db6db7 159.85714285714286 -3ff1e76c8b439581 1.119 -41d0aca570000000 1119000000 -4064000000000000 160 -3ff1eb851eb851ec 1.12 -41d0b07600000000 1120000000 -4064049249249249 160.14285714285714 -3ff1ef9db22d0e56 1.121 -41d0b44690000000 1121000000 -4064092492492492 160.28571428571428 -3ff1f3b645a1cac1 1.122 -41d0b81720000000 1122000000 -40640db6db6db6db 160.42857142857142 -3ff1f7ced916872b 1.123 -41d0bbe7b0000000 1123000000 -4064124924924925 160.57142857142858 -3ff1fbe76c8b4396 1.124 -41d0bfb840000000 1124000000 -406416db6db6db6e 160.71428571428572 -3ff2000000000000 1.125 -41d0c388d0000000 1125000000 -40641b6db6db6db7 160.85714285714286 -3ff204189374bc6b 1.1260000000000001 -41d0c75960000000 1126000000 -4064200000000000 161 -3ff2083126e978d5 1.127 -41d0cb29f0000000 1127000000 -4064249249249249 161.14285714285714 -3ff20c49ba5e3540 1.1280000000000001 -41d0cefa80000000 1128000000 -4064292492492492 161.28571428571428 -3ff210624dd2f1aa 1.129 -41d0d2cb10000000 1129000000 -40642db6db6db6db 161.42857142857142 -3ff2147ae147ae15 1.1300000000000001 -41d0d69ba0000000 1130000000 -4064324924924925 161.57142857142858 -3ff2189374bc6a7f 1.131 -41d0da6c30000000 1131000000 -406436db6db6db6e 161.71428571428572 -3ff21cac083126ea 1.1320000000000001 -41d0de3cc0000000 1132000000 -40643b6db6db6db7 161.85714285714286 -3ff220c49ba5e354 1.133 -41d0e20d50000000 1133000000 -4064400000000000 162 -3ff224dd2f1a9fbf 1.1340000000000001 -41d0e5dde0000000 1134000000 -4064449249249249 162.14285714285714 -3ff228f5c28f5c29 1.135 -41d0e9ae70000000 1135000000 -4064492492492492 162.28571428571428 -3ff22d0e56041894 1.1360000000000001 -41d0ed7f00000000 1136000000 -40644db6db6db6db 162.42857142857142 -3ff23126e978d4fe 1.137 -41d0f14f90000000 1137000000 -4064524924924925 162.57142857142858 -3ff2353f7ced9169 1.1380000000000001 -41d0f52020000000 1138000000 -406456db6db6db6e 162.71428571428572 -3ff2395810624dd3 1.139 -41d0f8f0b0000000 1139000000 -40645b6db6db6db7 162.85714285714286 -3ff23d70a3d70a3e 1.1400000000000001 -41d0fcc140000000 1140000000 -4064600000000000 163 -3ff24189374bc6a8 1.141 -41d10091d0000000 1141000000 -4064649249249249 163.14285714285714 -3ff245a1cac08313 1.1420000000000001 -41d1046260000000 1142000000 -4064692492492492 163.28571428571428 -3ff249ba5e353f7d 1.143 -41d10832f0000000 1143000000 -40646db6db6db6db 163.42857142857142 -3ff24dd2f1a9fbe8 1.1440000000000001 -41d10c0380000000 1144000000 -4064724924924925 163.57142857142858 -3ff251eb851eb852 1.145 -41d10fd410000000 1145000000 -406476db6db6db6e 163.71428571428572 -3ff25604189374bd 1.1460000000000001 -41d113a4a0000000 1146000000 -40647b6db6db6db7 163.85714285714286 -3ff25a1cac083127 1.147 -41d1177530000000 1147000000 -4064800000000000 164 -3ff25e353f7ced92 1.1480000000000001 -41d11b45c0000000 1148000000 -4064849249249249 164.14285714285714 -3ff2624dd2f1a9fc 1.149 -41d11f1650000000 1149000000 -4064892492492492 164.28571428571428 -3ff2666666666667 1.1500000000000001 -41d122e6e0000000 1150000000 -40648db6db6db6db 164.42857142857142 -3ff26a7ef9db22d1 1.151 -41d126b770000000 1151000000 -4064924924924925 164.57142857142858 -3ff26e978d4fdf3c 1.1520000000000001 -41d12a8800000000 1152000000 -406496db6db6db6e 164.71428571428572 -3ff272b020c49ba6 1.153 -41d12e5890000000 1153000000 -40649b6db6db6db7 164.85714285714286 -3ff276c8b4395810 1.154 -41d1322920000000 1154000000 -4064a00000000000 165 -3ff27ae147ae147b 1.155 -41d135f9b0000000 1155000000 -4064a49249249249 165.14285714285714 -3ff27ef9db22d0e5 1.156 -41d139ca40000000 1156000000 -4064a92492492492 165.28571428571428 -3ff283126e978d50 1.157 -41d13d9ad0000000 1157000000 -4064adb6db6db6db 165.42857142857142 -3ff2872b020c49ba 1.158 -41d1416b60000000 1158000000 -4064b24924924925 165.57142857142858 -3ff28b4395810625 1.159 -41d1453bf0000000 1159000000 -4064b6db6db6db6e 165.71428571428572 -3ff28f5c28f5c28f 1.16 -41d1490c80000000 1160000000 -4064bb6db6db6db7 165.85714285714286 -3ff29374bc6a7efa 1.161 -41d14cdd10000000 1161000000 -4064c00000000000 166 -3ff2978d4fdf3b64 1.162 -41d150ada0000000 1162000000 -4064c49249249249 166.14285714285714 -3ff29ba5e353f7cf 1.163 -41d1547e30000000 1163000000 -4064c92492492492 166.28571428571428 -3ff29fbe76c8b439 1.164 -41d1584ec0000000 1164000000 -4064cdb6db6db6db 166.42857142857142 -3ff2a3d70a3d70a4 1.165 -41d15c1f50000000 1165000000 -4064d24924924925 166.57142857142858 -3ff2a7ef9db22d0e 1.166 -41d15fefe0000000 1166000000 -4064d6db6db6db6e 166.71428571428572 -3ff2ac083126e979 1.167 -41d163c070000000 1167000000 -4064db6db6db6db7 166.85714285714286 -3ff2b020c49ba5e3 1.168 -41d1679100000000 1168000000 -4064e00000000000 167 -3ff2b4395810624e 1.169 -41d16b6190000000 1169000000 -4064e49249249249 167.14285714285714 -3ff2b851eb851eb8 1.17 -41d16f3220000000 1170000000 -4064e92492492492 167.28571428571428 -3ff2bc6a7ef9db23 1.171 -41d17302b0000000 1171000000 -4064edb6db6db6db 167.42857142857142 -3ff2c083126e978d 1.172 -41d176d340000000 1172000000 -4064f24924924925 167.57142857142858 -3ff2c49ba5e353f8 1.173 -41d17aa3d0000000 1173000000 -4064f6db6db6db6e 167.71428571428572 -3ff2c8b439581062 1.174 -41d17e7460000000 1174000000 -4064fb6db6db6db7 167.85714285714286 -3ff2cccccccccccd 1.175 -41d18244f0000000 1175000000 -4065000000000000 168 -3ff2d0e560418937 1.176 -41d1861580000000 1176000000 -4065049249249249 168.14285714285714 -3ff2d4fdf3b645a2 1.177 -41d189e610000000 1177000000 -4065092492492492 168.28571428571428 -3ff2d916872b020c 1.178 -41d18db6a0000000 1178000000 -40650db6db6db6db 168.42857142857142 -3ff2dd2f1a9fbe77 1.179 -41d1918730000000 1179000000 -4065124924924925 168.57142857142858 -3ff2e147ae147ae1 1.18 -41d19557c0000000 1180000000 -406516db6db6db6e 168.71428571428572 -3ff2e5604189374c 1.181 -41d1992850000000 1181000000 -40651b6db6db6db7 168.85714285714286 -3ff2e978d4fdf3b6 1.182 -41d19cf8e0000000 1182000000 -4065200000000000 169 -3ff2ed916872b021 1.183 -41d1a0c970000000 1183000000 -4065249249249249 169.14285714285714 -3ff2f1a9fbe76c8b 1.184 -41d1a49a00000000 1184000000 -4065292492492492 169.28571428571428 -3ff2f5c28f5c28f6 1.185 -41d1a86a90000000 1185000000 -40652db6db6db6db 169.42857142857142 -3ff2f9db22d0e560 1.186 -41d1ac3b20000000 1186000000 -4065324924924925 169.57142857142858 -3ff2fdf3b645a1cb 1.187 -41d1b00bb0000000 1187000000 -406536db6db6db6e 169.71428571428572 -3ff3020c49ba5e35 1.188 -41d1b3dc40000000 1188000000 -40653b6db6db6db7 169.85714285714286 -3ff30624dd2f1aa0 1.189 -41d1b7acd0000000 1189000000 -4065400000000000 170 -3ff30a3d70a3d70a 1.19 -41d1bb7d60000000 1190000000 -4065449249249249 170.14285714285714 -3ff30e5604189375 1.191 -41d1bf4df0000000 1191000000 -4065492492492492 170.28571428571428 -3ff3126e978d4fdf 1.192 -41d1c31e80000000 1192000000 -40654db6db6db6db 170.42857142857142 -3ff316872b020c4a 1.193 -41d1c6ef10000000 1193000000 -4065524924924925 170.57142857142858 -3ff31a9fbe76c8b4 1.194 -41d1cabfa0000000 1194000000 -406556db6db6db6e 170.71428571428572 -3ff31eb851eb851f 1.195 -41d1ce9030000000 1195000000 -40655b6db6db6db7 170.85714285714286 -3ff322d0e5604189 1.196 -41d1d260c0000000 1196000000 -4065600000000000 171 -3ff326e978d4fdf4 1.197 -41d1d63150000000 1197000000 -4065649249249249 171.14285714285714 -3ff32b020c49ba5e 1.198 -41d1da01e0000000 1198000000 -4065692492492492 171.28571428571428 -3ff32f1a9fbe76c9 1.199 -41d1ddd270000000 1199000000 -40656db6db6db6db 171.42857142857142 -3ff3333333333333 1.2 -41d1e1a300000000 1200000000 -4065724924924925 171.57142857142858 -3ff3374bc6a7ef9e 1.201 -41d1e57390000000 1201000000 -406576db6db6db6e 171.71428571428572 -3ff33b645a1cac08 1.202 -41d1e94420000000 1202000000 -40657b6db6db6db7 171.85714285714286 -3ff33f7ced916873 1.203 -41d1ed14b0000000 1203000000 -4065800000000000 172 -3ff34395810624dd 1.204 -41d1f0e540000000 1204000000 -4065849249249249 172.14285714285714 -3ff347ae147ae148 1.205 -41d1f4b5d0000000 1205000000 -4065892492492492 172.28571428571428 -3ff34bc6a7ef9db2 1.206 -41d1f88660000000 1206000000 -40658db6db6db6db 172.42857142857142 -3ff34fdf3b645a1d 1.207 -41d1fc56f0000000 1207000000 -4065924924924925 172.57142857142858 -3ff353f7ced91687 1.208 -41d2002780000000 1208000000 -406596db6db6db6e 172.71428571428572 -3ff35810624dd2f2 1.209 -41d203f810000000 1209000000 -40659b6db6db6db7 172.85714285714286 -3ff35c28f5c28f5c 1.21 -41d207c8a0000000 1210000000 -4065a00000000000 173 -3ff3604189374bc7 1.211 -41d20b9930000000 1211000000 -4065a49249249249 173.14285714285714 -3ff3645a1cac0831 1.212 -41d20f69c0000000 1212000000 -4065a92492492492 173.28571428571428 -3ff36872b020c49c 1.213 -41d2133a50000000 1213000000 -4065adb6db6db6db 173.42857142857142 -3ff36c8b43958106 1.214 -41d2170ae0000000 1214000000 -4065b24924924925 173.57142857142858 -3ff370a3d70a3d71 1.215 -41d21adb70000000 1215000000 -4065b6db6db6db6e 173.71428571428572 -3ff374bc6a7ef9db 1.216 -41d21eac00000000 1216000000 -4065bb6db6db6db7 173.85714285714286 -3ff378d4fdf3b646 1.217 -41d2227c90000000 1217000000 -4065c00000000000 174 -3ff37ced916872b0 1.218 -41d2264d20000000 1218000000 -4065c49249249249 174.14285714285714 -3ff3810624dd2f1b 1.219 -41d22a1db0000000 1219000000 -4065c92492492492 174.28571428571428 -3ff3851eb851eb85 1.22 -41d22dee40000000 1220000000 -4065cdb6db6db6db 174.42857142857142 -3ff389374bc6a7f0 1.221 -41d231bed0000000 1221000000 -4065d24924924925 174.57142857142858 -3ff38d4fdf3b645a 1.222 -41d2358f60000000 1222000000 -4065d6db6db6db6e 174.71428571428572 -3ff3916872b020c5 1.223 -41d2395ff0000000 1223000000 -4065db6db6db6db7 174.85714285714286 -3ff395810624dd2f 1.224 -41d23d3080000000 1224000000 -4065e00000000000 175 -3ff399999999999a 1.225 -41d2410110000000 1225000000 -4065e49249249249 175.14285714285714 -3ff39db22d0e5604 1.226 -41d244d1a0000000 1226000000 -4065e92492492492 175.28571428571428 -3ff3a1cac083126f 1.227 -41d248a230000000 1227000000 -4065edb6db6db6db 175.42857142857142 -3ff3a5e353f7ced9 1.228 -41d24c72c0000000 1228000000 -4065f24924924925 175.57142857142858 -3ff3a9fbe76c8b44 1.229 -41d2504350000000 1229000000 -4065f6db6db6db6e 175.71428571428572 -3ff3ae147ae147ae 1.23 -41d25413e0000000 1230000000 -4065fb6db6db6db7 175.85714285714286 -3ff3b22d0e560419 1.231 -41d257e470000000 1231000000 -4066000000000000 176 -3ff3b645a1cac083 1.232 -41d25bb500000000 1232000000 -4066049249249249 176.14285714285714 -3ff3ba5e353f7cee 1.233 -41d25f8590000000 1233000000 -4066092492492492 176.28571428571428 -3ff3be76c8b43958 1.234 -41d2635620000000 1234000000 -40660db6db6db6db 176.42857142857142 -3ff3c28f5c28f5c3 1.235 -41d26726b0000000 1235000000 -4066124924924925 176.57142857142858 -3ff3c6a7ef9db22d 1.236 -41d26af740000000 1236000000 -406616db6db6db6e 176.71428571428572 -3ff3cac083126e98 1.237 -41d26ec7d0000000 1237000000 -40661b6db6db6db7 176.85714285714286 -3ff3ced916872b02 1.238 -41d2729860000000 1238000000 -4066200000000000 177 -3ff3d2f1a9fbe76d 1.239 -41d27668f0000000 1239000000 -4066249249249249 177.14285714285714 -3ff3d70a3d70a3d7 1.24 -41d27a3980000000 1240000000 -4066292492492492 177.28571428571428 -3ff3db22d0e56042 1.241 -41d27e0a10000000 1241000000 -40662db6db6db6db 177.42857142857142 -3ff3df3b645a1cac 1.242 -41d281daa0000000 1242000000 -4066324924924925 177.57142857142858 -3ff3e353f7ced917 1.243 -41d285ab30000000 1243000000 -406636db6db6db6e 177.71428571428572 -3ff3e76c8b439581 1.244 -41d2897bc0000000 1244000000 -40663b6db6db6db7 177.85714285714286 -3ff3eb851eb851ec 1.245 -41d28d4c50000000 1245000000 -4066400000000000 178 -3ff3ef9db22d0e56 1.246 -41d2911ce0000000 1246000000 -4066449249249249 178.14285714285714 -3ff3f3b645a1cac1 1.247 -41d294ed70000000 1247000000 -4066492492492492 178.28571428571428 -3ff3f7ced916872b 1.248 -41d298be00000000 1248000000 -40664db6db6db6db 178.42857142857142 -3ff3fbe76c8b4396 1.249 -41d29c8e90000000 1249000000 -4066524924924925 178.57142857142858 -3ff4000000000000 1.25 -41d2a05f20000000 1250000000 -406656db6db6db6e 178.71428571428572 -3ff404189374bc6b 1.2510000000000001 -41d2a42fb0000000 1251000000 -40665b6db6db6db7 178.85714285714286 -3ff4083126e978d5 1.252 -41d2a80040000000 1252000000 -4066600000000000 179 -3ff40c49ba5e3540 1.2530000000000001 -41d2abd0d0000000 1253000000 -4066649249249249 179.14285714285714 -3ff410624dd2f1aa 1.254 -41d2afa160000000 1254000000 -4066692492492492 179.28571428571428 -3ff4147ae147ae15 1.2550000000000001 -41d2b371f0000000 1255000000 -40666db6db6db6db 179.42857142857142 -3ff4189374bc6a7f 1.256 -41d2b74280000000 1256000000 -4066724924924925 179.57142857142858 -3ff41cac083126ea 1.2570000000000001 -41d2bb1310000000 1257000000 -406676db6db6db6e 179.71428571428572 -3ff420c49ba5e354 1.258 -41d2bee3a0000000 1258000000 -40667b6db6db6db7 179.85714285714286 -3ff424dd2f1a9fbf 1.2590000000000001 -41d2c2b430000000 1259000000 -4066800000000000 180 -3ff428f5c28f5c29 1.26 -41d2c684c0000000 1260000000 -4066849249249249 180.14285714285714 -3ff42d0e56041894 1.2610000000000001 -41d2ca5550000000 1261000000 -4066892492492492 180.28571428571428 -3ff43126e978d4fe 1.262 -41d2ce25e0000000 1262000000 -40668db6db6db6db 180.42857142857142 -3ff4353f7ced9169 1.2630000000000001 -41d2d1f670000000 1263000000 -4066924924924925 180.57142857142858 -3ff4395810624dd3 1.264 -41d2d5c700000000 1264000000 -406696db6db6db6e 180.71428571428572 -3ff43d70a3d70a3e 1.2650000000000001 -41d2d99790000000 1265000000 -40669b6db6db6db7 180.85714285714286 -3ff44189374bc6a8 1.266 -41d2dd6820000000 1266000000 -4066a00000000000 181 -3ff445a1cac08313 1.2670000000000001 -41d2e138b0000000 1267000000 -4066a49249249249 181.14285714285714 -3ff449ba5e353f7d 1.268 -41d2e50940000000 1268000000 -4066a92492492492 181.28571428571428 -3ff44dd2f1a9fbe8 1.2690000000000001 -41d2e8d9d0000000 1269000000 -4066adb6db6db6db 181.42857142857142 -3ff451eb851eb852 1.27 -41d2ecaa60000000 1270000000 -4066b24924924925 181.57142857142858 -3ff45604189374bd 1.2710000000000001 -41d2f07af0000000 1271000000 -4066b6db6db6db6e 181.71428571428572 -3ff45a1cac083127 1.272 -41d2f44b80000000 1272000000 -4066bb6db6db6db7 181.85714285714286 -3ff45e353f7ced92 1.2730000000000001 -41d2f81c10000000 1273000000 -4066c00000000000 182 -3ff4624dd2f1a9fc 1.274 -41d2fbeca0000000 1274000000 -4066c49249249249 182.14285714285714 -3ff4666666666667 1.2750000000000001 -41d2ffbd30000000 1275000000 -4066c92492492492 182.28571428571428 -3ff46a7ef9db22d1 1.276 -41d3038dc0000000 1276000000 -4066cdb6db6db6db 182.42857142857142 -3ff46e978d4fdf3c 1.2770000000000001 -41d3075e50000000 1277000000 -4066d24924924925 182.57142857142858 -3ff472b020c49ba6 1.278 -41d30b2ee0000000 1278000000 -4066d6db6db6db6e 182.71428571428572 -3ff476c8b4395811 1.2790000000000001 -41d30eff70000000 1279000000 -4066db6db6db6db7 182.85714285714286 -3ff47ae147ae147b 1.28 -41d312d000000000 1280000000 -4066e00000000000 183 -3ff47ef9db22d0e5 1.281 -41d316a090000000 1281000000 -4066e49249249249 183.14285714285714 -3ff483126e978d50 1.282 -41d31a7120000000 1282000000 -4066e92492492492 183.28571428571428 -3ff4872b020c49ba 1.283 -41d31e41b0000000 1283000000 -4066edb6db6db6db 183.42857142857142 -3ff48b4395810625 1.284 -41d3221240000000 1284000000 -4066f24924924925 183.57142857142858 -3ff48f5c28f5c28f 1.285 -41d325e2d0000000 1285000000 -4066f6db6db6db6e 183.71428571428572 -3ff49374bc6a7efa 1.286 -41d329b360000000 1286000000 -4066fb6db6db6db7 183.85714285714286 -3ff4978d4fdf3b64 1.287 -41d32d83f0000000 1287000000 -4067000000000000 184 -3ff49ba5e353f7cf 1.288 -41d3315480000000 1288000000 -4067049249249249 184.14285714285714 -3ff49fbe76c8b439 1.289 -41d3352510000000 1289000000 -4067092492492492 184.28571428571428 -3ff4a3d70a3d70a4 1.29 -41d338f5a0000000 1290000000 -40670db6db6db6db 184.42857142857142 -3ff4a7ef9db22d0e 1.291 -41d33cc630000000 1291000000 -4067124924924925 184.57142857142858 -3ff4ac083126e979 1.292 -41d34096c0000000 1292000000 -406716db6db6db6e 184.71428571428572 -3ff4b020c49ba5e3 1.293 -41d3446750000000 1293000000 -40671b6db6db6db7 184.85714285714286 -3ff4b4395810624e 1.294 -41d34837e0000000 1294000000 -4067200000000000 185 -3ff4b851eb851eb8 1.295 -41d34c0870000000 1295000000 -4067249249249249 185.14285714285714 -3ff4bc6a7ef9db23 1.296 -41d34fd900000000 1296000000 -4067292492492492 185.28571428571428 -3ff4c083126e978d 1.297 -41d353a990000000 1297000000 -40672db6db6db6db 185.42857142857142 -3ff4c49ba5e353f8 1.298 -41d3577a20000000 1298000000 -4067324924924925 185.57142857142858 -3ff4c8b439581062 1.299 -41d35b4ab0000000 1299000000 -406736db6db6db6e 185.71428571428572 -3ff4cccccccccccd 1.3 -41d35f1b40000000 1300000000 -40673b6db6db6db7 185.85714285714286 -3ff4d0e560418937 1.301 -41d362ebd0000000 1301000000 -4067400000000000 186 -3ff4d4fdf3b645a2 1.302 -41d366bc60000000 1302000000 -4067449249249249 186.14285714285714 -3ff4d916872b020c 1.303 -41d36a8cf0000000 1303000000 -4067492492492492 186.28571428571428 -3ff4dd2f1a9fbe77 1.304 -41d36e5d80000000 1304000000 -40674db6db6db6db 186.42857142857142 -3ff4e147ae147ae1 1.305 -41d3722e10000000 1305000000 -4067524924924925 186.57142857142858 -3ff4e5604189374c 1.306 -41d375fea0000000 1306000000 -406756db6db6db6e 186.71428571428572 -3ff4e978d4fdf3b6 1.307 -41d379cf30000000 1307000000 -40675b6db6db6db7 186.85714285714286 -3ff4ed916872b021 1.308 -41d37d9fc0000000 1308000000 -4067600000000000 187 -3ff4f1a9fbe76c8b 1.309 -41d3817050000000 1309000000 -4067649249249249 187.14285714285714 -3ff4f5c28f5c28f6 1.31 -41d38540e0000000 1310000000 -4067692492492492 187.28571428571428 -3ff4f9db22d0e560 1.311 -41d3891170000000 1311000000 -40676db6db6db6db 187.42857142857142 -3ff4fdf3b645a1cb 1.312 -41d38ce200000000 1312000000 -4067724924924925 187.57142857142858 -3ff5020c49ba5e35 1.313 -41d390b290000000 1313000000 -406776db6db6db6e 187.71428571428572 -3ff50624dd2f1aa0 1.314 -41d3948320000000 1314000000 -40677b6db6db6db7 187.85714285714286 -3ff50a3d70a3d70a 1.315 -41d39853b0000000 1315000000 -4067800000000000 188 -3ff50e5604189375 1.316 -41d39c2440000000 1316000000 -4067849249249249 188.14285714285714 -3ff5126e978d4fdf 1.317 -41d39ff4d0000000 1317000000 -4067892492492492 188.28571428571428 -3ff516872b020c4a 1.318 -41d3a3c560000000 1318000000 -40678db6db6db6db 188.42857142857142 -3ff51a9fbe76c8b4 1.319 -41d3a795f0000000 1319000000 -4067924924924925 188.57142857142858 -3ff51eb851eb851f 1.32 -41d3ab6680000000 1320000000 -406796db6db6db6e 188.71428571428572 -3ff522d0e5604189 1.321 -41d3af3710000000 1321000000 -40679b6db6db6db7 188.85714285714286 -3ff526e978d4fdf4 1.322 -41d3b307a0000000 1322000000 -4067a00000000000 189 -3ff52b020c49ba5e 1.323 -41d3b6d830000000 1323000000 -4067a49249249249 189.14285714285714 -3ff52f1a9fbe76c9 1.324 -41d3baa8c0000000 1324000000 -4067a92492492492 189.28571428571428 -3ff5333333333333 1.325 -41d3be7950000000 1325000000 -4067adb6db6db6db 189.42857142857142 -3ff5374bc6a7ef9e 1.326 -41d3c249e0000000 1326000000 -4067b24924924925 189.57142857142858 -3ff53b645a1cac08 1.327 -41d3c61a70000000 1327000000 -4067b6db6db6db6e 189.71428571428572 -3ff53f7ced916873 1.328 -41d3c9eb00000000 1328000000 -4067bb6db6db6db7 189.85714285714286 -3ff54395810624dd 1.329 -41d3cdbb90000000 1329000000 -4067c00000000000 190 -3ff547ae147ae148 1.33 -41d3d18c20000000 1330000000 -4067c49249249249 190.14285714285714 -3ff54bc6a7ef9db2 1.331 -41d3d55cb0000000 1331000000 -4067c92492492492 190.28571428571428 -3ff54fdf3b645a1d 1.332 -41d3d92d40000000 1332000000 -4067cdb6db6db6db 190.42857142857142 -3ff553f7ced91687 1.333 -41d3dcfdd0000000 1333000000 -4067d24924924925 190.57142857142858 -3ff55810624dd2f2 1.334 -41d3e0ce60000000 1334000000 -4067d6db6db6db6e 190.71428571428572 -3ff55c28f5c28f5c 1.335 -41d3e49ef0000000 1335000000 -4067db6db6db6db7 190.85714285714286 -3ff5604189374bc7 1.336 -41d3e86f80000000 1336000000 -4067e00000000000 191 -3ff5645a1cac0831 1.337 -41d3ec4010000000 1337000000 -4067e49249249249 191.14285714285714 -3ff56872b020c49c 1.338 -41d3f010a0000000 1338000000 -4067e92492492492 191.28571428571428 -3ff56c8b43958106 1.339 -41d3f3e130000000 1339000000 -4067edb6db6db6db 191.42857142857142 -3ff570a3d70a3d71 1.34 -41d3f7b1c0000000 1340000000 -4067f24924924925 191.57142857142858 -3ff574bc6a7ef9db 1.341 -41d3fb8250000000 1341000000 -4067f6db6db6db6e 191.71428571428572 -3ff578d4fdf3b646 1.342 -41d3ff52e0000000 1342000000 -4067fb6db6db6db7 191.85714285714286 -3ff57ced916872b0 1.343 -41d4032370000000 1343000000 -4068000000000000 192 -3ff5810624dd2f1b 1.344 -41d406f400000000 1344000000 -4068049249249249 192.14285714285714 -3ff5851eb851eb85 1.345 -41d40ac490000000 1345000000 -4068092492492492 192.28571428571428 -3ff589374bc6a7f0 1.346 -41d40e9520000000 1346000000 -40680db6db6db6db 192.42857142857142 -3ff58d4fdf3b645a 1.347 -41d41265b0000000 1347000000 -4068124924924925 192.57142857142858 -3ff5916872b020c5 1.348 -41d4163640000000 1348000000 -406816db6db6db6e 192.71428571428572 -3ff595810624dd2f 1.349 -41d41a06d0000000 1349000000 -40681b6db6db6db7 192.85714285714286 -3ff599999999999a 1.35 -41d41dd760000000 1350000000 -4068200000000000 193 -3ff59db22d0e5604 1.351 -41d421a7f0000000 1351000000 -4068249249249249 193.14285714285714 -3ff5a1cac083126f 1.352 -41d4257880000000 1352000000 -4068292492492492 193.28571428571428 -3ff5a5e353f7ced9 1.353 -41d4294910000000 1353000000 -40682db6db6db6db 193.42857142857142 -3ff5a9fbe76c8b44 1.354 -41d42d19a0000000 1354000000 -4068324924924925 193.57142857142858 -3ff5ae147ae147ae 1.355 -41d430ea30000000 1355000000 -406836db6db6db6e 193.71428571428572 -3ff5b22d0e560419 1.356 -41d434bac0000000 1356000000 -40683b6db6db6db7 193.85714285714286 -3ff5b645a1cac083 1.357 -41d4388b50000000 1357000000 -4068400000000000 194 -3ff5ba5e353f7cee 1.358 -41d43c5be0000000 1358000000 -4068449249249249 194.14285714285714 -3ff5be76c8b43958 1.359 -41d4402c70000000 1359000000 -4068492492492492 194.28571428571428 -3ff5c28f5c28f5c3 1.36 -41d443fd00000000 1360000000 -40684db6db6db6db 194.42857142857142 -3ff5c6a7ef9db22d 1.361 -41d447cd90000000 1361000000 -4068524924924925 194.57142857142858 -3ff5cac083126e98 1.362 -41d44b9e20000000 1362000000 -406856db6db6db6e 194.71428571428572 -3ff5ced916872b02 1.363 -41d44f6eb0000000 1363000000 -40685b6db6db6db7 194.85714285714286 -3ff5d2f1a9fbe76d 1.364 -41d4533f40000000 1364000000 -4068600000000000 195 -3ff5d70a3d70a3d7 1.365 -41d4570fd0000000 1365000000 -4068649249249249 195.14285714285714 -3ff5db22d0e56042 1.366 -41d45ae060000000 1366000000 -4068692492492492 195.28571428571428 -3ff5df3b645a1cac 1.367 -41d45eb0f0000000 1367000000 -40686db6db6db6db 195.42857142857142 -3ff5e353f7ced917 1.368 -41d4628180000000 1368000000 -4068724924924925 195.57142857142858 -3ff5e76c8b439581 1.369 -41d4665210000000 1369000000 -406876db6db6db6e 195.71428571428572 -3ff5eb851eb851ec 1.37 -41d46a22a0000000 1370000000 -40687b6db6db6db7 195.85714285714286 -3ff5ef9db22d0e56 1.371 -41d46df330000000 1371000000 -4068800000000000 196 -3ff5f3b645a1cac1 1.372 -41d471c3c0000000 1372000000 -4068849249249249 196.14285714285714 -3ff5f7ced916872b 1.373 -41d4759450000000 1373000000 -4068892492492492 196.28571428571428 -3ff5fbe76c8b4396 1.374 -41d47964e0000000 1374000000 -40688db6db6db6db 196.42857142857142 -3ff6000000000000 1.375 -41d47d3570000000 1375000000 -4068924924924925 196.57142857142858 -3ff604189374bc6b 1.3760000000000001 -41d4810600000000 1376000000 -406896db6db6db6e 196.71428571428572 -3ff6083126e978d5 1.377 -41d484d690000000 1377000000 -40689b6db6db6db7 196.85714285714286 -3ff60c49ba5e3540 1.3780000000000001 -41d488a720000000 1378000000 -4068a00000000000 197 -3ff610624dd2f1aa 1.379 -41d48c77b0000000 1379000000 -4068a49249249249 197.14285714285714 -3ff6147ae147ae15 1.3800000000000001 -41d4904840000000 1380000000 -4068a92492492492 197.28571428571428 -3ff6189374bc6a7f 1.381 -41d49418d0000000 1381000000 -4068adb6db6db6db 197.42857142857142 -3ff61cac083126ea 1.3820000000000001 -41d497e960000000 1382000000 -4068b24924924925 197.57142857142858 -3ff620c49ba5e354 1.383 -41d49bb9f0000000 1383000000 -4068b6db6db6db6e 197.71428571428572 -3ff624dd2f1a9fbf 1.3840000000000001 -41d49f8a80000000 1384000000 -4068bb6db6db6db7 197.85714285714286 -3ff628f5c28f5c29 1.385 -41d4a35b10000000 1385000000 -4068c00000000000 198 -3ff62d0e56041894 1.3860000000000001 -41d4a72ba0000000 1386000000 -4068c49249249249 198.14285714285714 -3ff63126e978d4fe 1.387 -41d4aafc30000000 1387000000 -4068c92492492492 198.28571428571428 -3ff6353f7ced9169 1.3880000000000001 -41d4aeccc0000000 1388000000 -4068cdb6db6db6db 198.42857142857142 -3ff6395810624dd3 1.389 -41d4b29d50000000 1389000000 -4068d24924924925 198.57142857142858 -3ff63d70a3d70a3e 1.3900000000000001 -41d4b66de0000000 1390000000 -4068d6db6db6db6e 198.71428571428572 -3ff64189374bc6a8 1.391 -41d4ba3e70000000 1391000000 -4068db6db6db6db7 198.85714285714286 -3ff645a1cac08313 1.3920000000000001 -41d4be0f00000000 1392000000 -4068e00000000000 199 -3ff649ba5e353f7d 1.393 -41d4c1df90000000 1393000000 -4068e49249249249 199.14285714285714 -3ff64dd2f1a9fbe8 1.3940000000000001 -41d4c5b020000000 1394000000 -4068e92492492492 199.28571428571428 -3ff651eb851eb852 1.395 -41d4c980b0000000 1395000000 -4068edb6db6db6db 199.42857142857142 -3ff65604189374bd 1.3960000000000001 -41d4cd5140000000 1396000000 -4068f24924924925 199.57142857142858 -3ff65a1cac083127 1.397 -41d4d121d0000000 1397000000 -4068f6db6db6db6e 199.71428571428572 -3ff65e353f7ced92 1.3980000000000001 -41d4d4f260000000 1398000000 -4068fb6db6db6db7 199.85714285714286 -3ff6624dd2f1a9fc 1.399 -41d4d8c2f0000000 1399000000 -4069000000000000 200 -3ff6666666666667 1.4000000000000001 -41d4dc9380000000 1400000000 -4069049249249249 200.14285714285714 -3ff66a7ef9db22d1 1.401 -41d4e06410000000 1401000000 -4069092492492492 200.28571428571428 -3ff66e978d4fdf3c 1.4020000000000001 -41d4e434a0000000 1402000000 -40690db6db6db6db 200.42857142857142 -3ff672b020c49ba6 1.403 -41d4e80530000000 1403000000 -4069124924924925 200.57142857142858 -3ff676c8b4395811 1.4040000000000001 -41d4ebd5c0000000 1404000000 -406916db6db6db6e 200.71428571428572 -3ff67ae147ae147b 1.405 -41d4efa650000000 1405000000 -40691b6db6db6db7 200.85714285714286 -3ff67ef9db22d0e6 1.4060000000000001 -41d4f376e0000000 1406000000 -4069200000000000 201 -3ff683126e978d50 1.407 -41d4f74770000000 1407000000 -4069249249249249 201.14285714285714 -3ff6872b020c49ba 1.408 -41d4fb1800000000 1408000000 -4069292492492492 201.28571428571428 -3ff68b4395810625 1.409 -41d4fee890000000 1409000000 -40692db6db6db6db 201.42857142857142 -3ff68f5c28f5c28f 1.41 -41d502b920000000 1410000000 -4069324924924925 201.57142857142858 -3ff69374bc6a7efa 1.411 -41d50689b0000000 1411000000 -406936db6db6db6e 201.71428571428572 -3ff6978d4fdf3b64 1.412 -41d50a5a40000000 1412000000 -40693b6db6db6db7 201.85714285714286 -3ff69ba5e353f7cf 1.413 -41d50e2ad0000000 1413000000 -4069400000000000 202 -3ff69fbe76c8b439 1.414 -41d511fb60000000 1414000000 -4069449249249249 202.14285714285714 -3ff6a3d70a3d70a4 1.415 -41d515cbf0000000 1415000000 -4069492492492492 202.28571428571428 -3ff6a7ef9db22d0e 1.416 -41d5199c80000000 1416000000 -40694db6db6db6db 202.42857142857142 -3ff6ac083126e979 1.417 -41d51d6d10000000 1417000000 -4069524924924925 202.57142857142858 -3ff6b020c49ba5e3 1.418 -41d5213da0000000 1418000000 -406956db6db6db6e 202.71428571428572 -3ff6b4395810624e 1.419 -41d5250e30000000 1419000000 -40695b6db6db6db7 202.85714285714286 -3ff6b851eb851eb8 1.42 -41d528dec0000000 1420000000 -4069600000000000 203 -3ff6bc6a7ef9db23 1.421 -41d52caf50000000 1421000000 -4069649249249249 203.14285714285714 -3ff6c083126e978d 1.422 -41d5307fe0000000 1422000000 -4069692492492492 203.28571428571428 -3ff6c49ba5e353f8 1.423 -41d5345070000000 1423000000 -40696db6db6db6db 203.42857142857142 -3ff6c8b439581062 1.424 -41d5382100000000 1424000000 -4069724924924925 203.57142857142858 -3ff6cccccccccccd 1.425 -41d53bf190000000 1425000000 -406976db6db6db6e 203.71428571428572 -3ff6d0e560418937 1.426 -41d53fc220000000 1426000000 -40697b6db6db6db7 203.85714285714286 -3ff6d4fdf3b645a2 1.427 -41d54392b0000000 1427000000 -4069800000000000 204 -3ff6d916872b020c 1.428 -41d5476340000000 1428000000 -4069849249249249 204.14285714285714 -3ff6dd2f1a9fbe77 1.429 -41d54b33d0000000 1429000000 -4069892492492492 204.28571428571428 -3ff6e147ae147ae1 1.43 -41d54f0460000000 1430000000 -40698db6db6db6db 204.42857142857142 -3ff6e5604189374c 1.431 -41d552d4f0000000 1431000000 -4069924924924925 204.57142857142858 -3ff6e978d4fdf3b6 1.432 -41d556a580000000 1432000000 -406996db6db6db6e 204.71428571428572 -3ff6ed916872b021 1.433 -41d55a7610000000 1433000000 -40699b6db6db6db7 204.85714285714286 -3ff6f1a9fbe76c8b 1.434 -41d55e46a0000000 1434000000 -4069a00000000000 205 -3ff6f5c28f5c28f6 1.435 -41d5621730000000 1435000000 -4069a49249249249 205.14285714285714 -3ff6f9db22d0e560 1.436 -41d565e7c0000000 1436000000 -4069a92492492492 205.28571428571428 -3ff6fdf3b645a1cb 1.437 -41d569b850000000 1437000000 -4069adb6db6db6db 205.42857142857142 -3ff7020c49ba5e35 1.438 -41d56d88e0000000 1438000000 -4069b24924924925 205.57142857142858 -3ff70624dd2f1aa0 1.439 -41d5715970000000 1439000000 -4069b6db6db6db6e 205.71428571428572 -3ff70a3d70a3d70a 1.44 -41d5752a00000000 1440000000 -4069bb6db6db6db7 205.85714285714286 -3ff70e5604189375 1.441 -41d578fa90000000 1441000000 -4069c00000000000 206 -3ff7126e978d4fdf 1.442 -41d57ccb20000000 1442000000 -4069c49249249249 206.14285714285714 -3ff716872b020c4a 1.443 -41d5809bb0000000 1443000000 -4069c92492492492 206.28571428571428 -3ff71a9fbe76c8b4 1.444 -41d5846c40000000 1444000000 -4069cdb6db6db6db 206.42857142857142 -3ff71eb851eb851f 1.445 -41d5883cd0000000 1445000000 -4069d24924924925 206.57142857142858 -3ff722d0e5604189 1.446 -41d58c0d60000000 1446000000 -4069d6db6db6db6e 206.71428571428572 -3ff726e978d4fdf4 1.447 -41d58fddf0000000 1447000000 -4069db6db6db6db7 206.85714285714286 -3ff72b020c49ba5e 1.448 -41d593ae80000000 1448000000 -4069e00000000000 207 -3ff72f1a9fbe76c9 1.449 -41d5977f10000000 1449000000 -4069e49249249249 207.14285714285714 -3ff7333333333333 1.45 -41d59b4fa0000000 1450000000 -4069e92492492492 207.28571428571428 -3ff7374bc6a7ef9e 1.451 -41d59f2030000000 1451000000 -4069edb6db6db6db 207.42857142857142 -3ff73b645a1cac08 1.452 -41d5a2f0c0000000 1452000000 -4069f24924924925 207.57142857142858 -3ff73f7ced916873 1.453 -41d5a6c150000000 1453000000 -4069f6db6db6db6e 207.71428571428572 -3ff74395810624dd 1.454 -41d5aa91e0000000 1454000000 -4069fb6db6db6db7 207.85714285714286 -3ff747ae147ae148 1.455 -41d5ae6270000000 1455000000 -406a000000000000 208 -3ff74bc6a7ef9db2 1.456 -41d5b23300000000 1456000000 -406a049249249249 208.14285714285714 -3ff74fdf3b645a1d 1.457 -41d5b60390000000 1457000000 -406a092492492492 208.28571428571428 -3ff753f7ced91687 1.458 -41d5b9d420000000 1458000000 -406a0db6db6db6db 208.42857142857142 -3ff75810624dd2f2 1.459 -41d5bda4b0000000 1459000000 -406a124924924925 208.57142857142858 -3ff75c28f5c28f5c 1.46 -41d5c17540000000 1460000000 -406a16db6db6db6e 208.71428571428572 -3ff7604189374bc7 1.461 -41d5c545d0000000 1461000000 -406a1b6db6db6db7 208.85714285714286 -3ff7645a1cac0831 1.462 -41d5c91660000000 1462000000 -406a200000000000 209 -3ff76872b020c49c 1.463 -41d5cce6f0000000 1463000000 -406a249249249249 209.14285714285714 -3ff76c8b43958106 1.464 -41d5d0b780000000 1464000000 -406a292492492492 209.28571428571428 -3ff770a3d70a3d71 1.465 -41d5d48810000000 1465000000 -406a2db6db6db6db 209.42857142857142 -3ff774bc6a7ef9db 1.466 -41d5d858a0000000 1466000000 -406a324924924925 209.57142857142858 -3ff778d4fdf3b646 1.467 -41d5dc2930000000 1467000000 -406a36db6db6db6e 209.71428571428572 -3ff77ced916872b0 1.468 -41d5dff9c0000000 1468000000 -406a3b6db6db6db7 209.85714285714286 -3ff7810624dd2f1b 1.469 -41d5e3ca50000000 1469000000 -406a400000000000 210 -3ff7851eb851eb85 1.47 -41d5e79ae0000000 1470000000 -406a449249249249 210.14285714285714 -3ff789374bc6a7f0 1.471 -41d5eb6b70000000 1471000000 -406a492492492492 210.28571428571428 -3ff78d4fdf3b645a 1.472 -41d5ef3c00000000 1472000000 -406a4db6db6db6db 210.42857142857142 -3ff7916872b020c5 1.473 -41d5f30c90000000 1473000000 -406a524924924925 210.57142857142858 -3ff795810624dd2f 1.474 -41d5f6dd20000000 1474000000 -406a56db6db6db6e 210.71428571428572 -3ff799999999999a 1.475 -41d5faadb0000000 1475000000 -406a5b6db6db6db7 210.85714285714286 -3ff79db22d0e5604 1.476 -41d5fe7e40000000 1476000000 -406a600000000000 211 -3ff7a1cac083126f 1.477 -41d6024ed0000000 1477000000 -406a649249249249 211.14285714285714 -3ff7a5e353f7ced9 1.478 -41d6061f60000000 1478000000 -406a692492492492 211.28571428571428 -3ff7a9fbe76c8b44 1.479 -41d609eff0000000 1479000000 -406a6db6db6db6db 211.42857142857142 -3ff7ae147ae147ae 1.48 -41d60dc080000000 1480000000 -406a724924924925 211.57142857142858 -3ff7b22d0e560419 1.481 -41d6119110000000 1481000000 -406a76db6db6db6e 211.71428571428572 -3ff7b645a1cac083 1.482 -41d61561a0000000 1482000000 -406a7b6db6db6db7 211.85714285714286 -3ff7ba5e353f7cee 1.483 -41d6193230000000 1483000000 -406a800000000000 212 -3ff7be76c8b43958 1.484 -41d61d02c0000000 1484000000 -406a849249249249 212.14285714285714 -3ff7c28f5c28f5c3 1.485 -41d620d350000000 1485000000 -406a892492492492 212.28571428571428 -3ff7c6a7ef9db22d 1.486 -41d624a3e0000000 1486000000 -406a8db6db6db6db 212.42857142857142 -3ff7cac083126e98 1.487 -41d6287470000000 1487000000 -406a924924924925 212.57142857142858 -3ff7ced916872b02 1.488 -41d62c4500000000 1488000000 -406a96db6db6db6e 212.71428571428572 -3ff7d2f1a9fbe76d 1.489 -41d6301590000000 1489000000 -406a9b6db6db6db7 212.85714285714286 -3ff7d70a3d70a3d7 1.49 -41d633e620000000 1490000000 -406aa00000000000 213 -3ff7db22d0e56042 1.491 -41d637b6b0000000 1491000000 -406aa49249249249 213.14285714285714 -3ff7df3b645a1cac 1.492 -41d63b8740000000 1492000000 -406aa92492492492 213.28571428571428 -3ff7e353f7ced917 1.493 -41d63f57d0000000 1493000000 -406aadb6db6db6db 213.42857142857142 -3ff7e76c8b439581 1.494 -41d6432860000000 1494000000 -406ab24924924925 213.57142857142858 -3ff7eb851eb851ec 1.495 -41d646f8f0000000 1495000000 -406ab6db6db6db6e 213.71428571428572 -3ff7ef9db22d0e56 1.496 -41d64ac980000000 1496000000 -406abb6db6db6db7 213.85714285714286 -3ff7f3b645a1cac1 1.497 -41d64e9a10000000 1497000000 -406ac00000000000 214 -3ff7f7ced916872b 1.498 -41d6526aa0000000 1498000000 -406ac49249249249 214.14285714285714 -3ff7fbe76c8b4396 1.499 -41d6563b30000000 1499000000 -406ac92492492492 214.28571428571428 -3ff8000000000000 1.5 -41d65a0bc0000000 1500000000 -406acdb6db6db6db 214.42857142857142 -3ff804189374bc6b 1.5010000000000001 -41d65ddc50000000 1501000000 -406ad24924924925 214.57142857142858 -3ff8083126e978d5 1.502 -41d661ace0000000 1502000000 -406ad6db6db6db6e 214.71428571428572 -3ff80c49ba5e3540 1.5030000000000001 -41d6657d70000000 1503000000 -406adb6db6db6db7 214.85714285714286 -3ff810624dd2f1aa 1.504 -41d6694e00000000 1504000000 -406ae00000000000 215 -3ff8147ae147ae15 1.5050000000000001 -41d66d1e90000000 1505000000 -406ae49249249249 215.14285714285714 -3ff8189374bc6a7f 1.506 -41d670ef20000000 1506000000 -406ae92492492492 215.28571428571428 -3ff81cac083126ea 1.5070000000000001 -41d674bfb0000000 1507000000 -406aedb6db6db6db 215.42857142857142 -3ff820c49ba5e354 1.508 -41d6789040000000 1508000000 -406af24924924925 215.57142857142858 -3ff824dd2f1a9fbf 1.5090000000000001 -41d67c60d0000000 1509000000 -406af6db6db6db6e 215.71428571428572 -3ff828f5c28f5c29 1.51 -41d6803160000000 1510000000 -406afb6db6db6db7 215.85714285714286 -3ff82d0e56041894 1.5110000000000001 -41d68401f0000000 1511000000 -406b000000000000 216 -3ff83126e978d4fe 1.512 -41d687d280000000 1512000000 -406b049249249249 216.14285714285714 -3ff8353f7ced9169 1.5130000000000001 -41d68ba310000000 1513000000 -406b092492492492 216.28571428571428 -3ff8395810624dd3 1.514 -41d68f73a0000000 1514000000 -406b0db6db6db6db 216.42857142857142 -3ff83d70a3d70a3e 1.5150000000000001 -41d6934430000000 1515000000 -406b124924924925 216.57142857142858 -3ff84189374bc6a8 1.516 -41d69714c0000000 1516000000 -406b16db6db6db6e 216.71428571428572 -3ff845a1cac08313 1.5170000000000001 -41d69ae550000000 1517000000 -406b1b6db6db6db7 216.85714285714286 -3ff849ba5e353f7d 1.518 -41d69eb5e0000000 1518000000 -406b200000000000 217 -3ff84dd2f1a9fbe8 1.5190000000000001 -41d6a28670000000 1519000000 -406b249249249249 217.14285714285714 -3ff851eb851eb852 1.52 -41d6a65700000000 1520000000 -406b292492492492 217.28571428571428 -3ff85604189374bd 1.5210000000000001 -41d6aa2790000000 1521000000 -406b2db6db6db6db 217.42857142857142 -3ff85a1cac083127 1.522 -41d6adf820000000 1522000000 -406b324924924925 217.57142857142858 -3ff85e353f7ced92 1.5230000000000001 -41d6b1c8b0000000 1523000000 -406b36db6db6db6e 217.71428571428572 -3ff8624dd2f1a9fc 1.524 -41d6b59940000000 1524000000 -406b3b6db6db6db7 217.85714285714286 -3ff8666666666667 1.5250000000000001 -41d6b969d0000000 1525000000 -406b400000000000 218 -3ff86a7ef9db22d1 1.526 -41d6bd3a60000000 1526000000 -406b449249249249 218.14285714285714 -3ff86e978d4fdf3c 1.5270000000000001 -41d6c10af0000000 1527000000 -406b492492492492 218.28571428571428 -3ff872b020c49ba6 1.528 -41d6c4db80000000 1528000000 -406b4db6db6db6db 218.42857142857142 -3ff876c8b4395811 1.5290000000000001 -41d6c8ac10000000 1529000000 -406b524924924925 218.57142857142858 -3ff87ae147ae147b 1.53 -41d6cc7ca0000000 1530000000 -406b56db6db6db6e 218.71428571428572 -3ff87ef9db22d0e6 1.5310000000000001 -41d6d04d30000000 1531000000 -406b5b6db6db6db7 218.85714285714286 -3ff883126e978d50 1.532 -41d6d41dc0000000 1532000000 -406b600000000000 219 -3ff8872b020c49bb 1.5330000000000001 -41d6d7ee50000000 1533000000 -406b649249249249 219.14285714285714 -3ff88b4395810625 1.534 -41d6dbbee0000000 1534000000 -406b692492492492 219.28571428571428 -3ff88f5c28f5c290 1.5350000000000001 -41d6df8f70000000 1535000000 -406b6db6db6db6db 219.42857142857142 -3ff89374bc6a7efa 1.536 -41d6e36000000000 1536000000 -406b724924924925 219.57142857142858 -3ff8978d4fdf3b64 1.537 -41d6e73090000000 1537000000 -406b76db6db6db6e 219.71428571428572 -3ff89ba5e353f7cf 1.538 -41d6eb0120000000 1538000000 -406b7b6db6db6db7 219.85714285714286 -3ff89fbe76c8b439 1.539 -41d6eed1b0000000 1539000000 -406b800000000000 220 -3ff8a3d70a3d70a4 1.54 -41d6f2a240000000 1540000000 -406b849249249249 220.14285714285714 -3ff8a7ef9db22d0e 1.541 -41d6f672d0000000 1541000000 -406b892492492492 220.28571428571428 -3ff8ac083126e979 1.542 -41d6fa4360000000 1542000000 -406b8db6db6db6db 220.42857142857142 -3ff8b020c49ba5e3 1.543 -41d6fe13f0000000 1543000000 -406b924924924925 220.57142857142858 -3ff8b4395810624e 1.544 -41d701e480000000 1544000000 -406b96db6db6db6e 220.71428571428572 -3ff8b851eb851eb8 1.545 -41d705b510000000 1545000000 -406b9b6db6db6db7 220.85714285714286 -3ff8bc6a7ef9db23 1.546 -41d70985a0000000 1546000000 -406ba00000000000 221 -3ff8c083126e978d 1.547 -41d70d5630000000 1547000000 -406ba49249249249 221.14285714285714 -3ff8c49ba5e353f8 1.548 -41d71126c0000000 1548000000 -406ba92492492492 221.28571428571428 -3ff8c8b439581062 1.549 -41d714f750000000 1549000000 -406badb6db6db6db 221.42857142857142 -3ff8cccccccccccd 1.55 -41d718c7e0000000 1550000000 -406bb24924924925 221.57142857142858 -3ff8d0e560418937 1.551 -41d71c9870000000 1551000000 -406bb6db6db6db6e 221.71428571428572 -3ff8d4fdf3b645a2 1.552 -41d7206900000000 1552000000 -406bbb6db6db6db7 221.85714285714286 -3ff8d916872b020c 1.553 -41d7243990000000 1553000000 -406bc00000000000 222 -3ff8dd2f1a9fbe77 1.554 -41d7280a20000000 1554000000 -406bc49249249249 222.14285714285714 -3ff8e147ae147ae1 1.555 -41d72bdab0000000 1555000000 -406bc92492492492 222.28571428571428 -3ff8e5604189374c 1.556 -41d72fab40000000 1556000000 -406bcdb6db6db6db 222.42857142857142 -3ff8e978d4fdf3b6 1.557 -41d7337bd0000000 1557000000 -406bd24924924925 222.57142857142858 -3ff8ed916872b021 1.558 -41d7374c60000000 1558000000 -406bd6db6db6db6e 222.71428571428572 -3ff8f1a9fbe76c8b 1.559 -41d73b1cf0000000 1559000000 -406bdb6db6db6db7 222.85714285714286 -3ff8f5c28f5c28f6 1.56 -41d73eed80000000 1560000000 -406be00000000000 223 -3ff8f9db22d0e560 1.561 -41d742be10000000 1561000000 -406be49249249249 223.14285714285714 -3ff8fdf3b645a1cb 1.562 -41d7468ea0000000 1562000000 -406be92492492492 223.28571428571428 -3ff9020c49ba5e35 1.563 -41d74a5f30000000 1563000000 -406bedb6db6db6db 223.42857142857142 -3ff90624dd2f1aa0 1.564 -41d74e2fc0000000 1564000000 -406bf24924924925 223.57142857142858 -3ff90a3d70a3d70a 1.565 -41d7520050000000 1565000000 -406bf6db6db6db6e 223.71428571428572 -3ff90e5604189375 1.566 -41d755d0e0000000 1566000000 -406bfb6db6db6db7 223.85714285714286 -3ff9126e978d4fdf 1.567 -41d759a170000000 1567000000 -406c000000000000 224 -3ff916872b020c4a 1.568 -41d75d7200000000 1568000000 -406c049249249249 224.14285714285714 -3ff91a9fbe76c8b4 1.569 -41d7614290000000 1569000000 -406c092492492492 224.28571428571428 -3ff91eb851eb851f 1.57 -41d7651320000000 1570000000 -406c0db6db6db6db 224.42857142857142 -3ff922d0e5604189 1.571 -41d768e3b0000000 1571000000 -406c124924924925 224.57142857142858 -3ff926e978d4fdf4 1.572 -41d76cb440000000 1572000000 -406c16db6db6db6e 224.71428571428572 -3ff92b020c49ba5e 1.573 -41d77084d0000000 1573000000 -406c1b6db6db6db7 224.85714285714286 -3ff92f1a9fbe76c9 1.574 -41d7745560000000 1574000000 -406c200000000000 225 -3ff9333333333333 1.575 -41d77825f0000000 1575000000 -406c249249249249 225.14285714285714 -3ff9374bc6a7ef9e 1.576 -41d77bf680000000 1576000000 -406c292492492492 225.28571428571428 -3ff93b645a1cac08 1.577 -41d77fc710000000 1577000000 -406c2db6db6db6db 225.42857142857142 -3ff93f7ced916873 1.578 -41d78397a0000000 1578000000 -406c324924924925 225.57142857142858 -3ff94395810624dd 1.579 -41d7876830000000 1579000000 -406c36db6db6db6e 225.71428571428572 -3ff947ae147ae148 1.58 -41d78b38c0000000 1580000000 -406c3b6db6db6db7 225.85714285714286 -3ff94bc6a7ef9db2 1.581 -41d78f0950000000 1581000000 -406c400000000000 226 -3ff94fdf3b645a1d 1.582 -41d792d9e0000000 1582000000 -406c449249249249 226.14285714285714 -3ff953f7ced91687 1.583 -41d796aa70000000 1583000000 -406c492492492492 226.28571428571428 -3ff95810624dd2f2 1.584 -41d79a7b00000000 1584000000 -406c4db6db6db6db 226.42857142857142 -3ff95c28f5c28f5c 1.585 -41d79e4b90000000 1585000000 -406c524924924925 226.57142857142858 -3ff9604189374bc7 1.586 -41d7a21c20000000 1586000000 -406c56db6db6db6e 226.71428571428572 -3ff9645a1cac0831 1.587 -41d7a5ecb0000000 1587000000 -406c5b6db6db6db7 226.85714285714286 -3ff96872b020c49c 1.588 -41d7a9bd40000000 1588000000 -406c600000000000 227 -3ff96c8b43958106 1.589 -41d7ad8dd0000000 1589000000 -406c649249249249 227.14285714285714 -3ff970a3d70a3d71 1.59 -41d7b15e60000000 1590000000 -406c692492492492 227.28571428571428 -3ff974bc6a7ef9db 1.591 -41d7b52ef0000000 1591000000 -406c6db6db6db6db 227.42857142857142 -3ff978d4fdf3b646 1.592 -41d7b8ff80000000 1592000000 -406c724924924925 227.57142857142858 -3ff97ced916872b0 1.593 -41d7bcd010000000 1593000000 -406c76db6db6db6e 227.71428571428572 -3ff9810624dd2f1b 1.594 -41d7c0a0a0000000 1594000000 -406c7b6db6db6db7 227.85714285714286 -3ff9851eb851eb85 1.595 -41d7c47130000000 1595000000 -406c800000000000 228 -3ff989374bc6a7f0 1.596 -41d7c841c0000000 1596000000 -406c849249249249 228.14285714285714 -3ff98d4fdf3b645a 1.597 -41d7cc1250000000 1597000000 -406c892492492492 228.28571428571428 -3ff9916872b020c5 1.598 -41d7cfe2e0000000 1598000000 -406c8db6db6db6db 228.42857142857142 -3ff995810624dd2f 1.599 -41d7d3b370000000 1599000000 -406c924924924925 228.57142857142858 -3ff999999999999a 1.6 -41d7d78400000000 1600000000 -406c96db6db6db6e 228.71428571428572 -3ff99db22d0e5604 1.601 -41d7db5490000000 1601000000 -406c9b6db6db6db7 228.85714285714286 -3ff9a1cac083126f 1.602 -41d7df2520000000 1602000000 -406ca00000000000 229 -3ff9a5e353f7ced9 1.603 -41d7e2f5b0000000 1603000000 -406ca49249249249 229.14285714285714 -3ff9a9fbe76c8b44 1.604 -41d7e6c640000000 1604000000 -406ca92492492492 229.28571428571428 -3ff9ae147ae147ae 1.605 -41d7ea96d0000000 1605000000 -406cadb6db6db6db 229.42857142857142 -3ff9b22d0e560419 1.606 -41d7ee6760000000 1606000000 -406cb24924924925 229.57142857142858 -3ff9b645a1cac083 1.607 -41d7f237f0000000 1607000000 -406cb6db6db6db6e 229.71428571428572 -3ff9ba5e353f7cee 1.608 -41d7f60880000000 1608000000 -406cbb6db6db6db7 229.85714285714286 -3ff9be76c8b43958 1.609 -41d7f9d910000000 1609000000 -406cc00000000000 230 -3ff9c28f5c28f5c3 1.61 -41d7fda9a0000000 1610000000 -406cc49249249249 230.14285714285714 -3ff9c6a7ef9db22d 1.611 -41d8017a30000000 1611000000 -406cc92492492492 230.28571428571428 -3ff9cac083126e98 1.612 -41d8054ac0000000 1612000000 -406ccdb6db6db6db 230.42857142857142 -3ff9ced916872b02 1.613 -41d8091b50000000 1613000000 -406cd24924924925 230.57142857142858 -3ff9d2f1a9fbe76d 1.614 -41d80cebe0000000 1614000000 -406cd6db6db6db6e 230.71428571428572 -3ff9d70a3d70a3d7 1.615 -41d810bc70000000 1615000000 -406cdb6db6db6db7 230.85714285714286 -3ff9db22d0e56042 1.616 -41d8148d00000000 1616000000 -406ce00000000000 231 -3ff9df3b645a1cac 1.617 -41d8185d90000000 1617000000 -406ce49249249249 231.14285714285714 -3ff9e353f7ced917 1.618 -41d81c2e20000000 1618000000 -406ce92492492492 231.28571428571428 -3ff9e76c8b439581 1.619 -41d81ffeb0000000 1619000000 -406cedb6db6db6db 231.42857142857142 -3ff9eb851eb851ec 1.62 -41d823cf40000000 1620000000 -406cf24924924925 231.57142857142858 -3ff9ef9db22d0e56 1.621 -41d8279fd0000000 1621000000 -406cf6db6db6db6e 231.71428571428572 -3ff9f3b645a1cac1 1.622 -41d82b7060000000 1622000000 -406cfb6db6db6db7 231.85714285714286 -3ff9f7ced916872b 1.623 -41d82f40f0000000 1623000000 -406d000000000000 232 -3ff9fbe76c8b4396 1.624 -41d8331180000000 1624000000 -406d049249249249 232.14285714285714 -3ffa000000000000 1.625 -41d836e210000000 1625000000 -406d092492492492 232.28571428571428 -3ffa04189374bc6b 1.6260000000000001 -41d83ab2a0000000 1626000000 -406d0db6db6db6db 232.42857142857142 -3ffa083126e978d5 1.627 -41d83e8330000000 1627000000 -406d124924924925 232.57142857142858 -3ffa0c49ba5e3540 1.6280000000000001 -41d84253c0000000 1628000000 -406d16db6db6db6e 232.71428571428572 -3ffa10624dd2f1aa 1.629 -41d8462450000000 1629000000 -406d1b6db6db6db7 232.85714285714286 -3ffa147ae147ae15 1.6300000000000001 -41d849f4e0000000 1630000000 -406d200000000000 233 -3ffa189374bc6a7f 1.631 -41d84dc570000000 1631000000 -406d249249249249 233.14285714285714 -3ffa1cac083126ea 1.6320000000000001 -41d8519600000000 1632000000 -406d292492492492 233.28571428571428 -3ffa20c49ba5e354 1.633 -41d8556690000000 1633000000 -406d2db6db6db6db 233.42857142857142 -3ffa24dd2f1a9fbf 1.6340000000000001 -41d8593720000000 1634000000 -406d324924924925 233.57142857142858 -3ffa28f5c28f5c29 1.635 -41d85d07b0000000 1635000000 -406d36db6db6db6e 233.71428571428572 -3ffa2d0e56041894 1.6360000000000001 -41d860d840000000 1636000000 -406d3b6db6db6db7 233.85714285714286 -3ffa3126e978d4fe 1.637 -41d864a8d0000000 1637000000 -406d400000000000 234 -3ffa353f7ced9169 1.6380000000000001 -41d8687960000000 1638000000 -406d449249249249 234.14285714285714 -3ffa395810624dd3 1.639 -41d86c49f0000000 1639000000 -406d492492492492 234.28571428571428 -3ffa3d70a3d70a3e 1.6400000000000001 -41d8701a80000000 1640000000 -406d4db6db6db6db 234.42857142857142 -3ffa4189374bc6a8 1.641 -41d873eb10000000 1641000000 -406d524924924925 234.57142857142858 -3ffa45a1cac08313 1.6420000000000001 -41d877bba0000000 1642000000 -406d56db6db6db6e 234.71428571428572 -3ffa49ba5e353f7d 1.643 -41d87b8c30000000 1643000000 -406d5b6db6db6db7 234.85714285714286 -3ffa4dd2f1a9fbe8 1.6440000000000001 -41d87f5cc0000000 1644000000 -406d600000000000 235 -3ffa51eb851eb852 1.645 -41d8832d50000000 1645000000 -406d649249249249 235.14285714285714 -3ffa5604189374bd 1.6460000000000001 -41d886fde0000000 1646000000 -406d692492492492 235.28571428571428 -3ffa5a1cac083127 1.647 -41d88ace70000000 1647000000 -406d6db6db6db6db 235.42857142857142 -3ffa5e353f7ced92 1.6480000000000001 -41d88e9f00000000 1648000000 -406d724924924925 235.57142857142858 -3ffa624dd2f1a9fc 1.649 -41d8926f90000000 1649000000 -406d76db6db6db6e 235.71428571428572 -3ffa666666666667 1.6500000000000001 -41d8964020000000 1650000000 -406d7b6db6db6db7 235.85714285714286 -3ffa6a7ef9db22d1 1.651 -41d89a10b0000000 1651000000 -406d800000000000 236 -3ffa6e978d4fdf3c 1.6520000000000001 -41d89de140000000 1652000000 -406d849249249249 236.14285714285714 -3ffa72b020c49ba6 1.653 -41d8a1b1d0000000 1653000000 -406d892492492492 236.28571428571428 -3ffa76c8b4395811 1.6540000000000001 -41d8a58260000000 1654000000 -406d8db6db6db6db 236.42857142857142 -3ffa7ae147ae147b 1.655 -41d8a952f0000000 1655000000 -406d924924924925 236.57142857142858 -3ffa7ef9db22d0e6 1.6560000000000001 -41d8ad2380000000 1656000000 -406d96db6db6db6e 236.71428571428572 -3ffa83126e978d50 1.657 -41d8b0f410000000 1657000000 -406d9b6db6db6db7 236.85714285714286 -3ffa872b020c49bb 1.6580000000000001 -41d8b4c4a0000000 1658000000 -406da00000000000 237 -3ffa8b4395810625 1.659 -41d8b89530000000 1659000000 -406da49249249249 237.14285714285714 -3ffa8f5c28f5c290 1.6600000000000001 -41d8bc65c0000000 1660000000 -406da92492492492 237.28571428571428 -3ffa9374bc6a7efa 1.661 -41d8c03650000000 1661000000 -406dadb6db6db6db 237.42857142857142 -3ffa978d4fdf3b65 1.6620000000000001 -41d8c406e0000000 1662000000 -406db24924924925 237.57142857142858 -3ffa9ba5e353f7cf 1.663 -41d8c7d770000000 1663000000 -406db6db6db6db6e 237.71428571428572 -3ffa9fbe76c8b43a 1.6640000000000001 -41d8cba800000000 1664000000 -406dbb6db6db6db7 237.85714285714286 -3ffaa3d70a3d70a4 1.665 -41d8cf7890000000 1665000000 -406dc00000000000 238 -3ffaa7ef9db22d0e 1.666 -41d8d34920000000 1666000000 -406dc49249249249 238.14285714285714 -3ffaac083126e979 1.667 -41d8d719b0000000 1667000000 -406dc92492492492 238.28571428571428 -3ffab020c49ba5e3 1.668 -41d8daea40000000 1668000000 -406dcdb6db6db6db 238.42857142857142 -3ffab4395810624e 1.669 -41d8debad0000000 1669000000 -406dd24924924925 238.57142857142858 -3ffab851eb851eb8 1.67 -41d8e28b60000000 1670000000 -406dd6db6db6db6e 238.71428571428572 -3ffabc6a7ef9db23 1.671 -41d8e65bf0000000 1671000000 -406ddb6db6db6db7 238.85714285714286 -3ffac083126e978d 1.672 -41d8ea2c80000000 1672000000 -406de00000000000 239 -3ffac49ba5e353f8 1.673 -41d8edfd10000000 1673000000 -406de49249249249 239.14285714285714 -3ffac8b439581062 1.674 -41d8f1cda0000000 1674000000 -406de92492492492 239.28571428571428 -3ffacccccccccccd 1.675 -41d8f59e30000000 1675000000 -406dedb6db6db6db 239.42857142857142 -3ffad0e560418937 1.676 -41d8f96ec0000000 1676000000 -406df24924924925 239.57142857142858 -3ffad4fdf3b645a2 1.677 -41d8fd3f50000000 1677000000 -406df6db6db6db6e 239.71428571428572 -3ffad916872b020c 1.678 -41d9010fe0000000 1678000000 -406dfb6db6db6db7 239.85714285714286 -3ffadd2f1a9fbe77 1.679 -41d904e070000000 1679000000 -406e000000000000 240 -3ffae147ae147ae1 1.68 -41d908b100000000 1680000000 -406e049249249249 240.14285714285714 -3ffae5604189374c 1.681 -41d90c8190000000 1681000000 -406e092492492492 240.28571428571428 -3ffae978d4fdf3b6 1.682 -41d9105220000000 1682000000 -406e0db6db6db6db 240.42857142857142 -3ffaed916872b021 1.683 -41d91422b0000000 1683000000 -406e124924924925 240.57142857142858 -3ffaf1a9fbe76c8b 1.684 -41d917f340000000 1684000000 -406e16db6db6db6e 240.71428571428572 -3ffaf5c28f5c28f6 1.685 -41d91bc3d0000000 1685000000 -406e1b6db6db6db7 240.85714285714286 -3ffaf9db22d0e560 1.686 -41d91f9460000000 1686000000 -406e200000000000 241 -3ffafdf3b645a1cb 1.687 -41d92364f0000000 1687000000 -406e249249249249 241.14285714285714 -3ffb020c49ba5e35 1.688 -41d9273580000000 1688000000 -406e292492492492 241.28571428571428 -3ffb0624dd2f1aa0 1.689 -41d92b0610000000 1689000000 -406e2db6db6db6db 241.42857142857142 -3ffb0a3d70a3d70a 1.69 -41d92ed6a0000000 1690000000 -406e324924924925 241.57142857142858 -3ffb0e5604189375 1.691 -41d932a730000000 1691000000 -406e36db6db6db6e 241.71428571428572 -3ffb126e978d4fdf 1.692 -41d93677c0000000 1692000000 -406e3b6db6db6db7 241.85714285714286 -3ffb16872b020c4a 1.693 -41d93a4850000000 1693000000 -406e400000000000 242 -3ffb1a9fbe76c8b4 1.694 -41d93e18e0000000 1694000000 -406e449249249249 242.14285714285714 -3ffb1eb851eb851f 1.695 -41d941e970000000 1695000000 -406e492492492492 242.28571428571428 -3ffb22d0e5604189 1.696 -41d945ba00000000 1696000000 -406e4db6db6db6db 242.42857142857142 -3ffb26e978d4fdf4 1.697 -41d9498a90000000 1697000000 -406e524924924925 242.57142857142858 -3ffb2b020c49ba5e 1.698 -41d94d5b20000000 1698000000 -406e56db6db6db6e 242.71428571428572 -3ffb2f1a9fbe76c9 1.699 -41d9512bb0000000 1699000000 -406e5b6db6db6db7 242.85714285714286 -3ffb333333333333 1.7 -41d954fc40000000 1700000000 -406e600000000000 243 -3ffb374bc6a7ef9e 1.701 -41d958ccd0000000 1701000000 -406e649249249249 243.14285714285714 -3ffb3b645a1cac08 1.702 -41d95c9d60000000 1702000000 -406e692492492492 243.28571428571428 -3ffb3f7ced916873 1.703 -41d9606df0000000 1703000000 -406e6db6db6db6db 243.42857142857142 -3ffb4395810624dd 1.704 -41d9643e80000000 1704000000 -406e724924924925 243.57142857142858 -3ffb47ae147ae148 1.705 -41d9680f10000000 1705000000 -406e76db6db6db6e 243.71428571428572 -3ffb4bc6a7ef9db2 1.706 -41d96bdfa0000000 1706000000 -406e7b6db6db6db7 243.85714285714286 -3ffb4fdf3b645a1d 1.707 -41d96fb030000000 1707000000 -406e800000000000 244 -3ffb53f7ced91687 1.708 -41d97380c0000000 1708000000 -406e849249249249 244.14285714285714 -3ffb5810624dd2f2 1.709 -41d9775150000000 1709000000 -406e892492492492 244.28571428571428 -3ffb5c28f5c28f5c 1.71 -41d97b21e0000000 1710000000 -406e8db6db6db6db 244.42857142857142 -3ffb604189374bc7 1.711 -41d97ef270000000 1711000000 -406e924924924925 244.57142857142858 -3ffb645a1cac0831 1.712 -41d982c300000000 1712000000 -406e96db6db6db6e 244.71428571428572 -3ffb6872b020c49c 1.713 -41d9869390000000 1713000000 -406e9b6db6db6db7 244.85714285714286 -3ffb6c8b43958106 1.714 -41d98a6420000000 1714000000 -406ea00000000000 245 -3ffb70a3d70a3d71 1.715 -41d98e34b0000000 1715000000 -406ea49249249249 245.14285714285714 -3ffb74bc6a7ef9db 1.716 -41d9920540000000 1716000000 -406ea92492492492 245.28571428571428 -3ffb78d4fdf3b646 1.717 -41d995d5d0000000 1717000000 -406eadb6db6db6db 245.42857142857142 -3ffb7ced916872b0 1.718 -41d999a660000000 1718000000 -406eb24924924925 245.57142857142858 -3ffb810624dd2f1b 1.719 -41d99d76f0000000 1719000000 -406eb6db6db6db6e 245.71428571428572 -3ffb851eb851eb85 1.72 -41d9a14780000000 1720000000 -406ebb6db6db6db7 245.85714285714286 -3ffb89374bc6a7f0 1.721 -41d9a51810000000 1721000000 -406ec00000000000 246 -3ffb8d4fdf3b645a 1.722 -41d9a8e8a0000000 1722000000 -406ec49249249249 246.14285714285714 -3ffb916872b020c5 1.723 -41d9acb930000000 1723000000 -406ec92492492492 246.28571428571428 -3ffb95810624dd2f 1.724 -41d9b089c0000000 1724000000 -406ecdb6db6db6db 246.42857142857142 -3ffb99999999999a 1.725 -41d9b45a50000000 1725000000 -406ed24924924925 246.57142857142858 -3ffb9db22d0e5604 1.726 -41d9b82ae0000000 1726000000 -406ed6db6db6db6e 246.71428571428572 -3ffba1cac083126f 1.727 -41d9bbfb70000000 1727000000 -406edb6db6db6db7 246.85714285714286 -3ffba5e353f7ced9 1.728 -41d9bfcc00000000 1728000000 -406ee00000000000 247 -3ffba9fbe76c8b44 1.729 -41d9c39c90000000 1729000000 -406ee49249249249 247.14285714285714 -3ffbae147ae147ae 1.73 -41d9c76d20000000 1730000000 -406ee92492492492 247.28571428571428 -3ffbb22d0e560419 1.731 -41d9cb3db0000000 1731000000 -406eedb6db6db6db 247.42857142857142 -3ffbb645a1cac083 1.732 -41d9cf0e40000000 1732000000 -406ef24924924925 247.57142857142858 -3ffbba5e353f7cee 1.733 -41d9d2ded0000000 1733000000 -406ef6db6db6db6e 247.71428571428572 -3ffbbe76c8b43958 1.734 -41d9d6af60000000 1734000000 -406efb6db6db6db7 247.85714285714286 -3ffbc28f5c28f5c3 1.735 -41d9da7ff0000000 1735000000 -406f000000000000 248 -3ffbc6a7ef9db22d 1.736 -41d9de5080000000 1736000000 -406f049249249249 248.14285714285714 -3ffbcac083126e98 1.737 -41d9e22110000000 1737000000 -406f092492492492 248.28571428571428 -3ffbced916872b02 1.738 -41d9e5f1a0000000 1738000000 -406f0db6db6db6db 248.42857142857142 -3ffbd2f1a9fbe76d 1.739 -41d9e9c230000000 1739000000 -406f124924924925 248.57142857142858 -3ffbd70a3d70a3d7 1.74 -41d9ed92c0000000 1740000000 -406f16db6db6db6e 248.71428571428572 -3ffbdb22d0e56042 1.741 -41d9f16350000000 1741000000 -406f1b6db6db6db7 248.85714285714286 -3ffbdf3b645a1cac 1.742 -41d9f533e0000000 1742000000 -406f200000000000 249 -3ffbe353f7ced917 1.743 -41d9f90470000000 1743000000 -406f249249249249 249.14285714285714 -3ffbe76c8b439581 1.744 -41d9fcd500000000 1744000000 -406f292492492492 249.28571428571428 -3ffbeb851eb851ec 1.745 -41da00a590000000 1745000000 -406f2db6db6db6db 249.42857142857142 -3ffbef9db22d0e56 1.746 -41da047620000000 1746000000 -406f324924924925 249.57142857142858 -3ffbf3b645a1cac1 1.747 -41da0846b0000000 1747000000 -406f36db6db6db6e 249.71428571428572 -3ffbf7ced916872b 1.748 -41da0c1740000000 1748000000 -406f3b6db6db6db7 249.85714285714286 -3ffbfbe76c8b4396 1.749 -41da0fe7d0000000 1749000000 -406f400000000000 250 -3ffc000000000000 1.75 -41da13b860000000 1750000000 -406f449249249249 250.14285714285714 -3ffc04189374bc6b 1.7510000000000001 -41da1788f0000000 1751000000 -406f492492492492 250.28571428571428 -3ffc083126e978d5 1.752 -41da1b5980000000 1752000000 -406f4db6db6db6db 250.42857142857142 -3ffc0c49ba5e3540 1.7530000000000001 -41da1f2a10000000 1753000000 -406f524924924925 250.57142857142858 -3ffc10624dd2f1aa 1.754 -41da22faa0000000 1754000000 -406f56db6db6db6e 250.71428571428572 -3ffc147ae147ae15 1.7550000000000001 -41da26cb30000000 1755000000 -406f5b6db6db6db7 250.85714285714286 -3ffc189374bc6a7f 1.756 -41da2a9bc0000000 1756000000 -406f600000000000 251 -3ffc1cac083126ea 1.7570000000000001 -41da2e6c50000000 1757000000 -406f649249249249 251.14285714285714 -3ffc20c49ba5e354 1.758 -41da323ce0000000 1758000000 -406f692492492492 251.28571428571428 -3ffc24dd2f1a9fbf 1.7590000000000001 -41da360d70000000 1759000000 -406f6db6db6db6db 251.42857142857142 -3ffc28f5c28f5c29 1.76 -41da39de00000000 1760000000 -406f724924924925 251.57142857142858 -3ffc2d0e56041894 1.7610000000000001 -41da3dae90000000 1761000000 -406f76db6db6db6e 251.71428571428572 -3ffc3126e978d4fe 1.762 -41da417f20000000 1762000000 -406f7b6db6db6db7 251.85714285714286 -3ffc353f7ced9169 1.7630000000000001 -41da454fb0000000 1763000000 -406f800000000000 252 -3ffc395810624dd3 1.764 -41da492040000000 1764000000 -406f849249249249 252.14285714285714 -3ffc3d70a3d70a3e 1.7650000000000001 -41da4cf0d0000000 1765000000 -406f892492492492 252.28571428571428 -3ffc4189374bc6a8 1.766 -41da50c160000000 1766000000 -406f8db6db6db6db 252.42857142857142 -3ffc45a1cac08313 1.7670000000000001 -41da5491f0000000 1767000000 -406f924924924925 252.57142857142858 -3ffc49ba5e353f7d 1.768 -41da586280000000 1768000000 -406f96db6db6db6e 252.71428571428572 -3ffc4dd2f1a9fbe8 1.7690000000000001 -41da5c3310000000 1769000000 -406f9b6db6db6db7 252.85714285714286 -3ffc51eb851eb852 1.77 -41da6003a0000000 1770000000 -406fa00000000000 253 -3ffc5604189374bd 1.7710000000000001 -41da63d430000000 1771000000 -406fa49249249249 253.14285714285714 -3ffc5a1cac083127 1.772 -41da67a4c0000000 1772000000 -406fa92492492492 253.28571428571428 -3ffc5e353f7ced92 1.7730000000000001 -41da6b7550000000 1773000000 -406fadb6db6db6db 253.42857142857142 -3ffc624dd2f1a9fc 1.774 -41da6f45e0000000 1774000000 -406fb24924924925 253.57142857142858 -3ffc666666666667 1.7750000000000001 -41da731670000000 1775000000 -406fb6db6db6db6e 253.71428571428572 -3ffc6a7ef9db22d1 1.776 -41da76e700000000 1776000000 -406fbb6db6db6db7 253.85714285714286 -3ffc6e978d4fdf3c 1.7770000000000001 -41da7ab790000000 1777000000 -406fc00000000000 254 -3ffc72b020c49ba6 1.778 -41da7e8820000000 1778000000 -406fc49249249249 254.14285714285714 -3ffc76c8b4395811 1.7790000000000001 -41da8258b0000000 1779000000 -406fc92492492492 254.28571428571428 -3ffc7ae147ae147b 1.78 -41da862940000000 1780000000 -406fcdb6db6db6db 254.42857142857142 -3ffc7ef9db22d0e6 1.7810000000000001 -41da89f9d0000000 1781000000 -406fd24924924925 254.57142857142858 -3ffc83126e978d50 1.782 -41da8dca60000000 1782000000 -406fd6db6db6db6e 254.71428571428572 -3ffc872b020c49bb 1.7830000000000001 -41da919af0000000 1783000000 -406fdb6db6db6db7 254.85714285714286 -3ffc8b4395810625 1.784 -41da956b80000000 1784000000 -406fe00000000000 255 -3ffc8f5c28f5c290 1.7850000000000001 -41da993c10000000 1785000000 -406fe49249249249 255.14285714285714 -3ffc9374bc6a7efa 1.786 -41da9d0ca0000000 1786000000 -406fe92492492492 255.28571428571428 -3ffc978d4fdf3b65 1.7870000000000001 -41daa0dd30000000 1787000000 -406fedb6db6db6db 255.42857142857142 -3ffc9ba5e353f7cf 1.788 -41daa4adc0000000 1788000000 -406ff24924924925 255.57142857142858 -3ffc9fbe76c8b43a 1.7890000000000001 -41daa87e50000000 1789000000 -406ff6db6db6db6e 255.71428571428572 -3ffca3d70a3d70a4 1.79 -41daac4ee0000000 1790000000 -406ffb6db6db6db7 255.85714285714286 -3ffca7ef9db22d0f 1.7910000000000001 -41dab01f70000000 1791000000 -4070000000000000 256 -3ffcac083126e979 1.792 -41dab3f000000000 1792000000 -4070024924924925 256.14285714285717 -3ffcb020c49ba5e3 1.793 -41dab7c090000000 1793000000 -4070049249249249 256.2857142857143 -3ffcb4395810624e 1.794 -41dabb9120000000 1794000000 -407006db6db6db6e 256.42857142857144 -3ffcb851eb851eb8 1.795 -41dabf61b0000000 1795000000 -4070092492492492 256.57142857142856 -3ffcbc6a7ef9db23 1.796 -41dac33240000000 1796000000 -40700b6db6db6db7 256.7142857142857 -3ffcc083126e978d 1.797 -41dac702d0000000 1797000000 -40700db6db6db6db 256.85714285714283 -3ffcc49ba5e353f8 1.798 -41dacad360000000 1798000000 -4070100000000000 257 -3ffcc8b439581062 1.799 -41dacea3f0000000 1799000000 -4070124924924925 257.14285714285717 -3ffccccccccccccd 1.8 -41dad27480000000 1800000000 -4070149249249249 257.2857142857143 -3ffcd0e560418937 1.801 -41dad64510000000 1801000000 -407016db6db6db6e 257.42857142857144 -3ffcd4fdf3b645a2 1.802 -41dada15a0000000 1802000000 -4070192492492492 257.57142857142856 -3ffcd916872b020c 1.803 -41dadde630000000 1803000000 -40701b6db6db6db7 257.7142857142857 -3ffcdd2f1a9fbe77 1.804 -41dae1b6c0000000 1804000000 -40701db6db6db6db 257.85714285714283 -3ffce147ae147ae1 1.805 -41dae58750000000 1805000000 -4070200000000000 258 -3ffce5604189374c 1.806 -41dae957e0000000 1806000000 -4070224924924925 258.14285714285717 -3ffce978d4fdf3b6 1.807 -41daed2870000000 1807000000 -4070249249249249 258.2857142857143 -3ffced916872b021 1.808 -41daf0f900000000 1808000000 -407026db6db6db6e 258.42857142857144 -3ffcf1a9fbe76c8b 1.809 -41daf4c990000000 1809000000 -4070292492492492 258.57142857142856 -3ffcf5c28f5c28f6 1.81 -41daf89a20000000 1810000000 -40702b6db6db6db7 258.7142857142857 -3ffcf9db22d0e560 1.811 -41dafc6ab0000000 1811000000 -40702db6db6db6db 258.85714285714283 -3ffcfdf3b645a1cb 1.812 -41db003b40000000 1812000000 -4070300000000000 259 -3ffd020c49ba5e35 1.813 -41db040bd0000000 1813000000 -4070324924924925 259.14285714285717 -3ffd0624dd2f1aa0 1.814 -41db07dc60000000 1814000000 -4070349249249249 259.2857142857143 -3ffd0a3d70a3d70a 1.815 -41db0bacf0000000 1815000000 -407036db6db6db6e 259.42857142857144 -3ffd0e5604189375 1.816 -41db0f7d80000000 1816000000 -4070392492492492 259.57142857142856 -3ffd126e978d4fdf 1.817 -41db134e10000000 1817000000 -40703b6db6db6db7 259.7142857142857 -3ffd16872b020c4a 1.818 -41db171ea0000000 1818000000 -40703db6db6db6db 259.85714285714283 -3ffd1a9fbe76c8b4 1.819 -41db1aef30000000 1819000000 -4070400000000000 260 -3ffd1eb851eb851f 1.82 -41db1ebfc0000000 1820000000 -4070424924924925 260.14285714285717 -3ffd22d0e5604189 1.821 -41db229050000000 1821000000 -4070449249249249 260.2857142857143 -3ffd26e978d4fdf4 1.822 -41db2660e0000000 1822000000 -407046db6db6db6e 260.42857142857144 -3ffd2b020c49ba5e 1.823 -41db2a3170000000 1823000000 -4070492492492492 260.57142857142856 -3ffd2f1a9fbe76c9 1.824 -41db2e0200000000 1824000000 -40704b6db6db6db7 260.7142857142857 -3ffd333333333333 1.825 -41db31d290000000 1825000000 -40704db6db6db6db 260.85714285714283 -3ffd374bc6a7ef9e 1.826 -41db35a320000000 1826000000 -4070500000000000 261 -3ffd3b645a1cac08 1.827 -41db3973b0000000 1827000000 -4070524924924925 261.14285714285717 -3ffd3f7ced916873 1.828 -41db3d4440000000 1828000000 -4070549249249249 261.2857142857143 -3ffd4395810624dd 1.829 -41db4114d0000000 1829000000 -407056db6db6db6e 261.42857142857144 -3ffd47ae147ae148 1.83 -41db44e560000000 1830000000 -4070592492492492 261.57142857142856 -3ffd4bc6a7ef9db2 1.831 -41db48b5f0000000 1831000000 -40705b6db6db6db7 261.7142857142857 -3ffd4fdf3b645a1d 1.832 -41db4c8680000000 1832000000 -40705db6db6db6db 261.85714285714283 -3ffd53f7ced91687 1.833 -41db505710000000 1833000000 -4070600000000000 262 -3ffd5810624dd2f2 1.834 -41db5427a0000000 1834000000 -4070624924924925 262.14285714285717 -3ffd5c28f5c28f5c 1.835 -41db57f830000000 1835000000 -4070649249249249 262.2857142857143 -3ffd604189374bc7 1.836 -41db5bc8c0000000 1836000000 -407066db6db6db6e 262.42857142857144 -3ffd645a1cac0831 1.837 -41db5f9950000000 1837000000 -4070692492492492 262.57142857142856 -3ffd6872b020c49c 1.838 -41db6369e0000000 1838000000 -40706b6db6db6db7 262.7142857142857 -3ffd6c8b43958106 1.839 -41db673a70000000 1839000000 -40706db6db6db6db 262.85714285714283 -3ffd70a3d70a3d71 1.84 -41db6b0b00000000 1840000000 -4070700000000000 263 -3ffd74bc6a7ef9db 1.841 -41db6edb90000000 1841000000 -4070724924924925 263.14285714285717 -3ffd78d4fdf3b646 1.842 -41db72ac20000000 1842000000 -4070749249249249 263.2857142857143 -3ffd7ced916872b0 1.843 -41db767cb0000000 1843000000 -407076db6db6db6e 263.42857142857144 -3ffd810624dd2f1b 1.844 -41db7a4d40000000 1844000000 -4070792492492492 263.57142857142856 -3ffd851eb851eb85 1.845 -41db7e1dd0000000 1845000000 -40707b6db6db6db7 263.7142857142857 -3ffd89374bc6a7f0 1.846 -41db81ee60000000 1846000000 -40707db6db6db6db 263.85714285714283 -3ffd8d4fdf3b645a 1.847 -41db85bef0000000 1847000000 -4070800000000000 264 -3ffd916872b020c5 1.848 -41db898f80000000 1848000000 -4070824924924925 264.14285714285717 -3ffd95810624dd2f 1.849 -41db8d6010000000 1849000000 -4070849249249249 264.2857142857143 -3ffd99999999999a 1.85 -41db9130a0000000 1850000000 -407086db6db6db6e 264.42857142857144 -3ffd9db22d0e5604 1.851 -41db950130000000 1851000000 -4070892492492492 264.57142857142856 -3ffda1cac083126f 1.852 -41db98d1c0000000 1852000000 -40708b6db6db6db7 264.7142857142857 -3ffda5e353f7ced9 1.853 -41db9ca250000000 1853000000 -40708db6db6db6db 264.85714285714283 -3ffda9fbe76c8b44 1.854 -41dba072e0000000 1854000000 -4070900000000000 265 -3ffdae147ae147ae 1.855 -41dba44370000000 1855000000 -4070924924924925 265.14285714285717 -3ffdb22d0e560419 1.856 -41dba81400000000 1856000000 -4070949249249249 265.2857142857143 -3ffdb645a1cac083 1.857 -41dbabe490000000 1857000000 -407096db6db6db6e 265.42857142857144 -3ffdba5e353f7cee 1.858 -41dbafb520000000 1858000000 -4070992492492492 265.57142857142856 -3ffdbe76c8b43958 1.859 -41dbb385b0000000 1859000000 -40709b6db6db6db7 265.7142857142857 -3ffdc28f5c28f5c3 1.86 -41dbb75640000000 1860000000 -40709db6db6db6db 265.85714285714283 -3ffdc6a7ef9db22d 1.861 -41dbbb26d0000000 1861000000 -4070a00000000000 266 -3ffdcac083126e98 1.862 -41dbbef760000000 1862000000 -4070a24924924925 266.14285714285717 -3ffdced916872b02 1.863 -41dbc2c7f0000000 1863000000 -4070a49249249249 266.2857142857143 -3ffdd2f1a9fbe76d 1.864 -41dbc69880000000 1864000000 -4070a6db6db6db6e 266.42857142857144 -3ffdd70a3d70a3d7 1.865 -41dbca6910000000 1865000000 -4070a92492492492 266.57142857142856 -3ffddb22d0e56042 1.866 -41dbce39a0000000 1866000000 -4070ab6db6db6db7 266.7142857142857 -3ffddf3b645a1cac 1.867 -41dbd20a30000000 1867000000 -4070adb6db6db6db 266.85714285714283 -3ffde353f7ced917 1.868 -41dbd5dac0000000 1868000000 -4070b00000000000 267 -3ffde76c8b439581 1.869 -41dbd9ab50000000 1869000000 -4070b24924924925 267.14285714285717 -3ffdeb851eb851ec 1.87 -41dbdd7be0000000 1870000000 -4070b49249249249 267.2857142857143 -3ffdef9db22d0e56 1.871 -41dbe14c70000000 1871000000 -4070b6db6db6db6e 267.42857142857144 -3ffdf3b645a1cac1 1.872 -41dbe51d00000000 1872000000 -4070b92492492492 267.57142857142856 -3ffdf7ced916872b 1.873 -41dbe8ed90000000 1873000000 -4070bb6db6db6db7 267.7142857142857 -3ffdfbe76c8b4396 1.874 -41dbecbe20000000 1874000000 -4070bdb6db6db6db 267.85714285714283 -3ffe000000000000 1.875 -41dbf08eb0000000 1875000000 -4070c00000000000 268 -3ffe04189374bc6b 1.8760000000000001 -41dbf45f40000000 1876000000 -4070c24924924925 268.14285714285717 -3ffe083126e978d5 1.877 -41dbf82fd0000000 1877000000 -4070c49249249249 268.2857142857143 -3ffe0c49ba5e3540 1.8780000000000001 -41dbfc0060000000 1878000000 -4070c6db6db6db6e 268.42857142857144 -3ffe10624dd2f1aa 1.879 -41dbffd0f0000000 1879000000 -4070c92492492492 268.57142857142856 -3ffe147ae147ae15 1.8800000000000001 -41dc03a180000000 1880000000 -4070cb6db6db6db7 268.7142857142857 -3ffe189374bc6a7f 1.881 -41dc077210000000 1881000000 -4070cdb6db6db6db 268.85714285714283 -3ffe1cac083126ea 1.8820000000000001 -41dc0b42a0000000 1882000000 -4070d00000000000 269 -3ffe20c49ba5e354 1.883 -41dc0f1330000000 1883000000 -4070d24924924925 269.14285714285717 -3ffe24dd2f1a9fbf 1.8840000000000001 -41dc12e3c0000000 1884000000 -4070d49249249249 269.2857142857143 -3ffe28f5c28f5c29 1.885 -41dc16b450000000 1885000000 -4070d6db6db6db6e 269.42857142857144 -3ffe2d0e56041894 1.8860000000000001 -41dc1a84e0000000 1886000000 -4070d92492492492 269.57142857142856 -3ffe3126e978d4fe 1.887 -41dc1e5570000000 1887000000 -4070db6db6db6db7 269.7142857142857 -3ffe353f7ced9169 1.8880000000000001 -41dc222600000000 1888000000 -4070ddb6db6db6db 269.85714285714283 -3ffe395810624dd3 1.889 -41dc25f690000000 1889000000 -4070e00000000000 270 -3ffe3d70a3d70a3e 1.8900000000000001 -41dc29c720000000 1890000000 -4070e24924924925 270.14285714285717 -3ffe4189374bc6a8 1.891 -41dc2d97b0000000 1891000000 -4070e49249249249 270.2857142857143 -3ffe45a1cac08313 1.8920000000000001 -41dc316840000000 1892000000 -4070e6db6db6db6e 270.42857142857144 -3ffe49ba5e353f7d 1.893 -41dc3538d0000000 1893000000 -4070e92492492492 270.57142857142856 -3ffe4dd2f1a9fbe8 1.8940000000000001 -41dc390960000000 1894000000 -4070eb6db6db6db7 270.7142857142857 -3ffe51eb851eb852 1.895 -41dc3cd9f0000000 1895000000 -4070edb6db6db6db 270.85714285714283 -3ffe5604189374bd 1.8960000000000001 -41dc40aa80000000 1896000000 -4070f00000000000 271 -3ffe5a1cac083127 1.897 -41dc447b10000000 1897000000 -4070f24924924925 271.14285714285717 -3ffe5e353f7ced92 1.8980000000000001 -41dc484ba0000000 1898000000 -4070f49249249249 271.2857142857143 -3ffe624dd2f1a9fc 1.899 -41dc4c1c30000000 1899000000 -4070f6db6db6db6e 271.42857142857144 -3ffe666666666667 1.9000000000000001 -41dc4fecc0000000 1900000000 -4070f92492492492 271.57142857142856 -3ffe6a7ef9db22d1 1.901 -41dc53bd50000000 1901000000 -4070fb6db6db6db7 271.7142857142857 -3ffe6e978d4fdf3c 1.9020000000000001 -41dc578de0000000 1902000000 -4070fdb6db6db6db 271.85714285714283 -3ffe72b020c49ba6 1.903 -41dc5b5e70000000 1903000000 -4071000000000000 272 -3ffe76c8b4395811 1.9040000000000001 -41dc5f2f00000000 1904000000 -4071024924924925 272.14285714285717 -3ffe7ae147ae147b 1.905 -41dc62ff90000000 1905000000 -4071049249249249 272.2857142857143 -3ffe7ef9db22d0e6 1.9060000000000001 -41dc66d020000000 1906000000 -407106db6db6db6e 272.42857142857144 -3ffe83126e978d50 1.907 -41dc6aa0b0000000 1907000000 -4071092492492492 272.57142857142856 -3ffe872b020c49bb 1.9080000000000001 -41dc6e7140000000 1908000000 -40710b6db6db6db7 272.7142857142857 -3ffe8b4395810625 1.909 -41dc7241d0000000 1909000000 -40710db6db6db6db 272.85714285714283 -3ffe8f5c28f5c290 1.9100000000000001 -41dc761260000000 1910000000 -4071100000000000 273 -3ffe9374bc6a7efa 1.911 -41dc79e2f0000000 1911000000 -4071124924924925 273.14285714285717 -3ffe978d4fdf3b65 1.9120000000000001 -41dc7db380000000 1912000000 -4071149249249249 273.2857142857143 -3ffe9ba5e353f7cf 1.913 -41dc818410000000 1913000000 -407116db6db6db6e 273.42857142857144 -3ffe9fbe76c8b43a 1.9140000000000001 -41dc8554a0000000 1914000000 -4071192492492492 273.57142857142856 -3ffea3d70a3d70a4 1.915 -41dc892530000000 1915000000 -40711b6db6db6db7 273.7142857142857 -3ffea7ef9db22d0f 1.9160000000000001 -41dc8cf5c0000000 1916000000 -40711db6db6db6db 273.85714285714283 -3ffeac083126e979 1.917 -41dc90c650000000 1917000000 -4071200000000000 274 -3ffeb020c49ba5e4 1.9180000000000001 -41dc9496e0000000 1918000000 -4071224924924925 274.14285714285717 -3ffeb4395810624e 1.919 -41dc986770000000 1919000000 -4071249249249249 274.2857142857143 -3ffeb851eb851eb8 1.92 -41dc9c3800000000 1920000000 -407126db6db6db6e 274.42857142857144 -3ffebc6a7ef9db23 1.921 -41dca00890000000 1921000000 -4071292492492492 274.57142857142856 -3ffec083126e978d 1.922 -41dca3d920000000 1922000000 -40712b6db6db6db7 274.7142857142857 -3ffec49ba5e353f8 1.923 -41dca7a9b0000000 1923000000 -40712db6db6db6db 274.85714285714283 -3ffec8b439581062 1.924 -41dcab7a40000000 1924000000 -4071300000000000 275 -3ffecccccccccccd 1.925 -41dcaf4ad0000000 1925000000 -4071324924924925 275.14285714285717 -3ffed0e560418937 1.926 -41dcb31b60000000 1926000000 -4071349249249249 275.2857142857143 -3ffed4fdf3b645a2 1.927 -41dcb6ebf0000000 1927000000 -407136db6db6db6e 275.42857142857144 -3ffed916872b020c 1.928 -41dcbabc80000000 1928000000 -4071392492492492 275.57142857142856 -3ffedd2f1a9fbe77 1.929 -41dcbe8d10000000 1929000000 -40713b6db6db6db7 275.7142857142857 -3ffee147ae147ae1 1.93 -41dcc25da0000000 1930000000 -40713db6db6db6db 275.85714285714283 -3ffee5604189374c 1.931 -41dcc62e30000000 1931000000 -4071400000000000 276 -3ffee978d4fdf3b6 1.932 -41dcc9fec0000000 1932000000 -4071424924924925 276.14285714285717 -3ffeed916872b021 1.933 -41dccdcf50000000 1933000000 -4071449249249249 276.2857142857143 -3ffef1a9fbe76c8b 1.934 -41dcd19fe0000000 1934000000 -407146db6db6db6e 276.42857142857144 -3ffef5c28f5c28f6 1.935 -41dcd57070000000 1935000000 -4071492492492492 276.57142857142856 -3ffef9db22d0e560 1.936 -41dcd94100000000 1936000000 -40714b6db6db6db7 276.7142857142857 -3ffefdf3b645a1cb 1.937 -41dcdd1190000000 1937000000 -40714db6db6db6db 276.85714285714283 -3fff020c49ba5e35 1.938 -41dce0e220000000 1938000000 -4071500000000000 277 -3fff0624dd2f1aa0 1.939 -41dce4b2b0000000 1939000000 -4071524924924925 277.14285714285717 -3fff0a3d70a3d70a 1.94 -41dce88340000000 1940000000 -4071549249249249 277.2857142857143 -3fff0e5604189375 1.941 -41dcec53d0000000 1941000000 -407156db6db6db6e 277.42857142857144 -3fff126e978d4fdf 1.942 -41dcf02460000000 1942000000 -4071592492492492 277.57142857142856 -3fff16872b020c4a 1.943 -41dcf3f4f0000000 1943000000 -40715b6db6db6db7 277.7142857142857 -3fff1a9fbe76c8b4 1.944 -41dcf7c580000000 1944000000 -40715db6db6db6db 277.85714285714283 -3fff1eb851eb851f 1.945 -41dcfb9610000000 1945000000 -4071600000000000 278 -3fff22d0e5604189 1.946 -41dcff66a0000000 1946000000 -4071624924924925 278.14285714285717 -3fff26e978d4fdf4 1.947 -41dd033730000000 1947000000 -4071649249249249 278.2857142857143 -3fff2b020c49ba5e 1.948 -41dd0707c0000000 1948000000 -407166db6db6db6e 278.42857142857144 -3fff2f1a9fbe76c9 1.949 -41dd0ad850000000 1949000000 -4071692492492492 278.57142857142856 -3fff333333333333 1.95 -41dd0ea8e0000000 1950000000 -40716b6db6db6db7 278.7142857142857 -3fff374bc6a7ef9e 1.951 -41dd127970000000 1951000000 -40716db6db6db6db 278.85714285714283 -3fff3b645a1cac08 1.952 -41dd164a00000000 1952000000 -4071700000000000 279 -3fff3f7ced916873 1.953 -41dd1a1a90000000 1953000000 -4071724924924925 279.14285714285717 -3fff4395810624dd 1.954 -41dd1deb20000000 1954000000 -4071749249249249 279.2857142857143 -3fff47ae147ae148 1.955 -41dd21bbb0000000 1955000000 -407176db6db6db6e 279.42857142857144 -3fff4bc6a7ef9db2 1.956 -41dd258c40000000 1956000000 -4071792492492492 279.57142857142856 -3fff4fdf3b645a1d 1.957 -41dd295cd0000000 1957000000 -40717b6db6db6db7 279.7142857142857 -3fff53f7ced91687 1.958 -41dd2d2d60000000 1958000000 -40717db6db6db6db 279.85714285714283 -3fff5810624dd2f2 1.959 -41dd30fdf0000000 1959000000 -4071800000000000 280 -3fff5c28f5c28f5c 1.96 -41dd34ce80000000 1960000000 -4071824924924925 280.14285714285717 -3fff604189374bc7 1.961 -41dd389f10000000 1961000000 -4071849249249249 280.2857142857143 -3fff645a1cac0831 1.962 -41dd3c6fa0000000 1962000000 -407186db6db6db6e 280.42857142857144 -3fff6872b020c49c 1.963 -41dd404030000000 1963000000 -4071892492492492 280.57142857142856 -3fff6c8b43958106 1.964 -41dd4410c0000000 1964000000 -40718b6db6db6db7 280.7142857142857 -3fff70a3d70a3d71 1.965 -41dd47e150000000 1965000000 -40718db6db6db6db 280.85714285714283 -3fff74bc6a7ef9db 1.966 -41dd4bb1e0000000 1966000000 -4071900000000000 281 -3fff78d4fdf3b646 1.967 -41dd4f8270000000 1967000000 -4071924924924925 281.14285714285717 -3fff7ced916872b0 1.968 -41dd535300000000 1968000000 -4071949249249249 281.2857142857143 -3fff810624dd2f1b 1.969 -41dd572390000000 1969000000 -407196db6db6db6e 281.42857142857144 -3fff851eb851eb85 1.97 -41dd5af420000000 1970000000 -4071992492492492 281.57142857142856 -3fff89374bc6a7f0 1.971 -41dd5ec4b0000000 1971000000 -40719b6db6db6db7 281.7142857142857 -3fff8d4fdf3b645a 1.972 -41dd629540000000 1972000000 -40719db6db6db6db 281.85714285714283 -3fff916872b020c5 1.973 -41dd6665d0000000 1973000000 -4071a00000000000 282 -3fff95810624dd2f 1.974 -41dd6a3660000000 1974000000 -4071a24924924925 282.14285714285717 -3fff99999999999a 1.975 -41dd6e06f0000000 1975000000 -4071a49249249249 282.2857142857143 -3fff9db22d0e5604 1.976 -41dd71d780000000 1976000000 -4071a6db6db6db6e 282.42857142857144 -3fffa1cac083126f 1.977 -41dd75a810000000 1977000000 -4071a92492492492 282.57142857142856 -3fffa5e353f7ced9 1.978 -41dd7978a0000000 1978000000 -4071ab6db6db6db7 282.7142857142857 -3fffa9fbe76c8b44 1.979 -41dd7d4930000000 1979000000 -4071adb6db6db6db 282.85714285714283 -3fffae147ae147ae 1.98 -41dd8119c0000000 1980000000 -4071b00000000000 283 -3fffb22d0e560419 1.981 -41dd84ea50000000 1981000000 -4071b24924924925 283.14285714285717 -3fffb645a1cac083 1.982 -41dd88bae0000000 1982000000 -4071b49249249249 283.2857142857143 -3fffba5e353f7cee 1.983 -41dd8c8b70000000 1983000000 -4071b6db6db6db6e 283.42857142857144 -3fffbe76c8b43958 1.984 -41dd905c00000000 1984000000 -4071b92492492492 283.57142857142856 -3fffc28f5c28f5c3 1.985 -41dd942c90000000 1985000000 -4071bb6db6db6db7 283.7142857142857 -3fffc6a7ef9db22d 1.986 -41dd97fd20000000 1986000000 -4071bdb6db6db6db 283.85714285714283 -3fffcac083126e98 1.987 -41dd9bcdb0000000 1987000000 -4071c00000000000 284 -3fffced916872b02 1.988 -41dd9f9e40000000 1988000000 -4071c24924924925 284.14285714285717 -3fffd2f1a9fbe76d 1.989 -41dda36ed0000000 1989000000 -4071c49249249249 284.2857142857143 -3fffd70a3d70a3d7 1.99 -41dda73f60000000 1990000000 -4071c6db6db6db6e 284.42857142857144 -3fffdb22d0e56042 1.991 -41ddab0ff0000000 1991000000 -4071c92492492492 284.57142857142856 -3fffdf3b645a1cac 1.992 -41ddaee080000000 1992000000 -4071cb6db6db6db7 284.7142857142857 -3fffe353f7ced917 1.993 -41ddb2b110000000 1993000000 -4071cdb6db6db6db 284.85714285714283 -3fffe76c8b439581 1.994 -41ddb681a0000000 1994000000 -4071d00000000000 285 -3fffeb851eb851ec 1.995 -41ddba5230000000 1995000000 -4071d24924924925 285.14285714285717 -3fffef9db22d0e56 1.996 -41ddbe22c0000000 1996000000 -4071d49249249249 285.2857142857143 -3ffff3b645a1cac1 1.997 -41ddc1f350000000 1997000000 -4071d6db6db6db6e 285.42857142857144 -3ffff7ced916872b 1.998 -41ddc5c3e0000000 1998000000 -4071d92492492492 285.57142857142856 -3ffffbe76c8b4396 1.999 -41ddc99470000000 1999000000 -4071db6db6db6db7 285.7142857142857 -4000000000000000 2 -41ddcd6500000000 2000000000 diff --git a/crates/cf-gosort/src/lib.rs b/crates/cf-gosort/src/lib.rs index 902deca8..42d30bc7 100644 --- a/crates/cf-gosort/src/lib.rs +++ b/crates/cf-gosort/src/lib.rs @@ -9,7 +9,7 @@ //! implementation's permutation bit-for-bit. //! //! Compatibility: output ordering is pinned against the reference binary by -//! `tests/compat`; do not swap in `slice::sort_unstable` or alter the +//! the report e2e suites (`bins/codefang/tests`); do not swap in `slice::sort_unstable` or alter the //! constants. //! //! # Example diff --git a/crates/cf-goyaml/src/lib.rs b/crates/cf-goyaml/src/lib.rs index 0d7a7065..5a12a1db 100644 --- a/crates/cf-goyaml/src/lib.rs +++ b/crates/cf-goyaml/src/lib.rs @@ -23,7 +23,7 @@ //! //! Compatibility: output bytes are pinned against the reference implementation //! (`gopkg.in/yaml.v3` v3.0.1 `Marshal`) by this crate's oracle tests and the -//! differential gate in `tests/compat`. +//! the report e2e suites (`bins/codefang/tests`). //! //! # Quick start //! diff --git a/crates/cf-goyaml/tests/oracle.rs b/crates/cf-goyaml/tests/oracle.rs deleted file mode 100644 index 34d98d5a..00000000 --- a/crates/cf-goyaml/tests/oracle.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Differential oracle test against `gopkg.in/yaml.v3`. -//! -//! Reads a TSV of `<spec>\t<base64(expected_yaml)>` lines (generated by the Go -//! oracle in this crate's dev notes) from the path in `CF_GOYAML_ORACLE`, parses -//! each spec into a [`GoValue`], marshals it with [`cf_goyaml::marshal`], and -//! asserts byte-equality with the captured `yaml.Marshal` output. -//! -//! The test is skipped (passes trivially) when the env var is unset, so it never -//! blocks a normal `cargo test` run while remaining available for the parity gate. - -use cf_gojson::{GoMap, GoValue}; - -#[test] -fn oracle_byte_parity() { - let Ok(path) = std::env::var("CF_GOYAML_ORACLE") else { - eprintln!("CF_GOYAML_ORACLE unset; skipping oracle parity test"); - return; - }; - let data = std::fs::read_to_string(&path).expect("read oracle file"); - let mut total = 0usize; - let mut fail = 0usize; - for (lineno, line) in data.lines().enumerate() { - if line.is_empty() { - continue; - } - let (spec, b64) = line.split_once('\t').expect("tab-separated"); - let expected = b64_decode(b64); - let mut p = Parser { - b: spec.as_bytes(), - i: 0, - }; - let value = p.parse_value(); - let got = cf_goyaml::marshal(&value); - total += 1; - if got != expected { - fail += 1; - if fail <= 20 { - eprintln!( - "MISMATCH line {} spec={:?}\n expected={:?}\n got ={:?}", - lineno + 1, - spec, - String::from_utf8_lossy(&expected), - String::from_utf8_lossy(&got), - ); - } - } - } - eprintln!("oracle: {total} cases, {fail} mismatches"); - assert_eq!(fail, 0, "{fail}/{total} oracle mismatches"); -} - -/// Mini-DSL parser matching the Go generator: -/// `I<int> U<uint> F<bits> Bt Bf N S<b64> A(..;..) M(b64=val;..)`. -struct Parser<'a> { - b: &'a [u8], - i: usize, -} - -impl Parser<'_> { - fn peek(&self) -> u8 { - self.b[self.i] - } - fn parse_value(&mut self) -> GoValue { - match self.peek() { - b'I' => { - self.i += 1; - let s = self.take_token(); - GoValue::Int(s.parse().unwrap()) - } - b'U' => { - self.i += 1; - let s = self.take_token(); - GoValue::Uint(s.parse().unwrap()) - } - b'F' => { - self.i += 1; - let s = self.take_token(); - let bits: u64 = s.parse().unwrap(); - GoValue::Float(f64::from_bits(bits)) - } - b'B' => { - self.i += 1; - let t = self.b[self.i]; - self.i += 1; - GoValue::Bool(t == b't') - } - b'N' => { - self.i += 1; - GoValue::Null - } - b'S' => { - self.i += 1; - let s = self.take_token(); - GoValue::Str(String::from_utf8(b64_decode(&s)).unwrap()) - } - b'A' => { - self.i += 1; // 'A' - self.expect(b'('); - let mut items = Vec::new(); - if self.peek() != b')' { - loop { - items.push(self.parse_value()); - if self.peek() == b';' { - self.i += 1; - } else { - break; - } - } - } - self.expect(b')'); - GoValue::Array(items) - } - b'M' => { - self.i += 1; // 'M' - self.expect(b'('); - let mut m = GoMap::new_struct(); - if self.peek() != b')' { - loop { - // key is base64 up to the '>' kv separator - let kb = self.take_until(b'>'); - self.expect(b'>'); - let key = String::from_utf8(b64_decode(&kb)).unwrap(); - let val = self.parse_value(); - m.push(key, val); - if self.peek() == b';' { - self.i += 1; - } else { - break; - } - } - } - self.expect(b')'); - GoValue::Map(m) - } - other => panic!("bad spec char {other:?} at {}", self.i), - } - } - - fn expect(&mut self, c: u8) { - assert_eq!(self.b[self.i], c, "expected {} at {}", c as char, self.i); - self.i += 1; - } - - /// Token = run of base64 chars / digits / sign until a DSL delimiter. - fn take_token(&mut self) -> String { - let start = self.i; - while self.i < self.b.len() && !matches!(self.b[self.i], b';' | b')') { - self.i += 1; - } - String::from_utf8(self.b[start..self.i].to_vec()).unwrap() - } - - fn take_until(&mut self, stop: u8) -> String { - let start = self.i; - while self.i < self.b.len() && self.b[self.i] != stop { - self.i += 1; - } - String::from_utf8(self.b[start..self.i].to_vec()).unwrap() - } -} - -/// Standard base64 decode (with '=' padding). -fn b64_decode(s: &str) -> Vec<u8> { - fn val(c: u8) -> i32 { - match c { - b'A'..=b'Z' => (c - b'A') as i32, - b'a'..=b'z' => (c - b'a' + 26) as i32, - b'0'..=b'9' => (c - b'0' + 52) as i32, - b'+' => 62, - b'/' => 63, - _ => -1, - } - } - let mut out = Vec::new(); - let mut buf = 0i32; - let mut bits = 0; - for &c in s.as_bytes() { - if c == b'=' { - break; - } - let v = val(c); - if v < 0 { - continue; - } - buf = (buf << 6) | v; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((buf >> bits) as u8); - } - } - out -} diff --git a/crates/cf-halstead/Cargo.toml b/crates/cf-halstead/Cargo.toml index b5926e74..5b112a60 100644 --- a/crates/cf-halstead/Cargo.toml +++ b/crates/cf-halstead/Cargo.toml @@ -6,10 +6,13 @@ rust-version.workspace = true license.workspace = true repository.workspace = true publish = false -description = "Halstead complexity metrics: operator/operand detection, derived measures (vocabulary/length/volume/difficulty/effort/time/bugs), and the standalone file-level analysis consumed by the quality analyzer. Output bytes are pinned by the differential gate (tests/compat); the static-pipeline report builder lives in cf-commands." +description = "Halstead complexity metrics: operator/operand classification and source-span recovery, derived measures (vocabulary/length/volume/difficulty/effort/time/bugs), the union aggregate with thresholds and health score, and the file-level analysis consumed by the quality analyzer. The static-pipeline report serialization lives in cf-commands." [dependencies] -# UAST node type the standalone analysis traverses. +# UAST node type the detector walks. cf-uast-node = { path = "../cf-uast-node" } [dev-dependencies] +# Real-source tests: parse Go / Python / JavaScript / Rust / Java / C with the +# production parser and assert hand-computed Halstead numbers. +cf-uast = { path = "../cf-uast" } diff --git a/crates/cf-halstead/src/aggregate.rs b/crates/cf-halstead/src/aggregate.rs new file mode 100644 index 00000000..a41f9134 --- /dev/null +++ b/crates/cf-halstead/src/aggregate.rs @@ -0,0 +1,278 @@ +//! Per-function metrics and the union aggregate. +//! +//! A set of functions (a file, or the whole codebase) is aggregated by taking +//! the **union of the operator and operand multisets**: n1 and n2 are the +//! distinct operators / operands across every function, N1 and N2 their total +//! occurrences, and V / D / E / T / B derive from those four numbers exactly +//! as they do for a single function. Alongside the union, the aggregate +//! carries per-function statistics (mean and max of volume, difficulty and +//! effort), the volume-band distribution, and the health score and message +//! of [`crate::thresholds`]. + +use std::collections::BTreeMap; + +use cf_uast_node::Node; + +use crate::calculator::{derive, DerivedMetrics, HalsteadCounts}; +use crate::detector::{collect_functions, FunctionCounts}; +use crate::thresholds::{health_score, is_high_effort, message, VolumeLevel}; + +/// One function's multisets, raw counts, and derived measures. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct FunctionMetrics { + /// Function name. + pub name: String, + /// n1, n2, N1, N2. + pub counts: HalsteadCounts, + /// The eight derived measures. + pub derived: DerivedMetrics, + /// Operator text → occurrences. + pub operators: BTreeMap<String, i64>, + /// Operand text → occurrences. + pub operands: BTreeMap<String, i64>, +} + +impl From<FunctionCounts> for FunctionMetrics { + fn from(fc: FunctionCounts) -> Self { + let counts = fc.totals(); + Self { + name: fc.name, + counts, + derived: derive(counts), + operators: fc.operators, + operands: fc.operands, + } + } +} + +/// Collects and measures every function of `root` (see +/// [`collect_functions`] for the `source` contract). +#[must_use] +pub fn analyze_functions(root: &Node, source: Option<&[u8]>) -> Vec<FunctionMetrics> { + collect_functions(root, source) + .into_iter() + .map(FunctionMetrics::from) + .collect() +} + +/// Per-function statistics of an aggregated set. +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub struct FunctionStats { + /// Mean per-function volume. + pub mean_volume: f64, + /// Largest per-function volume. + pub max_volume: f64, + /// Mean per-function difficulty. + pub mean_difficulty: f64, + /// Largest per-function difficulty. + pub max_difficulty: f64, + /// Mean per-function effort. + pub mean_effort: f64, + /// Largest per-function effort. + pub max_effort: f64, + /// Functions with effort at or above [`crate::thresholds::EFFORT_HIGH`]. + pub high_effort_count: i64, +} + +/// The union aggregate of a set of functions. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Summary { + /// Number of functions in the set. + pub total_functions: i64, + /// Union counts: distinct / total operators and operands across the set. + pub counts: HalsteadCounts, + /// Measures derived from the union counts. + pub derived: DerivedMetrics, + /// Per-function statistics. + pub stats: FunctionStats, + /// Function count per volume band, in [`VolumeLevel::ALL`] order. + pub distribution: [i64; 4], + /// `high_effort_count / total_functions` (0 for an empty set). + pub high_effort_share: f64, + /// Health score (0..=100) from the high-effort share. + pub health_score: f64, + /// Aggregate message from the high-effort share. + pub message: &'static str, +} + +/// Aggregates `functions` by the union of their multisets. +/// +/// ``` +/// use cf_halstead::aggregate::{summarize, FunctionMetrics}; +/// use cf_halstead::detector::FunctionCounts; +/// +/// let f = |ops: &[(&str, i64)], opnds: &[(&str, i64)]| { +/// FunctionMetrics::from(FunctionCounts { +/// name: "f".into(), +/// operators: ops.iter().map(|(k, v)| (k.to_string(), *v)).collect(), +/// operands: opnds.iter().map(|(k, v)| (k.to_string(), *v)).collect(), +/// }) +/// }; +/// let a = f(&[("+", 1), ("return", 1)], &[("x", 2)]); +/// let b = f(&[("*", 1), ("return", 1)], &[("x", 1), ("y", 1)]); +/// let s = summarize([&a, &b]); +/// assert_eq!(s.total_functions, 2); +/// assert_eq!(s.counts.distinct_operators, 3); // +, return, * +/// assert_eq!(s.counts.total_operators, 4); +/// assert_eq!(s.counts.distinct_operands, 2); // x, y +/// assert_eq!(s.counts.total_operands, 4); +/// assert_eq!(s.derived.length, 8); +/// ``` +#[must_use] +#[allow(clippy::cast_precision_loss)] // function counts are far below 2^52 +pub fn summarize<'a, I>(functions: I) -> Summary +where + I: IntoIterator<Item = &'a FunctionMetrics>, +{ + let mut operators: BTreeMap<&str, i64> = BTreeMap::new(); + let mut operands: BTreeMap<&str, i64> = BTreeMap::new(); + let mut stats = FunctionStats::default(); + let mut distribution = [0i64; 4]; + let mut total_functions: i64 = 0; + + for f in functions { + total_functions += 1; + for (k, v) in &f.operators { + *operators.entry(k).or_insert(0) += v; + } + for (k, v) in &f.operands { + *operands.entry(k).or_insert(0) += v; + } + let d = &f.derived; + stats.mean_volume += d.volume; + stats.mean_difficulty += d.difficulty; + stats.mean_effort += d.effort; + stats.max_volume = stats.max_volume.max(d.volume); + stats.max_difficulty = stats.max_difficulty.max(d.difficulty); + stats.max_effort = stats.max_effort.max(d.effort); + stats.high_effort_count += i64::from(is_high_effort(d.effort)); + distribution[VolumeLevel::of(d.volume).index()] += 1; + } + + let high_effort_share = if total_functions > 0 { + let n = total_functions as f64; + stats.mean_volume /= n; + stats.mean_difficulty /= n; + stats.mean_effort /= n; + stats.high_effort_count as f64 / n + } else { + 0.0 + }; + + let counts = HalsteadCounts { + distinct_operators: i64::try_from(operators.len()).unwrap_or(i64::MAX), + distinct_operands: i64::try_from(operands.len()).unwrap_or(i64::MAX), + total_operators: operators.values().sum(), + total_operands: operands.values().sum(), + }; + + Summary { + total_functions, + counts, + derived: derive(counts), + stats, + distribution, + high_effort_share, + health_score: health_score(high_effort_share), + message: message(total_functions, high_effort_share), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::thresholds::{EFFORT_HIGH, HEALTH_MAX, MSG_LOW, MSG_NO_FUNCTIONS, MSG_VERY_HIGH}; + + fn metrics(name: &str, ops: &[(&str, i64)], opnds: &[(&str, i64)]) -> FunctionMetrics { + FunctionMetrics::from(FunctionCounts { + name: name.into(), + operators: ops.iter().map(|(k, v)| ((*k).to_string(), *v)).collect(), + operands: opnds.iter().map(|(k, v)| ((*k).to_string(), *v)).collect(), + }) + } + + #[test] + fn per_function_metrics_derive_from_the_multisets() { + let hal = metrics( + "hal", + &[(":=", 1), ("+", 1), ("*", 1), ("return", 1)], + &[("a", 1), ("b", 1), ("c", 2), ("2", 1)], + ); + assert_eq!(hal.counts.distinct_operators, 4); + assert_eq!(hal.counts.total_operands, 5); + assert_eq!(hal.derived.volume, 27.0); + assert_eq!(hal.derived.effort, 67.5); + } + + #[test] + fn empty_set_summary() { + let s = summarize(std::iter::empty()); + assert_eq!(s.total_functions, 0); + assert_eq!(s.counts, HalsteadCounts::default()); + assert_eq!(s.derived, DerivedMetrics::default()); + assert_eq!(s.distribution, [0; 4]); + assert_eq!(s.health_score, HEALTH_MAX); + assert_eq!(s.message, MSG_NO_FUNCTIONS); + } + + /// Two files' worth of functions: the aggregate is the UNION, so shared + /// operators are distinct once and totals add up — never an average. + #[test] + fn union_counts_distinct_once_and_totals_add() { + let a = metrics("a", &[("+", 2), ("return", 1)], &[("x", 3)]); + let b = metrics("b", &[("return", 1), ("*", 1)], &[("x", 1), ("y", 2)]); + let s = summarize([&a, &b]); + assert_eq!(s.total_functions, 2); + assert_eq!(s.counts.distinct_operators, 3); + assert_eq!(s.counts.total_operators, 5); + assert_eq!(s.counts.distinct_operands, 2); + assert_eq!(s.counts.total_operands, 6); + assert_eq!(s.derived.vocabulary, 5); + assert_eq!(s.derived.length, 11); + assert_eq!(s.derived.volume, 11.0 * 5f64.log2()); + assert_eq!(s.derived.difficulty, 1.5 * 3.0); + assert_eq!(s.distribution, [2, 0, 0, 0]); + assert_eq!(s.message, MSG_LOW); + } + + #[test] + fn per_function_stats_mean_and_max() { + let a = metrics("a", &[("+", 1)], &[("x", 1), ("y", 1)]); + let b = metrics("b", &[("+", 1), ("*", 1)], &[("x", 1), ("y", 1), ("z", 1)]); + let s = summarize([&a, &b]); + let (va, vb) = (a.derived.volume, b.derived.volume); + assert!((s.stats.mean_volume - (va + vb) / 2.0).abs() < 1e-12); + assert_eq!(s.stats.max_volume, va.max(vb)); + assert_eq!( + s.stats.max_difficulty, + a.derived.difficulty.max(b.derived.difficulty) + ); + assert_eq!(s.stats.max_effort, a.derived.effort.max(b.derived.effort)); + assert_eq!(s.stats.high_effort_count, 0); + } + + #[test] + fn high_effort_share_drives_health_and_message() { + // One huge function: many distinct operands used many times. + let ops: Vec<(String, i64)> = (0..40).map(|i| (format!("op{i}"), 20)).collect(); + let opnds: Vec<(String, i64)> = (0..60).map(|i| (format!("v{i}"), 20)).collect(); + let big = FunctionMetrics::from(FunctionCounts { + name: "big".into(), + operators: ops.into_iter().collect(), + operands: opnds.into_iter().collect(), + }); + assert!( + big.derived.effort >= EFFORT_HIGH, + "fixture must be high-effort" + ); + let s = summarize([&big]); + assert_eq!(s.stats.high_effort_count, 1); + assert_eq!(s.high_effort_share, 1.0); + assert_eq!(s.health_score, 0.0); + assert_eq!(s.message, MSG_VERY_HIGH); + assert_eq!( + s.distribution[VolumeLevel::of(big.derived.volume).index()], + 1 + ); + } +} diff --git a/crates/cf-halstead/src/calculator.rs b/crates/cf-halstead/src/calculator.rs index b072edc1..703bb2c8 100644 --- a/crates/cf-halstead/src/calculator.rs +++ b/crates/cf-halstead/src/calculator.rs @@ -1,8 +1,8 @@ //! Halstead derived-metric calculation. //! //! Takes the four raw Halstead counts ([`HalsteadCounts`]) and returns the -//! eight derived measures ([`DerivedMetrics`]), which both the file-level and -//! per-function aggregate views then store. +//! eight derived measures ([`DerivedMetrics`]) — the same arithmetic for one +//! function, one file, and the whole codebase. /// Standard constant used in time-to-program estimation (18 seconds). pub const TIME_CONSTANT: f64 = 18.0; @@ -47,216 +47,76 @@ pub struct DerivedMetrics { pub delivered_bugs: f64, } -/// Stateless calculator of Halstead derived metrics. -#[derive(Debug, Clone, Copy, Default)] -pub struct MetricsCalculator; - -impl MetricsCalculator { - /// Creates a new calculator. - #[must_use] - pub fn new() -> Self { - Self - } - - /// Sums all values in an integer count map. - #[must_use] - pub fn sum_map<S: ::std::hash::BuildHasher>( - &self, - m: &std::collections::HashMap<String, i64, S>, - ) -> i64 { - m.values().sum() - } - - /// Calculates all derived Halstead metrics from the four raw counts. - /// - /// The zero-guards are part of the report contract: estimated length is 0 - /// unless both distinct counts are positive; volume is 0 when vocabulary - /// is 0; difficulty is 0 when distinct operands is 0. - /// - /// Vocabulary (`n1 + n2`) and length (`N1 + N2`) are exact sums: - /// - /// ``` - /// use cf_halstead::calculator::{HalsteadCounts, MetricsCalculator}; - /// - /// let d = MetricsCalculator::new().calculate(HalsteadCounts { - /// distinct_operators: 3, - /// distinct_operands: 4, - /// total_operators: 6, - /// total_operands: 8, - /// }); - /// assert_eq!(d.vocabulary, 7); // 3 + 4 - /// assert_eq!(d.length, 14); // 6 + 8 - /// assert!(d.volume > 0.0); - /// assert!(d.difficulty > 0.0); - /// ``` - /// - /// All-zero counts produce all-zero measures (no division by zero): - /// - /// ``` - /// use cf_halstead::calculator::{HalsteadCounts, MetricsCalculator}; - /// - /// let d = MetricsCalculator::new().calculate(HalsteadCounts::default()); - /// assert_eq!(d.volume, 0.0); - /// assert_eq!(d.difficulty, 0.0); - /// assert_eq!(d.estimated_length, 0.0); - /// ``` - #[must_use] - #[allow(clippy::cast_precision_loss)] // token counts are far below 2^52 - pub fn calculate(&self, counts: HalsteadCounts) -> DerivedMetrics { - let distinct_ops = counts.distinct_operators; - let distinct_opnds = counts.distinct_operands; - let total_ops = counts.total_operators; - let total_opnds = counts.total_operands; - - let vocabulary = distinct_ops + distinct_opnds; - let length = total_ops + total_opnds; - - let estimated_length = if distinct_ops > 0 && distinct_opnds > 0 { - (distinct_ops as f64) * go_log2(distinct_ops as f64) - + (distinct_opnds as f64) * go_log2(distinct_opnds as f64) - } else { - 0.0 - }; - - let volume = if vocabulary > 0 { - (length as f64) * go_log2(vocabulary as f64) - } else { - 0.0 - }; - - let difficulty = if distinct_opnds > 0 { - (distinct_ops as f64 / DIFFICULTY_DIVISOR) - * (total_opnds as f64 / distinct_opnds as f64) - } else { - 0.0 - }; - - let effort = volume * difficulty; - let time_to_program = effort / TIME_CONSTANT; - let delivered_bugs = volume / BUG_CONSTANT; - - DerivedMetrics { - vocabulary, - length, - estimated_length, - volume, - difficulty, - effort, - time_to_program, - delivered_bugs, - } - } -} - -// --- Bit-exact base-2 logarithm (reference-implementation port) ------------- -// -// The platform libm `f64::log2` differs from the reference implementation's -// Log2 in the last ULP for some inputs (e.g. n=3), which shifts derived -// Halstead floats (volume/effort) and any downstream percentile that -// interpolates near those values. The reference computes -// `Log2(x) = log(frac)·(1/ln2) + exp` via Frexp, with its own polynomial log; -// this path is ported exactly so the volumes are byte-identical in reports -// (pinned by the differential gate). The algorithm and coefficients below are -// FROZEN. - -use std::f64::consts::{LN_2, SQRT_2}; - -/// Frexp (normal-positive path): `(frac, exp)` with `frac ∈ [0.5, 1)` and -/// `x == frac · 2^exp`. -fn go_frexp(f: f64) -> (f64, i32) { - if f == 0.0 || !f.is_finite() { - return (f, 0); - } - const SMALLEST_NORMAL: f64 = 2.225_073_858_507_201_4e-308; // 2**-1022 - let (x, mut exp) = if f.abs() < SMALLEST_NORMAL { - (f * (1u64 << 52) as f64, -52) - } else { - (f, 0) - }; - let bits = x.to_bits(); - const SHIFT: u64 = 52; - const MASK: u64 = 0x7FF; - const BIAS: i64 = 1023; - exp += (((bits >> SHIFT) & MASK) as i64 - BIAS + 1) as i32; - let mut nb = bits; - nb &= !(MASK << SHIFT); - nb |= ((-1 + BIAS) as u64) << SHIFT; - (f64::from_bits(nb), exp) -} - -/// Natural log via the pinned polynomial approximation. +/// Derives the eight Halstead measures from the four raw counts. /// -/// The coefficient literals keep their full published precision on purpose; -/// they round to the exact f64 bit patterns the reference uses. -#[allow(clippy::excessive_precision)] -fn go_log(x: f64) -> f64 { - const LN2HI: f64 = 6.931_471_803_691_238_164_9e-01; - const LN2LO: f64 = 1.908_214_929_270_587_700_02e-10; - const L1: f64 = 6.666_666_666_666_735_130e-01; - const L2: f64 = 3.999_999_999_940_941_908e-01; - const L3: f64 = 2.857_142_874_366_239_149e-01; - const L4: f64 = 2.222_219_843_214_978_396e-01; - const L5: f64 = 1.818_357_216_161_805_012e-01; - const L6: f64 = 1.531_383_769_920_937_332e-01; - const L7: f64 = 1.479_819_860_511_658_591e-01; - - if x.is_nan() || x == f64::INFINITY { - return x; - } - if x < 0.0 { - return f64::NAN; - } - if x == 0.0 { - return f64::NEG_INFINITY; - } - - let (mut f1, mut ki) = go_frexp(x); - if f1 < SQRT_2 / 2.0 { - f1 *= 2.0; - ki -= 1; - } - let f = f1 - 1.0; - let k = f64::from(ki); - - let s = f / (2.0 + f); - let s2 = s * s; - let s4 = s2 * s2; - let t1 = s2 * (L1 + s4 * (L3 + s4 * (L5 + s4 * L7))); - let t2 = s4 * (L2 + s4 * (L4 + s4 * L6)); - let r = t1 + t2; - let hfsq = 0.5 * f * f; - k * LN2HI - ((hfsq - (s * (hfsq + r) + k * LN2LO)) - f) -} - -/// Bit-exact base-2 logarithm (Frexp + pinned polynomial log). -/// -/// Exact powers of two return exact integers; non-powers match the reference -/// implementation bit-for-bit (which can differ from the platform -/// [`f64::log2`] in the last ULP, e.g. for `3.0`): +/// Zero guards: estimated length is 0 unless both distinct counts are +/// positive; volume is 0 when the vocabulary is 0; difficulty is 0 when there +/// are no distinct operands. /// /// ``` -/// use cf_halstead::calculator::go_log2; -/// assert_eq!(go_log2(8.0), 3.0); -/// assert_eq!(go_log2(3.0).to_bits(), 0x3ff9_5c01_a39f_bd69); +/// use cf_halstead::calculator::{derive, HalsteadCounts}; +/// +/// // c := a + b; return c * 2 → n1 = 4, n2 = 4, N1 = 4, N2 = 5 +/// let d = derive(HalsteadCounts { +/// distinct_operators: 4, +/// distinct_operands: 4, +/// total_operators: 4, +/// total_operands: 5, +/// }); +/// assert_eq!(d.vocabulary, 8); +/// assert_eq!(d.length, 9); +/// assert_eq!(d.volume, 27.0); // 9 · log2(8) +/// assert_eq!(d.difficulty, 2.5); // (4/2) · (5/4) +/// assert_eq!(d.effort, 67.5); +/// assert_eq!(d.time_to_program, 3.75); +/// assert_eq!(d.delivered_bugs, 0.009); /// ``` #[must_use] -pub fn go_log2(x: f64) -> f64 { - let (frac, exp) = go_frexp(x); - if frac == 0.5 { - return f64::from(exp - 1); +#[allow(clippy::cast_precision_loss)] // token counts are far below 2^52 +pub fn derive(counts: HalsteadCounts) -> DerivedMetrics { + let n1 = counts.distinct_operators as f64; + let n2 = counts.distinct_operands as f64; + let big_n2 = counts.total_operands as f64; + + let vocabulary = counts.distinct_operators + counts.distinct_operands; + let length = counts.total_operators + counts.total_operands; + + let estimated_length = if n1 > 0.0 && n2 > 0.0 { + n1 * n1.log2() + n2 * n2.log2() + } else { + 0.0 + }; + let volume = if vocabulary > 0 { + (length as f64) * (vocabulary as f64).log2() + } else { + 0.0 + }; + let difficulty = if n2 > 0.0 { + (n1 / DIFFICULTY_DIVISOR) * (big_n2 / n2) + } else { + 0.0 + }; + let effort = volume * difficulty; + + DerivedMetrics { + vocabulary, + length, + estimated_length, + volume, + difficulty, + effort, + time_to_program: effort / TIME_CONSTANT, + delivered_bugs: volume / BUG_CONSTANT, } - go_log(frac) * (1.0 / LN_2) + f64::from(exp) } #[cfg(test)] mod tests { use super::*; - /// Vocabulary and length are exact sums; the derived measures are - /// positive. #[test] - fn calculate_metrics_known_values() { - let d = MetricsCalculator::new().calculate(HalsteadCounts { + fn known_values() { + let d = derive(HalsteadCounts { distinct_operators: 3, distinct_operands: 4, total_operators: 6, @@ -264,45 +124,27 @@ mod tests { }); assert_eq!(d.vocabulary, 7); assert_eq!(d.length, 14); - assert!(d.volume > 0.0); - assert!(d.difficulty > 0.0); - assert!(d.effort > 0.0); - } - - #[test] - fn delivered_bugs_uses_volume() { - let d = MetricsCalculator::new().calculate(HalsteadCounts { - distinct_operators: 2, - distinct_operands: 3, - total_operators: 4, - total_operands: 6, - }); - let expected = d.volume / BUG_CONSTANT; - assert!((expected - d.delivered_bugs).abs() < 1e-12); + assert!((d.volume - 14.0 * 7f64.log2()).abs() < 1e-12); + assert!((d.difficulty - 3.0).abs() < 1e-12); + assert!((d.effort - d.volume * 3.0).abs() < 1e-9); + assert!((d.estimated_length - (3.0 * 3f64.log2() + 4.0 * 4f64.log2())).abs() < 1e-12); } #[test] fn zero_counts_are_zero() { - let d = MetricsCalculator::new().calculate(HalsteadCounts::default()); - assert_eq!(d.vocabulary, 0); - assert_eq!(d.length, 0); - assert_eq!(d.estimated_length, 0.0); - assert_eq!(d.volume, 0.0); - assert_eq!(d.difficulty, 0.0); - assert_eq!(d.effort, 0.0); - assert_eq!(d.delivered_bugs, 0.0); + assert_eq!(derive(HalsteadCounts::default()), DerivedMetrics::default()); } - /// Bit-level regression pin for the ported log2 path: these are the exact - /// outputs the report contract depends on (the platform `f64::log2` - /// differs in the last ULP for some of these, e.g. 3.0). #[test] - fn go_log2_bit_pins() { - assert_eq!(go_log2(3.0).to_bits(), 0x3ff9_5c01_a39f_bd69); - assert_eq!(go_log2(5.0).to_bits(), 0x4002_934f_0979_a371); - assert_eq!(go_log2(7.0).to_bits(), 0x4006_7576_7f54_042d); - assert_eq!(go_log2(10.0).to_bits(), 0x400a_934f_0979_a371); - assert_eq!(go_log2(1000.0).to_bits(), 0x4023_ee7b_471b_3a95); - assert_eq!(go_log2(8.0), 3.0); + fn operators_only_has_volume_but_no_difficulty() { + let d = derive(HalsteadCounts { + distinct_operators: 2, + distinct_operands: 0, + total_operators: 3, + total_operands: 0, + }); + assert_eq!(d.volume, 3.0); + assert_eq!(d.difficulty, 0.0); + assert_eq!(d.estimated_length, 0.0); } } diff --git a/crates/cf-halstead/src/classify.rs b/crates/cf-halstead/src/classify.rs new file mode 100644 index 00000000..4935248a --- /dev/null +++ b/crates/cf-halstead/src/classify.rs @@ -0,0 +1,398 @@ +//! The operator / operand classification table. +//! +//! Halstead counts two kinds of tokens. This module is the single place that +//! says which UAST node is which, and how a node's operator text is recovered +//! when the mapping does not spell it out: +//! +//! | UAST node | Class | Name | +//! |---------------------------------------------|----------------------|----------------------------------------| +//! | leaf `Identifier` / `Literal` | operand | `token`, else source span, else `props.name` / `props.value` | +//! | leaf `Identifier` with the `Type` role | operator (type spec) | same text sources as an operand | +//! | node with `props["operator"]` | operator | the property, verbatim (nothing else from that node) | +//! | `Call` / `Index` / `Slice` | operator | fixed `()` / `[]` / `[:]` | +//! | any other node inside a function body | operator(s) | the node's *residue* (see below) | +//! | `Parameter` (type or role), `Comment` | ignored subtree | — | +//! | function name / signature | ignored | — | +//! +//! The **residue** of a node is its source span with every child's span +//! blanked out: for `a + b` it is ` + `, for `c := a + b` it is ` := `, for +//! `return c * 2` it is `return `, for `fmt.Println` it is `.`, for `r++` it +//! is `++`. The residue is tokenized into runs of [`SYMBOL_CHARS`] (always +//! operators) and words (operators only when listed in +//! [`KEYWORD_OPERATORS`]); brackets, commas, semicolons, quotes and a lone +//! `:` are delimiters. A node TYPE name is never an operator, and a node's +//! whole text is never an operand. +//! +//! Without any recoverable source text, keyword statements fall back to the +//! [`CANONICAL_KEYWORDS`] spelling of their type (`Return` → `return`), and +//! symbol nodes are left uncounted rather than mislabelled. + +/// Node types that open a function context (a per-function record). +pub const FUNCTION_TYPES: &[&str] = &["Function", "Method"]; + +/// Role pair that marks a function-declaration node whatever its type is. +pub const FUNCTION_ROLES: &[&str] = &["Function", "Declaration"]; + +/// Node types whose leaves are operands. +pub const OPERAND_TYPES: &[&str] = &["Identifier", "Literal"]; + +/// Role that turns a leaf identifier into a type specifier (an operator). +pub const TYPE_ROLE: &str = "Type"; + +/// Parameter nodes (by type or role) belong to the signature and are skipped. +pub const PARAMETER: &str = "Parameter"; + +/// Node types whose subtrees are never scanned. +pub const IGNORED_TYPES: &[&str] = &["Comment", "DocString"]; + +/// Role / type that marks a function's body among its children. +pub const BODY_ROLE: &str = "Body"; +/// Block node type (a body without the `Body` role). +pub const BLOCK_TYPE: &str = "Block"; + +/// Property that carries a node's operator text when the mapping exposes it. +pub const OPERATOR_PROP: &str = "operator"; +/// Property carrying a declaration / entity name. +pub const NAME_PROP: &str = "name"; +/// Property carrying a literal value. +pub const VALUE_PROP: &str = "value"; + +/// Operators fixed by node type: a call's parentheses, an index, a slice. +pub const FIXED_OPERATORS: &[(&str, &str)] = &[("Call", "()"), ("Index", "[]"), ("Slice", "[:]")]; + +/// Keyword spelling used for a statement node when no source text can be +/// recovered for it. +pub const CANONICAL_KEYWORDS: &[(&str, &str)] = &[ + ("Await", "await"), + ("Break", "break"), + ("Case", "case"), + ("Catch", "catch"), + ("Continue", "continue"), + ("Finally", "finally"), + ("If", "if"), + ("Loop", "loop"), + ("Match", "match"), + ("Return", "return"), + ("Spread", "..."), + ("Switch", "switch"), + ("Throw", "throw"), + ("Try", "try"), + ("Yield", "yield"), +]; + +/// Characters that form operator symbols; a maximal run is one operator. +pub const SYMBOL_CHARS: &str = "+-*/%=<>!&|^~?:.@#$"; + +/// A lone colon is block / label punctuation, not an operator. +const LONE_COLON: &str = ":"; +/// The conditional operator, assembled from the `?` and `:` of one node. +const TERNARY: &str = "?:"; +const QUESTION: &str = "?"; + +/// Words that count as operators when they appear in a node's residue. +/// Sorted (binary search); language keywords that act as operators across +/// the supported grammars, including declaration and primitive-type keywords. +pub const KEYWORD_OPERATORS: &[&str] = &[ + "abstract", + "alignof", + "and", + "as", + "assert", + "async", + "await", + "begin", + "bool", + "boolean", + "break", + "byte", + "case", + "catch", + "chan", + "char", + "class", + "const", + "constexpr", + "continue", + "decltype", + "def", + "default", + "defer", + "del", + "delete", + "do", + "double", + "dyn", + "elif", + "else", + "elsif", + "end", + "ensure", + "enum", + "except", + "explicit", + "extends", + "extern", + "f32", + "f64", + "fallthrough", + "final", + "finally", + "float", + "fn", + "for", + "foreach", + "from", + "func", + "function", + "global", + "go", + "goto", + "i128", + "i16", + "i32", + "i64", + "i8", + "if", + "impl", + "implements", + "import", + "in", + "inline", + "instanceof", + "int", + "interface", + "is", + "isize", + "lambda", + "let", + "long", + "loop", + "match", + "move", + "mut", + "namespace", + "new", + "nonlocal", + "not", + "of", + "or", + "override", + "package", + "pass", + "private", + "protected", + "pub", + "public", + "raise", + "range", + "register", + "repeat", + "rescue", + "return", + "select", + "short", + "signed", + "sizeof", + "static", + "str", + "string", + "struct", + "switch", + "template", + "then", + "throw", + "throws", + "trait", + "try", + "type", + "typedef", + "typename", + "typeof", + "u128", + "u16", + "u32", + "u64", + "u8", + "unless", + "unsafe", + "unsigned", + "until", + "use", + "using", + "usize", + "val", + "var", + "virtual", + "void", + "volatile", + "when", + "where", + "while", + "with", + "xor", + "yield", +]; + +/// True when `word` is a keyword operator. +#[must_use] +pub fn is_keyword_operator(word: &str) -> bool { + KEYWORD_OPERATORS.binary_search(&word).is_ok() +} + +/// The fixed operator for a node type, if it has one. +#[must_use] +pub fn fixed_operator(node_type: &str) -> Option<&'static str> { + FIXED_OPERATORS + .iter() + .find(|(t, _)| *t == node_type) + .map(|(_, op)| *op) +} + +/// The canonical keyword for a statement node type, if it has one. +#[must_use] +pub fn canonical_keyword(node_type: &str) -> Option<&'static str> { + CANONICAL_KEYWORDS + .iter() + .find(|(t, _)| *t == node_type) + .map(|(_, kw)| *kw) +} + +/// True when `token` is exactly one operator: a symbol run or a keyword. +#[must_use] +pub fn is_bare_operator(token: &str) -> bool { + !token.is_empty() + && (token.chars().all(|c| SYMBOL_CHARS.contains(c)) && token != LONE_COLON + || is_keyword_operator(token)) +} + +/// Character classes of residue text. +#[derive(Clone, Copy, PartialEq, Eq)] +enum CharClass { + Word, + Symbol, + Delimiter, +} + +fn class_of(c: char) -> CharClass { + if c.is_alphanumeric() || c == '_' { + CharClass::Word + } else if SYMBOL_CHARS.contains(c) { + CharClass::Symbol + } else { + CharClass::Delimiter + } +} + +/// Tokenizes a node's residue into its operators: every symbol run, every +/// keyword word; a lone `:` is dropped unless it completes a `?` into the +/// conditional operator `?:`. +/// +/// ``` +/// use cf_halstead::classify::operator_tokens; +/// assert_eq!(operator_tokens(" := "), vec![":="]); +/// assert_eq!(operator_tokens("return "), vec!["return"]); +/// assert_eq!(operator_tokens("for , := range "), vec!["for", ":=", "range"]); +/// assert_eq!(operator_tokens(" ? : "), vec!["?:"]); +/// assert_eq!(operator_tokens("( ) ; Foo"), Vec::<String>::new()); +/// ``` +#[must_use] +pub fn operator_tokens(residue: &str) -> Vec<String> { + let mut out: Vec<String> = Vec::new(); + let mut run = String::new(); + let mut run_class = CharClass::Delimiter; + for c in residue.chars().chain(std::iter::once(' ')) { + let class = class_of(c); + if class != run_class { + push_run(&mut out, run_class, &run); + run.clear(); + run_class = class; + } + if class != CharClass::Delimiter { + run.push(c); + } + } + out +} + +fn push_run(out: &mut Vec<String>, class: CharClass, run: &str) { + match class { + CharClass::Word if is_keyword_operator(run) => out.push(run.to_string()), + CharClass::Symbol if run == LONE_COLON => { + if out.last().is_some_and(|last| last == QUESTION) { + out.pop(); + out.push(TERNARY.to_string()); + } + } + CharClass::Symbol => out.push(run.to_string()), + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn keyword_table_is_sorted_for_binary_search() { + let mut sorted = KEYWORD_OPERATORS.to_vec(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!( + sorted, KEYWORD_OPERATORS, + "KEYWORD_OPERATORS must be sorted and unique" + ); + } + + #[test] + fn keywords_are_operators_identifiers_are_not() { + assert!(is_keyword_operator("return")); + assert!(is_keyword_operator("int")); + assert!(!is_keyword_operator("Println")); + assert!(!is_keyword_operator("Return")); + } + + #[test] + fn symbol_runs_are_split_by_delimiters_and_words() { + assert_eq!(operator_tokens("a + b"), vec!["+"]); + assert_eq!(operator_tokens(" += "), vec!["+="]); + assert_eq!(operator_tokens("++;"), vec!["++"]); + assert_eq!(operator_tokens("let = ;"), vec!["let", "="]); + assert_eq!(operator_tokens("if else "), vec!["if", "else"]); + assert_eq!(operator_tokens("::"), vec!["::"]); + assert_eq!(operator_tokens("->"), vec!["->"]); + } + + #[test] + fn lone_colon_is_punctuation_but_completes_a_ternary() { + assert_eq!(operator_tokens("for ( : )"), vec!["for"]); + assert_eq!(operator_tokens("case 1:"), vec!["case"]); + assert_eq!(operator_tokens(" ? : "), vec!["?:"]); + assert_eq!(operator_tokens("x if else "), vec!["if", "else"]); + } + + #[test] + fn quotes_and_brackets_never_become_operators() { + assert!(operator_tokens("( \"x\" ) [ ] { }").is_empty()); + assert!(operator_tokens("Foo bar").is_empty()); + } + + #[test] + fn bare_operator_tokens() { + assert!(is_bare_operator("++")); + assert!(is_bare_operator("defer")); + assert!(is_bare_operator("mut")); + assert!(!is_bare_operator(":")); + assert!(!is_bare_operator("")); + assert!(!is_bare_operator("fmt.Println(\"x\")")); + assert!(!is_bare_operator("BinaryOp")); + } + + #[test] + fn fixed_and_canonical_tables() { + assert_eq!(fixed_operator("Call"), Some("()")); + assert_eq!(fixed_operator("Index"), Some("[]")); + assert_eq!(fixed_operator("BinaryOp"), None); + assert_eq!(canonical_keyword("Return"), Some("return")); + assert_eq!(canonical_keyword("BinaryOp"), None); + } +} diff --git a/crates/cf-halstead/src/detector.rs b/crates/cf-halstead/src/detector.rs index aa103588..bed732b3 100644 --- a/crates/cf-halstead/src/detector.rs +++ b/crates/cf-halstead/src/detector.rs @@ -1,465 +1,710 @@ -//! Operator / operand detection. +//! Function discovery and operator / operand collection. //! -//! Halstead classifies every token as an *operator* or an *operand*. The -//! classification tables and token-extraction rules here are part of the -//! report contract (pinned by the differential gate) and must not drift. +//! One pre-order walk over the UAST with a stack of open function contexts: +//! every operator and operand inside a function body is attributed to the +//! innermost enclosing function (a closure's tokens never leak into its +//! parent), the function's own signature (name, parameters, return type, +//! modifiers) is skipped, and nodes outside every function are only searched +//! for further functions. The classification itself is the table in +//! [`crate::classify`]; text recovery goes through [`crate::source`]. //! -//! ## Node abstraction -//! -//! The detector reads only a node's type, token, roles, props, and children. -//! To keep the analyzer testable in isolation it is generic over the -//! [`HalNode`] trait; the production wiring implements [`HalNode`] for -//! `cf_uast_node::Node` (see [`crate::standalone`]). +//! This is the ONE detector: the static `static/halstead` report and the +//! `history/quality` file-level measures both run [`collect_functions`]. + +use std::collections::BTreeMap; + +use cf_uast_node::Node; + +use crate::calculator::HalsteadCounts; +use crate::classify::{ + canonical_keyword, fixed_operator, is_bare_operator, operator_tokens, BLOCK_TYPE, BODY_ROLE, + FUNCTION_ROLES, FUNCTION_TYPES, IGNORED_TYPES, NAME_PROP, OPERAND_TYPES, OPERATOR_PROP, + PARAMETER, TYPE_ROLE, VALUE_PROP, +}; +use crate::source::{residue, span, Window}; + +/// Name reported for a function whose name cannot be recovered. +pub const ANONYMOUS: &str = "anonymous"; + +/// UAST type of identifier nodes. +const IDENTIFIER: &str = "Identifier"; +/// UAST type of literal nodes. +const LITERAL: &str = "Literal"; +/// Role marking a declaration's name identifier. +const NAME_ROLE: &str = "Name"; +/// Wrapper nodes with no children of their own that sit in a signature +/// (modifiers such as `public`, bare return types such as `int`). +const SYNTHETIC: &str = "Synthetic"; +/// Type annotations are signature parts. +const TYPE_ANNOTATION: &str = "TypeAnnotation"; + +/// The raw operator / operand multisets of one function. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FunctionCounts { + /// Function name (`props["name"]`, else its name identifier, else + /// [`ANONYMOUS`]). + pub name: String, + /// Operator text → occurrences. + pub operators: BTreeMap<String, i64>, + /// Operand text → occurrences. + pub operands: BTreeMap<String, i64>, +} -use std::collections::HashMap; +impl FunctionCounts { + /// The four Halstead counts (n1, n2, N1, N2) of these multisets. + #[must_use] + pub fn totals(&self) -> HalsteadCounts { + HalsteadCounts { + distinct_operators: to_i64(self.operators.len()), + distinct_operands: to_i64(self.operands.len()), + total_operators: self.operators.values().sum(), + total_operands: self.operands.values().sum(), + } + } +} -/// Minimal read-only view of a UAST node, covering exactly the fields the -/// Halstead detector inspects. +/// Collects every function of `root` with its operator / operand multisets, +/// in post-order (a function is emitted when its subtree is complete, so a +/// nested closure precedes its parent). +/// +/// `source` is the file the tree was parsed from; with it, token-less nodes +/// are read back by span. Without it, a node's own verbatim token or the +/// enclosing function's verbatim token serves as the window, and a node whose +/// text cannot be recovered at all is left uncounted. +/// +/// ``` +/// use cf_halstead::detector::collect_functions; +/// use cf_uast_node::{Node, Positions}; /// -/// Implement this for `cf_uast_node::Node` in the integration layer. The string -/// values returned for `node_type`, `token`, role names, and prop keys must be -/// the canonical UAST strings the parser emits (e.g. `"Function"`, -/// `"Identifier"`, role `"Operator"`, prop `"operator"`), because those strings -/// flow into the operator/operand maps and therefore into machine output. -pub trait HalNode { - /// The node's UAST type string, e.g. `"BinaryOp"`. - fn node_type(&self) -> &str; - /// The node's literal token text, empty if none. - fn token(&self) -> &str; - /// True if the node carries any of the named roles. - fn has_any_role(&self, roles: &[&str]) -> bool; - /// Looks up a property by key. - fn prop(&self, key: &str) -> Option<&str>; - /// The node's children, in order. - fn children(&self) -> &[Self] - where - Self: Sized; +/// fn at(t: &str, tok: &str, s: u64, e: u64) -> Node { +/// let mut n = Node::with_token(t, tok); +/// n.pos = Some(Positions { start_offset: s, end_offset: e, ..Positions::default() }); +/// n +/// } +/// // func f() { return x + 1 } +/// let src = b"func f() { return x + 1 }"; +/// let mut f = at("Function", "", 0, 25); +/// f.props.insert("name".into(), "f".into()); +/// let mut body = at("Block", "", 9, 25); +/// body.roles = vec!["Body".into()]; +/// let mut ret = at("Return", "", 11, 23); +/// let mut plus = at("BinaryOp", "", 18, 23); +/// plus.add_child(at("Identifier", "x", 18, 19)); +/// plus.add_child(at("Literal", "", 22, 23)); +/// ret.add_child(plus); +/// body.add_child(ret); +/// f.add_child(body); +/// let mut root = Node::with_token("File", ""); +/// root.add_child(f); +/// +/// let fns = collect_functions(&root, Some(src)); +/// assert_eq!(fns.len(), 1); +/// assert_eq!(fns[0].name, "f"); +/// assert_eq!(fns[0].operators.get("return"), Some(&1)); +/// assert_eq!(fns[0].operators.get("+"), Some(&1)); +/// assert_eq!(fns[0].operands.get("x"), Some(&1)); +/// assert_eq!(fns[0].operands.get("1"), Some(&1)); +/// ``` +#[must_use] +pub fn collect_functions<'a>(root: &'a Node, source: Option<&'a [u8]>) -> Vec<FunctionCounts> { + Collector { + file: source.map(Window::file), + contexts: Vec::new(), + out: Vec::new(), + } + .run(root) } -// --- Canonical UAST strings the detector matches against --- - -/// UAST type strings classified as operators. -pub const OPERATOR_TYPES: &[&str] = &[ - "BinaryOp", - "UnaryOp", - "Assignment", - "Call", - "Index", - "Slice", - "Return", -]; - -/// UAST role strings classified as operators. -pub const OPERATOR_ROLES: &[&str] = &["Operator", "Assignment", "Call", "Return"]; - -/// UAST type strings classified as operands. -pub const OPERAND_TYPES: &[&str] = &["Identifier", "Literal", "Field"]; - -/// UAST role strings classified as operands. -pub const OPERAND_ROLES: &[&str] = &["Name", "Literal", "Variable", "Argument"]; - -/// UAST type strings that represent declarations (parent context that suppresses -/// counting a declaration identifier as an operand). -pub const DECLARATION_TYPES: &[&str] = &[ - "Function", - "FunctionDecl", - "Method", - "Parameter", - "Variable", - "Field", - "Import", - "Package", - "Struct", - "Class", - "Interface", - "Enum", -]; - -/// Roles on the *parent* that mark a child name-identifier as a declaration. -const DECLARATION_PARENT_ROLES: &[&str] = &["Declaration", "Parameter", "Import", "Type"]; - -/// Exact operator tokens for membership testing. -const TOKEN_OPERATOR_SET: &[&str] = &[ - "===", "!==", "==", "!=", "<=", ">=", "&&", "||", "<<=", ">>=", "<<", ">>", "**", ":=", "+=", - "-=", "*=", "/=", "%=", "&=", "|=", "^=", "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", - "^", "!", -]; - -/// Operators sorted longest-first for containment matching so `===` matches -/// before `==`. Order is load-bearing. -const TOKEN_OPERATORS_BY_LENGTH: &[&str] = &[ - "===", "!==", "<<=", ">>=", "==", "!=", "<=", ">=", "&&", "||", "<<", ">>", "**", ":=", "+=", - "-=", "*=", "/=", "%=", "&=", "|=", "^=", "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", - "^", "!", -]; - -/// Detects operators and operands in UAST nodes. -#[derive(Debug, Clone, Copy, Default)] -pub struct OperatorOperandDetector; - -impl OperatorOperandDetector { - /// Creates a new detector. - #[must_use] - pub fn new() -> Self { - Self - } - - /// Recursively collects operators and operands from a node, accumulating - /// counts into the supplied maps. - /// - /// The classification is by node type/role (see [`OPERATOR_TYPES`] / - /// [`OPERAND_TYPES`]); a declaration's own name identifier is never counted - /// as an operand. Here `x = y + 5` yields the operators `=`/`+` and the - /// operands `x`/`y`/`5`: - /// - /// ``` - /// use std::collections::HashMap; - /// use cf_halstead::detector::OperatorOperandDetector; - /// use cf_uast_node::Node; - /// - /// fn id(token: &str) -> Node { - /// let mut n = Node::with_token("Identifier", token); - /// n.roles = vec!["Variable".into()]; - /// n - /// } - /// - /// let mut lit5 = Node::with_token("Literal", "5"); - /// lit5.roles = vec!["Literal".into()]; - /// - /// let mut plus = Node::with_token("BinaryOp", "+"); - /// plus.props.insert("operator".into(), "+".into()); - /// plus.add_child(id("y")); - /// plus.add_child(lit5); - /// - /// let mut assign = Node::with_token("Assignment", "="); - /// assign.props.insert("operator".into(), "=".into()); - /// assign.add_child(id("x")); - /// assign.add_child(plus); - /// - /// let (mut operators, mut operands) = (HashMap::new(), HashMap::new()); - /// OperatorOperandDetector::new().collect(&assign, &mut operators, &mut operands); - /// - /// assert_eq!(operators.get("="), Some(&1)); - /// assert_eq!(operators.get("+"), Some(&1)); - /// assert_eq!(operands.len(), 3); // x, y, 5 - /// ``` - pub fn collect<N: HalNode>( - &self, - node: &N, - operators: &mut HashMap<String, i64>, - operands: &mut HashMap<String, i64>, - ) { - self.walk(node, None, operators, operands); - } - - fn walk<N: HalNode>( - &self, - node: &N, - parent: Option<&N>, - operators: &mut HashMap<String, i64>, - operands: &mut HashMap<String, i64>, - ) { - if self.is_operator(node) { - self.record_operator(node, operators); - } else { - self.record_operand(node, parent, operands); +/// True when the node opens a function context. +#[must_use] +pub fn is_function_node(node: &Node) -> bool { + FUNCTION_TYPES.contains(&node.node_type.as_str()) || node.has_all_roles(FUNCTION_ROLES) +} + +struct Context<'a> { + counts: FunctionCounts, + /// The function's verbatim token, used as the text window when the walk + /// has no file source. + window: Option<Window<'a>>, +} + +struct Frame<'a> { + node: &'a Node, + next: usize, + opened: bool, +} + +struct Collector<'a> { + file: Option<Window<'a>>, + contexts: Vec<Context<'a>>, + out: Vec<FunctionCounts>, +} + +impl<'a> Collector<'a> { + fn run(mut self, root: &'a Node) -> Vec<FunctionCounts> { + let mut stack: Vec<Frame<'a>> = vec![self.enter(root)]; + while let Some(top) = stack.last_mut() { + let node: &'a Node = top.node; + if let Some(child) = node.children.get(top.next) { + top.next += 1; + let frame = self.enter(child); + stack.push(frame); + continue; + } + let done = stack.pop().expect("stack is non-empty inside the loop"); + if done.opened { + if let Some(ctx) = self.contexts.pop() { + self.out.push(ctx.counts); + } + } } + self.out + } - for child in node.children() { - self.walk(child, Some(node), operators, operands); + /// Classifies `node` on entry and returns the frame that drives the + /// descent into its children (none for skipped subtrees, the body only + /// for a function). + fn enter(&mut self, node: &'a Node) -> Frame<'a> { + if is_function_node(node) { + self.open_context(node); + return Frame { + node, + next: body_start(node), + opened: true, + }; + } + if self.contexts.is_empty() { + return Frame { + node, + next: 0, + opened: false, + }; + } + let descend = self.classify(node); + Frame { + node, + next: if descend { 0 } else { node.children.len() }, + opened: false, } } - fn record_operator<N: HalNode>(&self, node: &N, operators: &mut HashMap<String, i64>) { - let operator = self.operator_name(node); - if operator.is_empty() { - return; + fn open_context(&mut self, node: &'a Node) { + let window = if self.file.is_some() { + None + } else { + Window::of_token(node) + }; + let name = self.function_name(node, window); + self.contexts.push(Context { + counts: FunctionCounts { + name, + ..FunctionCounts::default() + }, + window, + }); + } + + /// Records the node's tokens; returns whether to descend into children. + fn classify(&mut self, node: &'a Node) -> bool { + if is_ignored(node) { + return false; + } + if is_operand_node(node) && counted_whole(node) { + if let Some(text) = self.text_of(node) { + if node.has_any_role(&[TYPE_ROLE]) { + self.record_operator(text); + } else { + self.record_operand(text); + } + } + return false; } - *operators.entry(operator).or_insert(0) += 1; + for op in self.operators_of(node) { + self.record_operator(op); + } + true } - fn record_operand<N: HalNode>( - &self, - node: &N, - parent: Option<&N>, - operands: &mut HashMap<String, i64>, - ) { - if !self.is_operand(node) || !self.should_count_operand(node, parent) { - return; + /// The operators a non-operand node contributes: the explicit + /// `operator` property alone when present; otherwise the type's fixed + /// operator plus the residue tokens, or — without recoverable residue — + /// the node's own bare token or its canonical keyword. + fn operators_of(&self, node: &'a Node) -> Vec<String> { + if let Some(op) = node.props.get(OPERATOR_PROP).filter(|s| !s.is_empty()) { + return vec![op.clone()]; } - let operand = self.operand_name(node); - if operand.is_empty() { - return; + let mut ops: Vec<String> = fixed_operator(&node.node_type) + .map(str::to_string) + .into_iter() + .collect(); + match self.residue_of(node) { + Some(res) => ops.extend(operator_tokens(&res)), + None => ops.extend(token_fallback(node)), } - *operands.entry(operand).or_insert(0) += 1; + ops } - /// True if the node is an operator by type or role. - #[must_use] - pub fn is_operator<N: HalNode>(&self, node: &N) -> bool { - OPERATOR_TYPES.contains(&node.node_type()) || node.has_any_role(OPERATOR_ROLES) + /// The node's residue through the first window that covers it: the file, + /// the node's own verbatim token, the enclosing function's token. + fn residue_of(&self, node: &'a Node) -> Option<String> { + let context_window = self.contexts.last().and_then(|c| c.window); + [self.file, Window::of_token(node), context_window] + .iter() + .flatten() + .find_map(|w| residue(node, w)) } - /// True if the node is an operand by type or role. - #[must_use] - pub fn is_operand<N: HalNode>(&self, node: &N) -> bool { - OPERAND_TYPES.contains(&node.node_type()) || node.has_any_role(OPERAND_ROLES) + /// A leaf's text: its token, else its span read through a window, else + /// its `name` / `value` property. + fn text_of(&self, node: &Node) -> Option<String> { + let context_window = self.contexts.last().and_then(|c| c.window); + self.text_through(node, context_window) } - /// Extracts the operator name: prefer the `operator` prop, then a - /// containment match inside the token, then the raw token, then the node - /// type. - #[must_use] - pub fn operator_name<N: HalNode>(&self, node: &N) -> String { - if let Some(op) = node.prop("operator") { - return op.to_string(); + fn text_through(&self, node: &Node, context_window: Option<Window<'a>>) -> Option<String> { + if !node.token.is_empty() { + return Some(node.token.clone()); } - if let Some(op) = extract_operator_from_token(node.token()) { - return op.to_string(); - } - if !node.token().is_empty() { - return node.token().to_string(); + if let Some((start, end)) = span(node) { + let found = [self.file, context_window] + .iter() + .flatten() + .find_map(|w| w.slice(start, end)); + if let Some(bytes) = found { + return Some(String::from_utf8_lossy(bytes).into_owned()); + } } - node.node_type().to_string() + node.props + .get(NAME_PROP) + .or_else(|| node.props.get(VALUE_PROP)) + .filter(|s| !s.is_empty()) + .cloned() } - /// Extracts the operand name: prefer the token, then the `name` prop, then - /// the `value` prop, else empty. - #[must_use] - pub fn operand_name<N: HalNode>(&self, node: &N) -> String { - if !node.token().is_empty() { - return node.token().to_string(); + /// `props["name"]`, else the name identifier among the direct children, + /// else the name of a nested declarator function (C), else + /// [`ANONYMOUS`]. + fn function_name(&self, node: &Node, window: Option<Window<'a>>) -> String { + if let Some(name) = node.props.get(NAME_PROP).filter(|s| !s.is_empty()) { + return name.clone(); } - if let Some(name) = node.prop("name") { - return name.to_string(); + let named = node.children.iter().find(|c| { + c.node_type == IDENTIFIER + && c.has_any_role(&[NAME_ROLE]) + && !c.has_any_role(&[TYPE_ROLE]) + }); + if let Some(text) = named.and_then(|c| self.text_through(c, window)) { + return text; } - if let Some(value) = node.prop("value") { - return value.to_string(); + if let Some(inner) = node.children.iter().find(|c| is_function_node(c)) { + return self.function_name(inner, window); } - String::new() + ANONYMOUS.to_string() } - fn should_count_operand<N: HalNode>(&self, node: &N, parent: Option<&N>) -> bool { - if is_declaration_identifier(node, parent) { - return false; + fn record_operator(&mut self, op: String) { + if let Some(ctx) = self.contexts.last_mut() { + *ctx.counts.operators.entry(op).or_insert(0) += 1; } - !self.operand_name(node).is_empty() } -} -/// True if `node` is a declaration's name identifier given its `parent`. -/// Such identifiers are NOT counted as operands. -fn is_declaration_identifier<N: HalNode>(node: &N, parent: Option<&N>) -> bool { - let Some(parent) = parent else { return false }; + fn record_operand(&mut self, operand: String) { + if let Some(ctx) = self.contexts.last_mut() { + *ctx.counts.operands.entry(operand).or_insert(0) += 1; + } + } +} - if node.node_type() != "Identifier" || !node.has_any_role(&["Name"]) { - return false; +/// Without residue: the node's own token when it is exactly one operator, +/// else the canonical keyword of its type. +fn token_fallback(node: &Node) -> Option<String> { + if is_bare_operator(&node.token) { + return Some(node.token.clone()); } + canonical_keyword(&node.node_type).map(str::to_string) +} - if parent.has_any_role(DECLARATION_PARENT_ROLES) { - return true; +/// Index of the first body child of a function node; children before it are +/// the signature. Without an explicit body, everything after the last +/// signature part (name, parameters, modifiers, type annotations) is body. +fn body_start(node: &Node) -> usize { + if let Some(i) = node + .children + .iter() + .position(|c| c.has_any_role(&[BODY_ROLE]) || c.node_type == BLOCK_TYPE) + { + return i; } + node.children + .iter() + .rposition(is_signature_part) + .map_or(0, |i| i + 1) +} - DECLARATION_TYPES.contains(&parent.node_type()) +fn is_signature_part(node: &Node) -> bool { + node.node_type == IDENTIFIER + || is_parameter(node) + || node.node_type == TYPE_ANNOTATION + || (node.node_type == SYNTHETIC && node.children.is_empty()) } -/// Extracts an operator from a free-form token: trims, tries an exact set -/// match, then a longest-first containment match where the operator is -/// surrounded by spaces (`" op "`). -fn extract_operator_from_token(token: &str) -> Option<&'static str> { - if token.trim().is_empty() { - return None; - } +fn is_parameter(node: &Node) -> bool { + node.node_type == PARAMETER || node.has_any_role(&[PARAMETER]) +} + +fn is_ignored(node: &Node) -> bool { + IGNORED_TYPES.contains(&node.node_type.as_str()) || is_parameter(node) +} + +fn is_operand_node(node: &Node) -> bool { + OPERAND_TYPES.contains(&node.node_type.as_str()) +} - if let Some(&op) = TOKEN_OPERATOR_SET.iter().find(|&&op| op == token) { - return Some(op); +/// An operand node is counted by its own text when it is a leaf, a quoted +/// literal wrapping its content literal, or a composite with no operand +/// parts; otherwise its parts are classified individually. +fn counted_whole(node: &Node) -> bool { + if node.children.is_empty() { + return true; + } + if node.node_type == LITERAL + && node.children.len() == 1 + && node.children[0].node_type == LITERAL + { + return true; } + !has_operand_descendant(node) +} - for &op in TOKEN_OPERATORS_BY_LENGTH { - let needle = format!(" {op} "); - if token.contains(&needle) { - return Some(op); +fn has_operand_descendant(node: &Node) -> bool { + let mut stack: Vec<&Node> = node.children.iter().collect(); + while let Some(n) = stack.pop() { + if is_operand_node(n) { + return true; } + stack.extend(n.children.iter()); } + false +} - None +fn to_i64(n: usize) -> i64 { + i64::try_from(n).unwrap_or(i64::MAX) } #[cfg(test)] -pub(crate) mod test_support { - //! A concrete in-crate node used by unit tests. - use super::HalNode; - use std::collections::HashMap; - - /// A simple owned node for tests. - #[derive(Debug, Clone, Default)] - pub struct TestNode { - pub node_type: String, - pub token: String, - pub roles: Vec<String>, - pub props: HashMap<String, String>, - pub children: Vec<TestNode>, - } - - impl TestNode { - pub fn new(node_type: &str) -> Self { - Self { - node_type: node_type.to_string(), - ..Default::default() - } - } +mod tests { + use super::*; + use cf_uast_node::Positions; + + /// A node positioned at `[start, end)` with the given token. + fn at(node_type: &str, token: &str, start: u64, end: u64) -> Node { + let mut n = Node::with_token(node_type, token); + n.pos = Some(Positions { + start_offset: start, + end_offset: end, + ..Positions::default() + }); + n + } - pub fn with_token(mut self, token: &str) -> Self { - self.token = token.to_string(); - self - } + fn with_roles(mut n: Node, roles: &[&str]) -> Node { + n.roles = roles.iter().map(|r| (*r).to_string()).collect(); + n + } - pub fn with_roles(mut self, roles: &[&str]) -> Self { - self.roles = roles.iter().map(|r| (*r).to_string()).collect(); - self + fn with_children(mut n: Node, children: Vec<Node>) -> Node { + for c in children { + n.add_child(c); } + n + } - pub fn with_prop(mut self, key: &str, value: &str) -> Self { - self.props.insert(key.to_string(), value.to_string()); - self - } + fn counts(pairs: &[(&str, i64)]) -> BTreeMap<String, i64> { + pairs.iter().map(|(k, v)| ((*k).to_string(), *v)).collect() + } - pub fn child(mut self, child: TestNode) -> Self { - self.children.push(child); - self - } + /// Mirrors the Go mapping's tree for + /// `func hal(a int, b int) int {\n\tc := a + b\n\treturn c * 2\n}` + /// (token-less BinaryOps and literal, whole-statement tokens on the + /// declaration and the return, name identifiers on the signature). + const HAL_SRC: &str = "func hal(a int, b int) int {\n\tc := a + b\n\treturn c * 2\n}"; + + fn hal_tree(function_token: &str) -> Node { + let ident = |tok: &str, s: u64, e: u64| with_roles(at("Identifier", tok, s, e), &["Name"]); + let typ = |s: u64, e: u64| with_roles(at("Identifier", "int", s, e), &["Name", "Type"]); + let param = |name: &str, s: u64, e: u64| { + let mut p = with_roles(at("Parameter", "", s, e), &["Parameter"]); + p.props.insert("name".into(), name.into()); + with_children(p, vec![ident(name, s, s + 1), typ(s + 2, e)]) + }; + let params = with_children( + with_roles(at("Parameter", "(a int, b int)", 8, 22), &["Parameter"]), + vec![param("a", 9, 14), param("b", 16, 21)], + ); + let decl = with_children( + with_roles( + at("Variable", "c := a + b", 30, 40), + &["Variable", "Declaration"], + ), + vec![ + with_children(at("List", "", 30, 31), vec![ident("c", 30, 31)]), + with_children( + at("List", "", 35, 40), + vec![with_children( + with_roles(at("BinaryOp", "", 35, 40), &["Operator"]), + vec![ident("a", 35, 36), ident("b", 39, 40)], + )], + ), + ], + ); + let ret = with_children( + with_roles(at("Return", "return c * 2", 42, 54), &["Return"]), + vec![with_children( + at("List", "", 49, 54), + vec![with_children( + with_roles(at("BinaryOp", "", 49, 54), &["Operator"]), + vec![ + ident("c", 49, 50), + with_roles(at("Literal", "", 53, 54), &["Literal"]), + ], + )], + )], + ); + let body = with_children( + with_roles(at("Block", "", 27, 56), &["Body"]), + vec![decl, ret], + ); + let mut f = with_roles( + at("Function", function_token, 0, 56), + &["Function", "Declaration"], + ); + f.props.insert("name".into(), "hal".into()); + let f = with_children(f, vec![ident("hal", 5, 8), params, typ(23, 26), body]); + with_children(Node::with_token("File", ""), vec![f]) } - impl HalNode for TestNode { - fn node_type(&self) -> &str { - &self.node_type - } - fn token(&self) -> &str { - &self.token - } - fn has_any_role(&self, roles: &[&str]) -> bool { - self.roles.iter().any(|r| roles.contains(&r.as_str())) - } - fn prop(&self, key: &str) -> Option<&str> { - self.props.get(key).map(String::as_str) - } - fn children(&self) -> &[Self] { - &self.children + fn expected_hal() -> FunctionCounts { + FunctionCounts { + name: "hal".into(), + operators: counts(&[(":=", 1), ("+", 1), ("*", 1), ("return", 1)]), + operands: counts(&[("a", 1), ("b", 1), ("c", 2), ("2", 1)]), } } -} -#[cfg(test)] -mod tests { - use super::test_support::TestNode; - use super::*; + #[test] + fn hal_with_file_source_recovers_every_operator_and_the_literal() { + let fns = collect_functions(&hal_tree(""), Some(HAL_SRC.as_bytes())); + assert_eq!(fns, vec![expected_hal()]); + } #[test] - fn operator_and_operand_detection() { - let d = OperatorOperandDetector::new(); + fn hal_without_file_source_uses_the_function_token_window() { + let fns = collect_functions(&hal_tree(HAL_SRC), None); + assert_eq!(fns, vec![expected_hal()]); + } - let op = TestNode::new("BinaryOp") - .with_token("+") - .with_roles(&["Operator"]) - .with_prop("operator", "+"); - assert!(d.is_operator(&op)); + #[test] + fn without_any_source_unrecoverable_symbols_are_uncounted_never_placeholders() { + let fns = collect_functions(&hal_tree(""), None); + assert_eq!(fns.len(), 1); + // `:=` and `return` come from the statements' own verbatim tokens; + // the token-less `+` / `*` have no text anywhere and stay uncounted + // (no "BinaryOp" placeholder). + assert_eq!(fns[0].operators, counts(&[(":=", 1), ("return", 1)])); + // The token-less literal `2` is lost too; the identifiers survive. + assert_eq!(fns[0].operands, counts(&[("a", 1), ("b", 1), ("c", 2)])); + } - let operand = TestNode::new("Identifier") - .with_token("x") - .with_roles(&["Variable"]) - .with_prop("name", "x"); - assert!(d.is_operand(&operand)); + #[test] + fn operator_property_wins_over_residue() { + let mut tree = hal_tree(""); + let body = &mut tree.children[0].children[3]; + let plus = &mut body.children[0].children[1].children[0]; + plus.props.insert("operator".into(), "+".into()); + let fns = collect_functions(&tree, Some(HAL_SRC.as_bytes())); + assert_eq!(fns[0].operators, expected_hal().operators); + } - let lit = TestNode::new("Literal") - .with_token("42") - .with_roles(&["Literal"]) - .with_prop("value", "42"); - assert!(d.is_operand(&lit)); + #[test] + fn totals_are_the_four_halstead_counts() { + let t = expected_hal().totals(); + assert_eq!( + t, + HalsteadCounts { + distinct_operators: 4, + distinct_operands: 4, + total_operators: 4, + total_operands: 5, + } + ); + } + + #[test] + fn empty_tree_has_no_functions() { + assert!(collect_functions(&Node::with_token("File", ""), None).is_empty()); } + /// `func outer() { inner := func() { return 1 }; return 2 }`-shaped tree: + /// the closure is its own record and its literal never reaches `outer`. #[test] - fn extract_operator_from_token_cases() { - assert_eq!(extract_operator_from_token("=="), Some("==")); - assert_eq!(extract_operator_from_token("a + b"), Some("+")); - assert_eq!(extract_operator_from_token("x"), None); - assert_eq!(extract_operator_from_token(" "), None); - // longest-first: "===" must win over "==" on an exact token. - assert_eq!(extract_operator_from_token("==="), Some("===")); + fn closure_tokens_belong_to_the_innermost_function() { + let lit = |tok: &str| with_roles(Node::with_token("Literal", tok), &["Literal"]); + let ret = |tok: &str| { + with_children( + with_roles(Node::with_token("Return", ""), &["Return"]), + vec![lit(tok)], + ) + }; + let inner = with_children( + with_roles(Node::with_token("Function", ""), &["Function"]), + vec![with_children( + with_roles(Node::with_token("Block", ""), &["Body"]), + vec![ret("1")], + )], + ); + let mut outer = with_roles(Node::with_token("Function", ""), &["Function"]); + outer.props.insert("name".into(), "outer".into()); + let outer = with_children( + outer, + vec![with_children( + with_roles(Node::with_token("Block", ""), &["Body"]), + vec![inner, ret("2")], + )], + ); + let fns = collect_functions(&outer, None); + assert_eq!(fns.len(), 2, "closure first (post-order), then outer"); + assert_eq!(fns[0].name, ANONYMOUS); + assert_eq!(fns[0].operands, counts(&[("1", 1)])); + assert_eq!(fns[1].name, "outer"); + assert_eq!(fns[1].operands, counts(&[("2", 1)])); + assert_eq!(fns[1].operators, counts(&[("return", 1)])); } - /// `x = (y + 5)` yields 2 operators (`=`,`+`) and 3 operands - /// (`x`,`y`,`5`). #[test] - fn collect_simple_function() { - let function = TestNode::new("Function") - .with_roles(&["Function", "Declaration"]) - .with_prop("name", "testFunction") - .child( - TestNode::new("Assignment") - .with_token("=") - .with_roles(&["Assignment"]) - .with_prop("operator", "=") - .child( - TestNode::new("Identifier") - .with_token("x") - .with_roles(&["Variable"]) - .with_prop("name", "x"), - ) - .child( - TestNode::new("BinaryOp") - .with_token("+") - .with_roles(&["Operator"]) - .with_prop("operator", "+") - .child( - TestNode::new("Identifier") - .with_token("y") - .with_roles(&["Variable"]) - .with_prop("name", "y"), - ) - .child( - TestNode::new("Literal") - .with_token("5") - .with_roles(&["Literal"]) - .with_prop("value", "5"), - ), - ), - ); - - let mut operators = HashMap::new(); - let mut operands = HashMap::new(); - OperatorOperandDetector::new().collect(&function, &mut operators, &mut operands); - - assert_eq!(operators.len(), 2); - assert_eq!(operators.get("="), Some(&1)); - assert_eq!(operators.get("+"), Some(&1)); - - assert_eq!(operands.len(), 3); - assert_eq!(operands.get("x"), Some(&1)); - assert_eq!(operands.get("y"), Some(&1)); - assert_eq!(operands.get("5"), Some(&1)); - } - - /// A declaration name-identifier under a declaration parent is NOT an - /// operand. + fn parameters_comments_and_signature_are_never_counted() { + let ident = |tok: &str| with_roles(Node::with_token("Identifier", tok), &["Name"]); + let param = with_children( + with_roles(Node::with_token("Synthetic", "(x)"), &["Parameter"]), + vec![ident("x")], + ); + let comment = with_roles(Node::with_token("Comment", "// uses y"), &["Comment"]); + let body = with_children( + with_roles(Node::with_token("Block", ""), &["Body"]), + vec![comment, ident("y")], + ); + let f = with_children( + with_roles( + Node::with_token("Function", ""), + &["Function", "Declaration"], + ), + vec![ident("f"), param, ident("int"), body], + ); + let fns = collect_functions(&f, None); + assert_eq!(fns[0].name, "f"); + assert_eq!(fns[0].operands, counts(&[("y", 1)])); + assert!(fns[0].operators.is_empty()); + } + + #[test] + fn type_role_identifier_in_a_body_is_a_type_specifier_operator() { + let decl = with_children( + with_roles( + Node::with_token("Variable", ""), + &["Variable", "Declaration"], + ), + vec![ + with_roles(Node::with_token("Identifier", "int"), &["Type", "Name"]), + with_roles(Node::with_token("Identifier", "r"), &["Name"]), + ], + ); + let f = with_children( + with_roles(Node::with_token("Function", ""), &["Function"]), + vec![with_children( + with_roles(Node::with_token("Block", ""), &["Body"]), + vec![decl], + )], + ); + let fns = collect_functions(&f, None); + assert_eq!(fns[0].operators, counts(&[("int", 1)])); + assert_eq!(fns[0].operands, counts(&[("r", 1)])); + } + + /// C: `int f(void) { return 0; }` lowers to Function → [Identifier(int, + /// Type), Function(declarator) → Identifier(f), Block]. The declarator is + /// signature (no empty record), and the outer record takes its name. + #[test] + fn c_declarator_names_the_outer_function_without_an_extra_record() { + let declarator = with_children( + with_roles( + Node::with_token("Function", ""), + &["Function", "Declaration"], + ), + vec![with_roles(Node::with_token("Identifier", "f"), &["Name"])], + ); + let body = with_children( + with_roles(Node::with_token("Block", ""), &["Body"]), + vec![with_children( + with_roles(Node::with_token("Return", "return 0;"), &["Return"]), + vec![with_roles(Node::with_token("Literal", "0"), &["Literal"])], + )], + ); + let f = with_children( + with_roles( + Node::with_token("Function", ""), + &["Function", "Declaration"], + ), + vec![ + with_roles(Node::with_token("Identifier", "int"), &["Type", "Name"]), + declarator, + body, + ], + ); + let fns = collect_functions(&f, None); + assert_eq!(fns.len(), 1); + assert_eq!(fns[0].name, "f"); + assert_eq!(fns[0].operators, counts(&[("return", 1)])); + assert_eq!(fns[0].operands, counts(&[("0", 1)])); + } + + #[test] + fn quoted_literal_is_counted_once_with_its_quotes() { + let src = b"f(\"x\")"; + let content = with_roles(at("Literal", "", 3, 4), &["Literal"]); + let quoted = with_children( + with_roles(at("Literal", "", 2, 5), &["Literal"]), + vec![content], + ); + let call = with_children( + with_roles(at("Call", "", 0, 6), &["Call"]), + vec![ + with_roles(at("Identifier", "f", 0, 1), &["Name"]), + with_children(at("List", "", 1, 6), vec![quoted]), + ], + ); + let f = with_children( + with_roles(at("Function", "", 0, 6), &["Function"]), + vec![with_children( + with_roles(at("Block", "", 0, 6), &["Body"]), + vec![call], + )], + ); + let fns = collect_functions(&f, Some(src)); + assert_eq!(fns[0].operators, counts(&[("()", 1)])); + assert_eq!(fns[0].operands, counts(&[("f", 1), ("\"x\"", 1)])); + } + #[test] - fn declaration_identifier_not_counted() { - let function = TestNode::new("Function") - .with_roles(&["Function", "Declaration"]) - .child( - TestNode::new("Identifier") - .with_token("foo") - .with_roles(&["Name"]) - .with_prop("name", "foo"), - ); - - let mut operators = HashMap::new(); - let mut operands = HashMap::new(); - OperatorOperandDetector::new().collect(&function, &mut operators, &mut operands); - - // The name identifier under the Function (a declaration type) is skipped. - assert!( - operands.is_empty(), - "declaration name must not be an operand" + fn function_without_a_body_contributes_nothing() { + let f = with_children( + with_roles(Node::with_token("Method", ""), &["Function", "Declaration"]), + vec![ + Node::with_token("Synthetic", "int"), + with_roles(Node::with_token("Identifier", "abs"), &["Name"]), + with_roles(Node::with_token("Parameter", ""), &["Parameter"]), + ], ); + let fns = collect_functions(&f, None); + assert_eq!(fns[0].name, "abs"); + assert!(fns[0].operators.is_empty() && fns[0].operands.is_empty()); } } diff --git a/crates/cf-halstead/src/lib.rs b/crates/cf-halstead/src/lib.rs index 3a43b519..7444f416 100644 --- a/crates/cf-halstead/src/lib.rs +++ b/crates/cf-halstead/src/lib.rs @@ -1,23 +1,30 @@ -//! `cf-halstead` — Halstead complexity metrics analyzer. +//! `cf-halstead` — Halstead complexity metrics. //! -//! The full per-function/per-file report builder, formatter, visitor, and -//! aggregator for the static pipeline live in -//! `cf-commands/src/handlers/static_halstead.rs`. The module surface exposed -//! here is the part the **quality** history analyzer consumes: the -//! operator/operand [`detector`], the derived-metric [`calculator`], and the -//! standalone file-level [`analyze`]. +//! The crate owns the whole measurement: the operator / operand +//! classification table ([`classify`]), source-span text recovery +//! ([`source`]), the function walker ([`detector`]), the derived-measure +//! arithmetic ([`calculator`]), the union aggregate with its per-function +//! statistics ([`aggregate`]), and every threshold, label and score formula +//! ([`thresholds`]). The `static/halstead` command handler in `cf-commands` +//! only walks files, parses them, and serializes these results; the +//! `history/quality` analyzer reads the file-level scalars through +//! [`analyze`] / [`analyze_with_source`] — the same detector, so both +//! surfaces agree. //! -//! Compatibility: output bytes are pinned against the reference implementation -//! by the differential gate in `tests/compat`. +//! Definitions: `n1` = distinct operators, `n2` = distinct operands, `N1` = +//! total operators, `N2` = total operands; vocabulary `n = n1 + n2`; length +//! `N = N1 + N2`; volume `V = N · log2(n)`; difficulty +//! `D = (n1 / 2) · (N2 / n2)`; effort `E = D · V`; time `T = E / 18`; bugs +//! `B = V / 3000`. //! //! # Example //! -//! Run the standalone file-level analysis over a one-function UAST and read the -//! file-level measures. The function body `x = y + 5` contributes the operators -//! `=`/`+` and the operands `x`/`y`/`5`, so the volume is positive: +//! Measure a one-function tree. The body `x = y + 5` contributes the +//! operators `=` / `+` (exposed by the mapping as `props["operator"]`) and +//! the operands `x` / `y` / `5`: //! //! ``` -//! use cf_halstead::analyze; +//! use cf_halstead::{analyze, analyze_functions}; //! use cf_uast_node::Node; //! //! fn id(token: &str) -> Node { @@ -25,33 +32,34 @@ //! n.roles = vec!["Variable".into()]; //! n //! } -//! fn lit(token: &str) -> Node { -//! let mut n = Node::with_token("Literal", token); -//! n.roles = vec!["Literal".into()]; -//! n -//! } //! -//! // function f() { x = y + 5 } -//! let mut plus = Node::with_token("BinaryOp", "+"); +//! let mut plus = Node::with_token("BinaryOp", ""); //! plus.props.insert("operator".into(), "+".into()); //! plus.add_child(id("y")); -//! plus.add_child(lit("5")); +//! plus.add_child(Node::literal("5")); //! -//! let mut assign = Node::with_token("Assignment", "="); +//! let mut assign = Node::with_token("Assignment", ""); //! assign.props.insert("operator".into(), "=".into()); //! assign.add_child(id("x")); //! assign.add_child(plus); //! +//! let mut body = Node::with_token("Block", ""); +//! body.roles = vec!["Body".into()]; +//! body.add_child(assign); +//! //! let mut func = Node::with_token("Function", ""); -//! func.roles = vec!["Function".into(), "Declaration".into()]; -//! func.add_child(assign); +//! func.props.insert("name".into(), "f".into()); +//! func.add_child(body); //! //! let mut root = Node::with_token("File", ""); //! root.add_child(func); //! -//! let h = analyze(&root); -//! assert!(h.volume > 0.0, "non-empty function has positive volume"); -//! assert!(h.effort > 0.0); +//! let fns = analyze_functions(&root, None); +//! assert_eq!(fns[0].name, "f"); +//! assert_eq!(fns[0].counts.distinct_operators, 2); +//! assert_eq!(fns[0].counts.total_operands, 3); +//! // n = 5, N = 5 → V = 5 · log2(5) +//! assert_eq!(analyze(&root).volume, 5.0 * 5f64.log2()); //! ``` //! //! A tree with no functions yields all-zero measures: @@ -60,32 +68,19 @@ //! use cf_halstead::{analyze, FileHalstead}; //! use cf_uast_node::Node; //! -//! let root = Node::with_token("File", ""); -//! assert_eq!(analyze(&root), FileHalstead::default()); +//! assert_eq!(analyze(&Node::with_token("File", "")), FileHalstead::default()); //! ``` -/// Crate name, used by smoke tests to confirm the module links. -pub const CRATE_NAME: &str = "cf-halstead"; - /// Analyzer name as it appears in reports (`analyzer_name`). pub const ANALYZER_NAME: &str = "halstead"; -/// Minimum total tokens (operators + operands) before the count-min-sketch -/// path activates. The CMS path only affects the `estimated_total_*` fields, -/// which the quality analyzer does not read. -pub const CMS_TOKEN_THRESHOLD: i64 = 1000; - -/// CMS approximation error bound. -pub const CMS_EPSILON: f64 = 0.001; - -/// CMS failure probability. -pub const CMS_DELTA: f64 = 0.01; - -/// Maximum UAST traversal depth for function discovery. -pub const MAX_DEPTH: i64 = 10; - +pub mod aggregate; pub mod calculator; +pub mod classify; pub mod detector; +pub mod source; pub mod standalone; +pub mod thresholds; -pub use standalone::{analyze, FileHalstead}; +pub use aggregate::{analyze_functions, summarize, FunctionMetrics, FunctionStats, Summary}; +pub use standalone::{analyze, analyze_with_source, FileHalstead}; diff --git a/crates/cf-halstead/src/source.rs b/crates/cf-halstead/src/source.rs new file mode 100644 index 00000000..9c802d83 --- /dev/null +++ b/crates/cf-halstead/src/source.rs @@ -0,0 +1,140 @@ +//! Source-text windows used to recover operator and operand text by span. +//! +//! Many mappings leave `BinaryOp` / `Literal` / `Identifier` nodes without a +//! token; their text is still addressable through the node's byte span. A +//! [`Window`] maps a span onto a byte buffer that starts at a known offset: +//! the whole file (base 0) when the caller has it, or a node's own token when +//! that token is a verbatim slice of the file (its length equals its span). + +use cf_uast_node::Node; + +/// A byte buffer positioned at `base` in the original file. +#[derive(Debug, Clone, Copy)] +pub struct Window<'a> { + base: usize, + bytes: &'a [u8], +} + +impl<'a> Window<'a> { + /// The whole file, starting at byte 0. + #[must_use] + pub fn file(bytes: &'a [u8]) -> Self { + Self { base: 0, bytes } + } + + /// A window over `node.token` when that token is a verbatim slice of the + /// file (`token.len()` equals the node's span length), else `None`. + #[must_use] + pub fn of_token(node: &'a Node) -> Option<Self> { + let (start, end) = span(node)?; + if node.token.is_empty() || node.token.len() != end - start { + return None; + } + Some(Self { + base: start, + bytes: node.token.as_bytes(), + }) + } + + /// The bytes of `[start, end)` (absolute file offsets) when they fall + /// inside this window. + #[must_use] + pub fn slice(&self, start: usize, end: usize) -> Option<&'a [u8]> { + let lo = start.checked_sub(self.base)?; + let hi = end.checked_sub(self.base)?; + self.bytes.get(lo..hi) + } +} + +/// The `[start, end)` byte span of a node, when it carries positions. +#[must_use] +pub fn span(node: &Node) -> Option<(usize, usize)> { + let pos = node.pos?; + let start = usize::try_from(pos.start_offset).ok()?; + let end = usize::try_from(pos.end_offset).ok()?; + (end >= start).then_some((start, end)) +} + +/// The residue of `node` inside `window`: its span text with every child's +/// span blanked. `None` when the node or any child lacks a span, or the span +/// is outside the window. +#[must_use] +pub fn residue(node: &Node, window: &Window<'_>) -> Option<String> { + let (start, end) = span(node)?; + let mut text = window.slice(start, end)?.to_vec(); + for child in &node.children { + let (cs, ce) = span(child)?; + let lo = cs.clamp(start, end) - start; + let hi = ce.clamp(start, end) - start; + text[lo..hi].fill(b' '); + } + Some(String::from_utf8_lossy(&text).into_owned()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cf_uast_node::Positions; + + fn at(node_type: &str, token: &str, start: u64, end: u64) -> Node { + let mut n = Node::with_token(node_type, token); + n.pos = Some(Positions { + start_offset: start, + end_offset: end, + ..Positions::default() + }); + n + } + + #[test] + fn file_window_slices_absolute_offsets() { + let w = Window::file(b"c := a + b"); + assert_eq!(w.slice(5, 10), Some(&b"a + b"[..])); + assert_eq!(w.slice(5, 11), None); + } + + #[test] + fn token_window_requires_a_verbatim_token() { + let full = at("Function", "func f() {}", 10, 21); + let w = Window::of_token(&full).expect("verbatim token"); + assert_eq!(w.slice(16, 18), Some(&b"()"[..])); + assert_eq!(w.slice(0, 2), None, "before the window"); + + let trimmed = at("Function", "func", 10, 21); + assert!(Window::of_token(&trimmed).is_none()); + let empty = at("Function", "", 10, 21); + assert!(Window::of_token(&empty).is_none()); + } + + #[test] + fn residue_blanks_children() { + let src = b"c := a + b"; + let mut stmt = at("Variable", "", 0, 10); + stmt.add_child(at("Identifier", "c", 0, 1)); + let mut plus = at("BinaryOp", "", 5, 10); + plus.add_child(at("Identifier", "a", 5, 6)); + plus.add_child(at("Identifier", "b", 9, 10)); + stmt.add_child(plus.clone()); + let w = Window::file(src); + assert_eq!(residue(&stmt, &w).as_deref(), Some(" := ")); + assert_eq!(residue(&plus, &w).as_deref(), Some(" + ")); + } + + #[test] + fn residue_is_none_without_positions() { + let mut stmt = at("Return", "return x", 0, 8); + stmt.add_child(Node::with_token("Identifier", "x")); + assert!(residue(&stmt, &Window::file(b"return x")).is_none()); + assert!(residue(&Node::with_token("Return", "return"), &Window::file(b"")).is_none()); + } + + #[test] + fn residue_clamps_children_outside_the_parent_span() { + let mut outer = at("Synthetic", "", 2, 5); + outer.add_child(at("Identifier", "", 0, 9)); + assert_eq!( + residue(&outer, &Window::file(b"0123456789")).as_deref(), + Some(" ") + ); + } +} diff --git a/crates/cf-halstead/src/standalone.rs b/crates/cf-halstead/src/standalone.rs index 6719b209..9e77861d 100644 --- a/crates/cf-halstead/src/standalone.rs +++ b/crates/cf-halstead/src/standalone.rs @@ -1,31 +1,20 @@ -//! Standalone per-file Halstead analysis. +//! Standalone per-file Halstead analysis — the entry point the +//! `history/quality` analyzer consumes. //! -//! This is the path used by the **quality** history analyzer (NOT the -//! streaming visitor path the static pipeline uses). For each changed file's -//! UAST root it: -//! -//! 1. finds every function node — UAST `Function`/`Method` type ∪ `Function` -//! role, at traversal depth ≤ [`crate::MAX_DEPTH`], each node once; -//! 2. collects operators/operands over each function's **full subtree**; -//! 3. aggregates per-function operator/operand maps into file-level distinct/ -//! total counts and derives the file-level measures. -//! -//! Only the file-level scalars the quality analyzer reads — `volume`, -//! `effort`, `delivered_bugs` — are returned; the CMS `estimated_total_*` path -//! is omitted because it never feeds those scalars. A file with no functions -//! yields all zeros. - -use std::collections::HashMap; +//! It runs the same detector and the same union aggregate as the static +//! `static/halstead` report ([`crate::aggregate`]), so a file measured by +//! `quality` and by `static/halstead` yields the same volume, effort and bug +//! estimate. Only the three file-level scalars the quality analyzer reads +//! are returned; a file with no functions yields all zeros. use cf_uast_node::Node; -use crate::calculator::{HalsteadCounts, MetricsCalculator}; -use crate::detector::{HalNode, OperatorOperandDetector}; +use crate::aggregate::{analyze_functions, summarize}; /// File-level Halstead measures consumed by the quality analyzer. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct FileHalstead { - /// V — file-level volume. + /// V — file-level volume (union of every function's multisets). pub volume: f64, /// E — file-level effort. pub effort: f64, @@ -33,98 +22,26 @@ pub struct FileHalstead { pub delivered_bugs: f64, } -/// [`HalNode`] adapter over the production `cf_uast_node::Node`. -/// -/// The detector reads the type, token, roles, props, and children; these map -/// directly onto the node fields. The role/type strings are the canonical -/// UAST strings the parser emits, which is what the detector matches. -impl HalNode for Node { - fn node_type(&self) -> &str { - &self.node_type - } - fn token(&self) -> &str { - &self.token - } - fn has_any_role(&self, roles: &[&str]) -> bool { - Node::has_any_role(self, roles) - } - fn prop(&self, key: &str) -> Option<&str> { - self.props.get(key).map(String::as_str) - } - fn children(&self) -> &[Self] { - &self.children - } -} - -/// Runs the standalone Halstead analysis over `root`, returning the file-level -/// measures. +/// Runs the file-level analysis over `root` without the file's source bytes +/// (token-less nodes are recovered through verbatim tokens only; see +/// [`crate::detector::collect_functions`]). Prefer [`analyze_with_source`] +/// when the bytes are at hand. #[must_use] -#[allow(clippy::cast_possible_wrap)] // distinct-token counts fit i64 pub fn analyze(root: &Node) -> FileHalstead { - let functions = find_functions(root); - if functions.is_empty() { - return FileHalstead::default(); - } - - let detector = OperatorOperandDetector::new(); - let calc = MetricsCalculator::new(); - - // Aggregate per-function operator/operand maps into file-level maps. - let mut file_operators: HashMap<String, i64> = HashMap::new(); - let mut file_operands: HashMap<String, i64> = HashMap::new(); - - for f_node in &functions { - let mut operators: HashMap<String, i64> = HashMap::new(); - let mut operands: HashMap<String, i64> = HashMap::new(); - detector.collect(*f_node, &mut operators, &mut operands); - - for (op, count) in &operators { - *file_operators.entry(op.clone()).or_insert(0) += *count; - } - for (opnd, count) in &operands { - *file_operands.entry(opnd.clone()).or_insert(0) += *count; - } - } - - let counts = HalsteadCounts { - distinct_operators: file_operators.len() as i64, - distinct_operands: file_operands.len() as i64, - total_operators: calc.sum_map(&file_operators), - total_operands: calc.sum_map(&file_operands), - }; - let d = calc.calculate(counts); - - FileHalstead { - volume: d.volume, - effort: d.effort, - delivered_bugs: d.delivered_bugs, - } + analyze_with_source(root, None) } -/// Finds all function nodes: UAST `Function`/`Method` types ∪ `Function` role, -/// traversal depth ≤ [`crate::MAX_DEPTH`], each node once. -/// -/// The reference behavior unions a type-traversal and a role-traversal into an -/// identity set; both share the same pre-order DFS, so a node matching both -/// appears once. A single DFS that yields any node matching either criterion -/// is equivalent because each node is visited exactly once. -fn find_functions(root: &Node) -> Vec<&Node> { - let mut out: Vec<&Node> = Vec::new(); - // Iterative pre-order DFS with depth; root at depth 0. - let mut stack: Vec<(&Node, i64)> = vec![(root, 0)]; - while let Some((node, depth)) = stack.pop() { - if depth <= crate::MAX_DEPTH { - let by_type = node.node_type == "Function" || node.node_type == "Method"; - let by_role = HalNode::has_any_role(node, &["Function"]); - if by_type || by_role { - out.push(node); - } - } - for child in node.children.iter().rev() { - stack.push((child, depth + 1)); - } +/// Runs the file-level analysis over `root`, reading token-less nodes back +/// from `source` by span. +#[must_use] +pub fn analyze_with_source(root: &Node, source: Option<&[u8]>) -> FileHalstead { + let functions = analyze_functions(root, source); + let summary = summarize(&functions); + FileHalstead { + volume: summary.derived.volume, + effort: summary.derived.effort, + delivered_bugs: summary.derived.delivered_bugs, } - out } #[cfg(test)] @@ -133,11 +50,34 @@ mod tests { #[test] fn empty_tree_is_zero() { - let root = Node { - node_type: "File".to_string(), - ..Node::default() - }; - let r = analyze(&root); - assert_eq!(r, FileHalstead::default()); + assert_eq!( + analyze(&Node::with_token("File", "")), + FileHalstead::default() + ); + } + + /// `func same() int { return 1 }` has one operator and one operand: + /// n = 2, N = 2, V = 2 · log2(2) = 2. + #[test] + fn one_return_function_has_positive_volume() { + let mut lit = Node::with_token("Literal", "1"); + lit.roles = vec!["Literal".into()]; + let mut ret = Node::with_token("Return", "return 1"); + ret.roles = vec!["Return".into()]; + ret.add_child(lit); + let mut body = Node::with_token("Block", ""); + body.roles = vec!["Body".into()]; + body.add_child(ret); + let mut f = Node::with_token("Function", ""); + f.roles = vec!["Function".into(), "Declaration".into()]; + f.props.insert("name".into(), "same".into()); + f.add_child(body); + let mut root = Node::with_token("File", ""); + root.add_child(f); + + let h = analyze(&root); + assert_eq!(h.volume, 2.0); + assert_eq!(h.effort, 1.0); // D = (1/2) · (1/1) = 0.5 + assert_eq!(h.delivered_bugs, 2.0 / 3000.0); } } diff --git a/crates/cf-halstead/src/thresholds.rs b/crates/cf-halstead/src/thresholds.rs new file mode 100644 index 00000000..bb7a8114 --- /dev/null +++ b/crates/cf-halstead/src/thresholds.rs @@ -0,0 +1,334 @@ +//! Every Halstead threshold, label, and score formula — defined once and +//! used by every report format. +//! +//! All bands are lower-inclusive: a value *at* a bound belongs to the higher +//! band. +//! +//! | Measure (per function) | Low / Simple | Medium / Moderate | High / Complex | Very High | +//! |------------------------|--------------|-------------------|----------------|-----------| +//! | Volume V | V < 100 | 100 ≤ V < 1000 | 1000 ≤ V < 5000| V ≥ 5000 | +//! | Difficulty D | D < 5 | 5 ≤ D < 15 | D ≥ 15 | — | +//! | Effort E | E < 1000 | 1000 ≤ E < 10000 | E ≥ 10000 | — | +//! +//! A function is **high-effort** when `E ≥ 10000`; its risk is `HIGH` from +//! `E ≥ 50000`, else `MEDIUM`. Issue severity is `poor` when `E ≥ 50000` or +//! `B ≥ 1.0`, `fair` when `E ≥ 10000` or `B ≥ 0.3`, else `good`. +//! +//! The codebase (or file) **health** is keyed off the share `h` of its +//! functions that are high-effort: `h < 5%` → low complexity, `< 15%` → +//! moderate, `< 30%` → high, else very high. The health score interpolates +//! linearly through `(0, 100)`, `(5%, 90)`, `(15%, 70)`, `(30%, 40)`, +//! `(100%, 0)`; a set with no functions scores 100 with the +//! [`MSG_NO_FUNCTIONS`] message. + +/// Volume at which a function leaves the `Low` band. +pub const VOLUME_MEDIUM: f64 = 100.0; +/// Volume at which a function enters the `High` band. +pub const VOLUME_HIGH: f64 = 1000.0; +/// Volume at which a function enters the `Very High` band. +pub const VOLUME_VERY_HIGH: f64 = 5000.0; + +/// Difficulty at which a function leaves `Simple`. +pub const DIFFICULTY_MODERATE: f64 = 5.0; +/// Difficulty at which a function becomes `Complex`. +pub const DIFFICULTY_COMPLEX: f64 = 15.0; + +/// Effort at which a function leaves `Low`. +pub const EFFORT_MEDIUM: f64 = 1000.0; +/// Effort at which a function is `High` — the high-effort bound. +pub const EFFORT_HIGH: f64 = 10_000.0; +/// Effort at which a high-effort function's risk is `HIGH`. +pub const EFFORT_VERY_HIGH: f64 = 50_000.0; + +/// Delivered-bugs estimate at which an issue is at least `fair`. +pub const BUGS_FAIR: f64 = 0.3; +/// Delivered-bugs estimate at which an issue is `poor`. +pub const BUGS_POOR: f64 = 1.0; + +/// High-effort share below which the codebase has low complexity. +pub const SHARE_LOW: f64 = 0.05; +/// High-effort share below which the codebase has moderate complexity. +pub const SHARE_MODERATE: f64 = 0.15; +/// High-effort share below which the codebase has high complexity. +pub const SHARE_HIGH: f64 = 0.30; + +/// Health score of a codebase with no high-effort functions. +pub const HEALTH_MAX: f64 = 100.0; +/// Health score at the low / moderate boundary ([`SHARE_LOW`]). +pub const HEALTH_LOW_EDGE: f64 = 90.0; +/// Health score at the moderate / high boundary ([`SHARE_MODERATE`]). +pub const HEALTH_MODERATE_EDGE: f64 = 70.0; +/// Health score at the high / very-high boundary ([`SHARE_HIGH`]). +pub const HEALTH_HIGH_EDGE: f64 = 40.0; +/// Health score when every function is high-effort. +pub const HEALTH_MIN: f64 = 0.0; + +/// Aggregate message when the analyzed set has no functions. +pub const MSG_NO_FUNCTIONS: &str = "No functions found"; +/// Aggregate message for a high-effort share below [`SHARE_LOW`]. +pub const MSG_LOW: &str = "Low Halstead complexity - well-structured code"; +/// Aggregate message for a high-effort share below [`SHARE_MODERATE`]. +pub const MSG_MODERATE: &str = "Moderate Halstead complexity - acceptable"; +/// Aggregate message for a high-effort share below [`SHARE_HIGH`]. +pub const MSG_HIGH: &str = "High Halstead complexity - consider refactoring"; +/// Aggregate message for a high-effort share at or above [`SHARE_HIGH`]. +pub const MSG_VERY_HIGH: &str = + "Very high Halstead complexity - significant refactoring recommended"; + +/// Per-function volume band. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VolumeLevel { + /// `V < 100`. + Low, + /// `100 ≤ V < 1000`. + Medium, + /// `1000 ≤ V < 5000`. + High, + /// `V ≥ 5000`. + VeryHigh, +} + +impl VolumeLevel { + /// Every band, lowest first (the distribution order). + pub const ALL: [Self; 4] = [Self::Low, Self::Medium, Self::High, Self::VeryHigh]; + + /// The band of a volume. + #[must_use] + pub fn of(volume: f64) -> Self { + if volume >= VOLUME_VERY_HIGH { + Self::VeryHigh + } else if volume >= VOLUME_HIGH { + Self::High + } else if volume >= VOLUME_MEDIUM { + Self::Medium + } else { + Self::Low + } + } + + /// Position in [`Self::ALL`]. + #[must_use] + pub fn index(self) -> usize { + match self { + Self::Low => 0, + Self::Medium => 1, + Self::High => 2, + Self::VeryHigh => 3, + } + } + + /// Human label (`complexity_level`). + #[must_use] + pub fn name(self) -> &'static str { + match self { + Self::Low => "Low", + Self::Medium => "Medium", + Self::High => "High", + Self::VeryHigh => "Very High", + } + } + + /// Machine key (yaml / bin `distribution`). + #[must_use] + pub fn key(self) -> &'static str { + match self { + Self::Low => "low", + Self::Medium => "medium", + Self::High => "high", + Self::VeryHigh => "very_high", + } + } + + /// Distribution bucket label with its bounds (json `distribution`). + #[must_use] + pub fn bucket_label(self) -> &'static str { + match self { + Self::Low => "Low (<100)", + Self::Medium => "Medium (100-1000)", + Self::High => "High (1000-5000)", + Self::VeryHigh => "Very High (>=5000)", + } + } +} + +/// Per-function volume assessment: 🟢 below 100, 🟡 below 1000, 🔴 above. +#[must_use] +pub fn volume_assessment(volume: f64) -> &'static str { + match VolumeLevel::of(volume) { + VolumeLevel::Low => "🟢 Low", + VolumeLevel::Medium => "🟡 Medium", + VolumeLevel::High | VolumeLevel::VeryHigh => "🔴 High", + } +} + +/// Per-function difficulty assessment. +#[must_use] +pub fn difficulty_assessment(difficulty: f64) -> &'static str { + if difficulty >= DIFFICULTY_COMPLEX { + "🔴 Complex" + } else if difficulty >= DIFFICULTY_MODERATE { + "🟡 Moderate" + } else { + "🟢 Simple" + } +} + +/// Per-function effort assessment. +#[must_use] +pub fn effort_assessment(effort: f64) -> &'static str { + if effort >= EFFORT_HIGH { + "🔴 High" + } else if effort >= EFFORT_MEDIUM { + "🟡 Medium" + } else { + "🟢 Low" + } +} + +/// True for a high-effort function (`E ≥ 10000`). +#[must_use] +pub fn is_high_effort(effort: f64) -> bool { + effort >= EFFORT_HIGH +} + +/// Risk level of a high-effort function. +#[must_use] +pub fn risk_level(effort: f64) -> &'static str { + if effort >= EFFORT_VERY_HIGH { + "HIGH" + } else { + "MEDIUM" + } +} + +/// Issue severity of a function from its effort and bug estimate. +#[must_use] +pub fn severity(effort: f64, bugs: f64) -> &'static str { + if effort >= EFFORT_VERY_HIGH || bugs >= BUGS_POOR { + "poor" + } else if effort >= EFFORT_HIGH || bugs >= BUGS_FAIR { + "fair" + } else { + "good" + } +} + +/// The aggregate message for `total_functions` functions of which the share +/// `high_effort_share` (0..=1) is high-effort. +#[must_use] +pub fn message(total_functions: i64, high_effort_share: f64) -> &'static str { + if total_functions == 0 { + MSG_NO_FUNCTIONS + } else if high_effort_share < SHARE_LOW { + MSG_LOW + } else if high_effort_share < SHARE_MODERATE { + MSG_MODERATE + } else if high_effort_share < SHARE_HIGH { + MSG_HIGH + } else { + MSG_VERY_HIGH + } +} + +/// Health score (0..=100) from the high-effort share, interpolated linearly +/// through the documented anchors. +#[must_use] +pub fn health_score(high_effort_share: f64) -> f64 { + const ANCHORS: [(f64, f64); 5] = [ + (0.0, HEALTH_MAX), + (SHARE_LOW, HEALTH_LOW_EDGE), + (SHARE_MODERATE, HEALTH_MODERATE_EDGE), + (SHARE_HIGH, HEALTH_HIGH_EDGE), + (1.0, HEALTH_MIN), + ]; + let share = high_effort_share.clamp(0.0, 1.0); + for pair in ANCHORS.windows(2) { + let ((x0, y0), (x1, y1)) = (pair[0], pair[1]); + if share <= x1 { + return y0 + (share - x0) / (x1 - x0) * (y1 - y0); + } + } + HEALTH_MIN +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn volume_bands_are_lower_inclusive() { + assert_eq!(VolumeLevel::of(99.999), VolumeLevel::Low); + assert_eq!(VolumeLevel::of(100.0), VolumeLevel::Medium); + assert_eq!(VolumeLevel::of(999.999), VolumeLevel::Medium); + assert_eq!(VolumeLevel::of(1000.0), VolumeLevel::High); + assert_eq!(VolumeLevel::of(5000.0), VolumeLevel::VeryHigh); + assert_eq!(VolumeLevel::of(0.0), VolumeLevel::Low); + } + + #[test] + fn every_band_has_a_distinct_index_name_key_and_label() { + for (i, level) in VolumeLevel::ALL.iter().enumerate() { + assert_eq!(level.index(), i); + } + assert_eq!(VolumeLevel::VeryHigh.name(), "Very High"); + assert_eq!(VolumeLevel::VeryHigh.key(), "very_high"); + assert_eq!(VolumeLevel::Medium.bucket_label(), "Medium (100-1000)"); + } + + #[test] + fn per_function_assessments_follow_the_table() { + assert_eq!(volume_assessment(99.0), "🟢 Low"); + assert_eq!(volume_assessment(100.0), "🟡 Medium"); + assert_eq!(volume_assessment(1000.0), "🔴 High"); + assert_eq!(difficulty_assessment(4.99), "🟢 Simple"); + assert_eq!(difficulty_assessment(5.0), "🟡 Moderate"); + assert_eq!(difficulty_assessment(15.0), "🔴 Complex"); + assert_eq!(effort_assessment(999.0), "🟢 Low"); + assert_eq!(effort_assessment(1000.0), "🟡 Medium"); + assert_eq!(effort_assessment(10_000.0), "🔴 High"); + } + + #[test] + fn high_effort_risk_and_severity() { + assert!(!is_high_effort(9999.0)); + assert!(is_high_effort(10_000.0)); + assert_eq!(risk_level(10_000.0), "MEDIUM"); + assert_eq!(risk_level(50_000.0), "HIGH"); + assert_eq!(severity(0.0, 0.0), "good"); + assert_eq!(severity(10_000.0, 0.0), "fair"); + assert_eq!(severity(0.0, 0.3), "fair"); + assert_eq!(severity(50_000.0, 0.0), "poor"); + assert_eq!(severity(0.0, 1.0), "poor"); + } + + #[test] + fn message_bands_on_the_high_effort_share() { + assert_eq!(message(0, 0.0), MSG_NO_FUNCTIONS); + assert_eq!(message(10, 0.0), MSG_LOW); + assert_eq!(message(100, 0.04), MSG_LOW); + assert_eq!(message(100, 0.05), MSG_MODERATE); + assert_eq!(message(100, 0.15), MSG_HIGH); + assert_eq!(message(100, 0.30), MSG_VERY_HIGH); + assert_eq!(message(1, 1.0), MSG_VERY_HIGH); + } + + #[test] + fn health_score_interpolates_through_the_anchors() { + assert_eq!(health_score(0.0), HEALTH_MAX); + assert_eq!(health_score(SHARE_LOW), HEALTH_LOW_EDGE); + assert_eq!(health_score(SHARE_MODERATE), HEALTH_MODERATE_EDGE); + assert_eq!(health_score(SHARE_HIGH), HEALTH_HIGH_EDGE); + assert_eq!(health_score(1.0), HEALTH_MIN); + // Midpoint of the first segment. + assert!((health_score(0.025) - 95.0).abs() < 1e-9); + // Monotone non-increasing. + let mut last = HEALTH_MAX; + for i in 0..=100 { + let h = health_score(f64::from(i) / 100.0); + assert!(h <= last + 1e-9, "health must not increase with the share"); + last = h; + } + assert_eq!(health_score(-1.0), HEALTH_MAX); + assert_eq!(health_score(2.0), HEALTH_MIN); + } +} diff --git a/crates/cf-halstead/tests/real_source.rs b/crates/cf-halstead/tests/real_source.rs new file mode 100644 index 00000000..2d558681 --- /dev/null +++ b/crates/cf-halstead/tests/real_source.rs @@ -0,0 +1,262 @@ +//! Halstead numbers asserted end-to-end from SOURCE: each fixture is parsed +//! with the production `cf_uast::Parser`, and the operator / operand +//! multisets plus the derived measures are compared against values computed +//! by hand from the standard definitions (n1 / n2 / N1 / N2, V = N·log2 n, +//! D = (n1/2)·(N2/n2), E = D·V). +//! +//! Every fixture is the same two-statement body — `c = a + b; return c * 2` +//! in the language's own spelling — so the expected operands are always +//! `a, b, c, c, 2` (n2 = 4, N2 = 5) and only the operator spelling varies. + +use std::collections::BTreeMap; + +use cf_halstead::{analyze_functions, analyze_with_source, summarize, FunctionMetrics}; +use cf_uast::Parser; + +/// Parses `src` as `filename` and measures its functions with the file bytes. +fn functions(filename: &str, src: &str) -> Vec<FunctionMetrics> { + let root = Parser::new() + .parse(filename, src.as_bytes()) + .expect("fixture parses"); + analyze_functions(&root, Some(src.as_bytes())) +} + +fn map(pairs: &[(&str, i64)]) -> BTreeMap<String, i64> { + pairs.iter().map(|(k, v)| ((*k).to_string(), *v)).collect() +} + +/// The operands shared by every fixture: `a, b, c, c, 2`. +fn body_operands() -> BTreeMap<String, i64> { + map(&[("a", 1), ("b", 1), ("c", 2), ("2", 1)]) +} + +/// Asserts the one function of a fixture and its hand-computed measures. +fn assert_single(fns: &[FunctionMetrics], name: &str, operators: &[(&str, i64)]) { + assert_eq!(fns.len(), 1, "exactly one function: {fns:#?}"); + let f = &fns[0]; + assert_eq!(f.name, name); + assert_eq!(f.operators, map(operators), "operators of {name}"); + assert_eq!(f.operands, body_operands(), "operands of {name}"); + + let n1 = i64::try_from(operators.len()).unwrap(); + let big_n1: i64 = operators.iter().map(|(_, c)| c).sum(); + assert_eq!(f.counts.distinct_operators, n1); + assert_eq!(f.counts.total_operators, big_n1); + assert_eq!(f.counts.distinct_operands, 4); + assert_eq!(f.counts.total_operands, 5); + + #[allow(clippy::cast_precision_loss)] + let (n, big_n) = ((n1 + 4) as f64, (big_n1 + 5) as f64); + let volume = big_n * n.log2(); + #[allow(clippy::cast_precision_loss)] + let difficulty = (n1 as f64 / 2.0) * (5.0 / 4.0); + assert!((f.derived.volume - volume).abs() < 1e-9, "volume of {name}"); + assert!( + (f.derived.difficulty - difficulty).abs() < 1e-9, + "difficulty of {name}" + ); + assert!( + (f.derived.effort - volume * difficulty).abs() < 1e-9, + "effort of {name}" + ); +} + +const GO_HAL: &str = + "package main\n\nfunc hal(a int, b int) int {\n\tc := a + b\n\treturn c * 2\n}\n"; + +/// Go: `:=`, `+`, `*`, `return` → n1 = 4, N1 = 4; V = 9·log2(8) = 27, +/// D = 2.5, E = 67.5. +#[test] +fn go_short_var_declaration_and_return() { + let fns = functions("hal.go", GO_HAL); + assert_single(&fns, "hal", &[(":=", 1), ("+", 1), ("*", 1), ("return", 1)]); + assert_eq!(fns[0].derived.volume, 27.0); + assert_eq!(fns[0].derived.difficulty, 2.5); + assert_eq!(fns[0].derived.effort, 67.5); +} + +/// Python: `=`, `+`, `*`, `return`. +#[test] +fn python_assignment_and_return() { + let fns = functions( + "hal.py", + "def hal(a, b):\n c = a + b\n return c * 2\n", + ); + assert_single(&fns, "hal", &[("=", 1), ("+", 1), ("*", 1), ("return", 1)]); +} + +/// JavaScript: `const`, `=`, `+`, `*`, `return`. +#[test] +fn javascript_const_declaration_and_return() { + let fns = functions( + "hal.js", + "function hal(a, b) {\n const c = a + b;\n return c * 2;\n}\n", + ); + assert_single( + &fns, + "hal", + &[("const", 1), ("=", 1), ("+", 1), ("*", 1), ("return", 1)], + ); +} + +/// Rust: `let`, `=`, `+`, `*` (tail expression, no `return`); the +/// token-less identifiers and literal are read back by span. +#[test] +fn rust_let_binding_and_tail_expression() { + let fns = functions( + "hal.rs", + "fn hal(a: i32, b: i32) -> i32 {\n let c = a + b;\n c * 2\n}\n", + ); + assert_single(&fns, "hal", &[("let", 1), ("=", 1), ("+", 1), ("*", 1)]); +} + +/// Java: the local declaration's type keyword `int`, `=`, `+`, `*`, +/// `return`; the method signature (modifiers, parameter types) is excluded. +#[test] +fn java_local_declaration_and_return() { + let fns = functions( + "Foo.java", + "class Foo {\n int hal(int a, int b) {\n int c = a + b;\n return c * 2;\n }\n}\n", + ); + assert_single( + &fns, + "hal", + &[("int", 1), ("=", 1), ("+", 1), ("*", 1), ("return", 1)], + ); +} + +/// C: `int`, `=`, `+`, `*`, `return`; the declarator is signature and the +/// function is named through it. +#[test] +fn c_local_declaration_and_return() { + let fns = functions( + "hal.c", + "int hal(int a, int b) {\n int c = a + b;\n return c * 2;\n}\n", + ); + assert_single( + &fns, + "hal", + &[("int", 1), ("=", 1), ("+", 1), ("*", 1), ("return", 1)], + ); +} + +/// Calls: each `fmt.Println("x")` is the call operator `()`, the member +/// access `.`, and the operands `fmt`, `Println`, `"x"`. The parameter `x` +/// of the signature is not an operand; the string `"x"` is. +#[test] +fn go_calls_count_parentheses_member_access_and_string_arguments() { + let fns = functions( + "calls.go", + "package main\n\nimport \"fmt\"\n\nfunc calls(x int) {\n\tfmt.Println(\"x\")\n\tfmt.Println(\"x\")\n\tfmt.Println(\"y\")\n}\n", + ); + assert_eq!(fns.len(), 1); + assert_eq!(fns[0].name, "calls"); + assert_eq!(fns[0].operators, map(&[("()", 3), (".", 3)])); + assert_eq!( + fns[0].operands, + map(&[("fmt", 3), ("Println", 3), ("\"x\"", 2), ("\"y\"", 1)]) + ); + // n1 = 2, n2 = 4, N1 = 6, N2 = 9 → V = 15·log2(6), D = 1·(9/4). + assert!((fns[0].derived.volume - 15.0 * 6f64.log2()).abs() < 1e-9); + assert_eq!(fns[0].derived.difficulty, 2.25); +} + +/// A one-line function has a positive volume: `return 1` is one operator and +/// one operand → n = 2, N = 2, V = 2. +#[test] +fn go_single_return_has_volume_two() { + let fns = functions("x.go", "package a\n\nfunc same() int { return 1 }\n"); + assert_eq!(fns[0].operators, map(&[("return", 1)])); + assert_eq!(fns[0].operands, map(&[("1", 1)])); + assert_eq!(fns[0].derived.volume, 2.0); +} + +/// Comments inside a body contribute nothing, in Go and in Python. +#[test] +fn comments_inside_bodies_are_not_tokens() { + let go = functions( + "c.go", + "package main\n\n// doc\nfunc f(a int) int {\n\t// a + b is not code\n\treturn a /* * 2 */\n}\n", + ); + assert_eq!(go[0].operators, map(&[("return", 1)])); + assert_eq!(go[0].operands, map(&[("a", 1)])); + + let py = functions("c.py", "def f(a):\n # return a + b\n return a\n"); + assert_eq!(py[0].operators, map(&[("return", 1)])); + assert_eq!(py[0].operands, map(&[("a", 1)])); +} + +/// Control-flow keywords are operators, and a closure (`Lambda`) belongs to +/// the function that contains it — the same function predicate the +/// complexity analyzer uses — so `withClosure` is one record: the closure's +/// `func` keyword, its `if` / `>` / `+=`, the loop's `for` / `:=` / `range`, +/// the call `()` and the `return`. Its parameter `x` is signature; the loop +/// variable `x` is an operand. +#[test] +fn go_control_flow_and_closure_attribution() { + let fns = functions( + "cl.go", + "package main\n\nfunc withClosure(xs []int) int {\n\ttotal := 0\n\tf := func(x int) {\n\t\tif x > 0 {\n\t\t\ttotal += x\n\t\t}\n\t}\n\tfor _, x := range xs {\n\t\tf(x)\n\t}\n\treturn total\n}\n", + ); + assert_eq!(fns.len(), 1, "the closure is part of withClosure: {fns:#?}"); + let outer = &fns[0]; + assert_eq!(outer.name, "withClosure"); + assert_eq!( + outer.operators, + map(&[ + (":=", 3), + ("func", 1), + ("if", 1), + (">", 1), + ("+=", 1), + ("for", 1), + ("range", 1), + ("()", 1), + ("return", 1), + ]) + ); + assert_eq!( + outer.operands, + map(&[ + ("total", 3), + ("0", 2), + ("f", 2), + ("x", 4), + ("_", 1), + ("xs", 1) + ]) + ); +} + +/// Without the file bytes, Go recovers everything through the function's +/// verbatim token, so the quality path and the static path agree. +#[test] +fn go_without_source_bytes_matches_with_source_bytes() { + let root = Parser::new() + .parse("hal.go", GO_HAL.as_bytes()) + .expect("parse"); + let with = analyze_functions(&root, Some(GO_HAL.as_bytes())); + let without = analyze_functions(&root, None); + assert_eq!(with, without); +} + +/// The quality entry point returns the union aggregate of the same functions. +#[test] +fn file_level_measures_are_the_union_of_the_functions() { + let src = "package main\n\nfunc a(x int) int { return x + 1 }\n\nfunc b(y int) int { return y * 2 }\n"; + let root = Parser::new().parse("ab.go", src.as_bytes()).expect("parse"); + let fns = analyze_functions(&root, Some(src.as_bytes())); + assert_eq!(fns.len(), 2); + let summary = summarize(&fns); + // Union: operators {return: 2, +: 1, *: 1} → n1 = 3, N1 = 4; + // operands {x, 1, y, 2} → n2 = 4, N2 = 4. + assert_eq!(summary.counts.distinct_operators, 3); + assert_eq!(summary.counts.total_operators, 4); + assert_eq!(summary.counts.distinct_operands, 4); + assert_eq!(summary.counts.total_operands, 4); + let file = analyze_with_source(&root, Some(src.as_bytes())); + assert_eq!(file.volume, summary.derived.volume); + assert_eq!(file.volume, 8.0 * 7f64.log2()); + assert_eq!(file.effort, summary.derived.effort); + assert_eq!(file.delivered_bugs, summary.derived.delivered_bugs); +} diff --git a/crates/cf-identity/src/lib.rs b/crates/cf-identity/src/lib.rs index 351905f0..90446d80 100644 --- a/crates/cf-identity/src/lib.rs +++ b/crates/cf-identity/src/lib.rs @@ -12,7 +12,7 @@ //! constants and parsing behavior feed analyzer output downstream, so they are //! reproduced exactly — including the deliberately-not-`(1 << 18) - 1` value of //! [`AUTHOR_MISSING`]. Output bytes are pinned against the reference binary by -//! `tests/compat`. +//! the report e2e suites (`bins/codefang/tests`). #![forbid(unsafe_code)] diff --git a/crates/cf-imports/Cargo.toml b/crates/cf-imports/Cargo.toml index 47b1fdc4..042fa423 100644 --- a/crates/cf-imports/Cargo.toml +++ b/crates/cf-imports/Cargo.toml @@ -6,20 +6,24 @@ rust-version.workspace = true license.workspace = true repository.workspace = true publish.workspace = true -description = "Import/dependency analysis: static import IDs (`imports`) plus per-developer/per-language import usage history (`ImportsPerDeveloper`, id `history/imports`, additive merge)." +description = "Import/dependency analysis: language-aware import extraction over the UAST (`static/imports`) plus per-developer/per-language import usage history (`history/imports`, additive merge)." [dependencies] -# Report-format JSON encoder: models struct-declaration key order vs byte-sorted -# map keys and nil-slice -> JSON `null`. `ComputedMetrics::to_go_value` builds a -# struct-origin `GoValue` so json/bin output keeps the pinned report contract -# (e.g. `dependencies:null` for the empty report, and declaration field order -# import_list/categories/dependencies/aggregate rather than byte-sorted order). -# Pinned by the differential gate in tests/compat. +# Spill encoding of finished history results (cf-commands `spill`). +serde = { workspace = true } +# Report-format JSON encoder: `ComputedMetrics::to_go_value` builds a +# struct-origin `GoValue` so json/yaml/bin output keeps declaration field order +# (import_list/categories/dependencies/aggregate) and `dependencies: null` for +# the empty report. cf-gojson = { workspace = true } -# CFB1 `bin` envelope: "CFB1" + LE u32 len + compact JSON payload. Routes -# through cf-gojson internally. -cf-reportutil = { path = "../cf-reportutil" } -# -# This crate intentionally carries minimal local shims (`node`, `report`) so the -# analyzer logic and its tests stand alone and verifiable; the canonical types -# live in cf-uast-node / cf-reportutil and the pipeline crates. +# The canonical UAST node the extractor walks (type, token, roles, positions, +# children). The parser facade (cf-uast) is only needed to BUILD trees, which +# the callers and the tests do. +cf-uast-node = { path = "../cf-uast-node" } + +[dev-dependencies] +# Real tree-sitter parses for the per-language extraction tests: every +# expectation is asserted against the tree the production parser produces. +cf-uast = { path = "../cf-uast" } +# The yaml rendering of the history report is asserted alongside the json one. +cf-goyaml = { path = "../cf-goyaml" } diff --git a/crates/cf-imports/src/aggregate.rs b/crates/cf-imports/src/aggregate.rs new file mode 100644 index 00000000..ee41f738 --- /dev/null +++ b/crates/cf-imports/src/aggregate.rs @@ -0,0 +1,142 @@ +//! Cross-file aggregation for the static analyzer. +//! +//! [`Aggregate`] folds the per-file import lists into one table keyed by +//! import path: the **number of files** importing the path, plus the language +//! and local-file flag of the first file that imported it (which drive the +//! stdlib / relative classification in [`crate::metrics`]). + +use std::collections::BTreeMap; + +use crate::extract::Import; +use crate::metrics::{ImportRecord, ReportData}; + +/// One aggregated import. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Entry { + /// Number of analyzed files importing the path. + pub count: i64, + /// Parser language name of the first file that imported the path. + pub language: String, + /// Whether the first importing file named it as a local file include. + pub local_file: bool, +} + +/// Import path -> [`Entry`] across every analyzed file. +#[derive(Debug, Default, Clone)] +pub struct Aggregate { + entries: BTreeMap<String, Entry>, + total_files: i64, +} + +impl Aggregate { + /// An empty aggregate. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Folds one analyzed file (import-free files still count towards + /// `total_files`). `imports` is expected deduplicated, as + /// [`crate::extract_imports`] returns it; each path counts once per file. + pub fn add_file(&mut self, language: &str, imports: &[Import]) { + self.total_files += 1; + for import in imports { + self.entries + .entry(import.path.clone()) + .and_modify(|e| e.count += 1) + .or_insert_with(|| Entry { + count: 1, + language: language.to_string(), + local_file: import.local_file, + }); + } + } + + /// Number of analyzed files. + #[must_use] + pub fn total_files(&self) -> i64 { + self.total_files + } + + /// Number of unique import paths. + #[must_use] + pub fn unique_count(&self) -> i64 { + self.entries.len() as i64 + } + + /// `(path, entry)` pairs in byte-sorted path order. + pub fn entries(&self) -> impl Iterator<Item = (&str, &Entry)> { + self.entries.iter().map(|(k, v)| (k.as_str(), v)) + } + + /// `(path, count)` pairs ordered by count descending, then path + /// ascending — the deterministic issue order of the report section. + #[must_use] + pub fn ranked(&self) -> Vec<(&str, i64)> { + let mut ranked: Vec<(&str, i64)> = self + .entries + .iter() + .map(|(k, v)| (k.as_str(), v.count)) + .collect(); + ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0))); + ranked + } + + /// The metrics input: one record per unique path, in path order. + #[must_use] + pub fn report_data(&self) -> ReportData { + ReportData { + imports: self + .entries + .iter() + .map(|(path, e)| ImportRecord { + path: path.clone(), + language: e.language.clone(), + local_file: e.local_file, + }) + .collect(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counts_files_not_occurrences_and_keeps_first_language() { + let mut agg = Aggregate::new(); + agg.add_file("go", &[Import::package("fmt"), Import::package("os")]); + agg.add_file("python", &[Import::package("os"), Import::package("sys")]); + agg.add_file("markdown", &[]); + + assert_eq!(agg.total_files(), 3); + assert_eq!(agg.unique_count(), 3); + assert_eq!(agg.ranked(), vec![("os", 2), ("fmt", 1), ("sys", 1)]); + + let os = agg.entries().find(|(p, _)| *p == "os").expect("os").1; + assert_eq!(os.language, "go"); + let data = agg.report_data(); + assert_eq!(data.imports.len(), 3); + assert_eq!(data.imports[0].path, "fmt"); + assert_eq!(data.imports[2].language, "python"); + } + + #[test] + fn local_file_flag_comes_from_the_first_importer() { + let mut agg = Aggregate::new(); + agg.add_file("c", &[Import::local("local.h")]); + agg.add_file("c", &[Import::package("local.h")]); + assert!(agg.report_data().imports[0].local_file); + assert_eq!(agg.ranked(), vec![("local.h", 2)]); + } + + #[test] + fn empty_aggregate_is_the_empty_report() { + let agg = Aggregate::new(); + assert_eq!(agg.total_files(), 0); + assert_eq!(agg.unique_count(), 0); + assert!(agg.ranked().is_empty()); + assert!(agg.report_data().imports.is_empty()); + } +} diff --git a/crates/cf-imports/src/aggregator.rs b/crates/cf-imports/src/aggregator.rs deleted file mode 100644 index 54feb442..00000000 --- a/crates/cf-imports/src/aggregator.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Cross-file aggregation for the static analyzer. -//! -//! The [`Aggregator`] combines per-file static reports into a single report -//! holding the unique import set, per-import counts, the unique-import count, -//! and the total file count. - -use std::collections::BTreeMap; - -use crate::report::ReportValue; - -/// Aggregates import analysis results across multiple files. -/// -/// `all_imports` maps import path -> occurrence count. -#[derive(Debug, Default, Clone)] -pub struct Aggregator { - /// Import path -> count across all files. - all_imports: BTreeMap<String, i64>, - /// Number of files aggregated. - total_files: i64, -} - -impl Aggregator { - /// Creates a new aggregator. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Aggregates one file's report: increments the file count and, for each - /// entry in the report's `imports` list, increments that import's count. - pub fn aggregate_report(&mut self, report: &ReportValue) { - self.total_files += 1; - if let Some(map) = report.as_map() { - if let Some(ReportValue::List(imports)) = map.get("imports") { - for v in imports { - if let ReportValue::Str(imp) = v { - *self.all_imports.entry(imp.clone()).or_insert(0) += 1; - } - } - } - } - } - - /// Returns the aggregated report: `imports` (the unique import keys), - /// `import_counts`, `count` (number of unique imports), and `total_files`. - /// - /// The reference implementation iterates a hash map for `imports` - /// (nondeterministic order); this implementation emits them sorted (the - /// [`BTreeMap`] key order) for determinism. - #[must_use] - pub fn get_result(&self) -> ReportValue { - let imports: Vec<ReportValue> = self - .all_imports - .keys() - .map(|k| ReportValue::Str(k.clone())) - .collect(); - - let mut import_counts = ReportValue::map(); - for (imp, count) in &self.all_imports { - import_counts.insert(imp.clone(), ReportValue::Int(*count)); - } - - let mut result = ReportValue::map(); - result.insert("imports", ReportValue::List(imports)); - result.insert("import_counts", import_counts); - result.insert("count", ReportValue::Int(self.all_imports.len() as i64)); - result.insert("total_files", ReportValue::Int(self.total_files)); - result - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn report(imports: &[&str]) -> ReportValue { - let mut r = ReportValue::map(); - r.insert( - "imports", - ReportValue::List( - imports - .iter() - .map(|s| ReportValue::Str(s.to_string())) - .collect(), - ), - ); - r - } - - #[test] - fn aggregate_counts_and_files() { - let mut agg = Aggregator::new(); - agg.aggregate_report(&report(&["fmt", "os"])); - agg.aggregate_report(&report(&["fmt", "io"])); - let result = agg.get_result(); - let map = result.as_map().unwrap(); - assert_eq!(map["total_files"], ReportValue::Int(2)); - assert_eq!(map["count"], ReportValue::Int(3)); // fmt, os, io. - let counts = map["import_counts"].as_map().unwrap(); - assert_eq!(counts["fmt"], ReportValue::Int(2)); - assert_eq!(counts["os"], ReportValue::Int(1)); - } -} diff --git a/crates/cf-imports/src/analyzer.rs b/crates/cf-imports/src/analyzer.rs deleted file mode 100644 index afb57677..00000000 --- a/crates/cf-imports/src/analyzer.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! Static import analyzer. -//! -//! The static [`Analyzer`] provides the `imports` report key (plus a `count`): -//! it walks a parsed UAST tree, extracts import paths from import nodes, -//! deduplicates them, and applies per-language cleanup heuristics -//! ([`clean_import_path`] / [`parse_import_format`] for Python/JavaScript/Go -//! import-statement shapes). - -use crate::metrics::{compute_all_metrics, ComputedMetrics}; -use crate::node::{self, Node}; -use crate::report::ReportValue; - -/// Configuration key for the max dependency-risk rows shown in plots. -pub const CONFIG_IMPORTS_MAX_DEPENDENCY_RISK_ROWS: &str = "Imports.MaxDependencyRiskRows"; - -/// Static import analyzer. -/// -/// `cfg_max_dependency_risk_rows` of 0 means "use the default" (the field only -/// affects plot output). -#[derive(Debug, Default, Clone)] -pub struct Analyzer { - /// Override for the default max dependency-risk rows; 0 = use default. - pub cfg_max_dependency_risk_rows: i64, -} - -impl Analyzer { - /// Creates a new analyzer. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Returns the analyzer name. - #[must_use] - pub fn name(&self) -> &'static str { - "imports" - } - - /// Returns the CLI flag. - #[must_use] - pub fn flag(&self) -> &'static str { - "imports-analysis" - } - - /// Returns the analyzer description. - #[must_use] - pub fn description(&self) -> &'static str { - "Extracts and analyzes import statements from code" - } - - /// Configures the analyzer from a facts map: reads - /// [`CONFIG_IMPORTS_MAX_DEPENDENCY_RISK_ROWS`] as an int when present. - /// - /// # Errors - /// Never fails; the `Result` keeps the analyzer-interface signature. - pub fn configure( - &mut self, - facts: &std::collections::BTreeMap<String, ReportValue>, - ) -> Result<(), std::convert::Infallible> { - if let Some(ReportValue::Int(v)) = facts.get(CONFIG_IMPORTS_MAX_DEPENDENCY_RISK_ROWS) { - self.cfg_max_dependency_risk_rows = *v; - } - Ok(()) - } - - /// Runs static analysis on a parsed UAST root, returning a report with - /// `imports` (the deduplicated import list, in first-seen order) and - /// `count`. - /// - /// # Errors - /// Never fails; the `Result` keeps the analyzer-interface signature. - pub fn analyze(&self, root: &Node) -> Result<ReportValue, std::convert::Infallible> { - let imports = extract_imports_from_uast(root); - let count = imports.len() as i64; - let mut report = ReportValue::map(); - report.insert( - "imports", - ReportValue::List(imports.into_iter().map(ReportValue::Str).collect()), - ); - report.insert("count", ReportValue::Int(count)); - Ok(report) - } - - /// Computes [`ComputedMetrics`] for a report (JSON/YAML/bin payload), - /// falling back to empty metrics on a parse error. - #[must_use] - pub fn compute_metrics(&self, report: &ReportValue) -> ComputedMetrics { - compute_all_metrics(report).unwrap_or_default() - } - - /// Renders the JSON form of a report: indented JSON of [`ComputedMetrics`]. - /// - /// Routes through `cf-gojson`'s `marshal_indent` over the struct-origin - /// [`ComputedMetrics::to_go_value`] so field order, the - /// `dependencies`-nil-slice `null`, and float formatting all follow the - /// pinned report contract. (The history-run path serializes the same value - /// with compact `cf_gojson::marshal`.) - #[must_use] - pub fn format_report_json(&self, report: &ReportValue) -> String { - let bytes = cf_gojson::marshal_indent(&self.compute_metrics(report).to_go_value()); - String::from_utf8(bytes).expect("cf-gojson emits valid UTF-8") - } - - /// Encodes a report as a CFB1 `bin` envelope. - /// - /// The envelope payload is the compact JSON encoding of the struct-origin - /// metrics value, via cf-reportutil. - pub fn format_report_binary(&self, report: &ReportValue, out: &mut Vec<u8>) { - let value = self.compute_metrics(report).to_go_value(); - let envelope = cf_reportutil::encode_binary_envelope(&value) - .expect("imports metrics never exceed the CFB1 length cap"); - out.extend_from_slice(&envelope); - } -} - -/// Extracts deduplicated import strings from a UAST tree. -/// -/// Pre-order traversal; a node contributes when its type is -/// [`node::uast::IMPORT`] or it has the [`node::role::IMPORT`] role. -/// Duplicates (by extracted path) are dropped, preserving first-seen order. -/// -/// The path is recovered from common statement shapes — `import os` -> `os`, -/// `from x import y` -> `x`, `import React from 'react'` -> `react`: -/// -/// ``` -/// use cf_imports::analyzer::extract_imports_from_uast; -/// use cf_imports::node::{uast, Node}; -/// -/// let root = Node::new("File").with_children(vec![ -/// Node::new(uast::IMPORT).with_token("import os"), -/// Node::new(uast::IMPORT).with_token("from x import y"), -/// Node::new(uast::IMPORT).with_token("import React from 'react'"), -/// // Duplicate of the first; dropped. -/// Node::new(uast::IMPORT).with_token("import os"), -/// ]); -/// assert_eq!(extract_imports_from_uast(&root), vec!["os", "x", "react"]); -/// ``` -#[must_use] -pub fn extract_imports_from_uast(root: &Node) -> Vec<String> { - let mut imports: Vec<String> = Vec::new(); - let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new(); - - root.visit_pre_order(&mut |n: &Node| { - if n.type_ == node::uast::IMPORT || n.has_any_role(node::role::IMPORT) { - let import_path = extract_import_path(n); - if !import_path.is_empty() && seen.insert(import_path.clone()) { - imports.push(import_path); - } - } - }); - - imports -} - -/// Extracts an import path from an import node: prefer the node token -/// (cleaned); otherwise search children. -fn extract_import_path(import_node: &Node) -> String { - if !import_node.token.is_empty() { - return clean_import_path(&import_node.token); - } - if import_node.children.is_empty() { - return String::new(); - } - extract_import_path_from_children(&import_node.children) -} - -/// Searches children for an import path by type priority: first a literal with -/// a token, then an identifier with a token, then recursively. -fn extract_import_path_from_children(children: &[Node]) -> String { - for child in children { - if child.type_ == node::uast::LITERAL && !child.token.is_empty() { - return clean_import_path(&child.token); - } - } - for child in children { - if child.type_ == node::uast::IDENTIFIER && !child.token.is_empty() { - return clean_import_path(&child.token); - } - } - for child in children { - let path = extract_import_path(child); - if !path.is_empty() { - return path; - } - } - String::new() -} - -/// Cleans an import path: strips quotes/semicolons and parses statement forms. -/// -/// Trims any of `"`, `'`, `;` from both ends, skips empty/`{`/`}` results, -/// then applies [`parse_import_format`]; falls back to the trimmed value. -fn clean_import_path(path: &str) -> String { - let path = path.trim_matches(|c| c == '"' || c == '\'' || c == ';'); - - if path.is_empty() || path == "{" || path == "}" { - return String::new(); - } - - let parsed = parse_import_format(path); - if !parsed.is_empty() { - return parsed; - } - - path.to_string() -} - -/// Extracts a module name from common import-statement formats, in this branch -/// order: -/// -/// 1. `from X import ...` (Python) -> `X`, -/// 2. `... from '...'` (JS) -> the quoted target, -/// 3. `import X ...` -> `X` (quotes trimmed), -/// 4. `... import ...` -> the part after `import ` (quotes trimmed). -/// -/// Returns empty for destructuring/other shapes. The branch order and the -/// whitespace-splitting details are part of the pinned classification behavior -/// (which strings land in machine output), so they must not be "simplified". -fn parse_import_format(path: &str) -> String { - if path.starts_with("from ") { - // Python: "from typing import List, Dict" -> "typing" (the word after - // "from", i.e. index 1 of the whitespace fields of the whole string). - let parts: Vec<&str> = path.split_whitespace().collect(); - if parts.len() >= 2 { - return parts[1].to_string(); - } - return String::new(); - } - - if path.contains(" from ") { - // JavaScript: "React from 'react'" -> "react". - let parts: Vec<&str> = path.splitn(2, " from ").collect(); - if parts.len() >= 2 { - return parts[1].trim_matches(|c| c == '"' || c == '\'').to_string(); - } - return String::new(); - } - - if path.starts_with("import ") { - // Python "import os" -> "os"; JS "import './styles.css'" -> "./styles.css". - let parts: Vec<&str> = path.split_whitespace().collect(); - if parts.len() >= 2 { - return parts[1].trim_matches(|c| c == '"' || c == '\'').to_string(); - } - return String::new(); - } - - if path.contains("import ") { - // JS fallback: "... import './styles.css'" -> "./styles.css". - let parts: Vec<&str> = path.splitn(2, "import ").collect(); - if parts.len() >= 2 { - return parts[1].trim_matches(|c| c == '"' || c == '\'').to_string(); - } - return String::new(); - } - - // JavaScript destructuring "{ useState, useEffect }" -> skip. - String::new() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::node::{role, uast}; - - #[test] - fn test_analyzer_analyze() { - let a = Analyzer::default(); - assert!(!a.name().is_empty()); - - // Python: "import os". - let root = Node::new(uast::IMPORT).with_token("import os"); - let report = a.analyze(&root).unwrap(); - let imports = report - .as_map() - .and_then(|m| m.get("imports")) - .and_then(|v| v.as_list()) - .expect("imports list"); - assert_eq!(imports.len(), 1); - assert_eq!(imports[0], ReportValue::Str("os".to_string())); - } - - #[test] - fn test_analyzer_format_json_contains_value() { - let a = Analyzer::default(); - let mut report = ReportValue::map(); - report.insert( - "imports", - ReportValue::List(vec![ - ReportValue::Str("os".to_string()), - ReportValue::Str("sys".to_string()), - ]), - ); - let json = a.format_report_json(&report); - assert!(json.contains("os"), "expected os in JSON output: {json}"); - } - - #[test] - fn test_extract_imports_from_uast() { - // 1. Python "import os". - let r1 = Node::new(uast::IMPORT).with_token("import os"); - let imps1 = extract_imports_from_uast(&r1); - assert_eq!(imps1, vec!["os".to_string()]); - - // 2. Python "from x import y". - let r2 = Node::new(uast::IMPORT).with_token("from x import y"); - let imps2 = extract_imports_from_uast(&r2); - assert_eq!(imps2, vec!["x".to_string()]); - - // 3. JS "import React from 'react'" -> react. - let r3 = Node::new(uast::IMPORT).with_token("import React from 'react'"); - let imps3 = extract_imports_from_uast(&r3); - assert_eq!(imps3, vec!["react".to_string()]); - - // 4. JS "import './styles.css'". - let r4 = Node::new(uast::IMPORT).with_token("import './styles.css'"); - let imps4 = extract_imports_from_uast(&r4); - assert_eq!(imps4, vec!["./styles.css".to_string()]); - - // 5. Children traversal (import role, literal child). - let r5 = Node::new("") - .with_roles([role::IMPORT]) - .with_children(vec![Node::new(uast::LITERAL).with_token("'module'")]); - let imps5 = extract_imports_from_uast(&r5); - assert_eq!(imps5, vec!["module".to_string()]); - } - - #[test] - fn test_format_report_binary_envelope() { - let a = Analyzer::default(); - let mut report = ReportValue::map(); - report.insert( - "imports", - ReportValue::List(vec![ReportValue::Str("fmt".to_string())]), - ); - report.insert("count", ReportValue::Int(1)); - let mut out = Vec::new(); - a.format_report_binary(&report, &mut out); - assert_eq!(&out[0..4], b"CFB1"); - assert!(out.len() > 8); - } -} diff --git a/crates/cf-imports/src/extract.rs b/crates/cf-imports/src/extract.rs new file mode 100644 index 00000000..c5a6c608 --- /dev/null +++ b/crates/cf-imports/src/extract.rs @@ -0,0 +1,541 @@ +//! Language-aware import extraction over the UAST. +//! +//! [`extract_imports`] walks a parsed file and returns the modules, packages +//! or files its import statements name — deduplicated per file in first-seen +//! order. The walk is **consuming**: once a node matches the language's +//! import rule, its subtree is never visited again, so specifier lists, +//! aliases and nested import-role nodes can never count as imports. +//! +//! What counts as one import, per language: +//! +//! | Language | Statement | Import(s) | +//! |---|---|---| +//! | Go | `import str "strings"`, `. "math"`, `_ "embed"` | the quoted path (`strings`, `math`, `embed`) | +//! | Python | `import a, b as c` / `from M import x` / `from . import y` | `a`, `b` / `M` / `.y` | +//! | JavaScript, TypeScript | `import X from 'm'`, `import 'm'`, `import x = require("m")`, `require('m')`, `import('m')` | the module specifier `m` | +//! | Rust | `use a::{b, c::{self, d}};`, `pub use x as y;`, `extern crate z;` | `a::b`, `a::c`, `a::c::d`; `x`; `z` | +//! | Java | `import java.util.*;`, `import static java.lang.Math.max;` | `java.util.*`, `java.lang.Math.max` | +//! | C, C++ | `#include <stdio.h>`, `#include "local.h"` | `stdio.h` (package), `local.h` (local file) | +//! | C# | `using Alias = System.IO;`, `using static System.Math;` | `System.IO`, `System.Math` | +//! | PHP | `use A\B\{C, D};`, `use X\Y as Z;`, `require_once "f.php";` | `A\B\C`, `A\B\D`; `X\Y`; `f.php` (local file) | +//! | Ruby | `require "json"`, `require_relative "lib/x"` | `json`; `lib/x` (local file) | +//! | Kotlin | `import a.b.c as d`, `import a.b.*` | `a.b.c`, `a.b.*` (recovered from the source span) | +//! | other | an `Import` node | the generic statement rule ([`crate::statement::generic_targets`]) | +//! +//! Re-exports (`export … from`), dynamic calls whose argument is not a string +//! literal, and Kotlin files parsed without their source bytes contribute +//! nothing. + +use std::collections::HashSet; + +use cf_uast_node::Node; + +use crate::language::Language; +use crate::statement; + +/// UAST type of an import statement node. +const TYPE_IMPORT: &str = "Import"; +/// UAST type of a string literal node. +const TYPE_LITERAL: &str = "Literal"; +/// UAST type of an identifier node. +const TYPE_IDENTIFIER: &str = "Identifier"; +/// UAST type of a synthetic (grammar-specific) node. +const TYPE_SYNTHETIC: &str = "Synthetic"; +/// UAST type of a call expression node. +const TYPE_CALL: &str = "Call"; +/// The CommonJS loader function. +const JS_REQUIRE: &str = "require"; +/// The token the dynamic-import keyword node carries. +const JS_IMPORT_KEYWORD: &str = "import"; +/// Ruby's library loader. +const RUBY_REQUIRE: &str = "require"; +/// Ruby's file-relative loader. +const RUBY_REQUIRE_RELATIVE: &str = "require_relative"; +/// Ruby's unconditional file loader. +const RUBY_LOAD: &str = "load"; +/// Opening brace of a PHP group `use`. +const PHP_GROUP_OPEN: char = '{'; +/// PHP namespace separator. +const PHP_SEP: char = '\\'; +/// Java / Kotlin on-demand (wildcard) import suffix. +const WILDCARD_SUFFIX: &str = ".*"; + +/// One import extracted from a file. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Import { + /// The module, package or file path the statement names. + pub path: String, + /// `true` when the statement names a file relative to the importing file + /// (C `#include "x.h"`, PHP `require "x.php"`, Ruby `require_relative`), + /// which classifies as `relative` regardless of the path's spelling. + pub local_file: bool, +} + +impl Import { + /// A module/package import. + #[must_use] + pub fn package(path: impl Into<String>) -> Self { + Import { + path: path.into(), + local_file: false, + } + } + + /// A file-relative include. + #[must_use] + pub fn local(path: impl Into<String>) -> Self { + Import { + path: path.into(), + local_file: true, + } + } +} + +/// Extracts the imports of one parsed file. +/// +/// * `language` is the parser's language name for the file (`go`, `python`, +/// …); unknown names use the generic rule. +/// * `source` is the file's bytes, used only where the tree carries no +/// tokens (Kotlin); pass an empty slice when unavailable. +/// +/// The result is deduplicated by path, first occurrence first. +#[must_use] +pub fn extract_imports(root: &Node, language: &str, source: &[u8]) -> Vec<Import> { + let lang = Language::from_name(language); + let mut sink = Sink::default(); + walk(root, lang, source, &mut sink); + sink.imports +} + +/// First-seen-order, path-deduplicated collector. +#[derive(Default)] +struct Sink { + imports: Vec<Import>, + seen: HashSet<String>, +} + +impl Sink { + fn push(&mut self, import: Import) { + if !import.path.is_empty() && self.seen.insert(import.path.clone()) { + self.imports.push(import); + } + } +} + +/// Pre-order walk that stops descending below a matched import node. +fn walk(node: &Node, lang: Language, source: &[u8], sink: &mut Sink) { + match match_node(node, lang, source) { + Some(found) => found.into_iter().for_each(|i| sink.push(i)), + None => node + .children + .iter() + .for_each(|c| walk(c, lang, source, sink)), + } +} + +/// Applies the language's rule: `Some(imports)` consumes the subtree (even +/// when empty), `None` keeps walking the children. +fn match_node(node: &Node, lang: Language, source: &[u8]) -> Option<Vec<Import>> { + match lang { + Language::Go => go_rule(node), + Language::Python => python_rule(node), + Language::JavaScript => javascript_rule(node), + Language::Rust => rust_rule(node), + Language::Java => java_rule(node), + Language::C | Language::Cpp => c_rule(node), + Language::CSharp => csharp_rule(node), + Language::Php => php_rule(node), + Language::Ruby => ruby_rule(node), + Language::Kotlin => kotlin_rule(node, source), + Language::Other => generic_rule(node), + } +} + +// --- per-language rules --- + +/// Go: an import *spec* carries its path as the last quoted string of its +/// token (`str "strings"` -> `strings`); the statement-level node has no +/// token and is walked through. +fn go_rule(node: &Node) -> Option<Vec<Import>> { + let token = tokened_import(node)?; + Some(packages(statement::last_quoted(token).map(str::to_string))) +} + +/// Python: the import node's token is the statement text. +fn python_rule(node: &Node) -> Option<Vec<Import>> { + let token = tokened_import(node)?; + Some(packages(statement::python_targets(token))) +} + +/// JavaScript / TypeScript: an ES import's specifier is its literal child +/// (never the specifier list); `require("m")` and `import("m")` are calls +/// whose first argument literal is the module. +fn javascript_rule(node: &Node) -> Option<Vec<Import>> { + if let Some(token) = tokened_import(node) { + let specifier = first_descendant_token(node, TYPE_LITERAL) + .map(statement::unquote) + .or_else(|| statement::last_quoted(token)); + return Some(packages(specifier.map(str::to_string))); + } + if node.node_type != TYPE_CALL { + return None; + } + let callee = node.children.first()?; + let is_loader = (callee.node_type == TYPE_IDENTIFIER && callee.token == JS_REQUIRE) + || (is_import(callee) && callee.token == JS_IMPORT_KEYWORD); + if !is_loader { + return None; + } + let module = call_argument_literal(node)?; + Some(vec![Import::package(module)]) +} + +/// Rust: the import node's token is the `use` / `extern crate` item. +fn rust_rule(node: &Node) -> Option<Vec<Import>> { + let token = tokened_import(node)?; + Some(packages(statement::rust_targets(token))) +} + +/// Java: the identifier child names the type/package; a trailing synthetic +/// child is the on-demand `*`. +fn java_rule(node: &Node) -> Option<Vec<Import>> { + if !is_import(node) { + return None; + } + let Some(path) = child_tokens(node, TYPE_IDENTIFIER).next() else { + return Some(Vec::new()); + }; + let wildcard = node.children.iter().any(|c| c.node_type == TYPE_SYNTHETIC); + let path = if wildcard { + format!("{path}{WILDCARD_SUFFIX}") + } else { + path.to_string() + }; + Some(vec![Import::package(path)]) +} + +/// C / C++: `<x>` is an identifier child (a system header), `"x"` a literal +/// child (a file next to the source). +fn c_rule(node: &Node) -> Option<Vec<Import>> { + if !is_import(node) { + return None; + } + if let Some(system) = child_tokens(node, TYPE_IDENTIFIER).next() { + return Some(vec![Import::package(statement::strip_angle(system))]); + } + Some(packages_or_local( + child_tokens(node, TYPE_LITERAL) + .next() + .map(statement::unquote), + true, + )) +} + +/// C#: `using [Alias =] [static] Target;` — the last identifier child is the +/// target. +fn csharp_rule(node: &Node) -> Option<Vec<Import>> { + if !is_import(node) { + return None; + } + Some(packages( + child_tokens(node, TYPE_IDENTIFIER) + .last() + .map(str::to_string), + )) +} + +/// PHP: `require`/`include` carry a literal; group `use A\{B, C}` expands +/// under its prefix; plain and aliased `use` nest one import node per +/// target. +fn php_rule(node: &Node) -> Option<Vec<Import>> { + if !is_import(node) { + return None; + } + if let Some(file) = first_descendant_token(node, TYPE_LITERAL) { + return Some(vec![Import::local(statement::unquote(file))]); + } + if let Some(group) = node + .children + .iter() + .find(|c| is_import(c) && c.token.starts_with(PHP_GROUP_OPEN)) + { + let prefix = child_tokens(node, TYPE_IDENTIFIER).next().unwrap_or(""); + return Some(packages(php_group_members(group, prefix))); + } + let nested: Vec<String> = node + .children + .iter() + .filter(|c| is_import(c)) + .filter_map(|c| first_descendant_token(c, TYPE_IDENTIFIER)) + .map(str::to_string) + .collect(); + if !nested.is_empty() { + return Some(packages(nested)); + } + Some(packages( + first_descendant_token(node, TYPE_IDENTIFIER).map(str::to_string), + )) +} + +/// The members of a PHP group `use`, each joined to `prefix`. +fn php_group_members(group: &Node, prefix: &str) -> Vec<String> { + group + .children + .iter() + .filter(|c| is_import(c)) + .filter_map(|c| first_descendant_token(c, TYPE_IDENTIFIER)) + .map(|name| { + if prefix.is_empty() { + name.to_string() + } else { + format!("{prefix}{PHP_SEP}{name}") + } + }) + .collect() +} + +/// Ruby: `require "m"` / `require_relative "f"` / `load "f"` calls. +fn ruby_rule(node: &Node) -> Option<Vec<Import>> { + if node.node_type != TYPE_CALL { + return None; + } + let callee = node.children.first()?; + if callee.node_type != TYPE_IDENTIFIER { + return None; + } + let local = match callee.token.as_str() { + RUBY_REQUIRE => false, + RUBY_REQUIRE_RELATIVE | RUBY_LOAD => true, + _ => return None, + }; + let target = call_argument_literal(node)?; + Some(packages_or_local(Some(target), local)) +} + +/// Kotlin: the import nodes carry no tokens, so the statement text is read +/// back from the node's source span (`import a.b.c as d` -> `a.b.c`). +fn kotlin_rule(node: &Node, source: &[u8]) -> Option<Vec<Import>> { + if !is_import(node) { + return None; + } + Some(packages( + span_text(node, source).and_then(statement::dotted_target), + )) +} + +/// Generic rule for languages without a dedicated one: statement text when +/// the node has a token; otherwise walk through to tokened nested imports, +/// else take the first literal / identifier child. +fn generic_rule(node: &Node) -> Option<Vec<Import>> { + if !is_import(node) { + return None; + } + if !node.token.is_empty() { + return Some(packages(statement::generic_targets(&node.token))); + } + if has_tokened_import_below(node) { + return None; + } + if let Some(literal) = first_descendant_token(node, TYPE_LITERAL) { + return Some(vec![Import::package(statement::unquote(literal))]); + } + Some(packages( + child_tokens(node, TYPE_IDENTIFIER) + .next() + .map(|t| statement::strip_angle(t).to_string()), + )) +} + +// --- node helpers --- + +/// True for an import statement node. +fn is_import(node: &Node) -> bool { + node.node_type == TYPE_IMPORT +} + +/// The token of an import node that carries one. +fn tokened_import(node: &Node) -> Option<&str> { + (is_import(node) && !node.token.is_empty()).then_some(node.token.as_str()) +} + +/// Tokens of the direct children of type `kind` that carry one. +fn child_tokens<'a>(node: &'a Node, kind: &'a str) -> impl Iterator<Item = &'a str> + 'a { + node.children + .iter() + .filter(move |c| c.node_type == kind && !c.token.is_empty()) + .map(|c| c.token.as_str()) +} + +/// The first descendant (pre-order, excluding `node`) of type `kind` that +/// carries a token. +fn first_descendant_token<'a>(node: &'a Node, kind: &str) -> Option<&'a str> { + for child in &node.children { + if child.node_type == kind && !child.token.is_empty() { + return Some(&child.token); + } + if let Some(found) = first_descendant_token(child, kind) { + return Some(found); + } + } + None +} + +/// True when some descendant is an import node with a token. +fn has_tokened_import_below(node: &Node) -> bool { + node.children + .iter() + .any(|c| (is_import(c) && !c.token.is_empty()) || has_tokened_import_below(c)) +} + +/// The first string literal among a call's arguments (every child after the +/// callee), unquoted. +fn call_argument_literal(call: &Node) -> Option<&str> { + call.children + .iter() + .skip(1) + .find_map(|arg| { + if arg.node_type == TYPE_LITERAL && !arg.token.is_empty() { + Some(arg.token.as_str()) + } else { + first_descendant_token(arg, TYPE_LITERAL) + } + }) + .map(statement::unquote) +} + +/// The source text under the node's byte span, when positions and source +/// are available and consistent. +fn span_text<'a>(node: &Node, source: &'a [u8]) -> Option<&'a str> { + let pos = node.pos.as_ref()?; + let start = usize::try_from(pos.start_offset).ok()?; + let end = usize::try_from(pos.end_offset).ok()?; + if start >= end || end > source.len() { + return None; + } + std::str::from_utf8(&source[start..end]).ok() +} + +/// Wraps package paths. +fn packages<I: IntoIterator<Item = String>>(paths: I) -> Vec<Import> { + paths.into_iter().map(Import::package).collect() +} + +/// Wraps an optional path as a package or a local file. +fn packages_or_local(path: Option<&str>, local: bool) -> Vec<Import> { + path.map(|p| { + if local { + Import::local(p) + } else { + Import::package(p) + } + }) + .into_iter() + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn node(kind: &str, token: &str, children: Vec<Node>) -> Node { + Node { + node_type: kind.to_string(), + token: token.to_string(), + children, + ..Node::default() + } + } + + fn paths(imports: &[Import]) -> Vec<&str> { + imports.iter().map(|i| i.path.as_str()).collect() + } + + #[test] + fn dedup_keeps_first_seen_order() { + let root = node( + "File", + "", + vec![ + node(TYPE_IMPORT, "import b", vec![]), + node(TYPE_IMPORT, "import a", vec![]), + node(TYPE_IMPORT, "import b", vec![]), + ], + ); + assert_eq!(paths(&extract_imports(&root, "python", &[])), ["b", "a"]); + } + + #[test] + fn nested_import_nodes_below_a_match_never_count() { + let root = node( + "File", + "", + vec![node( + TYPE_IMPORT, + "import React from 'react';", + vec![ + node( + TYPE_SYNTHETIC, + "React", + vec![node(TYPE_IDENTIFIER, "React", vec![])], + ), + node(TYPE_LITERAL, "'react'", vec![]), + ], + )], + ); + assert_eq!(paths(&extract_imports(&root, "javascript", &[])), ["react"]); + } + + #[test] + fn kotlin_without_source_or_positions_yields_nothing() { + let root = node( + "File", + "", + vec![node( + TYPE_IMPORT, + "", + vec![node(TYPE_IDENTIFIER, "", vec![])], + )], + ); + assert!(extract_imports(&root, "kotlin", b"import a.b").is_empty()); + } + + #[test] + fn span_text_rejects_inconsistent_offsets() { + let mut n = node(TYPE_IMPORT, "", vec![]); + n.pos = Some(cf_uast_node::Positions { + start_offset: 4, + end_offset: 40, + ..cf_uast_node::Positions::default() + }); + assert_eq!(span_text(&n, b"import a.b"), None); + n.pos.as_mut().expect("pos").end_offset = 10; + assert_eq!(span_text(&n, b"import a.b"), Some("rt a.b")); + } + + #[test] + fn unknown_language_generic_rule_handles_token_and_child_shapes() { + let root = node( + "File", + "", + vec![ + node(TYPE_IMPORT, "from x import y", vec![]), + node( + TYPE_IMPORT, + "", + vec![node(TYPE_IDENTIFIER, "<stdio.h>", vec![])], + ), + node( + TYPE_IMPORT, + "", + vec![node(TYPE_LITERAL, "\"local.h\"", vec![])], + ), + node(TYPE_IMPORT, "", vec![node(TYPE_IMPORT, "\"fmt\"", vec![])]), + node(TYPE_IMPORT, "", vec![]), + ], + ); + assert_eq!( + paths(&extract_imports(&root, "", &[])), + ["x", "stdio.h", "local.h", "fmt"] + ); + } +} diff --git a/crates/cf-imports/src/history.rs b/crates/cf-imports/src/history.rs index 03b077be..a997af46 100644 --- a/crates/cf-imports/src/history.rs +++ b/crates/cf-imports/src/history.rs @@ -1,120 +1,42 @@ //! Per-developer import history (`ImportsPerDeveloper`, id `history/imports`). //! //! The history analyzer tracks import usage across commit history, attributing -//! each import to the commit author, language, and tick. The accumulated state -//! is a 4-level map ([`ImportsMap`]: author -> lang -> import -> tick -> count) -//! that merges **additively** (counts are summed) — the analyzer's defining -//! behaviour. -//! -//! This module owns the self-contained data model and the -//! merge/aggregation/attribution logic, and defines the analyzer surface -//! (name/flag/extract-commit-timeseries) over it. The pipeline wiring -//! (consume/fork/snapshotting) lives in the framework crates. +//! each import to the commit author, the file's language and the commit's +//! tick. The accumulated state is a 4-level map ([`ImportsMap`]: author id -> +//! language -> import -> tick -> count) that merges **additively** (counts +//! are summed). [`HistoryReport`] is the report the machine formats encode: +//! the tick size, the author index and the map keyed by developer name. use std::collections::BTreeMap; -/// Default tick length, in hours. -pub const DEFAULT_TICK_HOURS: u64 = 24; +use cf_gojson::{GoMap, GoValue}; -/// 4-level import-usage map: author -> lang -> import -> tick -> count. +/// 4-level import-usage map: author id -> language -> import -> tick -> count. /// /// Every level uses a [`BTreeMap`] so iteration is deterministic (sorted), /// which matters for reproducible aggregation/serialization. -pub type ImportsMap = BTreeMap<i64, BTreeMap<String, BTreeMap<String, BTreeMap<i64, i64>>>>; +pub type ImportsMap = BTreeMap<i64, LanguageImports>; + +/// language -> import -> tick -> count: one developer's usage. +pub type LanguageImports = BTreeMap<String, BTreeMap<String, BTreeMap<i64, i64>>>; -/// A single import extracted from a commit, carrying its language. -#[derive(Debug, Clone, PartialEq, Eq)] +/// A single import extracted from a commit, carrying the language of the file +/// it was found in (the parser's language name, e.g. `go`, `python`). +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct ImportEntry { - /// The language the import was found in (e.g. `go`, `uast`). + /// The language of the importing file. pub lang: String, /// The import path. pub import: String, } -/// Per-commit summary for timeseries output. -/// -/// `languages` maps language -> import count. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct CommitSummary { - /// Total number of imports in the commit. - pub import_count: i64, - /// Per-language import counts. - pub languages: BTreeMap<String, i64>, -} - -/// Per-tick aggregated payload. -#[derive(Debug, Clone, Default)] -pub struct TickData { - /// The accumulated 4-level imports map for the tick. - pub imports: ImportsMap, - /// Per-commit summaries keyed by commit hash string. - pub commit_stats: BTreeMap<String, CommitSummary>, -} - -/// The per-developer import history analyzer. -/// -/// Stateless config holder; mutable per-tick state lives in -/// [`TickData`]/[`ImportsMap`]. -#[derive(Debug, Clone)] -pub struct HistoryAnalyzer { - /// Tick size in hours (defaults to [`DEFAULT_TICK_HOURS`]). - pub tick_hours: u64, - /// Author index: author id -> name. - pub reversed_people_dict: Vec<String>, -} - -impl Default for HistoryAnalyzer { - fn default() -> Self { - HistoryAnalyzer { - tick_hours: DEFAULT_TICK_HOURS, - reversed_people_dict: Vec::new(), - } - } -} - -impl HistoryAnalyzer { - /// Creates a new history analyzer. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Returns the analyzer name. - #[must_use] - pub fn name(&self) -> &'static str { - "ImportsPerDeveloper" - } - - /// Returns the CLI flag. - #[must_use] - pub fn flag(&self) -> &'static str { - "imports-per-dev" - } - - /// Returns the stable analyzer id. - #[must_use] - pub fn id(&self) -> &'static str { - "history/imports" - } - - /// Extracts per-commit timeseries data from a report's `commit_stats`: - /// `None` when there are no commit stats, otherwise a map of commit hash -> - /// `{import_count, languages}`. - #[must_use] - pub fn extract_commit_time_series( - &self, - commit_stats: &BTreeMap<String, CommitSummary>, - ) -> Option<BTreeMap<String, CommitSummary>> { - if commit_stats.is_empty() { - return None; - } - Some(commit_stats.clone()) - } -} - /// Adds extracted entries to the 4-level map under one author/tick: each entry -/// increments `map[author][lang][import][tick]` by one. +/// increments `map[author][lang][import][tick]` by one. An empty entry list +/// adds nothing — not even the author. pub fn add_entries_to_map(m: &mut ImportsMap, entries: &[ImportEntry], author_id: i64, tick: i64) { + if entries.is_empty() { + return; + } let langs = m.entry(author_id).or_default(); for entry in entries { let imps = langs.entry(entry.lang.clone()).or_default(); @@ -145,14 +67,19 @@ pub fn add_entries_to_map(m: &mut ImportsMap, entries: &[ImportEntry], author_id /// ``` pub fn merge_import_maps(dst: &mut ImportsMap, src: &ImportsMap) { for (auth, src_langs) in src { - let dst_langs = dst.entry(*auth).or_default(); - for (lang, src_imps) in src_langs { - let dst_imps = dst_langs.entry(lang.clone()).or_default(); - for (imp, src_ticks) in src_imps { - let dst_ticks = dst_imps.entry(imp.clone()).or_default(); - for (tick, count) in src_ticks { - *dst_ticks.entry(*tick).or_insert(0) += *count; - } + merge_language_imports(dst.entry(*auth).or_default(), src_langs); + } +} + +/// Merges one developer's `language -> import -> tick -> count` map into +/// `dst` additively. +fn merge_language_imports(dst: &mut LanguageImports, src: &LanguageImports) { + for (lang, src_imps) in src { + let dst_imps = dst.entry(lang.clone()).or_default(); + for (imp, src_ticks) in src_imps { + let dst_ticks = dst_imps.entry(imp.clone()).or_default(); + for (tick, count) in src_ticks { + *dst_ticks.entry(*tick).or_insert(0) += *count; } } } @@ -190,11 +117,9 @@ pub fn aggregate_import_counts(imports: &ImportsMap) -> BTreeMap<String, i64> { /// Maximum number of imports returned by [`top_imports`]. pub const TOP_IMPORTS_LIMIT: usize = 20; -/// Returns the top imports by count (descending), capped at [`TOP_IMPORTS_LIMIT`]. -/// -/// The reference implementation sorts unstably by count descending with -/// unspecified tie order; this implementation breaks ties by import name -/// ascending for determinism. Returns parallel `(labels, data)` vectors. +/// Returns the top imports by count (descending, ties by import name +/// ascending), capped at [`TOP_IMPORTS_LIMIT`], as parallel `(labels, data)` +/// vectors — the plot's bar chart input. /// /// ``` /// use std::collections::BTreeMap; @@ -213,135 +138,169 @@ pub const TOP_IMPORTS_LIMIT: usize = 20; pub fn top_imports(counts: &BTreeMap<String, i64>) -> (Vec<String>, Vec<i64>) { let mut items: Vec<(String, i64)> = counts.iter().map(|(k, v)| (k.clone(), *v)).collect(); items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); - if items.len() > TOP_IMPORTS_LIMIT { - items.truncate(TOP_IMPORTS_LIMIT); - } + items.truncate(TOP_IMPORTS_LIMIT); let labels = items.iter().map(|(k, _)| k.clone()).collect(); let data = items.iter().map(|(_, v)| *v).collect(); (labels, data) } -#[cfg(test)] -mod tests { - use super::*; +/// Report key: the tick length in seconds. +pub const KEY_TICK_SIZE: &str = "tick_size"; +/// Report key: developer names by author id. +pub const KEY_AUTHOR_INDEX: &str = "author_index"; +/// Report key: the developer → language → import → tick → count map. +pub const KEY_IMPORTS: &str = "imports"; - #[test] - fn test_history_analyzer_name() { - let h = HistoryAnalyzer::new(); - assert!(!h.name().is_empty()); - assert_eq!(h.name(), "ImportsPerDeveloper"); - } +/// The `history/imports` report: the tick size, the author index and the +/// per-developer import usage — the ONE value json/yaml/bin encode. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct HistoryReport { + /// Tick length in seconds (`--tick-size` hours × 3600). + pub tick_size_secs: i64, + /// Developer names by author id. + pub author_index: Vec<String>, + /// author id -> language -> import -> tick -> count. + pub imports: ImportsMap, +} - #[test] - fn test_history_analyzer_flag() { - let h = HistoryAnalyzer::new(); - assert!(!h.flag().is_empty()); - assert_eq!(h.flag(), "imports-per-dev"); +impl HistoryReport { + /// The developer name of `author_id`: its `author_index` entry, or the + /// id itself when it lies outside the index. + fn developer_name(&self, author_id: i64) -> String { + usize::try_from(author_id) + .ok() + .and_then(|i| self.author_index.get(i)) + .map_or_else(|| author_id.to_string(), Clone::clone) } - #[test] - fn test_history_analyzer_id() { - let h = HistoryAnalyzer::new(); - assert_eq!(h.id(), "history/imports"); + /// The map keyed by developer name: two author ids that resolve to the + /// same name merge additively. + fn imports_by_developer(&self) -> BTreeMap<String, LanguageImports> { + let mut by_name: BTreeMap<String, LanguageImports> = BTreeMap::new(); + for (author_id, langs) in &self.imports { + merge_language_imports( + by_name.entry(self.developer_name(*author_id)).or_default(), + langs, + ); + } + by_name } - #[test] - fn test_extract_commit_time_series() { - let h = HistoryAnalyzer::new(); - let hash = "aabbccdd00112233445566778899aabbccddeeff".to_string(); - let mut langs = BTreeMap::new(); - langs.insert("go".to_string(), 3); - langs.insert("python".to_string(), 2); - let mut stats = BTreeMap::new(); - stats.insert( - hash.clone(), - CommitSummary { - import_count: 5, - languages: langs, - }, - ); + /// The report value: `tick_size`, `author_index` and `imports` + /// (developer → language → import → tick → count; tick keys are + /// integers, rendered as decimal strings in json). + /// + /// ``` + /// use cf_imports::history::{add_entries_to_map, HistoryReport, ImportEntry, ImportsMap}; + /// + /// let mut imports = ImportsMap::new(); + /// add_entries_to_map(&mut imports, &[ImportEntry { lang: "go".into(), import: "fmt".into() }], 0, 3); + /// let report = HistoryReport { tick_size_secs: 86_400, author_index: vec!["alice".into()], imports }; + /// let json = String::from_utf8(cf_gojson::marshal(&report.to_go_value())).unwrap(); + /// assert_eq!( + /// json, + /// r#"{"tick_size":86400,"author_index":["alice"],"imports":{"alice":{"go":{"fmt":{"3":1}}}}}"# + /// ); + /// ``` + #[must_use] + pub fn to_go_value(&self) -> GoValue { + let mut developers = GoMap::new_map(); + for (name, langs) in self.imports_by_developer() { + let mut lang_map = GoMap::new_map(); + for (lang, imps) in &langs { + let mut imp_map = GoMap::new_map(); + for (imp, ticks) in imps { + let mut tick_map = GoMap::new_int_map(); + for (tick, count) in ticks { + tick_map.push(tick.to_string(), GoValue::Int(*count)); + } + imp_map.push(imp.clone(), GoValue::Map(tick_map)); + } + lang_map.push(lang.clone(), GoValue::Map(imp_map)); + } + developers.push(name, GoValue::Map(lang_map)); + } - let result = h.extract_commit_time_series(&stats).expect("not nil"); - let entry = result.get(&hash).expect("hash present"); - assert_eq!(entry.import_count, 5); - assert_eq!(entry.languages["go"], 3); - assert_eq!(entry.languages["python"], 2); + let mut root = GoMap::new_struct(); + root.push(KEY_TICK_SIZE, GoValue::Int(self.tick_size_secs)); + root.push( + KEY_AUTHOR_INDEX, + GoValue::Array( + self.author_index + .iter() + .cloned() + .map(GoValue::Str) + .collect(), + ), + ); + root.push(KEY_IMPORTS, GoValue::Map(developers)); + GoValue::Map(root) } - #[test] - fn test_extract_commit_time_series_empty() { - let h = HistoryAnalyzer::new(); - assert!(h.extract_commit_time_series(&BTreeMap::new()).is_none()); + /// The yaml value: the same tree as [`HistoryReport::to_go_value`] (the + /// yaml encoder renders the integer tick keys as plain scalars). + #[must_use] + pub fn to_go_value_yaml(&self) -> GoValue { + self.to_go_value() } +} - // --- additive-merge / aggregation coverage (the core behaviour) --- - - fn build_test_import_ticks() -> Vec<TickData> { - let mk = |pairs: &[(i64, &str, &str, i64, i64)]| -> ImportsMap { - let mut m: ImportsMap = BTreeMap::new(); - for (author, lang, imp, tick, count) in pairs { - m.entry(*author) - .or_default() - .entry(lang.to_string()) - .or_default() - .entry(imp.to_string()) - .or_default() - .insert(*tick, *count); - } - m - }; +#[cfg(test)] +mod tests { + use super::*; - vec![ - TickData { - imports: mk(&[ - (0, "go", "fmt", 0, 3), - (0, "go", "os", 0, 2), - (0, "go", "strings", 0, 1), - ]), - commit_stats: BTreeMap::new(), - }, - TickData { - imports: mk(&[ - (0, "go", "fmt", 1, 5), - (0, "go", "io", 1, 1), - (1, "go", "fmt", 1, 2), - (1, "go", "os", 1, 3), - ]), - commit_stats: BTreeMap::new(), - }, - ] + fn mk(pairs: &[(i64, &str, &str, i64, i64)]) -> ImportsMap { + let mut m: ImportsMap = BTreeMap::new(); + for (author, lang, imp, tick, count) in pairs { + m.entry(*author) + .or_default() + .entry(lang.to_string()) + .or_default() + .entry(imp.to_string()) + .or_default() + .insert(*tick, *count); + } + m } #[test] - fn test_merge_and_aggregate_counts() { - let ticks = build_test_import_ticks(); + fn merge_and_aggregate_counts() { + let tick0 = mk(&[ + (0, "go", "fmt", 0, 3), + (0, "go", "os", 0, 2), + (0, "go", "strings", 0, 1), + ]); + let tick1 = mk(&[ + (0, "go", "fmt", 1, 5), + (0, "go", "io", 1, 1), + (1, "go", "fmt", 1, 2), + (1, "go", "os", 1, 3), + ]); let mut merged: ImportsMap = BTreeMap::new(); - for td in &ticks { - merge_import_maps(&mut merged, &td.imports); - } + merge_import_maps(&mut merged, &tick0); + merge_import_maps(&mut merged, &tick1); let counts = aggregate_import_counts(&merged); // fmt: 3 + 5 + 2 = 10; os: 2 + 3 = 5; strings: 1; io: 1. assert_eq!(counts["fmt"], 10); assert_eq!(counts["os"], 5); assert_eq!(counts["strings"], 1); assert_eq!(counts["io"], 1); + let (labels, data) = top_imports(&counts); + assert_eq!(labels[0], "fmt"); + assert_eq!(data[0], 10); } #[test] - fn test_top_imports_orders_by_count_desc() { - let ticks = build_test_import_ticks(); - let mut merged: ImportsMap = BTreeMap::new(); - for td in &ticks { - merge_import_maps(&mut merged, &td.imports); - } - let counts = aggregate_import_counts(&merged); + fn top_imports_is_capped() { + let counts: BTreeMap<String, i64> = (0..30).map(|i| (format!("m{i:02}"), i)).collect(); let (labels, data) = top_imports(&counts); - assert_eq!(labels[0], "fmt"); - assert_eq!(data[0], 10); + assert_eq!(labels.len(), TOP_IMPORTS_LIMIT); + assert_eq!(labels[0], "m29"); + assert_eq!(data[0], 29); } #[test] - fn test_add_entries_to_map_increments() { + fn add_entries_to_map_increments() { let mut m: ImportsMap = BTreeMap::new(); let entries = vec![ ImportEntry { @@ -353,12 +312,69 @@ mod tests { import: "fmt".to_string(), }, ImportEntry { - lang: "go".to_string(), + lang: "python".to_string(), import: "os".to_string(), }, ]; add_entries_to_map(&mut m, &entries, 0, 0); assert_eq!(m[&0]["go"]["fmt"][&0], 2); - assert_eq!(m[&0]["go"]["os"][&0], 1); + assert_eq!(m[&0]["python"]["os"][&0], 1); + // A commit without imports leaves no trace of its author. + add_entries_to_map(&mut m, &[], 1, 0); + assert!(!m.contains_key(&1)); + } + + /// The documented shape: developer → language → import → tick → count, + /// plus `tick_size` and the author index. + #[test] + fn report_value_is_the_documented_four_level_map() { + let report = HistoryReport { + tick_size_secs: 86_400, + author_index: vec!["alice".into(), "bob".into()], + imports: mk(&[ + (0, "go", "fmt", 0, 1), + (0, "go", "fmt", 2, 1), + (0, "go", "net/http", 2, 1), + (1, "python", "os", 1, 1), + ]), + }; + let json = String::from_utf8(cf_gojson::marshal(&report.to_go_value())).unwrap(); + assert_eq!( + json, + concat!( + r#"{"tick_size":86400,"author_index":["alice","bob"],"#, + r#""imports":{"alice":{"go":{"fmt":{"0":1,"2":1},"net/http":{"2":1}}},"bob":{"python":{"os":{"1":1}}}}}"# + ) + ); + let yaml = String::from_utf8(cf_goyaml::marshal(&report.to_go_value_yaml())).unwrap(); + assert!(yaml.contains("tick_size: 86400"), "{yaml}"); + assert!(yaml.contains("net/http:"), "{yaml}"); + assert!(yaml.contains("python:"), "{yaml}"); + } + + #[test] + fn empty_report_keeps_every_key() { + let report = HistoryReport { + tick_size_secs: 3_600, + ..HistoryReport::default() + }; + let json = String::from_utf8(cf_gojson::marshal(&report.to_go_value())).unwrap(); + assert_eq!(json, r#"{"tick_size":3600,"author_index":[],"imports":{}}"#); + } + + #[test] + fn same_name_authors_merge_and_unknown_ids_keep_their_number() { + let report = HistoryReport { + tick_size_secs: 86_400, + author_index: vec!["alice".into(), "alice".into()], + imports: mk(&[ + (0, "go", "fmt", 0, 1), + (1, "go", "fmt", 0, 2), + (7, "go", "os", 0, 1), + ]), + }; + let json = String::from_utf8(cf_gojson::marshal(&report.to_go_value())).unwrap(); + assert!(json.contains(r#""alice":{"go":{"fmt":{"0":3}}}"#), "{json}"); + assert!(json.contains(r#""7":{"go":{"os":{"0":1}}}"#), "{json}"); } } diff --git a/crates/cf-imports/src/language.rs b/crates/cf-imports/src/language.rs new file mode 100644 index 00000000..6b36bbf6 --- /dev/null +++ b/crates/cf-imports/src/language.rs @@ -0,0 +1,96 @@ +//! The languages the import extractor and the stdlib classifier know by name. +//! +//! Names are the `cf_uast::Parser::get_language` values (the mapping +//! registry's language names). Anything else maps to [`Language::Other`], +//! which selects the generic extraction rule and the union stdlib table. + +/// A language with dedicated import semantics. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Language { + /// Go (`go`). + Go, + /// Python (`python`). + Python, + /// JavaScript, TypeScript and TSX (`javascript`, `typescript`, `tsx`): + /// one module system, one builtin table. + JavaScript, + /// Rust (`rust`). + Rust, + /// Java (`java`). + Java, + /// C (`c`). + C, + /// C++ (`cpp`). + Cpp, + /// C# (`csharp`). + CSharp, + /// PHP (`php`). + Php, + /// Ruby (`ruby`). + Ruby, + /// Kotlin (`kotlin`). + Kotlin, + /// Any other (or unknown) language. + Other, +} + +/// Parser language name for Go. +pub const NAME_GO: &str = "go"; +/// Parser language name for Python. +pub const NAME_PYTHON: &str = "python"; +/// Parser language name for JavaScript. +pub const NAME_JAVASCRIPT: &str = "javascript"; +/// Parser language name for TypeScript. +pub const NAME_TYPESCRIPT: &str = "typescript"; +/// Parser language name for TSX. +pub const NAME_TSX: &str = "tsx"; +/// Parser language name for Rust. +pub const NAME_RUST: &str = "rust"; +/// Parser language name for Java. +pub const NAME_JAVA: &str = "java"; +/// Parser language name for C. +pub const NAME_C: &str = "c"; +/// Parser language name for C++. +pub const NAME_CPP: &str = "cpp"; +/// Mapping-registry language name for C#. +pub const NAME_CSHARP: &str = "csharp"; +/// Grammar-stem language name for C# (what `cf_uast::Parser::get_language` +/// reports for `.cs`). +pub const NAME_C_SHARP: &str = "c_sharp"; +/// Parser language name for PHP. +pub const NAME_PHP: &str = "php"; +/// Parser language name for Ruby. +pub const NAME_RUBY: &str = "ruby"; +/// Parser language name for Kotlin. +pub const NAME_KOTLIN: &str = "kotlin"; + +impl Language { + /// Maps a parser language name to its [`Language`]; unknown or empty names + /// are [`Language::Other`]. + /// + /// ``` + /// use cf_imports::Language; + /// + /// assert_eq!(Language::from_name("go"), Language::Go); + /// assert_eq!(Language::from_name("tsx"), Language::JavaScript); + /// assert_eq!(Language::from_name("c_sharp"), Language::CSharp); + /// assert_eq!(Language::from_name(""), Language::Other); + /// ``` + #[must_use] + pub fn from_name(name: &str) -> Self { + match name { + NAME_GO => Self::Go, + NAME_PYTHON => Self::Python, + NAME_JAVASCRIPT | NAME_TYPESCRIPT | NAME_TSX => Self::JavaScript, + NAME_RUST => Self::Rust, + NAME_JAVA => Self::Java, + NAME_C => Self::C, + NAME_CPP => Self::Cpp, + NAME_CSHARP | NAME_C_SHARP => Self::CSharp, + NAME_PHP => Self::Php, + NAME_RUBY => Self::Ruby, + NAME_KOTLIN => Self::Kotlin, + _ => Self::Other, + } + } +} diff --git a/crates/cf-imports/src/lib.rs b/crates/cf-imports/src/lib.rs index 2a8d0445..2ef9d2bd 100644 --- a/crates/cf-imports/src/lib.rs +++ b/crates/cf-imports/src/lib.rs @@ -1,77 +1,65 @@ //! Import/dependency analysis. //! -//! Two related analyzers: +//! Two related analyzers share this crate: //! -//! * [`analyzer::Analyzer`] — **static** analysis that produces the `imports` -//! report key (the deduplicated import identifiers in a parsed file/tree) plus -//! a `count`, along with the derived [`metrics::ComputedMetrics`] used for -//! json/yaml/bin output. -//! * [`history::HistoryAnalyzer`] — **historical** analysis -//! (`ImportsPerDeveloper`, stable id `history/imports`) that tracks import -//! usage per developer, language, and tick in a 4-level -//! [`history::ImportsMap`] which merges **additively**. +//! * **`static/imports`** — [`extract_imports`] turns one parsed file into the +//! modules/packages/files it imports (language-aware, deduplicated per +//! file); [`Aggregate`] folds files into per-import file counts; [`metrics`] +//! classifies each import (`relative` / `stdlib` / `external`, using the +//! per-language tables in [`stdlib`]) and derives the [`ComputedMetrics`] +//! the json/yaml/bin formats serialize. +//! * **`history/imports`** ([`history`]) — per-developer, per-language, +//! per-tick import usage in a 4-level [`history::ImportsMap`] that merges +//! additively; [`history::HistoryReport`] is the value the machine +//! formats encode (`tick_size`, `author_index`, developer → language → +//! import → tick → count). //! -//! Supporting modules: [`metrics`] (classification + metric computation), -//! [`aggregator`] (cross-file static aggregation), [`store`] (history persistence -//! records), and [`report_section`] (terminal section data). -//! -//! # Compatibility and serialization -//! -//! Every machine-format report (json, yaml, ndjson, timeseries, compact, bin) -//! is a frozen output contract, pinned byte-for-byte against the reference -//! implementation by the differential gate in `tests/compat`. All such -//! output routes through the report-format encoders `cf-gojson` / `cf-goyaml` -//! and the CFB1 `bin` envelope from `cf-reportutil` — never raw serde. The -//! canonical [`node::Node`] model belongs to `cf-uast-node` and the framework -//! `Analyzer` trait / report type to `cf-framework` / `cf-analyze`. -//! -//! To remain self-contained and verifiable, this crate carries minimal local -//! shims — [`node`] (the UAST node subset the analyzer reads) and [`report`] -//! (the dynamic [`report::ReportValue`] model plus a deterministic compact-JSON -//! / CFB1 encoder). The analyzer logic depends only on the documented shim -//! contracts. +//! Serialization of every machine format routes through +//! [`ComputedMetrics::to_go_value`] / [`history::HistoryReport::to_go_value`] +//! and the `cf-gojson` / `cf-goyaml` encoders; the dynamic [`ReportValue`] +//! exists for the plot callers that rebuild the static report from a value +//! tree. //! //! # Example //! -//! Static analysis of a tree with two import nodes yields the deduplicated -//! `imports` list and its `count`: +//! Extract the imports of a Go file from its real parse tree and aggregate +//! them: //! //! ``` -//! use cf_imports::Analyzer; -//! use cf_imports::node::{uast, Node}; -//! use cf_imports::report::ReportValue; +//! use cf_imports::{extract_imports, Aggregate}; +//! use cf_uast::Parser; +//! +//! let src = b"package main\n\nimport (\n\t\"fmt\"\n\tstr \"strings\"\n\t_ \"embed\"\n)\n"; +//! let parser = Parser::new(); +//! let root = parser.parse("main.go", src).unwrap(); //! -//! let root = Node::new("File").with_children(vec![ -//! Node::new(uast::IMPORT).with_token("import os"), -//! Node::new(uast::IMPORT).with_token("from sys import argv"), -//! ]); +//! let imports = extract_imports(&root, &parser.get_language("main.go"), src); +//! let paths: Vec<&str> = imports.iter().map(|i| i.path.as_str()).collect(); +//! assert_eq!(paths, ["fmt", "strings", "embed"]); //! -//! let report = Analyzer::new().analyze(&root).unwrap(); -//! let m = report.as_map().unwrap(); -//! assert_eq!(m.get("count"), Some(&ReportValue::Int(2))); -//! let imports = m.get("imports").unwrap().as_list().unwrap(); -//! assert_eq!(imports[0], ReportValue::Str("os".into())); -//! assert_eq!(imports[1], ReportValue::Str("sys".into())); +//! let mut agg = Aggregate::new(); +//! agg.add_file("go", &imports); +//! assert_eq!(agg.total_files(), 1); +//! assert_eq!(agg.ranked(), vec![("embed", 1), ("fmt", 1), ("strings", 1)]); //! ``` -pub mod aggregator; -pub mod analyzer; +pub mod aggregate; +pub mod extract; pub mod history; +pub mod language; pub mod metrics; -pub mod node; pub mod report; -pub mod report_section; -pub mod store; +pub mod section; +pub mod statement; +pub mod stdlib; // Re-exports for the primary public surface. -pub use aggregator::Aggregator; -pub use analyzer::Analyzer; -pub use history::{CommitSummary, HistoryAnalyzer, ImportEntry, ImportsMap, TickData}; +pub use aggregate::{Aggregate, Entry}; +pub use extract::{extract_imports, Import}; +pub use history::{HistoryReport, ImportEntry, ImportsMap}; +pub use language::Language; pub use metrics::{ - compute_all_metrics, AggregateData, ComputedMetrics, ImportCategoryData, ImportData, - ImportDependencyData, ReportData, + compute_all_metrics, compute_metrics, AggregateData, ComputedMetrics, ImportCategoryData, + ImportData, ImportDependencyData, ImportRecord, ReportData, }; -pub use node::Node; pub use report::ReportValue; -pub use report_section::ReportSection; -pub use store::{ImportUsageRecord, KIND_IMPORT_USAGE}; diff --git a/crates/cf-imports/src/metrics.rs b/crates/cf-imports/src/metrics.rs index e2764e82..32dfdd24 100644 --- a/crates/cf-imports/src/metrics.rs +++ b/crates/cf-imports/src/metrics.rs @@ -1,92 +1,88 @@ //! Import metrics computation. //! -//! Given a parsed report ([`ReportData`]), computes a categorized import list, -//! a category distribution, dependency-risk findings, and aggregate statistics -//! ([`ComputedMetrics`]) — the structure the static analyzer marshals in the -//! json/yaml/bin formats. +//! Given the classified import records ([`ReportData`]), computes a +//! categorized import list, a category distribution, dependency-risk findings, +//! and aggregate statistics ([`ComputedMetrics`]) — the structure the static +//! analyzer marshals in the json/yaml/bin formats. //! -//! The classification helpers ([`categorize_import`], [`is_external_import`], -//! [`is_standard_library`]) are data-parity-critical: they decide which/what -//! values appear in machine output (pinned by the differential gate in -//! `tests/compat`), so the stdlib table and prefix rules are frozen -//! reference data. +//! Classification is per import record: `relative` for file-relative targets +//! (`./x`, `../x`, `/x`, Rust `crate::`/`super::`/`self::`, local-file +//! includes), `stdlib` when [`crate::stdlib::is_stdlib`] says so for the +//! record's language, `external` otherwise. +use crate::language::Language; use crate::report::ReportValue; +use crate::section::KEY_IMPORTS; +use crate::stdlib; /// Threshold: `..` occurrences flagged as a deeply-nested relative import. const DEEPLY_NESTED_THRESHOLD: usize = 3; /// Threshold: `/` occurrences flagged as an overly-long import path. const LONG_PATH_THRESHOLD: usize = 5; +/// Category of a file-relative import. +pub const CATEGORY_RELATIVE: &str = "relative"; +/// Category of a standard-library import. +pub const CATEGORY_STDLIB: &str = "stdlib"; +/// Category of a third-party import. +pub const CATEGORY_EXTERNAL: &str = "external"; + +/// Risk level of a deeply nested relative import. +const RISK_MEDIUM: &str = "MEDIUM"; +/// Risk level of an overly long import path. +const RISK_LOW: &str = "LOW"; +/// Reason attached to [`RISK_MEDIUM`] findings. +const REASON_DEEPLY_NESTED: &str = + "Deeply nested relative import may indicate poor module structure"; +/// Reason attached to [`RISK_LOW`] findings. +const REASON_LONG_PATH: &str = "Long import path may indicate overly complex package structure"; + +/// Rust path roots that name the importing crate itself. +const RUST_LOCAL_ROOTS: &[&str] = &["crate", "super", "self"]; +/// Rust path separator. +const RUST_SEP: &str = "::"; + +/// One import to classify. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ImportRecord { + /// The import path. + pub path: String, + /// Parser language name of the importing file (empty = unknown, which + /// classifies against the union of all stdlib tables). + pub language: String, + /// Whether the import names a file next to the importing source. + pub local_file: bool, +} + /// Parsed input data for metrics computation. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ReportData { - /// The import identifiers found in the report. - pub imports: Vec<String>, - /// The reported import count. - pub count: i64, + /// The import records found in the report. + pub imports: Vec<ImportRecord>, } impl ReportData { - /// Extracts [`ReportData`] from an analyzer report. - /// - /// The `imports` key is read as a string list when present; otherwise (the - /// binary-encode -> JSON-decode round trip renames it to `import_list` with - /// `{"path": ...}` objects) paths are pulled from the `path` field of each - /// `import_list` entry. `count` accepts an int or a float (the latter from - /// JSON decode). - /// - /// # Errors - /// Never fails; the `Result` keeps call-site parity with the analyzer - /// interface. - pub fn parse(report: &ReportValue) -> Result<ReportData, std::convert::Infallible> { - let mut data = ReportData::default(); - let Some(map) = report.as_map() else { - return Ok(data); + /// Extracts [`ReportData`] from a dynamic report: the `imports` key read + /// as a string list. Records carry no language (union classification) and + /// no local-file flag; the static path builds [`ReportData`] directly from + /// [`crate::Aggregate::report_data`] instead. + #[must_use] + pub fn parse(report: &ReportValue) -> ReportData { + let imports = match report.as_map().and_then(|m| m.get(KEY_IMPORTS)) { + Some(ReportValue::List(items)) => items + .iter() + .filter_map(|v| match v { + ReportValue::Str(s) => Some(ImportRecord { + path: s.clone(), + ..ImportRecord::default() + }), + _ => None, + }) + .collect(), + _ => Vec::new(), }; - - match map.get("imports") { - Some(ReportValue::List(items)) => { - data.imports = items - .iter() - .filter_map(|v| match v { - ReportValue::Str(s) => Some(s.clone()), - _ => None, - }) - .collect(); - } - _ => { - if let Some(items) = map.get("import_list") { - data.imports = extract_import_paths(items); - } - } - } - - match map.get("count") { - Some(ReportValue::Int(c)) => data.count = *c, - Some(ReportValue::Float(c)) => data.count = *c as i64, - _ => {} - } - - Ok(data) - } -} - -/// Extracts string paths from a JSON-decoded `import_list` (a list of -/// `{"path": ...}` objects). -fn extract_import_paths(items: &ReportValue) -> Vec<String> { - let ReportValue::List(list) = items else { - return Vec::new(); - }; - let mut paths = Vec::with_capacity(list.len()); - for item in list { - if let ReportValue::Map(m) = item { - if let Some(ReportValue::Str(p)) = m.get("path") { - paths.push(p.clone()); - } - } + ReportData { imports } } - paths } /// Information about a single import. @@ -94,7 +90,7 @@ fn extract_import_paths(items: &ReportValue) -> Vec<String> { pub struct ImportData { /// The import path. pub path: String, - /// `relative`, `stdlib`, or `external`. + /// [`CATEGORY_RELATIVE`], [`CATEGORY_STDLIB`], or [`CATEGORY_EXTERNAL`]. pub category: String, /// Whether the import is external (not stdlib, not relative). pub is_external: bool, @@ -149,103 +145,17 @@ pub struct ComputedMetrics { } impl ComputedMetrics { - /// Converts the metrics to a [`ReportValue`] tree. - /// - /// Key names match the report contract exactly. Note that the top-level - /// report object serializes with **declaration-order** keys (`import_list`, - /// `categories`, `dependencies`, `aggregate`), which a byte-sorted - /// [`ReportValue::Map`] cannot express — the authoritative encoder path is - /// [`Self::to_go_value`]; this conversion remains as the legacy local-shim - /// view. - #[must_use] - pub fn to_report_value(&self) -> ReportValue { - let mut root = ReportValue::map(); - - let import_list = self - .import_list - .iter() - .map(|d| { - let mut m = ReportValue::map(); - m.insert("path", ReportValue::Str(d.path.clone())); - m.insert("category", ReportValue::Str(d.category.clone())); - m.insert("is_external", ReportValue::Bool(d.is_external)); - m - }) - .collect(); - root.insert("import_list", ReportValue::List(import_list)); - - let categories = self - .categories - .iter() - .map(|c| { - let mut m = ReportValue::map(); - m.insert("category", ReportValue::Str(c.category.clone())); - m.insert("count", ReportValue::Int(c.count)); - m - }) - .collect(); - root.insert("categories", ReportValue::List(categories)); - - let dependencies = self - .dependencies - .iter() - .map(|d| { - let mut m = ReportValue::map(); - m.insert("path", ReportValue::Str(d.path.clone())); - m.insert("risk_level", ReportValue::Str(d.risk_level.clone())); - m.insert("reason", ReportValue::Str(d.reason.clone())); - m - }) - .collect(); - root.insert("dependencies", ReportValue::List(dependencies)); - - let mut agg = ReportValue::map(); - agg.insert( - "total_imports", - ReportValue::Int(self.aggregate.total_imports), - ); - agg.insert( - "external_imports", - ReportValue::Int(self.aggregate.external_imports), - ); - agg.insert( - "internal_imports", - ReportValue::Int(self.aggregate.internal_imports), - ); - agg.insert( - "unique_packages", - ReportValue::Int(self.aggregate.unique_packages), - ); - agg.insert( - "external_ratio", - ReportValue::Float(self.aggregate.external_ratio), - ); - root.insert("aggregate", agg); - - root - } - - /// Converts the metrics to a [`cf_gojson::GoValue`] for byte-exact machine - /// JSON output. This is the authoritative encoder path; - /// [`Self::to_report_value`] is the legacy local shim. - /// - /// Byte-parity details of the report contract (pinned by the differential - /// gate in `tests/compat`): + /// Converts the metrics to a [`cf_gojson::GoValue`] for the machine JSON + /// and bin formats. /// /// * The top-level object is **struct-origin**, so keys are emitted in /// declaration order — `import_list`, `categories`, `dependencies`, - /// `aggregate` — **not** byte-sorted. - /// * `import_list` and `categories` are always present slices, so they - /// serialize as `[]` when empty. - /// * `dependencies` is a nil-when-empty slice in the report contract: when - /// no risky imports exist it serializes as **`null`**, not `[]`. This is - /// the first-byte distinction in the `run/history_imports.json` golden - /// (`"dependencies":null`). - /// * `aggregate` is itself struct-origin: declaration order - /// `total_imports`, `external_imports`, `internal_imports`, - /// `unique_packages`, `external_ratio`; `external_ratio` is a float routed - /// through cf-gojson's shortest-round-trip formatter (`0` for the empty - /// report). + /// `aggregate`. + /// * `import_list` and `categories` serialize as `[]` when empty. + /// * `dependencies` serializes as **`null`** when no risky imports exist. + /// * `aggregate` keeps declaration order `total_imports`, + /// `external_imports`, `internal_imports`, `unique_packages`, + /// `external_ratio`. #[must_use] pub fn to_go_value(&self) -> cf_gojson::GoValue { use cf_gojson::{GoMap, GoValue}; @@ -273,9 +183,6 @@ impl ComputedMetrics { }) .collect(); - // Nil-when-empty slice -> `null` (report-format contract): the - // dependency metric only ever appends to an initially-nil slice, so an - // empty result serializes as null, not []. let dependencies = if self.dependencies.is_empty() { GoValue::Null } else { @@ -320,17 +227,9 @@ impl ComputedMetrics { GoValue::Map(root) } - /// Converts the metrics to a [`cf_gojson::GoValue`] for byte-exact machine - /// YAML output (the `--format yaml` static path). - /// - /// Identical to [`Self::to_go_value`] (struct field-declaration key order — - /// `import_list`, `categories`, `dependencies`, `aggregate`) with one - /// encoder-specific difference in the report contract: the YAML encoding of - /// a nil-when-empty slice is an empty sequence `[]`, not `null`. So when no - /// risky imports exist, `dependencies` is `[]` here (vs `null` in the json - /// encoder), matching the `static/static_imports.yaml` golden - /// (`dependencies: []`). `external_ratio` routes through cf-goyaml's float - /// formatter. + /// Converts the metrics to a [`cf_gojson::GoValue`] for the machine YAML + /// format: identical to [`Self::to_go_value`] except that an empty + /// `dependencies` is an empty sequence `[]`, not `null`. #[must_use] pub fn to_go_value_yaml(&self) -> cf_gojson::GoValue { use cf_gojson::GoValue; @@ -339,8 +238,6 @@ impl ComputedMetrics { GoValue::Map(m) => m, other => return other, }; - // YAML contract: nil slice -> `[]` (json -> `null`). Replace a `null` - // `dependencies` with an empty array. if matches!(root.get("dependencies"), Some(GoValue::Null)) { root.insert("dependencies", GoValue::Array(Vec::new())); } @@ -348,63 +245,96 @@ impl ComputedMetrics { } } -/// Runs all metrics over a report and returns the combined result. +/// Runs all metrics over a dynamic report (see [`ReportData::parse`]). /// /// # Errors /// Never fails; the `Result` keeps call-site parity with the analyzer -/// interface. +/// interface used by the history and plot paths. pub fn compute_all_metrics( report: &ReportValue, ) -> Result<ComputedMetrics, std::convert::Infallible> { - let input = ReportData::parse(report)?; - Ok(ComputedMetrics { - import_list: compute_import_list(&input), - categories: compute_categories(&input), - dependencies: compute_dependencies(&input), - aggregate: compute_aggregate(&input), - }) + Ok(compute_metrics(&ReportData::parse(report))) } -/// Computes the categorized import list, sorted by category then path. +/// Runs all metrics over classified import records. #[must_use] -pub fn compute_import_list(input: &ReportData) -> Vec<ImportData> { +pub fn compute_metrics(input: &ReportData) -> ComputedMetrics { + ComputedMetrics { + import_list: compute_import_list(input), + categories: compute_categories(input), + dependencies: compute_dependencies(input), + aggregate: compute_aggregate(input), + } +} + +/// Categorizes an import record as `relative`, `stdlib`, or `external`. +/// +/// ``` +/// use cf_imports::metrics::{categorize, ImportRecord, CATEGORY_STDLIB, CATEGORY_EXTERNAL, CATEGORY_RELATIVE}; +/// +/// let rec = |path: &str, language: &str| ImportRecord { path: path.into(), language: language.into(), local_file: false }; +/// assert_eq!(categorize(&rec("strconv", "go")), CATEGORY_STDLIB); +/// assert_eq!(categorize(&rec("github.com/x/y", "go")), CATEGORY_EXTERNAL); +/// assert_eq!(categorize(&rec("crate::foo", "rust")), CATEGORY_RELATIVE); +/// assert_eq!(categorize(&rec("../utils", "javascript")), CATEGORY_RELATIVE); +/// ``` +#[must_use] +pub fn categorize(record: &ImportRecord) -> &'static str { + if is_relative(record) { + CATEGORY_RELATIVE + } else if stdlib::is_stdlib(&record.language, &record.path) { + CATEGORY_STDLIB + } else { + CATEGORY_EXTERNAL + } +} + +/// File-relative: a local-file include, a `.`/`/`-prefixed path, or a Rust +/// path rooted in the importing crate. +fn is_relative(record: &ImportRecord) -> bool { + if record.local_file || record.path.starts_with('.') || record.path.starts_with('/') { + return true; + } + Language::from_name(&record.language) == Language::Rust + && RUST_LOCAL_ROOTS.contains(&record.path.split(RUST_SEP).next().unwrap_or("")) +} + +/// Computes the categorized import list, sorted by category then path. +fn compute_import_list(input: &ReportData) -> Vec<ImportData> { let mut result: Vec<ImportData> = input .imports .iter() - .map(|imp| ImportData { - path: imp.clone(), - category: categorize_import(imp), - is_external: is_external_import(imp), + .map(|rec| { + let category = categorize(rec); + ImportData { + path: rec.path.clone(), + category: category.to_string(), + is_external: category == CATEGORY_EXTERNAL, + } }) .collect(); - - // The (category, path) key is a total order, so stable vs unstable sorting - // cannot diverge here. result.sort_by(|a, b| { - if a.category == b.category { - a.path.cmp(&b.path) - } else { - a.category.cmp(&b.category) - } + a.category + .cmp(&b.category) + .then_with(|| a.path.cmp(&b.path)) }); result } -/// Computes the category distribution, sorted by count descending. -/// -/// The reference implementation leaves equal-count tie order unspecified -/// (unstable sort over nondeterministic map iteration); this implementation -/// breaks ties by category name for determinism. -#[must_use] -pub fn compute_categories(input: &ReportData) -> Vec<ImportCategoryData> { +/// Computes the category distribution, sorted by count descending then +/// category name. +fn compute_categories(input: &ReportData) -> Vec<ImportCategoryData> { use std::collections::BTreeMap; - let mut categories: BTreeMap<String, i64> = BTreeMap::new(); - for imp in &input.imports { - *categories.entry(categorize_import(imp)).or_insert(0) += 1; + let mut categories: BTreeMap<&str, i64> = BTreeMap::new(); + for rec in &input.imports { + *categories.entry(categorize(rec)).or_insert(0) += 1; } let mut result: Vec<ImportCategoryData> = categories .into_iter() - .map(|(category, count)| ImportCategoryData { category, count }) + .map(|(category, count)| ImportCategoryData { + category: category.to_string(), + count, + }) .collect(); result.sort_by(|a, b| { b.count @@ -414,55 +344,45 @@ pub fn compute_categories(input: &ReportData) -> Vec<ImportCategoryData> { result } -/// Identifies potential dependency issues, in input order. -/// -/// When both conditions hold, the long-path branch runs second and overwrites -/// the risk/reason — sequential `if`s, not `else if` (pinned classification -/// behavior). -#[must_use] -pub fn compute_dependencies(input: &ReportData) -> Vec<ImportDependencyData> { - let mut result = Vec::new(); - for imp in &input.imports { - let mut risk_level = String::new(); - let mut reason = String::new(); - - if count_occurrences(imp, "..") >= DEEPLY_NESTED_THRESHOLD { - risk_level = "MEDIUM".to_string(); - reason = "Deeply nested relative import may indicate poor module structure".to_string(); - } - - if count_occurrences(imp, "/") >= LONG_PATH_THRESHOLD { - risk_level = "LOW".to_string(); - reason = "Long import path may indicate overly complex package structure".to_string(); - } - - if !risk_level.is_empty() { - result.push(ImportDependencyData { - path: imp.clone(), - risk_level, - reason, - }); - } - } - result +/// Identifies potential dependency issues, in input order. When both +/// conditions hold the long-path finding wins. +fn compute_dependencies(input: &ReportData) -> Vec<ImportDependencyData> { + input + .imports + .iter() + .filter_map(|rec| { + let path = rec.path.as_str(); + let finding = if path.matches('/').count() >= LONG_PATH_THRESHOLD { + Some((RISK_LOW, REASON_LONG_PATH)) + } else if path.matches("..").count() >= DEEPLY_NESTED_THRESHOLD { + Some((RISK_MEDIUM, REASON_DEEPLY_NESTED)) + } else { + None + }; + finding.map(|(risk_level, reason)| ImportDependencyData { + path: path.to_string(), + risk_level: risk_level.to_string(), + reason: reason.to_string(), + }) + }) + .collect() } /// Computes aggregate statistics. -#[must_use] -pub fn compute_aggregate(input: &ReportData) -> AggregateData { +fn compute_aggregate(input: &ReportData) -> AggregateData { use std::collections::BTreeSet; let mut agg = AggregateData { total_imports: input.imports.len() as i64, ..Default::default() }; let mut packages: BTreeSet<&str> = BTreeSet::new(); - for imp in &input.imports { - if is_external_import(imp) { + for rec in &input.imports { + if categorize(rec) == CATEGORY_EXTERNAL { agg.external_imports += 1; } else { agg.internal_imports += 1; } - packages.insert(base_package(imp)); + packages.insert(base_package(&rec.path)); } agg.unique_packages = packages.len() as i64; if agg.total_imports > 0 { @@ -471,140 +391,63 @@ pub fn compute_aggregate(input: &ReportData) -> AggregateData { agg } -/// Counts non-overlapping occurrences of `sub` in `s`. -fn count_occurrences(s: &str, sub: &str) -> usize { - if sub.is_empty() { - // Degenerate case kept for contract completeness: rune count + 1. - return s.chars().count() + 1; - } - s.matches(sub).count() -} - /// Returns the base package: the substring before the first `/`. fn base_package(imp: &str) -> &str { imp.split_once('/').map_or(imp, |(head, _)| head) } -/// Categorizes an import as `relative`, `stdlib`, or `external`. -#[must_use] -pub fn categorize_import(imp: &str) -> String { - if imp.starts_with('.') || imp.starts_with('/') { - return "relative".to_string(); - } - if is_standard_library(imp) { - "stdlib".to_string() - } else { - "external".to_string() - } -} - -/// Reports whether an import is external (not relative, not stdlib). -#[must_use] -pub fn is_external_import(imp: &str) -> bool { - if imp.starts_with('.') || imp.starts_with('/') { - return false; - } - !is_standard_library(imp) -} - -/// Reports whether an import's base package is a known standard-library package. -/// -/// The stdlib table is frozen reference data (it decides classification in -/// machine output); `path` and `http` appear twice, which is harmless for -/// membership. -#[must_use] -pub fn is_standard_library(imp: &str) -> bool { - const STDLIBS: &[&str] = &[ - // Go. - "fmt", - "os", - "io", - "net", - "http", - "encoding", - "sync", - "context", - "time", - "strings", - "bytes", - "bufio", - "path", - "filepath", - "regexp", - "sort", - "math", - // Python. - "sys", - "typing", - "collections", - "itertools", - "functools", - "json", - "re", - // JavaScript/Node. - "fs", - "path", - "util", - "events", - "stream", - "crypto", - "http", - "https", - ]; - let base = base_package(imp); - STDLIBS.contains(&base) -} - #[cfg(test)] mod tests { use super::*; - const TEST_IMPORT_STDLIB: &str = "fmt"; - const TEST_IMPORT_EXTERNAL: &str = "github.com/user/repo"; - const TEST_IMPORT_RELATIVE: &str = "../utils"; + const GO_STDLIB: &str = "fmt"; + const GO_EXTERNAL: &str = "github.com/user/repo"; + const JS_RELATIVE: &str = "../utils"; - fn report_with_imports(imports: &[&str], count: i64) -> ReportValue { + fn rec(path: &str, language: &str) -> ImportRecord { + ImportRecord { + path: path.to_string(), + language: language.to_string(), + local_file: false, + } + } + + fn data(records: &[(&str, &str)]) -> ReportData { + ReportData { + imports: records.iter().map(|(p, l)| rec(p, l)).collect(), + } + } + + fn report_with_imports(imports: &[&str]) -> ReportValue { let mut r = ReportValue::map(); r.insert( - "imports", + KEY_IMPORTS, ReportValue::List( imports .iter() - .map(|s| ReportValue::Str(s.to_string())) + .map(|s| ReportValue::Str((*s).to_string())) .collect(), ), ); - r.insert("count", ReportValue::Int(count)); r } - fn data(imports: &[&str]) -> ReportData { - ReportData { - imports: imports.iter().map(|s| s.to_string()).collect(), - count: 0, - } - } - #[test] - fn test_parse_report_data_empty() { - let result = ReportData::parse(&ReportValue::map()).unwrap(); - assert!(result.imports.is_empty()); - assert_eq!(result.count, 0); - } - - /// Byte-for-byte parity of the empty-imports report against the binding - /// golden `tests/golden/run/history_imports.json` (history/imports - /// json, 10 commits with no extracted imports). Locks: declaration field - /// order (not byte-sorted), `import_list`/`categories` => `[]`, - /// `dependencies` => `null` (nil slice), `external_ratio` => `0`, and NO - /// trailing newline (compact encoding). + fn parse_reads_the_imports_list_without_language() { + assert!(ReportData::parse(&ReportValue::map()).imports.is_empty()); + let parsed = ReportData::parse(&report_with_imports(&[GO_STDLIB, GO_EXTERNAL])); + assert_eq!(parsed.imports.len(), 2); + assert_eq!(parsed.imports[0].path, GO_STDLIB); + assert_eq!(parsed.imports[0].language, ""); + assert!(!parsed.imports[1].local_file); + } + + /// The empty report's compact JSON: declaration field order, + /// `import_list`/`categories` => `[]`, `dependencies` => `null`, + /// `external_ratio` => `0`. #[test] - fn empty_report_matches_history_imports_golden() { - // The tick-to-report path always emits a non-empty report - // (imports/author_index/tick_size keys), so metrics computation runs - // even when no imports exist. - let report = ReportValue::map(); - let metrics = compute_all_metrics(&report).unwrap(); + fn empty_report_json_shape() { + let metrics = compute_all_metrics(&ReportValue::map()).unwrap(); let bytes = cf_gojson::marshal(&metrics.to_go_value()); assert_eq!( String::from_utf8(bytes).unwrap(), @@ -615,234 +458,163 @@ mod tests { } #[test] - fn test_parse_report_data_all_fields() { - let report = report_with_imports(&[TEST_IMPORT_STDLIB, TEST_IMPORT_EXTERNAL], 5); - let result = ReportData::parse(&report).unwrap(); - assert_eq!(result.imports.len(), 2); - assert_eq!(result.imports[0], TEST_IMPORT_STDLIB); - assert_eq!(result.imports[1], TEST_IMPORT_EXTERNAL); - assert_eq!(result.count, 5); - } - - #[test] - fn test_categorize_import() { + fn categorize_uses_the_record_language() { let cases = [ - ("./utils", "relative"), - ("../utils", "relative"), - ("/absolute/path", "relative"), - ("fmt", "stdlib"), - ("encoding/json", "stdlib"), - ("github.com/user/repo", "external"), - ("somepackage", "external"), - ]; - for (imp, expected) in cases { - assert_eq!(categorize_import(imp), expected, "imp={imp}"); - } - } - - #[test] - fn test_is_external_import() { - let cases = [ - ("./utils", false), - ("../utils", false), - ("fmt", false), - ("github.com/user/repo", true), - ("somepackage", true), - ]; - for (imp, expected) in cases { - assert_eq!(is_external_import(imp), expected, "imp={imp}"); - } - } - - #[test] - fn test_is_standard_library() { - let true_cases = [ - "fmt", - "os", - "io", - "net", - "net/http", - "encoding/json", - "sync", - "context", - "time", - "strings", - "bytes", - "path/filepath", - "regexp", - "sort", - "math/rand", - "sys", - "typing", - "collections", - "itertools", - "functools", - "json", - "re", - "fs", - "util", - "events", - "stream", - "crypto", - "https", + ("./utils", "javascript", CATEGORY_RELATIVE), + ("/absolute/path", "python", CATEGORY_RELATIVE), + ("..pkg.sub", "python", CATEGORY_RELATIVE), + ("crate::foo::bar", "rust", CATEGORY_RELATIVE), + ("super::baz", "rust", CATEGORY_RELATIVE), + ("self::x", "rust", CATEGORY_RELATIVE), + ("crate::foo", "python", CATEGORY_EXTERNAL), + ("fmt", "go", CATEGORY_STDLIB), + ("errors", "go", CATEGORY_STDLIB), + ("encoding/json", "go", CATEGORY_STDLIB), + ("github.com/user/repo", "go", CATEGORY_EXTERNAL), + ("datetime", "python", CATEGORY_STDLIB), + ("numpy", "python", CATEGORY_EXTERNAL), + ("fs", "javascript", CATEGORY_STDLIB), + ("react", "javascript", CATEGORY_EXTERNAL), + ("std::io::*", "rust", CATEGORY_STDLIB), + ("serde::Serialize", "rust", CATEGORY_EXTERNAL), + ("somepackage", "", CATEGORY_EXTERNAL), + ("fmt", "", CATEGORY_STDLIB), ]; - for imp in true_cases { - assert!(is_standard_library(imp), "expected stdlib: {imp}"); - } - for imp in ["github.com/user/repo", "somepackage"] { - assert!(!is_standard_library(imp), "expected non-stdlib: {imp}"); + for (path, language, expected) in cases { + assert_eq!( + categorize(&rec(path, language)), + expected, + "{language}: {path}" + ); } + let local = ImportRecord { + path: "local.h".into(), + language: "c".into(), + local_file: true, + }; + assert_eq!(categorize(&local), CATEGORY_RELATIVE); + assert_eq!(categorize(&rec("stdio.h", "c")), CATEGORY_STDLIB); } #[test] - fn test_import_list_metric_empty() { - assert!(compute_import_list(&ReportData::default()).is_empty()); - } - - #[test] - fn test_import_list_metric_single_import() { - let result = compute_import_list(&data(&[TEST_IMPORT_STDLIB])); - assert_eq!(result.len(), 1); - assert_eq!(result[0].path, TEST_IMPORT_STDLIB); - assert_eq!(result[0].category, "stdlib"); - assert!(!result[0].is_external); - } - - #[test] - fn test_import_list_metric_multiple_imports_sorted() { + fn import_list_is_sorted_by_category_then_path() { let result = compute_import_list(&data(&[ - TEST_IMPORT_EXTERNAL, - TEST_IMPORT_STDLIB, - TEST_IMPORT_RELATIVE, - "os", + (GO_EXTERNAL, "go"), + (GO_STDLIB, "go"), + (JS_RELATIVE, "javascript"), + ("os", "go"), ])); - assert_eq!(result.len(), 4); - // external < relative < stdlib. - assert_eq!(result[0].category, "external"); - assert_eq!(result[1].category, "relative"); - assert_eq!(result[2].category, "stdlib"); - assert_eq!(result[3].category, "stdlib"); - // Within stdlib, sorted by path: "fmt" < "os". - assert_eq!(result[2].path, TEST_IMPORT_STDLIB); - assert_eq!(result[3].path, "os"); + let got: Vec<(&str, &str, bool)> = result + .iter() + .map(|d| (d.category.as_str(), d.path.as_str(), d.is_external)) + .collect(); + assert_eq!( + got, + vec![ + (CATEGORY_EXTERNAL, GO_EXTERNAL, true), + (CATEGORY_RELATIVE, JS_RELATIVE, false), + (CATEGORY_STDLIB, GO_STDLIB, false), + (CATEGORY_STDLIB, "os", false), + ] + ); } #[test] - fn test_import_category_metric_empty() { + fn categories_are_counted_and_ranked() { assert!(compute_categories(&ReportData::default()).is_empty()); - } - - #[test] - fn test_import_category_metric_all_categories() { let result = compute_categories(&data(&[ - TEST_IMPORT_STDLIB, - "os", - "io", - TEST_IMPORT_EXTERNAL, - "github.com/other/pkg", - TEST_IMPORT_RELATIVE, + (GO_STDLIB, "go"), + ("os", "go"), + ("io", "go"), + (GO_EXTERNAL, "go"), + ("github.com/other/pkg", "go"), + (JS_RELATIVE, "javascript"), ])); - assert_eq!(result.len(), 3); - assert_eq!(result[0].category, "stdlib"); - assert_eq!(result[0].count, 3); - assert_eq!(result[1].category, "external"); - assert_eq!(result[1].count, 2); - assert_eq!(result[2].category, "relative"); - assert_eq!(result[2].count, 1); + let got: Vec<(&str, i64)> = result + .iter() + .map(|c| (c.category.as_str(), c.count)) + .collect(); + assert_eq!( + got, + vec![ + (CATEGORY_STDLIB, 3), + (CATEGORY_EXTERNAL, 2), + (CATEGORY_RELATIVE, 1) + ] + ); } #[test] - fn test_import_dependency_metric_empty() { + fn dependency_findings() { assert!(compute_dependencies(&ReportData::default()).is_empty()); - } + assert!(compute_dependencies(&data(&[(GO_STDLIB, "go"), (GO_EXTERNAL, "go")])).is_empty()); - #[test] - fn test_import_dependency_metric_no_issues() { - let result = compute_dependencies(&data(&[ - TEST_IMPORT_STDLIB, - TEST_IMPORT_EXTERNAL, - TEST_IMPORT_RELATIVE, + let nested = compute_dependencies(&data(&[ + ("../../../utils", "javascript"), + ("../../../../other/utils", "javascript"), ])); - assert!(result.is_empty()); - } + assert_eq!(nested.len(), 2); + assert_eq!(nested[0].risk_level, RISK_MEDIUM); + assert_eq!(nested[0].reason, REASON_DEEPLY_NESTED); - #[test] - fn test_import_dependency_metric_deeply_nested_relative() { - let result = compute_dependencies(&data(&["../../../utils", "../../../../other/utils"])); - assert_eq!(result.len(), 2); - assert_eq!(result[0].risk_level, "MEDIUM"); - assert!(result[0].reason.contains("Deeply nested")); - } + let long = compute_dependencies(&data(&[( + "github.com/org/repo/pkg/internal/utils/helper", + "go", + )])); + assert_eq!(long.len(), 1); + assert_eq!(long[0].risk_level, RISK_LOW); + assert_eq!(long[0].reason, REASON_LONG_PATH); - #[test] - fn test_import_dependency_metric_long_path() { - let result = - compute_dependencies(&data(&["github.com/org/repo/pkg/internal/utils/helper"])); - assert_eq!(result.len(), 1); - assert_eq!(result[0].risk_level, "LOW"); - assert!(result[0].reason.contains("Long import path")); + // Both conditions: the long-path finding wins. + let both = compute_dependencies(&data(&[("../../../a/b/c/d/e", "javascript")])); + assert_eq!(both[0].risk_level, RISK_LOW); } #[test] - fn test_aggregate_metric_empty() { - let result = compute_aggregate(&ReportData::default()); - assert_eq!(result.total_imports, 0); - assert_eq!(result.external_imports, 0); - assert_eq!(result.internal_imports, 0); - assert_eq!(result.unique_packages, 0); - assert!((result.external_ratio - 0.0).abs() < 0.01); - } - - #[test] - fn test_aggregate_metric_mixed_imports() { - let result = compute_aggregate(&data(&[ - TEST_IMPORT_STDLIB, - "os", - TEST_IMPORT_EXTERNAL, - "github.com/other", - TEST_IMPORT_RELATIVE, + fn aggregate_statistics() { + let empty = compute_aggregate(&ReportData::default()); + assert_eq!(empty.total_imports, 0); + assert!((empty.external_ratio - 0.0).abs() < f64::EPSILON); + + let mixed = compute_aggregate(&data(&[ + (GO_STDLIB, "go"), + ("os", "go"), + (GO_EXTERNAL, "go"), + ("github.com/other", "go"), + (JS_RELATIVE, "javascript"), ])); - assert_eq!(result.total_imports, 5); - assert_eq!(result.external_imports, 2); - assert_eq!(result.internal_imports, 3); - assert!(result.unique_packages >= 3); - assert!((result.external_ratio - 2.0 / 5.0).abs() < 0.01); - } - - #[test] - fn test_aggregate_metric_all_external() { - let result = compute_aggregate(&data(&[TEST_IMPORT_EXTERNAL, "github.com/other/pkg"])); - assert_eq!(result.total_imports, 2); - assert_eq!(result.external_imports, 2); - assert_eq!(result.internal_imports, 0); - assert!((result.external_ratio - 1.0).abs() < 0.01); - } - - #[test] - fn test_compute_all_metrics_empty() { - let result = compute_all_metrics(&ReportValue::map()).unwrap(); - assert!(result.import_list.is_empty()); - assert!(result.categories.is_empty()); - assert!(result.dependencies.is_empty()); - assert_eq!(result.aggregate.total_imports, 0); + assert_eq!(mixed.total_imports, 5); + assert_eq!(mixed.external_imports, 2); + assert_eq!(mixed.internal_imports, 3); + assert_eq!(mixed.unique_packages, 4); // fmt, os, github.com, .. + assert!((mixed.external_ratio - 2.0 / 5.0).abs() < 0.01); + + let all_external = compute_aggregate(&data(&[ + (GO_EXTERNAL, "go"), + ("github.com/other/pkg", "go"), + ])); + assert!((all_external.external_ratio - 1.0).abs() < f64::EPSILON); } #[test] - fn test_compute_all_metrics_full() { - let report = report_with_imports( - &[ - TEST_IMPORT_STDLIB, - TEST_IMPORT_EXTERNAL, - TEST_IMPORT_RELATIVE, - "../../../deep/nested", - ], - 4, - ); + fn compute_all_metrics_full_report() { + let report = + report_with_imports(&[GO_STDLIB, GO_EXTERNAL, JS_RELATIVE, "../../../deep/nested"]); let result = compute_all_metrics(&report).unwrap(); assert_eq!(result.import_list.len(), 4); - assert!(result.categories.len() >= 2); + assert_eq!(result.categories.len(), 3); assert_eq!(result.dependencies.len(), 1); - assert_eq!(result.dependencies[0].risk_level, "MEDIUM"); + assert_eq!(result.dependencies[0].risk_level, RISK_MEDIUM); assert_eq!(result.aggregate.total_imports, 4); } + + #[test] + fn yaml_value_renders_empty_dependencies_as_sequence() { + use cf_gojson::GoValue; + let metrics = compute_metrics(&ReportData::default()); + let GoValue::Map(root) = metrics.to_go_value_yaml() else { + panic!("map expected"); + }; + assert!(matches!(root.get("dependencies"), Some(GoValue::Array(a)) if a.is_empty())); + } } diff --git a/crates/cf-imports/src/node.rs b/crates/cf-imports/src/node.rs deleted file mode 100644 index 26f0dc70..00000000 --- a/crates/cf-imports/src/node.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Minimal UAST node model (local shim). -//! -//! The subset of the canonical UAST node that the import analyzer reads: -//! `type_`, `token`, `roles`, `children`. The full model (with map/JSON -//! conversion) lives in `cf-uast-node`; the analyzer only touches the fields -//! modelled here, so the crate stays self-contained. -//! -//! The node-type and role tags used by the analyzer are defined as constants on -//! [`uast`] and [`role`] so the traversal reads declaratively -//! (`n.type_ == uast::IMPORT`, `n.has_any_role(role::IMPORT)`). - -/// A semantic role tag attached to a node. -pub type Role = String; - -/// Canonical UAST node-type strings used by the imports analyzer. -pub mod uast { - /// An import statement node. - pub const IMPORT: &str = "Import"; - /// A literal (e.g. a quoted import path). - pub const LITERAL: &str = "Literal"; - /// An identifier (e.g. a module name). - pub const IDENTIFIER: &str = "Identifier"; -} - -/// Canonical UAST role strings used by the imports analyzer. -pub mod role { - /// Marks a node as participating in an import. - pub const IMPORT: &str = "Import"; -} - -/// A node in the Universal Abstract Syntax Tree (restricted to the fields the -/// imports analyzer uses). -/// -/// `children` holds owned child nodes; ownership is irrelevant to the -/// read-only traversal. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct Node { - /// The node type (e.g. [`uast::IMPORT`]). - pub type_: String, - /// The raw token text, if any. - pub token: String, - /// Semantic roles attached to this node. - pub roles: Vec<Role>, - /// Child nodes. - pub children: Vec<Node>, -} - -impl Node { - /// Creates a node with the given type and no other attributes. - #[must_use] - pub fn new(type_: impl Into<String>) -> Self { - Node { - type_: type_.into(), - ..Default::default() - } - } - - /// Builder: set the token text. - #[must_use] - pub fn with_token(mut self, token: impl Into<String>) -> Self { - self.token = token.into(); - self - } - - /// Builder: set the roles. - #[must_use] - pub fn with_roles<I, S>(mut self, roles: I) -> Self - where - I: IntoIterator<Item = S>, - S: Into<String>, - { - self.roles = roles.into_iter().map(Into::into).collect(); - self - } - - /// Builder: append children. - #[must_use] - pub fn with_children<I>(mut self, children: I) -> Self - where - I: IntoIterator<Item = Node>, - { - self.children.extend(children); - self - } - - /// Reports whether this node carries the given role. - /// - /// Takes a single role because the analyzer only ever queries - /// [`role::IMPORT`]. - #[must_use] - pub fn has_any_role(&self, role: &str) -> bool { - self.roles.iter().any(|r| r == role) - } - - /// Visits this node and all descendants in pre-order (node before children). - pub fn visit_pre_order<F: FnMut(&Node)>(&self, f: &mut F) { - f(self); - for child in &self.children { - child.visit_pre_order(f); - } - } -} diff --git a/crates/cf-imports/src/report.rs b/crates/cf-imports/src/report.rs index a551243b..cd4b946e 100644 --- a/crates/cf-imports/src/report.rs +++ b/crates/cf-imports/src/report.rs @@ -1,42 +1,25 @@ -//! Report value model and serialization shim. +//! Dynamic report value model. //! -//! The analyzers build reports as a dynamic string-keyed value tree and emit -//! them in MACHINE formats (json, yaml, ndjson, timeseries, compact, bin). -//! Those bytes are a frozen contract (pinned against the reference binary by -//! `tests/compat`), so the integrated pipeline routes all serialization -//! through the report-format encoders `cf-gojson` / `cf-goyaml` and the CFB1 -//! `bin` envelope from `cf-reportutil`. -//! -//! To keep `cf-imports` self-contained and verifiable, this module provides: -//! -//! * [`ReportValue`] — a dynamic value enum (the same role `cf_gojson::GoValue` -//! plays in the integrated design), with **map keys held in a [`BTreeMap`] so -//! they iterate in byte-sorted order**, matching the report contract's -//! map-key ordering; -//! * [`ReportValue::to_go_json_compact`] — a deterministic compact-JSON encoder -//! reproducing the contract's compact, HTML-escape-ON output (no space after -//! `:`); -//! * [`encode_binary_envelope`] — the CFB1 record layout (`"CFB1"` + LE u32 -//! length + compact-JSON payload). -//! -//! The integrated swap replaces [`ReportValue`] with `cf_reportutil`'s model -//! and these inline encoders with the shared ones. +//! The history and plot paths hand the analyzer a string-keyed value tree +//! (the `imports` list, or the nested per-developer map); [`ReportValue`] is +//! that tree. Machine output never serializes it directly — every machine +//! format routes through [`crate::ComputedMetrics::to_go_value`] and the +//! `cf-gojson` / `cf-goyaml` encoders. use std::collections::BTreeMap; /// A value in a report tree (dynamic, string-keyed). /// -/// Map keys use a [`BTreeMap`] so iteration order is byte-sorted, matching the -/// report contract's ordering for map-origin objects. +/// Map keys use a [`BTreeMap`] so iteration order is byte-sorted. #[derive(Debug, Clone, PartialEq)] pub enum ReportValue { /// JSON `null`. Null, /// A boolean. Bool(bool), - /// A signed integer; never routed through the float path. + /// A signed integer. Int(i64), - /// A 64-bit float, rendered with shortest-round-trip rules. + /// A 64-bit float. Float(f64), /// A UTF-8 string. Str(String), @@ -74,129 +57,6 @@ impl ReportValue { _ => None, } } - - /// Borrows the inner list, if this is a [`ReportValue::List`]. - #[must_use] - pub fn as_list(&self) -> Option<&[ReportValue]> { - match self { - ReportValue::List(v) => Some(v), - _ => None, - } - } - - /// Serializes the value as compact JSON per the report-format contract. - /// - /// Object keys are byte-sorted (guaranteed by [`BTreeMap`]); there is no - /// space after `:` or `,`; HTML escaping is ON (`<`, `>`, `&`, `U+2028`, - /// `U+2029`). This is the deterministic stand-in for `cf-gojson`'s compact - /// encoder; the integration swaps it for the shared encoder. - #[must_use] - pub fn to_go_json_compact(&self) -> String { - let mut out = String::new(); - self.write_compact(&mut out); - out - } - - fn write_compact(&self, out: &mut String) { - match self { - ReportValue::Null => out.push_str("null"), - ReportValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }), - ReportValue::Int(n) => out.push_str(&n.to_string()), - ReportValue::Float(f) => out.push_str(&go_float(*f)), - ReportValue::Str(s) => write_go_json_string(out, s), - ReportValue::List(items) => { - out.push('['); - for (i, item) in items.iter().enumerate() { - if i > 0 { - out.push(','); - } - item.write_compact(out); - } - out.push(']'); - } - ReportValue::Map(m) => { - out.push('{'); - for (i, (k, v)) in m.iter().enumerate() { - if i > 0 { - out.push(','); - } - write_go_json_string(out, k); - out.push(':'); - v.write_compact(out); - } - out.push('}'); - } - } - } -} - -/// Writes a JSON-escaped string (with surrounding quotes) into `out`, matching -/// the report-format contract's string escaping (HTML escaping ON). -/// -/// Escapes `"`, `\`, control chars (as `\u00XX`, with `\n \r \t` shortcuts), -/// and the HTML-sensitive bytes `<`, `>`, `&` plus `U+2028` / `U+2029`. -fn write_go_json_string(out: &mut String, s: &str) { - out.push('"'); - for ch in s.chars() { - match ch { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - '<' => out.push_str("\\u003c"), - '>' => out.push_str("\\u003e"), - '&' => out.push_str("\\u0026"), - '\u{2028}' => out.push_str("\\u2028"), - '\u{2029}' => out.push_str("\\u2029"), - c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)), - c => out.push(c), - } - } - out.push('"'); -} - -/// Renders an `f64` per the report-format contract (shortest round-trip, -/// `'g'`-style). -/// -/// This is a best-effort stand-in for the authoritative `cf_gojson` float -/// formatter. It reproduces the integer-valued-float case (`1.0` -> `1`) and -/// the `e±NN` exponent shape that the imports report can hit via -/// `AggregateData::external_ratio`. The full fuzz against the reference -/// formatter lives with `cf-gojson`. -fn go_float(f: f64) -> String { - if f == 0.0 { - // Preserve the sign of negative zero. - return if f.is_sign_negative() { - "-0".to_string() - } else { - "0".to_string() - }; - } - if f == f.trunc() && f.abs() < 1e21 { - // Integer-valued float with no fractional part prints "1", not "1.0". - return format!("{}", f as i64); - } - // Shortest round-trip representation; Rust's default {} for f64 already - // yields the shortest digits. Exponent-threshold parity is owned by - // cf-gojson. - format!("{f}") -} - -/// Encodes a single CFB1 `bin` record into `out`. -/// -/// Layout: `b"CFB1"` + `len(payload) as u32` little-endian + `payload`, where -/// the payload is the compact-JSON encoding of `value` (HTML-escape ON, no -/// trailing newline). Records concatenate back-to-back. -/// -/// The authoritative implementation belongs to `cf-reportutil`; this mirrors -/// the exact byte layout so the crate stands alone. -pub fn encode_binary_envelope(value: &ReportValue, out: &mut Vec<u8>) { - let payload = value.to_go_json_compact(); - let payload_bytes = payload.as_bytes(); - out.extend_from_slice(b"CFB1"); - out.extend_from_slice(&(payload_bytes.len() as u32).to_le_bytes()); - out.extend_from_slice(payload_bytes); } #[cfg(test)] @@ -204,36 +64,18 @@ mod tests { use super::*; #[test] - fn compact_sorts_keys_and_has_no_spaces() { + fn map_insert_and_lookup() { let mut m = ReportValue::map(); m.insert("b", ReportValue::Int(2)); - m.insert("a", ReportValue::Int(1)); - assert_eq!(m.to_go_json_compact(), r#"{"a":1,"b":2}"#); - } - - #[test] - fn compact_html_escapes() { - // HTML escaping is ON in the report contract: `<`, `>`, `&` are - // emitted as their \u00xx escape sequences. - let v = ReportValue::Str("a<b>&c".to_string()); - assert_eq!(v.to_go_json_compact(), "\"a\\u003cb\\u003e\\u0026c\""); - } - - #[test] - fn go_float_integer_valued() { - assert_eq!(go_float(1.0), "1"); - assert_eq!(go_float(0.0), "0"); - assert_eq!(go_float(0.4), "0.4"); + m.insert("a", ReportValue::Str("x".into())); + let keys: Vec<&String> = m.as_map().expect("map").keys().collect(); + assert_eq!(keys, ["a", "b"]); + assert!(ReportValue::Null.as_map().is_none()); } #[test] - fn binary_envelope_layout() { - let v = ReportValue::Map(BTreeMap::new()); - let mut out = Vec::new(); - encode_binary_envelope(&v, &mut out); - assert_eq!(&out[0..4], b"CFB1"); - // payload is "{}" => length 2. - assert_eq!(&out[4..8], &2u32.to_le_bytes()); - assert_eq!(&out[8..], b"{}"); + #[should_panic(expected = "non-map")] + fn insert_on_non_map_panics() { + ReportValue::Int(1).insert("k", ReportValue::Null); } } diff --git a/crates/cf-imports/src/report_section.rs b/crates/cf-imports/src/report_section.rs deleted file mode 100644 index dfb93c79..00000000 --- a/crates/cf-imports/src/report_section.rs +++ /dev/null @@ -1,388 +0,0 @@ -//! Terminal report section for static import analysis. -//! -//! The [`ReportSection`] is an info-only section (no score): it surfaces a -//! status message, two key metrics (unique imports, total files), and a list of -//! import "issues" ordered by usage count when `import_counts` is present, else -//! alphabetically. -//! -//! This module owns the data the section exposes; the actual terminal rendering -//! (non-binding, cosmetic output) belongs to the renderer layer. - -use crate::report::ReportValue; - -/// Section title. -pub const SECTION_TITLE: &str = "IMPORTS"; -/// Label for the unique-imports metric. -pub const METRIC_UNIQUE_IMPORTS: &str = "Unique Imports"; -/// Label for the total-files metric. -pub const METRIC_TOTAL_FILES: &str = "Total Files"; - -/// Report key for the imports list. -pub const KEY_IMPORTS: &str = "imports"; -/// Report key for the import count. -pub const KEY_COUNT: &str = "count"; -/// Report key for the total file count. -pub const KEY_TOTAL_FILES: &str = "total_files"; -/// Report key for the per-import counts map. -pub const KEY_IMPORT_COUNTS: &str = "import_counts"; -/// Report key for the per-file source path. -pub const SOURCE_FILE_KEY: &str = "_source_file"; - -/// Fallback message when no import data is available. -pub const DEFAULT_STATUS_MESSAGE: &str = "No import data available"; -const STATUS_MESSAGE_PREFIX: &str = "Found "; -const STATUS_MESSAGE_SUFFIX: &str = " unique imports"; - -/// Severity label for info issues. -pub const SEVERITY_INFO: &str = "info"; - -/// A key metric label/value pair. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Metric { - /// Metric label. - pub label: String, - /// Metric value (already formatted as a string). - pub value: String, -} - -/// A reported import "issue" (info item). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Issue { - /// The import name. - pub name: String, - /// The source file location (empty when unknown). - pub location: String, - /// The displayed value (usage count, or `"1"` for list fallback). - pub value: String, - /// Severity (always [`SEVERITY_INFO`] here). - pub severity: String, -} - -/// Info-only report section for import analysis. -#[derive(Debug, Clone)] -pub struct ReportSection { - report: ReportValue, - status_message: String, -} - -impl ReportSection { - /// Builds a section from a report (a `None`/empty report is treated as `{}`). - #[must_use] - pub fn new(report: Option<ReportValue>) -> Self { - let report = report.unwrap_or_else(ReportValue::map); - let count = get_int(&report, KEY_COUNT); - let status_message = build_status_message(count); - ReportSection { - report, - status_message, - } - } - - /// Returns the section title. - #[must_use] - pub fn section_title(&self) -> &'static str { - SECTION_TITLE - } - - /// Returns the status message. - #[must_use] - pub fn status_message(&self) -> &str { - &self.status_message - } - - /// Returns the key metrics (unique imports, total files). - #[must_use] - pub fn key_metrics(&self) -> Vec<Metric> { - vec![ - Metric { - label: METRIC_UNIQUE_IMPORTS.to_string(), - value: get_int(&self.report, KEY_COUNT).to_string(), - }, - Metric { - label: METRIC_TOTAL_FILES.to_string(), - value: get_int(&self.report, KEY_TOTAL_FILES).to_string(), - }, - ] - } - - /// Returns the top `n` most-used imports as info issues. - #[must_use] - pub fn top_issues(&self, n: usize) -> Vec<Issue> { - self.import_issues(n) - } - - /// Returns all imports as info issues. - #[must_use] - pub fn all_issues(&self) -> Vec<Issue> { - self.import_issues(0) - } - - /// Builds import issues sorted by frequency (or name), limited to `limit` - /// (0 = all). - fn import_issues(&self, limit: usize) -> Vec<Issue> { - let location = get_string(&self.report, SOURCE_FILE_KEY); - - let counts = get_string_int_map(&self.report, KEY_IMPORT_COUNTS); - if !counts.is_empty() { - return build_issues_from_counts(&counts, limit, &location); - } - - let imports = get_string_slice(&self.report, KEY_IMPORTS); - if imports.is_empty() { - return Vec::new(); - } - build_issues_from_list(&imports, limit, &location) - } -} - -/// Builds issues from an `import_counts` map, sorted by count descending; ties -/// are resolved by name ascending for determinism. -fn build_issues_from_counts( - counts: &std::collections::BTreeMap<String, i64>, - limit: usize, - location: &str, -) -> Vec<Issue> { - let mut entries: Vec<(&String, &i64)> = counts.iter().collect(); - entries.sort_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0))); - if limit > 0 && entries.len() > limit { - entries.truncate(limit); - } - entries - .into_iter() - .map(|(name, count)| Issue { - name: name.clone(), - location: location.to_string(), - value: count.to_string(), - severity: SEVERITY_INFO.to_string(), - }) - .collect() -} - -/// Builds issues from a simple imports list, sorted alphabetically. -fn build_issues_from_list(imports: &[String], limit: usize, location: &str) -> Vec<Issue> { - let mut sorted: Vec<String> = imports.to_vec(); - sorted.sort(); - if limit > 0 && sorted.len() > limit { - sorted.truncate(limit); - } - sorted - .into_iter() - .map(|imp| Issue { - name: imp, - location: location.to_string(), - value: "1".to_string(), - severity: SEVERITY_INFO.to_string(), - }) - .collect() -} - -/// Builds the status message from the import count. -fn build_status_message(count: i64) -> String { - if count == 0 { - return DEFAULT_STATUS_MESSAGE.to_string(); - } - format!("{STATUS_MESSAGE_PREFIX}{count}{STATUS_MESSAGE_SUFFIX}") -} - -// --- small typed accessors over the dynamic report --- - -fn get_int(report: &ReportValue, key: &str) -> i64 { - match report.as_map().and_then(|m| m.get(key)) { - Some(ReportValue::Int(n)) => *n, - Some(ReportValue::Float(f)) => *f as i64, - _ => 0, - } -} - -fn get_string(report: &ReportValue, key: &str) -> String { - match report.as_map().and_then(|m| m.get(key)) { - Some(ReportValue::Str(s)) => s.clone(), - _ => String::new(), - } -} - -fn get_string_slice(report: &ReportValue, key: &str) -> Vec<String> { - match report.as_map().and_then(|m| m.get(key)) { - Some(ReportValue::List(items)) => items - .iter() - .filter_map(|v| match v { - ReportValue::Str(s) => Some(s.clone()), - _ => None, - }) - .collect(), - _ => Vec::new(), - } -} - -fn get_string_int_map(report: &ReportValue, key: &str) -> std::collections::BTreeMap<String, i64> { - let mut out = std::collections::BTreeMap::new(); - if let Some(ReportValue::Map(m)) = report.as_map().and_then(|m| m.get(key)) { - for (k, v) in m { - match v { - ReportValue::Int(n) => { - out.insert(k.clone(), *n); - } - ReportValue::Float(f) => { - out.insert(k.clone(), *f as i64); - } - _ => {} - } - } - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn test_imports_report() -> ReportValue { - let mut r = ReportValue::map(); - r.insert( - KEY_IMPORTS, - ReportValue::List( - ["os", "fmt", "strings", "errors"] - .iter() - .map(|s| ReportValue::Str(s.to_string())) - .collect(), - ), - ); - r.insert(KEY_COUNT, ReportValue::Int(4)); - r.insert(KEY_TOTAL_FILES, ReportValue::Int(10)); - let mut counts = ReportValue::map(); - counts.insert("os", ReportValue::Int(8)); - counts.insert("fmt", ReportValue::Int(12)); - counts.insert("strings", ReportValue::Int(3)); - counts.insert("errors", ReportValue::Int(5)); - r.insert(KEY_IMPORT_COUNTS, counts); - r - } - - fn simple_imports_report() -> ReportValue { - let mut r = ReportValue::map(); - r.insert( - KEY_IMPORTS, - ReportValue::List(vec![ - ReportValue::Str("os".to_string()), - ReportValue::Str("fmt".to_string()), - ]), - ); - r.insert(KEY_COUNT, ReportValue::Int(2)); - r - } - - #[test] - fn test_imports_title() { - let s = ReportSection::new(Some(test_imports_report())); - assert_eq!(s.section_title(), SECTION_TITLE); - } - - #[test] - fn test_imports_nil_report() { - let s = ReportSection::new(None); - assert_eq!(s.section_title(), SECTION_TITLE); - } - - #[test] - fn test_imports_status_message() { - let s = ReportSection::new(Some(test_imports_report())); - assert_eq!(s.status_message(), "Found 4 unique imports"); - } - - #[test] - fn test_imports_status_message_empty() { - let s = ReportSection::new(Some(ReportValue::map())); - assert_eq!(s.status_message(), DEFAULT_STATUS_MESSAGE); - } - - #[test] - fn test_imports_key_metrics_count() { - let s = ReportSection::new(Some(test_imports_report())); - assert_eq!(s.key_metrics().len(), 2); - } - - #[test] - fn test_imports_key_metrics_labels() { - let s = ReportSection::new(Some(test_imports_report())); - let m = s.key_metrics(); - assert_eq!(m[0].label, METRIC_UNIQUE_IMPORTS); - assert_eq!(m[1].label, METRIC_TOTAL_FILES); - } - - #[test] - fn test_imports_key_metrics_values() { - let s = ReportSection::new(Some(test_imports_report())); - let m = s.key_metrics(); - assert_eq!(m[0].value, "4"); - assert_eq!(m[1].value, "10"); - } - - #[test] - fn test_imports_top_issues_from_counts() { - let s = ReportSection::new(Some(test_imports_report())); - let issues = s.top_issues(2); - assert_eq!(issues.len(), 2); - // Sorted by count desc: fmt(12) first. - assert_eq!(issues[0].name, "fmt"); - assert_eq!(issues[0].severity, SEVERITY_INFO); - } - - #[test] - fn test_imports_top_issues_from_list() { - let s = ReportSection::new(Some(simple_imports_report())); - let issues = s.top_issues(2); - assert_eq!(issues.len(), 2); - // Alphabetical: fmt, os. - assert_eq!(issues[0].name, "fmt"); - } - - #[test] - fn test_imports_all_issues() { - let s = ReportSection::new(Some(test_imports_report())); - assert_eq!(s.all_issues().len(), 4); - } - - #[test] - fn test_imports_top_issues_empty() { - let s = ReportSection::new(Some(ReportValue::map())); - assert_eq!(s.top_issues(5).len(), 0); - } - - #[test] - fn test_imports_per_file_issues_have_location() { - let mut r = ReportValue::map(); - r.insert( - KEY_IMPORTS, - ReportValue::List(vec![ - ReportValue::Str("fmt".to_string()), - ReportValue::Str("os".to_string()), - ]), - ); - r.insert(KEY_COUNT, ReportValue::Int(2)); - let mut counts = ReportValue::map(); - counts.insert("fmt", ReportValue::Int(1)); - counts.insert("os", ReportValue::Int(1)); - r.insert(KEY_IMPORT_COUNTS, counts); - r.insert( - SOURCE_FILE_KEY, - ReportValue::Str("/repo/pkg/foo.go".to_string()), - ); - - let s = ReportSection::new(Some(r)); - let issues = s.all_issues(); - assert!(!issues.is_empty()); - for issue in &issues { - assert_eq!(issue.location, "/repo/pkg/foo.go"); - } - } - - #[test] - fn test_imports_per_file_no_source_file_empty_location() { - let s = ReportSection::new(Some(test_imports_report())); - let issues = s.all_issues(); - assert!(!issues.is_empty()); - for issue in &issues { - assert_eq!(issue.location, ""); - } - } -} diff --git a/crates/cf-imports/src/section.rs b/crates/cf-imports/src/section.rs new file mode 100644 index 00000000..0a9c1ccb --- /dev/null +++ b/crates/cf-imports/src/section.rs @@ -0,0 +1,48 @@ +//! Vocabulary of the info-only IMPORTS report section. +//! +//! The section renderers (text / json) live in the command layer; this +//! module owns the labels, keys and status line they and the raw report +//! share, so the strings exist exactly once. + +/// Section title. +pub const SECTION_TITLE: &str = "IMPORTS"; +/// Score label of an info-only section. +pub const SCORE_LABEL_INFO: &str = "Info"; +/// Score of an info-only section. +pub const SCORE_INFO: f64 = -1.0; +/// Label for the unique-imports metric. +pub const METRIC_UNIQUE_IMPORTS: &str = "Unique Imports"; +/// Label for the total-files metric. +pub const METRIC_TOTAL_FILES: &str = "Total Files"; +/// Severity label of an import issue. +pub const SEVERITY_INFO: &str = "info"; + +/// Raw-report key for the imports list. +pub const KEY_IMPORTS: &str = "imports"; +/// Raw-report key for the per-import file counts map. +pub const KEY_IMPORT_COUNTS: &str = "import_counts"; +/// Raw-report key for the unique-import count. +pub const KEY_COUNT: &str = "count"; +/// Raw-report key for the analyzed-file count. +pub const KEY_TOTAL_FILES: &str = "total_files"; + +/// Status line when no imports were found. +pub const DEFAULT_STATUS_MESSAGE: &str = "No import data available"; +const STATUS_MESSAGE_PREFIX: &str = "Found "; +const STATUS_MESSAGE_SUFFIX: &str = " unique imports"; + +/// The section status line for a unique-import count. +/// +/// ``` +/// use cf_imports::section::{status_message, DEFAULT_STATUS_MESSAGE}; +/// +/// assert_eq!(status_message(0), DEFAULT_STATUS_MESSAGE); +/// assert_eq!(status_message(4), "Found 4 unique imports"); +/// ``` +#[must_use] +pub fn status_message(unique_imports: i64) -> String { + if unique_imports == 0 { + return DEFAULT_STATUS_MESSAGE.to_string(); + } + format!("{STATUS_MESSAGE_PREFIX}{unique_imports}{STATUS_MESSAGE_SUFFIX}") +} diff --git a/crates/cf-imports/src/statement.rs b/crates/cf-imports/src/statement.rs new file mode 100644 index 00000000..783d56c3 --- /dev/null +++ b/crates/cf-imports/src/statement.rs @@ -0,0 +1,379 @@ +//! Statement-text parsing shared by the per-language extraction rules. +//! +//! Several grammars lower an import to a single node whose token is the whole +//! statement (`import os, sys`, `import React from 'react';`, +//! `use std::io::{self, Read};`). The functions here turn such text into the +//! module/package paths it names; they are pure and language-tagged so each +//! extraction rule picks exactly the form its language uses. + +/// Keyword that opens a Python `from M import …` statement. +const KW_FROM: &str = "from "; +/// Keyword that opens an `import …` statement. +const KW_IMPORT: &str = "import "; +/// Keyword that opens a Rust `use …;` item. +const KW_USE: &str = "use "; +/// Rust `extern crate x;` prefix. +const KW_EXTERN_CRATE: &str = "extern crate "; +/// Rust visibility prefix (`pub`, `pub(crate)`, `pub(in path)`). +const KW_PUB: &str = "pub"; +/// Java `import static …` modifier. +const KW_STATIC: &str = "static "; +/// Alias clause separator (` as x`). +const KW_AS: &str = " as "; +/// Rust path separator. +const RUST_SEP: &str = "::"; + +/// Strips one layer of matching single or double quotes. +#[must_use] +pub fn unquote(s: &str) -> &str { + let s = s.trim(); + let stripped = s + .strip_prefix('"') + .and_then(|t| t.strip_suffix('"')) + .or_else(|| s.strip_prefix('\'').and_then(|t| t.strip_suffix('\''))); + stripped.unwrap_or(s) +} + +/// Returns the content of the last quoted (`"…"` or `'…'`) string in `text`. +/// +/// ``` +/// use cf_imports::statement::last_quoted; +/// +/// assert_eq!(last_quoted(r#"str "strings""#), Some("strings")); +/// assert_eq!(last_quoted("import React from 'react';"), Some("react")); +/// assert_eq!(last_quoted("import os"), None); +/// ``` +#[must_use] +pub fn last_quoted(text: &str) -> Option<&str> { + let close = text.rfind(['"', '\''])?; + let quote = text.as_bytes()[close] as char; + let open = text[..close].rfind(quote)?; + Some(&text[open + 1..close]) +} + +/// Strips the `<` `>` of an angle-bracket include target. +#[must_use] +pub fn strip_angle(s: &str) -> &str { + s.trim() + .strip_prefix('<') + .and_then(|t| t.strip_suffix('>')) + .unwrap_or_else(|| s.trim()) +} + +/// The target before any ` as alias` clause, trimmed. +fn strip_alias(item: &str) -> &str { + item.split(KW_AS).next().unwrap_or(item).trim() +} + +/// Normalizes a multi-line statement: parentheses, line continuations and +/// newlines become spaces so comma lists split cleanly. +fn flatten(stmt: &str) -> String { + stmt.replace(['(', ')', '\\', '\n', '\r'], " ") +} + +/// Python `import a, b as c` / `from M import x, y`: +/// +/// * `import a, b as c` -> `[a, b]`; +/// * `from M import x` -> `[M]`; +/// * `from . import x, y` (purely relative `M`) -> `[.x, .y]`, so the +/// relative target keeps its module name. +/// +/// ``` +/// use cf_imports::statement::python_targets; +/// +/// assert_eq!(python_targets("import os, sys"), vec!["os", "sys"]); +/// assert_eq!(python_targets("from typing import List, Dict"), vec!["typing"]); +/// assert_eq!(python_targets("from . import sibling"), vec![".sibling"]); +/// assert_eq!(python_targets("from ..pkg.sub import thing"), vec!["..pkg.sub"]); +/// assert_eq!(python_targets("import numpy as np"), vec!["numpy"]); +/// ``` +#[must_use] +pub fn python_targets(stmt: &str) -> Vec<String> { + let flat = flatten(stmt); + let flat = flat.trim(); + if let Some(rest) = flat.strip_prefix(KW_FROM) { + return python_from_targets(rest); + } + match flat.strip_prefix(KW_IMPORT) { + Some(list) => comma_targets(list), + None => Vec::new(), + } +} + +/// `M import x, y` (after `from `): `M` itself, or `M` joined with each name +/// when `M` is purely relative (`.`, `..`). +fn python_from_targets(rest: &str) -> Vec<String> { + let (module, names) = match rest.split_once(KW_IMPORT) { + Some((m, n)) => (m.trim(), n), + None => (rest.trim(), ""), + }; + if module.is_empty() { + return Vec::new(); + } + if !module.chars().all(|c| c == '.') { + return vec![module.to_string()]; + } + comma_targets(names) + .into_iter() + .map(|name| format!("{module}{name}")) + .collect() +} + +/// Splits a comma list of targets, dropping aliases and empties. +fn comma_targets(list: &str) -> Vec<String> { + list.split(',') + .map(strip_alias) + .filter(|t| !t.is_empty()) + .map(str::to_string) + .collect() +} + +/// Dotted-path import statement (`import a.b.c [as d]`, Kotlin / Java / +/// Swift style): the dotted path, with Java's `static` modifier and any alias +/// removed. +/// +/// ``` +/// use cf_imports::statement::dotted_target; +/// +/// assert_eq!(dotted_target("import kotlinx.coroutines.*"), Some("kotlinx.coroutines.*".into())); +/// assert_eq!(dotted_target("import com.example.Thing as T"), Some("com.example.Thing".into())); +/// assert_eq!(dotted_target("import static java.lang.Math.max;"), Some("java.lang.Math.max".into())); +/// ``` +#[must_use] +pub fn dotted_target(stmt: &str) -> Option<String> { + let rest = stmt.trim().trim_end_matches(';').trim(); + let rest = rest.strip_prefix(KW_IMPORT)?.trim(); + let rest = rest.strip_prefix(KW_STATIC).unwrap_or(rest); + let target = strip_alias(rest); + (!target.is_empty()).then(|| target.to_string()) +} + +/// Rust `use` / `extern crate` items, expanded to leaf paths: +/// +/// * visibility (`pub`, `pub(crate)`, …) and the trailing `;` are dropped; +/// * ` as alias` clauses are dropped; +/// * brace groups expand recursively, `self` names the group prefix; +/// * glob imports keep their `*`. +/// +/// ``` +/// use cf_imports::statement::rust_targets; +/// +/// assert_eq!( +/// rust_targets("use std::collections::{HashMap, HashSet};"), +/// vec!["std::collections::HashMap", "std::collections::HashSet"] +/// ); +/// assert_eq!(rust_targets("pub(crate) use std::io::{self, Read};"), vec!["std::io", "std::io::Read"]); +/// assert_eq!(rust_targets("use super::baz as qux;"), vec!["super::baz"]); +/// assert_eq!(rust_targets("use std::io::*;"), vec!["std::io::*"]); +/// assert_eq!(rust_targets("extern crate alloc;"), vec!["alloc"]); +/// ``` +#[must_use] +pub fn rust_targets(stmt: &str) -> Vec<String> { + let item = strip_visibility(stmt.trim().trim_end_matches(';').trim()); + if let Some(krate) = item.strip_prefix(KW_EXTERN_CRATE) { + return vec![strip_alias(krate).to_string()]; + } + let Some(tree) = item.strip_prefix(KW_USE) else { + return Vec::new(); + }; + let mut out = Vec::new(); + expand_use_tree("", tree.trim().trim_start_matches(RUST_SEP), &mut out); + out +} + +/// Drops a leading `pub`, `pub(crate)`, `pub(super)` or `pub(in path)`. +fn strip_visibility(item: &str) -> &str { + let Some(rest) = item.strip_prefix(KW_PUB) else { + return item; + }; + let rest = match rest.strip_prefix('(') { + Some(scoped) => scoped.split_once(')').map_or("", |(_, after)| after), + None => rest, + }; + if rest.starts_with(char::is_whitespace) { + rest.trim_start() + } else { + item + } +} + +/// Recursively expands one use-tree under `prefix` into `out`. +fn expand_use_tree(prefix: &str, tree: &str, out: &mut Vec<String>) { + let tree = tree.trim(); + if let Some(inner) = tree.strip_prefix('{').and_then(|t| t.strip_suffix('}')) { + for item in split_top_level(inner) { + expand_use_tree(prefix, item, out); + } + return; + } + if let Some(idx) = tree.find("::{") { + let head = join_rust(prefix, &tree[..idx]); + expand_use_tree(&head, &tree[idx + RUST_SEP.len()..], out); + return; + } + let leaf = strip_alias(tree); + if leaf.is_empty() { + return; + } + if leaf == "self" { + if !prefix.is_empty() { + out.push(prefix.to_string()); + } + return; + } + out.push(join_rust(prefix, leaf)); +} + +/// Joins a Rust path prefix and segment with `::`. +fn join_rust(prefix: &str, segment: &str) -> String { + let segment = segment.trim(); + if prefix.is_empty() { + segment.to_string() + } else { + format!("{prefix}{RUST_SEP}{segment}") + } +} + +/// Splits on commas that are not nested inside `{…}`. +fn split_top_level(s: &str) -> Vec<&str> { + let mut parts = Vec::new(); + let mut depth = 0usize; + let mut start = 0usize; + for (i, c) in s.char_indices() { + match c { + '{' => depth += 1, + '}' => depth = depth.saturating_sub(1), + ',' if depth == 0 => { + parts.push(&s[start..i]); + start = i + 1; + } + _ => {} + } + } + parts.push(&s[start..]); + parts +} + +/// Generic statement rule for languages without a dedicated one: tries the +/// known statement forms in order and falls back to the statement text. +/// +/// 1. `from M import …` (Python form); +/// 2. `import … from 'x'` / any `import …` carrying a quoted target (ES / +/// TypeScript / Go spec form) -> the last quoted string; +/// 3. `import a, b as c` (list form, also dotted Java/Kotlin/Swift paths); +/// 4. `use …;` / `extern crate …;` (Rust form); +/// 5. any other text carrying a quoted string -> the last quoted string; +/// 6. `<x>` -> `x`; +/// 7. otherwise the statement text without a trailing `;`. +#[must_use] +pub fn generic_targets(stmt: &str) -> Vec<String> { + let text = stmt.trim(); + if text.starts_with(KW_FROM) { + return python_targets(text); + } + if text.starts_with(KW_IMPORT) { + return match last_quoted(text) { + Some(target) => vec![target.to_string()], + None => python_targets(text.trim_end_matches(';')), + }; + } + if text.starts_with(KW_USE) || text.starts_with(KW_EXTERN_CRATE) || text.starts_with(KW_PUB) { + return rust_targets(text); + } + if let Some(target) = last_quoted(text) { + return vec![target.to_string()]; + } + let bare = strip_angle(text.trim_end_matches(';')); + if bare.is_empty() { + Vec::new() + } else { + vec![bare.to_string()] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unquote_strips_one_matching_layer() { + assert_eq!(unquote("\"fmt\""), "fmt"); + assert_eq!(unquote("'react'"), "react"); + assert_eq!(unquote("bare"), "bare"); + assert_eq!(unquote("\"mismatched'"), "\"mismatched'"); + } + + #[test] + fn last_quoted_takes_the_final_string() { + assert_eq!(last_quoted(". \"math\""), Some("math")); + assert_eq!(last_quoted("_ \"embed\""), Some("embed")); + assert_eq!(last_quoted("import fs = require(\"fs\");"), Some("fs")); + assert_eq!( + last_quoted("import def, { named } from './local/mod';"), + Some("./local/mod") + ); + assert_eq!(last_quoted("\"unterminated"), None); + } + + #[test] + fn python_multiline_parenthesised_from_import() { + assert_eq!( + python_targets("from pkg.mod import (\n a,\n b as c,\n)"), + vec!["pkg.mod"] + ); + assert_eq!(python_targets("import a, \\\n b"), vec!["a", "b"]); + assert_eq!(python_targets("from . import a, b"), vec![".a", ".b"]); + assert_eq!(python_targets("from .. import x"), vec!["..x"]); + assert!(python_targets("not an import").is_empty()); + } + + #[test] + fn rust_nested_groups_and_visibility() { + assert_eq!( + rust_targets("pub use a::{b::{c, d as e}, f};"), + vec!["a::b::c", "a::b::d", "a::f"] + ); + assert_eq!(rust_targets("pub(in crate::x) use y::z;"), vec!["y::z"]); + assert_eq!(rust_targets("use ::std::env;"), vec!["std::env"]); + assert_eq!(rust_targets("use {a, b};"), vec!["a", "b"]); + assert_eq!(rust_targets("extern crate serde as s;"), vec!["serde"]); + assert!(rust_targets("fn main() {}").is_empty()); + // `pub` that is not a visibility prefix of a use item stays untouched. + assert!(rust_targets("public_fn();").is_empty()); + } + + #[test] + fn dotted_target_forms() { + assert_eq!( + dotted_target("import java.util.List;"), + Some("java.util.List".into()) + ); + assert_eq!(dotted_target("package a"), None); + assert_eq!(dotted_target("import "), None); + } + + #[test] + fn generic_rule_covers_every_statement_form() { + assert_eq!(generic_targets("from x import y"), vec!["x"]); + assert_eq!(generic_targets("import React from 'react';"), vec!["react"]); + assert_eq!( + generic_targets("import './styles.css'"), + vec!["./styles.css"] + ); + assert_eq!(generic_targets("import os, sys"), vec!["os", "sys"]); + assert_eq!( + generic_targets("import java.util.List;"), + vec!["java.util.List"] + ); + assert_eq!(generic_targets("use std::fmt;"), vec!["std::fmt"]); + assert_eq!(generic_targets("pub use a::b;"), vec!["a::b"]); + assert_eq!(generic_targets("str \"strings\""), vec!["strings"]); + assert_eq!(generic_targets("require_once \"x.php\""), vec!["x.php"]); + assert_eq!(generic_targets("<stdio.h>"), vec!["stdio.h"]); + assert_eq!( + generic_targets("App\\Models\\User;"), + vec!["App\\Models\\User"] + ); + assert!(generic_targets(" ").is_empty()); + } +} diff --git a/crates/cf-imports/src/stdlib.rs b/crates/cf-imports/src/stdlib.rs new file mode 100644 index 00000000..35e266f1 --- /dev/null +++ b/crates/cf-imports/src/stdlib.rs @@ -0,0 +1,1171 @@ +//! Per-language standard-library classification. +//! +//! [`is_stdlib`] answers "does this import path name the language's own +//! standard library?" using the importing file's language, so the +//! `stdlib` / `external` split in the metrics is meaningful per ecosystem: +//! +//! | Language | Rule | +//! |---|---| +//! | Go | the Go toolchain's `IsStandardImportPath`: the first path element has no `.` (`errors`, `net/http`; not `github.com/x/y`) | +//! | Python | top-level module in CPython's `sys.stdlib_module_names` (`os.path` -> `os`) | +//! | JavaScript / TypeScript | a `node:`-prefixed specifier, or a Node builtin (`fs`, `fs/promises`, `node:test`) | +//! | Rust | first `::` segment is `std`, `core`, `alloc`, `proc_macro` or `test` | +//! | Java | `java.`, `javax.`, `jdk.` prefixes | +//! | Kotlin | `kotlin.` prefix, or the Java rule | +//! | C# | `System` or `System.…` | +//! | C | a C standard / POSIX header name (`stdio.h`, `sys/types.h`) | +//! | C++ | the C rule, or a C++ standard header (`vector`, `cstdio`) | +//! | Ruby | a default or bundled library (`json`, `net/http`, `fileutils`) | +//! | PHP | none — PHP has no importable standard namespace | +//! | other / unknown | any of the tables above (the union) | + +use crate::language::Language; + +/// Node.js builtin-module specifier prefix (`node:fs`). +const NODE_PREFIX: &str = "node:"; +/// Rust path separator. +const RUST_SEP: &str = "::"; +/// The C# base-class-library root namespace. +const CSHARP_SYSTEM: &str = "System"; + +/// Go standard-library packages (`go list std`, Go 1.26, without +/// `internal`/`vendor` paths). Go files use the toolchain's first-element +/// rule ([`go_is_std`]); this table serves the language-less union, where +/// the rule would claim every bare name. +const GO_STDLIB: &[&str] = &[ + "archive/tar", + "archive/zip", + "bufio", + "bytes", + "cmp", + "compress/bzip2", + "compress/flate", + "compress/gzip", + "compress/lzw", + "compress/zlib", + "container/heap", + "container/list", + "container/ring", + "context", + "crypto", + "crypto/aes", + "crypto/cipher", + "crypto/des", + "crypto/dsa", + "crypto/ecdh", + "crypto/ecdsa", + "crypto/ed25519", + "crypto/elliptic", + "crypto/fips140", + "crypto/hkdf", + "crypto/hmac", + "crypto/hpke", + "crypto/md5", + "crypto/mlkem", + "crypto/mlkem/mlkemtest", + "crypto/pbkdf2", + "crypto/rand", + "crypto/rc4", + "crypto/rsa", + "crypto/sha1", + "crypto/sha256", + "crypto/sha3", + "crypto/sha512", + "crypto/subtle", + "crypto/tls", + "crypto/x509", + "crypto/x509/pkix", + "database/sql", + "database/sql/driver", + "debug/buildinfo", + "debug/dwarf", + "debug/elf", + "debug/gosym", + "debug/macho", + "debug/pe", + "debug/plan9obj", + "embed", + "encoding", + "encoding/ascii85", + "encoding/asn1", + "encoding/base32", + "encoding/base64", + "encoding/binary", + "encoding/csv", + "encoding/gob", + "encoding/hex", + "encoding/json", + "encoding/pem", + "encoding/xml", + "errors", + "expvar", + "flag", + "fmt", + "go/ast", + "go/build", + "go/build/constraint", + "go/constant", + "go/doc", + "go/doc/comment", + "go/format", + "go/importer", + "go/parser", + "go/printer", + "go/scanner", + "go/token", + "go/types", + "go/version", + "hash", + "hash/adler32", + "hash/crc32", + "hash/crc64", + "hash/fnv", + "hash/maphash", + "html", + "html/template", + "image", + "image/color", + "image/color/palette", + "image/draw", + "image/gif", + "image/jpeg", + "image/png", + "index/suffixarray", + "io", + "io/fs", + "io/ioutil", + "iter", + "log", + "log/slog", + "log/syslog", + "maps", + "math", + "math/big", + "math/bits", + "math/cmplx", + "math/rand", + "math/rand/v2", + "mime", + "mime/multipart", + "mime/quotedprintable", + "net", + "net/http", + "net/http/cgi", + "net/http/cookiejar", + "net/http/fcgi", + "net/http/httptest", + "net/http/httptrace", + "net/http/httputil", + "net/http/pprof", + "net/mail", + "net/netip", + "net/rpc", + "net/rpc/jsonrpc", + "net/smtp", + "net/textproto", + "net/url", + "os", + "os/exec", + "os/signal", + "os/user", + "path", + "path/filepath", + "plugin", + "reflect", + "regexp", + "regexp/syntax", + "runtime", + "runtime/cgo", + "runtime/coverage", + "runtime/debug", + "runtime/metrics", + "runtime/pprof", + "runtime/race", + "runtime/trace", + "slices", + "sort", + "strconv", + "strings", + "structs", + "sync", + "sync/atomic", + "syscall", + "testing", + "testing/cryptotest", + "testing/fstest", + "testing/iotest", + "testing/quick", + "testing/slogtest", + "testing/synctest", + "text/scanner", + "text/tabwriter", + "text/template", + "text/template/parse", + "time", + "time/tzdata", + "unicode", + "unicode/utf16", + "unicode/utf8", + "unique", + "unsafe", + "weak", +]; + +/// Rust crates that ship with the toolchain. +const RUST_STD_CRATES: &[&str] = &["std", "core", "alloc", "proc_macro", "test"]; + +/// Java platform package prefixes. +const JAVA_STD_PREFIXES: &[&str] = &["java.", "javax.", "jdk."]; + +/// Kotlin standard-library package prefix. +const KOTLIN_STD_PREFIXES: &[&str] = &["kotlin."]; + +/// Python standard-library top-level module names +/// (`sys.stdlib_module_names`, CPython 3.12). +const PYTHON_STDLIB: &[&str] = &[ + "__future__", + "_abc", + "_aix_support", + "_ast", + "_asyncio", + "_bisect", + "_blake2", + "_bz2", + "_codecs", + "_codecs_cn", + "_codecs_hk", + "_codecs_iso2022", + "_codecs_jp", + "_codecs_kr", + "_codecs_tw", + "_collections", + "_collections_abc", + "_compat_pickle", + "_compression", + "_contextvars", + "_crypt", + "_csv", + "_ctypes", + "_curses", + "_curses_panel", + "_datetime", + "_dbm", + "_decimal", + "_elementtree", + "_frozen_importlib", + "_frozen_importlib_external", + "_functools", + "_gdbm", + "_hashlib", + "_heapq", + "_imp", + "_io", + "_json", + "_locale", + "_lsprof", + "_lzma", + "_markupbase", + "_md5", + "_msi", + "_multibytecodec", + "_multiprocessing", + "_opcode", + "_operator", + "_osx_support", + "_overlapped", + "_pickle", + "_posixshmem", + "_posixsubprocess", + "_py_abc", + "_pydatetime", + "_pydecimal", + "_pyio", + "_pylong", + "_queue", + "_random", + "_scproxy", + "_sha1", + "_sha2", + "_sha3", + "_signal", + "_sitebuiltins", + "_socket", + "_sqlite3", + "_sre", + "_ssl", + "_stat", + "_statistics", + "_string", + "_strptime", + "_struct", + "_symtable", + "_thread", + "_threading_local", + "_tkinter", + "_tokenize", + "_tracemalloc", + "_typing", + "_uuid", + "_warnings", + "_weakref", + "_weakrefset", + "_winapi", + "_wmi", + "_zoneinfo", + "abc", + "aifc", + "antigravity", + "argparse", + "array", + "ast", + "asyncio", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "doctest", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "fractions", + "ftplib", + "functools", + "gc", + "genericpath", + "getopt", + "getpass", + "gettext", + "glob", + "graphlib", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "idlelib", + "imaplib", + "imghdr", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "nt", + "ntpath", + "nturl2path", + "numbers", + "opcode", + "operator", + "optparse", + "os", + "ossaudiodev", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "posixpath", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "pydoc_data", + "pyexpat", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "sre_compile", + "sre_constants", + "sre_parse", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "textwrap", + "this", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "tomllib", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", + "zoneinfo", +]; + +/// Node.js builtin modules (`require('module').builtinModules`, Node 22). +/// `node:`-only modules (`node:test`, `node:sqlite`, `node:sea`) are covered +/// by the prefix rule. +const NODE_BUILTINS: &[&str] = &[ + "_http_agent", + "_http_client", + "_http_common", + "_http_incoming", + "_http_outgoing", + "_http_server", + "_stream_duplex", + "_stream_passthrough", + "_stream_readable", + "_stream_transform", + "_stream_wrap", + "_stream_writable", + "_tls_common", + "_tls_wrap", + "assert", + "assert/strict", + "async_hooks", + "buffer", + "child_process", + "cluster", + "console", + "constants", + "crypto", + "dgram", + "diagnostics_channel", + "dns", + "dns/promises", + "domain", + "events", + "fs", + "fs/promises", + "http", + "http2", + "https", + "inspector", + "inspector/promises", + "module", + "net", + "os", + "path", + "path/posix", + "path/win32", + "perf_hooks", + "process", + "punycode", + "querystring", + "readline", + "readline/promises", + "repl", + "stream", + "stream/consumers", + "stream/promises", + "stream/web", + "string_decoder", + "sys", + "timers", + "timers/promises", + "tls", + "trace_events", + "tty", + "url", + "util", + "util/types", + "v8", + "vm", + "wasi", + "worker_threads", + "zlib", +]; + +/// C standard (C11) and common POSIX headers. +const C_STD_HEADERS: &[&str] = &[ + "assert.h", + "complex.h", + "ctype.h", + "errno.h", + "fenv.h", + "float.h", + "inttypes.h", + "iso646.h", + "limits.h", + "locale.h", + "math.h", + "setjmp.h", + "signal.h", + "stdalign.h", + "stdarg.h", + "stdatomic.h", + "stdbool.h", + "stddef.h", + "stdint.h", + "stdio.h", + "stdlib.h", + "stdnoreturn.h", + "string.h", + "tgmath.h", + "threads.h", + "time.h", + "uchar.h", + "wchar.h", + "wctype.h", + // POSIX. + "aio.h", + "arpa/inet.h", + "cpio.h", + "dirent.h", + "dlfcn.h", + "fcntl.h", + "fmtmsg.h", + "fnmatch.h", + "ftw.h", + "getopt.h", + "glob.h", + "grp.h", + "iconv.h", + "langinfo.h", + "libgen.h", + "monetary.h", + "mqueue.h", + "ndbm.h", + "net/if.h", + "netdb.h", + "netinet/in.h", + "netinet/tcp.h", + "nl_types.h", + "poll.h", + "pthread.h", + "pwd.h", + "regex.h", + "sched.h", + "search.h", + "semaphore.h", + "spawn.h", + "strings.h", + "stropts.h", + "sys/ipc.h", + "sys/mman.h", + "sys/msg.h", + "sys/resource.h", + "sys/select.h", + "sys/sem.h", + "sys/shm.h", + "sys/socket.h", + "sys/stat.h", + "sys/statvfs.h", + "sys/time.h", + "sys/times.h", + "sys/types.h", + "sys/uio.h", + "sys/un.h", + "sys/utsname.h", + "sys/wait.h", + "syslog.h", + "tar.h", + "termios.h", + "trace.h", + "ulimit.h", + "unistd.h", + "utime.h", + "utmpx.h", + "wordexp.h", +]; + +/// C++ standard headers (C++20, header names without `.h`). +const CPP_STD_HEADERS: &[&str] = &[ + "algorithm", + "any", + "array", + "atomic", + "barrier", + "bit", + "bitset", + "cassert", + "ccomplex", + "cctype", + "cerrno", + "cfenv", + "cfloat", + "charconv", + "chrono", + "cinttypes", + "ciso646", + "climits", + "clocale", + "cmath", + "codecvt", + "compare", + "complex", + "concepts", + "condition_variable", + "coroutine", + "csetjmp", + "csignal", + "cstdalign", + "cstdarg", + "cstdbool", + "cstddef", + "cstdint", + "cstdio", + "cstdlib", + "cstring", + "ctgmath", + "ctime", + "cuchar", + "cwchar", + "cwctype", + "deque", + "exception", + "execution", + "filesystem", + "format", + "forward_list", + "fstream", + "functional", + "future", + "initializer_list", + "iomanip", + "ios", + "iosfwd", + "iostream", + "istream", + "iterator", + "latch", + "limits", + "list", + "locale", + "map", + "memory", + "memory_resource", + "mutex", + "new", + "numbers", + "numeric", + "optional", + "ostream", + "queue", + "random", + "ranges", + "ratio", + "regex", + "scoped_allocator", + "semaphore", + "set", + "shared_mutex", + "source_location", + "span", + "sstream", + "stack", + "stdexcept", + "stop_token", + "streambuf", + "string", + "string_view", + "strstream", + "syncstream", + "system_error", + "thread", + "tuple", + "type_traits", + "typeindex", + "typeinfo", + "unordered_map", + "unordered_set", + "utility", + "valarray", + "variant", + "vector", + "version", +]; + +/// Ruby default and bundled libraries (`require` targets shipped with the +/// interpreter). +const RUBY_STDLIB: &[&str] = &[ + "English", + "abbrev", + "base64", + "benchmark", + "bigdecimal", + "bundler", + "cgi", + "coverage", + "csv", + "date", + "delegate", + "did_you_mean", + "digest", + "drb", + "erb", + "error_highlight", + "etc", + "expect", + "fcntl", + "fiddle", + "fileutils", + "find", + "forwardable", + "getoptlong", + "io/console", + "io/nonblock", + "io/wait", + "ipaddr", + "irb", + "json", + "logger", + "matrix", + "minitest", + "monitor", + "mutex_m", + "net/ftp", + "net/http", + "net/imap", + "net/pop", + "net/protocol", + "net/smtp", + "nkf", + "objspace", + "observer", + "open-uri", + "open3", + "openssl", + "optparse", + "ostruct", + "pathname", + "pp", + "prettyprint", + "prime", + "pstore", + "psych", + "pty", + "racc", + "rake", + "rbconfig", + "rdoc", + "readline", + "reline", + "resolv", + "resolv-replace", + "rexml", + "rinda", + "ripper", + "rss", + "rubygems", + "securerandom", + "set", + "shellwords", + "singleton", + "socket", + "stringio", + "strscan", + "syntax_suggest", + "syslog", + "tempfile", + "test/unit", + "thread", + "time", + "timeout", + "tmpdir", + "tsort", + "typeprof", + "un", + "unicode_normalize", + "uri", + "weakref", + "win32ole", + "yaml", + "zlib", +]; + +/// Every language with a dedicated table, used for the union rule (Go joins +/// through [`GO_STDLIB`], not its first-element rule). +const TABLED_LANGUAGES: &[Language] = &[ + Language::Python, + Language::JavaScript, + Language::Rust, + Language::Java, + Language::Kotlin, + Language::CSharp, + Language::C, + Language::Cpp, + Language::Ruby, +]; + +/// Reports whether `path` names the standard library of `language`. +/// +/// `language` is a parser language name (`go`, `python`, …); an unknown or +/// empty name applies every table (the union), which is what the plot path +/// and the language-less history path rely on. +/// +/// ``` +/// use cf_imports::stdlib::is_stdlib; +/// +/// assert!(is_stdlib("go", "strconv")); +/// assert!(is_stdlib("go", "net/http")); +/// assert!(!is_stdlib("go", "github.com/example/lib")); +/// assert!(is_stdlib("python", "os.path")); +/// assert!(is_stdlib("javascript", "node:fs/promises")); +/// assert!(is_stdlib("rust", "std::collections::HashMap")); +/// assert!(is_stdlib("", "subprocess")); +/// ``` +#[must_use] +pub fn is_stdlib(language: &str, path: &str) -> bool { + is_stdlib_in(Language::from_name(language), path) +} + +/// [`is_stdlib`] over a resolved [`Language`]. +fn is_stdlib_in(language: Language, path: &str) -> bool { + match language { + Language::Go => go_is_std(path), + Language::Python => PYTHON_STDLIB.contains(&first_segment(path, ".")), + Language::JavaScript => node_is_builtin(path), + Language::Rust => RUST_STD_CRATES.contains(&first_segment(path, RUST_SEP)), + Language::Java => has_any_prefix(path, JAVA_STD_PREFIXES), + Language::Kotlin => { + has_any_prefix(path, KOTLIN_STD_PREFIXES) || has_any_prefix(path, JAVA_STD_PREFIXES) + } + Language::CSharp => path == CSHARP_SYSTEM || path.starts_with("System."), + Language::C => C_STD_HEADERS.contains(&path), + Language::Cpp => C_STD_HEADERS.contains(&path) || CPP_STD_HEADERS.contains(&path), + Language::Ruby => { + RUBY_STDLIB.contains(&path) || RUBY_STDLIB.contains(&first_segment(path, "/")) + } + Language::Php => false, + Language::Other => { + GO_STDLIB.contains(&path) || TABLED_LANGUAGES.iter().any(|l| is_stdlib_in(*l, path)) + } + } +} + +/// The Go toolchain's `IsStandardImportPath`: a path whose first element +/// carries no dot is a standard-library (or toolchain-internal) package. +fn go_is_std(path: &str) -> bool { + let head = first_segment(path, "/"); + !head.is_empty() && !head.contains('.') +} + +/// Node builtin: `node:`-prefixed, or an exact / first-segment table match. +fn node_is_builtin(path: &str) -> bool { + if path.starts_with(NODE_PREFIX) { + return true; + } + NODE_BUILTINS.contains(&path) || NODE_BUILTINS.contains(&first_segment(path, "/")) +} + +/// The substring before the first `sep` (or the whole path). +fn first_segment<'a>(path: &'a str, sep: &str) -> &'a str { + path.split(sep).next().unwrap_or(path) +} + +/// True when `path` starts with any of `prefixes`. +fn has_any_prefix(path: &str, prefixes: &[&str]) -> bool { + prefixes.iter().any(|p| path.starts_with(p)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A sample of the `go list std` output, one per top-level family. + const GO_STD_SAMPLE: &[&str] = &[ + "archive/tar", + "bufio", + "bytes", + "compress/gzip", + "container/heap", + "context", + "crypto/sha256", + "database/sql", + "debug/elf", + "embed", + "encoding/json", + "errors", + "expvar", + "flag", + "fmt", + "go/ast", + "hash/fnv", + "html/template", + "image/png", + "io/fs", + "iter", + "log/slog", + "maps", + "math/rand/v2", + "mime/multipart", + "net/http/httptest", + "os/exec", + "path/filepath", + "plugin", + "reflect", + "regexp/syntax", + "runtime/debug", + "slices", + "sort", + "strconv", + "strings", + "sync/atomic", + "syscall", + "testing/fstest", + "text/template", + "time", + "unicode/utf8", + "unique", + "unsafe", + "C", + ]; + + #[test] + fn go_std_is_the_no_dot_first_element_rule() { + for p in GO_STD_SAMPLE { + assert!(is_stdlib("go", p), "expected Go std: {p}"); + } + // The rule covers the whole toolchain list. + for p in GO_STDLIB { + assert!(is_stdlib("go", p), "expected Go std: {p}"); + } + for p in [ + "github.com/example/lib/v2", + "golang.org/x/net/context", + "k8s.io/client-go/kubernetes", + "example.com", + "", + ] { + assert!(!is_stdlib("go", p), "expected external: {p}"); + } + } + + #[test] + fn python_std_by_top_level_module() { + for p in [ + "os", + "os.path", + "sys", + "datetime", + "subprocess", + "typing", + "json", + "re", + "__future__", + ] { + assert!(is_stdlib("python", p), "expected Python std: {p}"); + } + for p in ["numpy", "requests", "django.db", "x"] { + assert!(!is_stdlib("python", p), "expected external: {p}"); + } + } + + #[test] + fn node_builtins_including_prefix_and_subpaths() { + for lang in ["javascript", "typescript", "tsx"] { + for p in [ + "fs", + "fs/promises", + "node:fs", + "node:test", + "path", + "stream/web", + "events", + ] { + assert!(is_stdlib(lang, p), "expected Node builtin ({lang}): {p}"); + } + for p in ["react", "ns-lib", "@scope/pkg", "lodash/fp", "test"] { + assert!(!is_stdlib(lang, p), "expected external ({lang}): {p}"); + } + } + } + + #[test] + fn rust_toolchain_crates() { + for p in [ + "std", + "std::collections::HashMap", + "core::fmt", + "alloc", + "std::io::*", + "proc_macro::TokenStream", + ] { + assert!(is_stdlib("rust", p), "expected Rust std: {p}"); + } + for p in ["serde::Serialize", "crate::foo::bar", "super::baz", "tokio"] { + assert!(!is_stdlib("rust", p), "expected non-std: {p}"); + } + } + + #[test] + fn jvm_languages_and_csharp() { + assert!(is_stdlib("java", "java.util.List")); + assert!(is_stdlib("java", "javax.swing.JFrame")); + assert!(is_stdlib("java", "jdk.internal.misc.Unsafe")); + assert!(!is_stdlib("java", "org.junit.Test")); + assert!(is_stdlib("kotlin", "kotlin.collections.Map")); + assert!(is_stdlib("kotlin", "java.util.List")); + assert!(!is_stdlib("kotlin", "kotlinx.coroutines.*")); + assert!(is_stdlib("csharp", "System")); + assert!(is_stdlib("csharp", "System.Collections.Generic")); + assert!(!is_stdlib("csharp", "SystemX.Y")); + assert!(!is_stdlib("csharp", "MyLib.Utils")); + } + + #[test] + fn c_family_headers_and_ruby_libraries() { + assert!(is_stdlib("c", "stdio.h")); + assert!(is_stdlib("c", "sys/types.h")); + assert!(!is_stdlib("c", "local.h")); + assert!(!is_stdlib("c", "curl/curl.h")); + assert!(is_stdlib("cpp", "vector")); + assert!(is_stdlib("cpp", "cstdio")); + assert!(is_stdlib("cpp", "stdio.h")); + assert!(!is_stdlib("cpp", "boost/asio.hpp")); + assert!(is_stdlib("ruby", "json")); + assert!(is_stdlib("ruby", "net/http")); + assert!(is_stdlib("ruby", "json/ext")); + assert!(!is_stdlib("ruby", "nokogiri")); + assert!(!is_stdlib("ruby", "lib/x")); + assert!(!is_stdlib("php", "App\\Models\\User")); + } + + #[test] + fn unknown_language_applies_the_union() { + for p in [ + "fmt", + "strconv", + "net/http", + "subprocess", + "fs", + "std::io", + "java.util.List", + "stdio.h", + "json", + ] { + assert!(is_stdlib("", p), "expected union hit: {p}"); + assert!(is_stdlib("swift", p), "expected union hit: {p}"); + } + // The union goes by Go's package LIST, so bare third-party names + // (which Go's first-element rule would claim) stay external. + for p in ["github.com/example/lib", "react", "numpy", "nokogiri", "C"] { + assert!(!is_stdlib("", p), "expected external: {p}"); + } + } +} diff --git a/crates/cf-imports/src/store.rs b/crates/cf-imports/src/store.rs deleted file mode 100644 index fdefd52e..00000000 --- a/crates/cf-imports/src/store.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Store records for the history analyzer. -//! -//! The history analyzer persists pre-computed per-import usage as a stream of -//! [`ImportUsageRecord`]s (kind [`KIND_IMPORT_USAGE`]), aggregated across all -//! authors/languages/ticks and ordered by count descending, so plots can be -//! rebuilt without recomputing metrics. -//! -//! This module owns the record type and the pure transform -//! ([`compute_usage_records`]) that the store writer applies; the on-disk store -//! I/O belongs to the persistence layer. - -use crate::history::{aggregate_import_counts, top_imports, ImportsMap}; - -/// Store record kind for per-import usage. -pub const KIND_IMPORT_USAGE: &str = "import_usage"; - -/// Pre-computed import usage count for a single import path. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ImportUsageRecord { - /// The import path. - pub import: String, - /// Total usage count. - pub count: i64, -} - -/// Computes the ordered import-usage records the writer persists: merge all -/// ticks' maps, aggregate per-import counts, take the top imports (count -/// descending), and emit one record per import in that order. -#[must_use] -pub fn compute_usage_records(tick_maps: &[ImportsMap]) -> Vec<ImportUsageRecord> { - let mut merged: ImportsMap = ImportsMap::new(); - for m in tick_maps { - crate::history::merge_import_maps(&mut merged, m); - } - let counts = aggregate_import_counts(&merged); - let (labels, data) = top_imports(&counts); - labels - .into_iter() - .zip(data) - .map(|(import, count)| ImportUsageRecord { import, count }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - - fn mk(pairs: &[(i64, &str, &str, i64, i64)]) -> ImportsMap { - let mut m: ImportsMap = BTreeMap::new(); - for (author, lang, imp, tick, count) in pairs { - m.entry(*author) - .or_default() - .entry(lang.to_string()) - .or_default() - .entry(imp.to_string()) - .or_default() - .insert(*tick, *count); - } - m - } - - // Round-trip of the pure transform: fmt has the highest count (3+5+2=10) - // and must be first; os totals 5. - #[test] - fn test_compute_usage_records_round_trip() { - let tick0 = mk(&[ - (0, "go", "fmt", 0, 3), - (0, "go", "os", 0, 2), - (0, "go", "strings", 0, 1), - ]); - let tick1 = mk(&[ - (0, "go", "fmt", 1, 5), - (0, "go", "io", 1, 1), - (1, "go", "fmt", 1, 2), - (1, "go", "os", 1, 3), - ]); - let records = compute_usage_records(&[tick0, tick1]); - assert!(!records.is_empty()); - assert_eq!(records[0].import, "fmt"); - assert_eq!(records[0].count, 10); - - let os = records - .iter() - .find(|r| r.import == "os") - .expect("os record"); - assert_eq!(os.count, 5); - } - - #[test] - fn test_compute_usage_records_empty() { - assert!(compute_usage_records(&[]).is_empty()); - } -} diff --git a/crates/cf-imports/tests/extract_languages.rs b/crates/cf-imports/tests/extract_languages.rs new file mode 100644 index 00000000..52da7207 --- /dev/null +++ b/crates/cf-imports/tests/extract_languages.rs @@ -0,0 +1,271 @@ +//! Per-language extraction over the trees the production parser produces. +//! +//! Each test parses a small source with `cf_uast::Parser` (the same facade +//! the CLI uses) and asserts the exact import list, so every rule in +//! `cf_imports::extract` is pinned to a real tree shape rather than a +//! hand-built one. + +use cf_imports::{extract_imports, Import}; +use cf_uast::Parser; + +/// Parses `src` as `file` and extracts its imports with the parser's own +/// language name and the source bytes. +fn imports(file: &str, src: &str) -> Vec<Import> { + let parser = Parser::new(); + let root = parser + .parse(file, src.as_bytes()) + .unwrap_or_else(|e| panic!("parse {file}: {e}")); + extract_imports(&root, &parser.get_language(file), src.as_bytes()) +} + +/// The import paths of `src`, in extraction order. +fn paths(file: &str, src: &str) -> Vec<String> { + imports(file, src).into_iter().map(|i| i.path).collect() +} + +/// The `(path, local_file)` pairs of `src`. +fn flagged(file: &str, src: &str) -> Vec<(String, bool)> { + imports(file, src) + .into_iter() + .map(|i| (i.path, i.local_file)) + .collect() +} + +#[test] +fn go_specs_yield_the_quoted_path_without_alias_dot_or_blank() { + let src = "package main\n\nimport \"fmt\"\n\nimport (\n\t\"os\"\n\tstr \"strings\"\n\t. \"math\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"github.com/example/lib/v2\"\n\t\"github.com/example/lib/v2\"\n\t\"../relative/pkg\"\n)\n\nfunc main() {\n\tfmt.Println(os.Args, str.ToUpper(\"x\"), Pi, lib.X)\n}\n"; + assert_eq!( + paths("a.go", src), + [ + "fmt", + "os", + "strings", + "math", + "embed", + "github.com/example/lib/v2", + "../relative/pkg", + ] + ); +} + +#[test] +fn python_statements_yield_each_module_once_without_aliases() { + let src = "import os, sys\nimport os.path\nfrom typing import List, Dict\nfrom x import y as z\nfrom . import sibling\nfrom ..pkg.sub import thing\nimport os\nimport numpy as np\n"; + assert_eq!( + paths("a.py", src), + [ + "os", + "sys", + "os.path", + "typing", + "x", + ".sibling", + "..pkg.sub", + "numpy" + ] + ); +} + +#[test] +fn javascript_counts_modules_not_specifiers_and_sees_require_and_dynamic_import() { + let src = "const fs = require('fs');\nconst path = require(\"path\");\nimport React from 'react';\nimport { useState, useEffect } from 'react';\nimport * as ns from 'ns-lib';\nimport './side-effect.css';\nimport def, { named } from './local/mod';\nasync function f() { const m = await import('dyn-mod'); return m; }\n"; + assert_eq!( + paths("a.js", src), + [ + "fs", + "path", + "react", + "ns-lib", + "./side-effect.css", + "./local/mod", + "dyn-mod" + ] + ); +} + +#[test] +fn typescript_type_imports_and_import_equals_require() { + let src = "import type { T } from \"./types\";\nimport fs = require(\"fs\");\nimport { readFile } from \"node:fs/promises\";\nimport * as path from \"path\";\nimport express from \"express\";\nexport { x } from \"./reexport\";\n"; + assert_eq!( + paths("a.ts", src), + ["./types", "fs", "node:fs/promises", "path", "express"] + ); +} + +#[test] +fn rust_use_trees_expand_to_leaf_paths() { + let src = "use std::collections::{HashMap, HashSet};\nuse serde::Serialize;\nuse crate::foo::bar;\nuse super::baz as qux;\nuse std::io::*;\nextern crate alloc;\npub(crate) use std::io::{self, Read};\n\nfn main() {}\n"; + assert_eq!( + paths("a.rs", src), + [ + "std::collections::HashMap", + "std::collections::HashSet", + "serde::Serialize", + "crate::foo::bar", + "super::baz", + "std::io::*", + "alloc", + "std::io", + "std::io::Read", + ] + ); +} + +#[test] +fn java_imports_keep_wildcards_and_static_members() { + let src = "package a;\n\nimport java.util.List;\nimport java.util.*;\nimport static java.lang.Math.max;\nimport static java.lang.Math.*;\nimport java.util.List;\n\npublic class A {}\n"; + assert_eq!( + paths("A.java", src), + [ + "java.util.List", + "java.util.*", + "java.lang.Math.max", + "java.lang.Math.*" + ] + ); +} + +#[test] +fn c_includes_distinguish_system_and_local_headers() { + let src = "#include <stdio.h>\n#include <stdlib.h>\n#include \"local.h\"\n#include \"../up/other.h\"\nint main(void) { return 0; }\n"; + assert_eq!( + flagged("a.c", src), + [ + ("stdio.h".to_string(), false), + ("stdlib.h".to_string(), false), + ("local.h".to_string(), true), + ("../up/other.h".to_string(), true), + ] + ); +} + +#[test] +fn csharp_using_directives_take_the_target_not_the_alias() { + let src = "using System;\nusing System.Collections.Generic;\nusing Alias = System.IO;\nusing static System.Math;\nusing MyLib.Utils;\nnamespace X { class A {} }\n"; + assert_eq!( + paths("A.cs", src), + [ + "System", + "System.Collections.Generic", + "System.IO", + "System.Math", + "MyLib.Utils", + ] + ); +} + +#[test] +fn php_use_groups_aliases_and_file_includes() { + let src = "<?php\nnamespace App;\nuse App\\Models\\User;\nuse App\\Http\\{A, B};\nuse Vendor\\Pkg\\Thing as T;\nrequire_once \"x.php\";\ninclude \"y.php\";\nrequire 'z.php';\n"; + assert_eq!( + flagged("a.php", src), + [ + ("App\\Models\\User".to_string(), false), + ("App\\Http\\A".to_string(), false), + ("App\\Http\\B".to_string(), false), + ("Vendor\\Pkg\\Thing".to_string(), false), + ("x.php".to_string(), true), + ("y.php".to_string(), true), + ("z.php".to_string(), true), + ] + ); +} + +#[test] +fn ruby_require_calls() { + let src = "require \"json\"\nrequire 'set'\nrequire_relative \"lib/x\"\nrequire \"nokogiri\"\nputs \"hello\"\n"; + assert_eq!( + flagged("a.rb", src), + [ + ("json".to_string(), false), + ("set".to_string(), false), + ("lib/x".to_string(), true), + ("nokogiri".to_string(), false), + ] + ); +} + +#[test] +fn kotlin_imports_are_recovered_from_the_source_span() { + let src = "package a\n\nimport java.util.List\nimport kotlin.collections.Map\nimport com.example.lib.Thing as T\nimport kotlinx.coroutines.*\n\nfun main() {}\n"; + assert_eq!( + paths("A.kt", src), + [ + "java.util.List", + "kotlin.collections.Map", + "com.example.lib.Thing", + "kotlinx.coroutines.*", + ] + ); +} + +#[test] +fn kotlin_without_source_bytes_yields_no_imports() { + let src = "import java.util.List\n"; + let parser = Parser::new(); + let root = parser.parse("A.kt", src.as_bytes()).expect("parse"); + assert!(extract_imports(&root, "kotlin", &[]).is_empty()); +} + +#[test] +fn unknown_language_name_uses_the_generic_rule_on_real_trees() { + let parser = Parser::new(); + let go = "package main\n\nimport (\n\tstr \"strings\"\n\t. \"math\"\n)\n"; + let root = parser.parse("a.go", go.as_bytes()).expect("parse go"); + let got: Vec<String> = extract_imports(&root, "", go.as_bytes()) + .into_iter() + .map(|i| i.path) + .collect(); + assert_eq!(got, ["strings", "math"]); + + let py = "import os, sys\nfrom . import sibling\n"; + let root = parser.parse("a.py", py.as_bytes()).expect("parse py"); + let got: Vec<String> = extract_imports(&root, "", py.as_bytes()) + .into_iter() + .map(|i| i.path) + .collect(); + assert_eq!(got, ["os", "sys", ".sibling"]); + + let js = "import React from 'react';\nimport './x.css';\n"; + let root = parser.parse("a.js", js.as_bytes()).expect("parse js"); + let got: Vec<String> = extract_imports(&root, "", js.as_bytes()) + .into_iter() + .map(|i| i.path) + .collect(); + assert_eq!(got, ["react", "./x.css"]); +} + +#[test] +fn a_file_without_imports_yields_nothing() { + assert!(paths( + "types.go", + "package main\n\ntype Config struct {\n\tName string\n}\n" + ) + .is_empty()); + assert!(paths("README.md", "# Title\n\nimport nothing here\n").is_empty()); +} + +/// The parser's language names for the fixtures are the names the +/// extractor and the stdlib tables key on. +#[test] +fn parser_language_names_match_the_language_table() { + let parser = Parser::new(); + let cases = [ + ("a.go", "go"), + ("a.py", "python"), + ("a.js", "javascript"), + ("a.ts", "typescript"), + ("a.tsx", "tsx"), + ("a.rs", "rust"), + ("A.java", "java"), + ("a.c", "c"), + ("a.cpp", "cpp"), + ("A.cs", "c_sharp"), + ("a.php", "php"), + ("a.rb", "ruby"), + ("A.kt", "kotlin"), + ]; + for (file, want) in cases { + assert_eq!(parser.get_language(file), want, "{file}"); + } +} diff --git a/crates/cf-langpath/src/lib.rs b/crates/cf-langpath/src/lib.rs index 8d75c91a..ce63dd58 100644 --- a/crates/cf-langpath/src/lib.rs +++ b/crates/cf-langpath/src/lib.rs @@ -11,7 +11,7 @@ //! Per DESIGN §3.7, enry language classification is decision-parity-critical: //! the glob set selects which files an analysis includes, which in turn //! selects which bytes appear in machine reports — output that -//! `tests/compat` pins against the reference binary. We therefore vendor +//! the report e2e suites (`bins/codefang/tests`) pins against the reference binary. We therefore vendor //! the **same** three Linguist data tables that the reference binary links //! (`github.com/src-d/enry/v2@v2.1.0`), as a verbatim TSV snapshot in //! `data/enry-v2.1.0.tsv` (see `data/README.md`), and reproduce enry's lookup @@ -53,6 +53,26 @@ pub fn canonical_language(token: &str) -> Option<String> { get_language_by_alias(token).map(str::to_string) } +/// The primary file extension (with its leading dot) of the language a +/// token names, Linguist aliases included; `None` for an unknown token or a +/// language without extensions. +/// +/// # Examples +/// +/// ``` +/// use cf_langpath::primary_extension; +/// +/// assert_eq!(primary_extension("python").as_deref(), Some(".py")); +/// assert_eq!(primary_extension("JavaScript").as_deref(), Some(".js")); +/// assert_eq!(primary_extension("notalang"), None); +/// ``` +#[must_use] +pub fn primary_extension(token: &str) -> Option<String> { + get_language_extensions(get_language_by_alias(token.trim())?) + .first() + .cloned() +} + /// Error returned when a user-supplied token does not resolve to any Linguist /// language (including its aliases). /// diff --git a/crates/cf-langpath/tests/content_heuristics_diff.rs b/crates/cf-langpath/tests/content_heuristics_diff.rs deleted file mode 100644 index d407d973..00000000 --- a/crates/cf-langpath/tests/content_heuristics_diff.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Differential test of [`cf_langpath::content_heuristics::languages_by_content`] -//! against a live Go `src-d/enry@v2.1.0` oracle. -//! -//! The oracle (a Go program linking the real enry library) walks a real corpus, -//! and for every file whose last extension is one of the 80 content-heuristic -//! extensions emits `path<TAB>isBinary<TAB>json(GetLanguagesByContent(base, content))`. -//! This test replays the same files through the Rust port and requires the -//! ordered language list to match exactly on every non-binary file. -//! -//! It is `#[ignore]`d by default because it depends on the machine-local oracle -//! output at `CF_LANGPATH_ORACLE` (default `/tmp/oracle_out.tsv`) and the -//! corpus files it references. Run with: -//! -//! ```text -//! cargo test -p cf-langpath --test content_heuristics_diff -- --ignored -//! ``` - -use std::fs; - -use cf_langpath::content_heuristics::languages_by_content; - -/// Minimal JSON-array-of-strings parser for the oracle's `json.Marshal([]string)` -/// output (`[]`, `["A"]`, `["A","B"]`). Language names in enry never contain a -/// double quote or backslash, so no escape handling is needed. -fn parse_json_str_array(s: &str) -> Vec<String> { - let s = s.trim(); - if s == "[]" || s == "null" { - return Vec::new(); - } - let inner = &s[1..s.len() - 1]; // strip [ ] - inner - .split(',') - .map(|tok| tok.trim().trim_matches('"').to_string()) - .collect() -} - -#[test] -#[ignore = "requires the Go oracle output and corpus (see module docs)"] -fn matches_go_enry_oracle() { - let path = - std::env::var("CF_LANGPATH_ORACLE").unwrap_or_else(|_| "/tmp/oracle_out.tsv".to_string()); - let tsv = fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("oracle file {path}: {e}; run the Go oracle first")); - - let mut compared = 0usize; - let mut mismatches = Vec::new(); - - for line in tsv.lines() { - if line.is_empty() { - continue; - } - let mut cols = line.splitn(3, '\t'); - let file = cols.next().unwrap(); - let is_binary = cols.next().unwrap(); - let want = parse_json_str_array(cols.next().unwrap()); - - // The Rust caller (devs_detect_language) returns early on binary; the - // task only requires parity on non-binary content. - if is_binary == "true" { - continue; - } - - // Skip files that vanished since the oracle ran. - let Ok(content) = fs::read(file) else { - continue; - }; - // Pass the full path: enry computes filepath.Ext on it, and the Rust - // port mirrors filepath.Ext including separator handling. - let got = languages_by_content(file, &content); - - compared += 1; - if got != want && mismatches.len() < 50 { - mismatches.push(format!("{file}\n want={want:?}\n got={got:?}")); - } - } - - assert!( - mismatches.is_empty(), - "compared {compared} non-binary files, {} mismatches:\n{}", - mismatches.len(), - mismatches.join("\n") - ); - - assert!(compared > 0, "oracle produced no comparable rows"); - eprintln!("compared {compared} non-binary files, 0 mismatches"); -} diff --git a/crates/cf-mcp/Cargo.toml b/crates/cf-mcp/Cargo.toml index 0c653726..f28405b3 100644 --- a/crates/cf-mcp/Cargo.toml +++ b/crates/cf-mcp/Cargo.toml @@ -6,13 +6,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true publish.workspace = true -description = "MCP (Model Context Protocol) server exposing Codefang analysis as tools. Gated behind the non-default `mcp` feature; not shipped by default." +description = "MCP (Model Context Protocol) server exposing Codefang analysis as tools; `codefang mcp` launches it. The server body sits behind the `mcp` feature, which cf-commands enables." -# The MCP server is not part of the shipped codefang binary: the entire crate -# body is behind the non-default `mcp` feature. With the feature OFF the crate -# compiles to an empty shell, so it never participates in the default -# workspace build and `cf-commands` must opt in explicitly. See DESIGN.md §1 -# and §3. +# The crate body is behind the `mcp` feature; `cf-commands` enables it for +# `codefang mcp`. With the feature OFF the crate compiles to an empty shell. [features] default = [] mcp = ["dep:serde", "dep:serde_json", "dep:thiserror"] @@ -33,9 +30,9 @@ cf-uast-node = { path = "../cf-uast-node" } # envelope — it is NEVER used to encode the machine-format report payload, which # flows through the byte-compatible encoder in `gojson.rs`. See DESIGN.md §2. serde = { version = "1", features = ["derive"], optional = true } -serde_json = { version = "1", optional = true } +serde_json = { workspace = true, optional = true } # Error derive; every ToolError display string is a frozen tool-contract message. thiserror = { version = "1", optional = true } [dev-dependencies] -serde_json = "1" +serde_json = { workspace = true } diff --git a/crates/cf-mcp/src/gojson.rs b/crates/cf-mcp/src/gojson.rs index 442b7c5c..d400cd94 100644 --- a/crates/cf-mcp/src/gojson.rs +++ b/crates/cf-mcp/src/gojson.rs @@ -2,7 +2,7 @@ //! //! This encoder reproduces the report-format serialization contract: the //! output bytes are pinned against the reference implementation -//! (tests/compat). Its rules are frozen — tidy internals only, never the +//! (the report e2e suites in bins/codefang/tests). Its rules are frozen — tidy internals only, never the //! emitted bytes. //! //! **Temporary in-crate shim, not the real serialization crate.** DESIGN.md @@ -86,6 +86,32 @@ impl JsonValue { } } +/// A parsed JSON document as a [`JsonValue`]: objects keep their document +/// key order (struct-origin), numbers keep their kind (signed, unsigned, +/// float). +#[cfg(feature = "mcp")] +impl From<&serde_json::Value> for JsonValue { + fn from(value: &serde_json::Value) -> Self { + use serde_json::Value; + match value { + Value::Null => Self::Null, + Value::Bool(b) => Self::Bool(*b), + Value::Number(n) => n + .as_i64() + .map(Self::Int) + .or_else(|| n.as_u64().map(Self::Uint)) + .unwrap_or_else(|| Self::Float(n.as_f64().unwrap_or(0.0))), + Value::String(s) => Self::Str(s.clone()), + Value::Array(items) => Self::Array(items.iter().map(Self::from).collect()), + Value::Object(map) => Self::struct_object( + map.iter() + .map(|(k, v)| (k.clone(), Self::from(v))) + .collect(), + ), + } + } +} + /// Converts a [`cf_uast_node::Node`] subtree directly into a [`JsonValue`], /// reproducing the node-map report shape byte-for-byte (DESIGN §2.2). /// @@ -385,6 +411,16 @@ fn go_style_from_shortest(shortest: &str, f: f64) -> String { mod tests { use super::*; + /// A parsed JSON document converts with its keys in document order and + /// every number kind kept, so it re-encodes to the same bytes. + #[test] + fn parsed_json_converts_in_document_order() { + let src = r#"{"b":1,"a":[true,null,2.5,"x"],"c":{"z":-3,"y":18446744073709551615}}"#; + let parsed: serde_json::Value = serde_json::from_str(src).expect("json"); + let value = JsonValue::from(&parsed); + assert_eq!(Encoder::compact().encode_to_string(&value), src); + } + fn smap(entries: &[(&str, JsonValue)]) -> JsonValue { JsonValue::sorted_object( entries diff --git a/crates/cf-mcp/src/providers.rs b/crates/cf-mcp/src/providers.rs index f55644c6..65191db5 100644 --- a/crates/cf-mcp/src/providers.rs +++ b/crates/cf-mcp/src/providers.rs @@ -15,6 +15,12 @@ use cf_uast_node::Node; /// Abstraction over the UAST parser. Used by both `codefang_analyze` and /// `uast_parse`. pub trait UastParser { + /// The synthetic filename a snippet in `language` is parsed under: + /// `code.<language>` unless the backend knows the language's extension. + fn filename_for(&self, language: &str) -> String { + crate::tools::synthetic_filename(language) + } + /// Whether the parser supports the given (synthetic) filename. fn is_supported(&self, filename: &str) -> bool; @@ -27,18 +33,30 @@ pub trait UastParser { fn parse(&self, filename: &str, code: &[u8]) -> Result<Node, ToolError>; } +/// One inline source snippet a tool call analyzes. +#[derive(Debug, Clone, Copy)] +pub struct Snippet<'a> { + /// The synthetic filename the language maps to (`code.go`). + pub filename: &'a str, + /// The source bytes. + pub code: &'a [u8], + /// The parsed UAST root. + pub root: &'a Node, +} + /// Abstraction over the static-analysis factory. /// -/// Given a parsed root node and the selected analyzer names, `run` returns the -/// result map as a [`JsonValue`] (a map-origin object, byte-sorted on encode — -/// report-format contract). +/// Given a snippet (its synthetic filename, source and parsed root) and the +/// selected analyzer names, `run` returns the result map as a [`JsonValue`] +/// keyed by analyzer name. pub trait StaticAnalysisProvider { - /// Runs the named analyzers over `root`, returning the combined result map. + /// Runs the named analyzers over `snippet`, returning the combined result + /// map. /// /// # Errors /// /// Returns a [`ToolError`] describing the first analyzer failure. - fn run(&self, root: &Node, names: &[String]) -> Result<JsonValue, ToolError>; + fn run(&self, snippet: &Snippet<'_>, names: &[String]) -> Result<JsonValue, ToolError>; } /// Options passed to a [`HistoryAnalysisProvider`], derived from a validated diff --git a/crates/cf-mcp/src/tools_analyze.rs b/crates/cf-mcp/src/tools_analyze.rs index 7cc75dd5..cab91507 100644 --- a/crates/cf-mcp/src/tools_analyze.rs +++ b/crates/cf-mcp/src/tools_analyze.rs @@ -10,9 +10,9 @@ //! and error wording all live here and are part of the tool contract. use crate::errors::ToolError; -use crate::providers::{StaticAnalysisProvider, UastParser}; +use crate::providers::{Snippet, StaticAnalysisProvider, UastParser}; use crate::result::{ToolOutput, ToolResult}; -use crate::tools::{synthetic_filename, validate_code_input, AnalyzeInput}; +use crate::tools::{validate_code_input, AnalyzeInput}; /// Names of the default static analyzers, in their fixed registration order: /// `complexity, comments, halstead, cohesion, imports`. @@ -47,7 +47,7 @@ pub fn handle_analyze( return (ToolResult::error(&err), ToolOutput::empty()); } - let filename = synthetic_filename(&input.language); + let filename = parser.filename_for(&input.language); if !parser.is_supported(&filename) { let err = ToolError::UnsupportedLanguage { @@ -70,7 +70,12 @@ pub fn handle_analyze( input.analyzers.clone() }; - let results = match provider.run(&root, &names) { + let snippet = Snippet { + filename: &filename, + code: input.code.as_bytes(), + root: &root, + }; + let results = match provider.run(&snippet, &names) { Ok(v) => v, Err(e) => { let err = ToolError::wrap("run analyzers", e.to_string()); @@ -103,7 +108,7 @@ mod tests { /// contains the substring the tests assert on. struct FakeProvider; impl StaticAnalysisProvider for FakeProvider { - fn run(&self, _root: &Node, names: &[String]) -> Result<JsonValue, ToolError> { + fn run(&self, _snippet: &Snippet<'_>, names: &[String]) -> Result<JsonValue, ToolError> { let entries = names .iter() .map(|n| (n.clone(), JsonValue::Int(1))) diff --git a/crates/cf-mcp/src/tools_uast.rs b/crates/cf-mcp/src/tools_uast.rs index 39c25041..63c703f5 100644 --- a/crates/cf-mcp/src/tools_uast.rs +++ b/crates/cf-mcp/src/tools_uast.rs @@ -10,7 +10,7 @@ use crate::errors::ToolError; use crate::gojson::node_to_json; use crate::providers::UastParser; use crate::result::{ToolOutput, ToolResult}; -use crate::tools::{synthetic_filename, validate_code_input, UastParseInput}; +use crate::tools::{validate_code_input, UastParseInput}; /// The synthetic node type used to wrap query matches. pub const FILTERED_RESULTS_TYPE: &str = "filtered_results"; @@ -33,7 +33,7 @@ pub fn handle_uast_parse( return (ToolResult::error(&err), ToolOutput::empty()); } - let filename = synthetic_filename(&input.language); + let filename = parser.filename_for(&input.language); if !parser.is_supported(&filename) { let err = ToolError::UnsupportedLanguage { diff --git a/crates/cf-meminfo/src/lib.rs b/crates/cf-meminfo/src/lib.rs index 315c2e5b..e3efb424 100644 --- a/crates/cf-meminfo/src/lib.rs +++ b/crates/cf-meminfo/src/lib.rs @@ -1,7 +1,8 @@ //! Runtime memory introspection (ported from the Go package `meminfo`). //! -//! This crate exposes a single helper, [`read_rss_bytes`], that returns the -//! current process resident set size (RSS) in bytes. It is the byte-for-byte +//! This crate exposes [`read_rss_bytes`], which returns the current process +//! resident set size (RSS) in bytes, and [`release_free_heap`], which returns +//! freed heap pages to the operating system. It is the byte-for-byte //! behavioral port of `pkg/meminfo` (`ReadRSSBytes`), which is build-tagged //! per-OS in Go (`rss_linux.go` / `rss_other.go`) and consumed by `analyze`. //! @@ -98,6 +99,44 @@ pub fn read_rss_bytes() -> i64 { 0 } +/// Returns the heap's free pages to the operating system. +/// +/// glibc keeps freed memory inside its per-thread arenas and gives back only +/// the top of each heap, so a parallel walk whose workers each once held a +/// large transient working set keeps every worker's high-water mark resident +/// long after the memory was freed. Measured on a 7,973-commit kubernetes +/// prefix, `history/sentiment` sat at 1777 MB resident and dropped to 679 MB +/// after a forced trim. This asks glibc to release every whole free page in +/// every arena (`malloc_trim(0)`); live allocations are untouched. +/// +/// A no-op on other C libraries and platforms. +/// +/// # Examples +/// +/// ``` +/// cf_meminfo::release_free_heap(); +/// ``` +#[cfg(all(target_os = "linux", target_env = "gnu"))] +pub fn release_free_heap() { + // SAFETY: malloc_trim takes no pointers and is thread-safe: it locks each + // arena while it scans it. Its return value only reports whether anything + // was released. + unsafe { + libc::malloc_trim(0); + } +} + +/// Returns the heap's free pages to the operating system: a no-op off glibc, +/// whose allocators do not expose (or need) an explicit trim. +/// +/// # Examples +/// +/// ``` +/// cf_meminfo::release_free_heap(); +/// ``` +#[cfg(not(all(target_os = "linux", target_env = "gnu")))] +pub fn release_free_heap() {} + /// Returns the system memory page size in bytes. /// /// Equivalent to `os.Getpagesize()` in the reference runtime, which on Linux is backed by @@ -144,6 +183,65 @@ mod tests { assert_eq!(read_rss_bytes(), 0); } + /// 1 KiB blocks: small enough to live inside a malloc arena, never in a + /// dedicated mapping. + #[cfg(all(target_os = "linux", target_env = "gnu"))] + const FRAGMENT_BLOCK_SIZE: usize = 1024; + /// 64 MiB of blocks. + #[cfg(all(target_os = "linux", target_env = "gnu"))] + const FRAGMENT_BLOCKS: usize = 64 * 1024; + /// One block in this many stays alive, so the freed space around it is + /// trapped below live data and cannot be returned from the heap's top. + #[cfg(all(target_os = "linux", target_env = "gnu"))] + const PIN_EVERY: usize = 64; + /// The resident drop that proves trapped free pages went back to the OS + /// (about 60 MiB of the 64 MiB is free whole pages). + #[cfg(all(target_os = "linux", target_env = "gnu"))] + const MIN_RELEASED_BYTES: i64 = 32 * 1024 * 1024; + + /// Freed memory trapped between live allocations stays resident until + /// the heap is asked to release it; afterwards RSS drops by the trapped + /// pages while the live blocks stay intact. + #[cfg(all(target_os = "linux", target_env = "gnu"))] + #[test] + fn release_free_heap_returns_trapped_free_pages() { + let blocks: Vec<Vec<u8>> = (0..FRAGMENT_BLOCKS) + .map(|i| vec![(i % 251 + 1) as u8; FRAGMENT_BLOCK_SIZE]) + .collect(); + let pinned: Vec<Vec<u8>> = blocks + .into_iter() + .enumerate() + .filter_map(|(i, block)| (i % PIN_EVERY == 0).then_some(block)) + .collect(); + let before = read_rss_bytes(); + release_free_heap(); + let after = read_rss_bytes(); + assert!( + pinned + .iter() + .enumerate() + .all(|(i, b)| b.iter().all(|&v| v == ((i * PIN_EVERY) % 251 + 1) as u8)), + "live blocks survive the release" + ); + assert!( + before - after >= MIN_RELEASED_BYTES, + "released {} bytes (rss {before} -> {after})", + before - after + ); + } + + /// Releasing free memory is always safe to call, also with nothing to + /// release and from several threads at once. + #[test] + fn release_free_heap_is_safe_to_call_concurrently() { + std::thread::scope(|s| { + for _ in 0..4 { + s.spawn(release_free_heap); + } + }); + release_free_heap(); + } + /// The page size used to scale RSS must be a sane positive value. #[cfg(target_os = "linux")] #[test] diff --git a/crates/cf-metrics/Cargo.toml b/crates/cf-metrics/Cargo.toml index 3de4a4b3..e75cd722 100644 --- a/crates/cf-metrics/Cargo.toml +++ b/crates/cf-metrics/Cargo.toml @@ -5,7 +5,7 @@ # RiskLevel + risk_priority ordering, and a name-keyed Registry. # # The report-bearing types (TimeSeriesPoint, RiskResult) appear in MACHINE-format -# output, whose bytes are a frozen contract (pinned by tests/compat). Per +# output, whose bytes are a frozen contract (pinned by the report e2e suites in bins/codefang/tests). Per # DESIGN.md §1 rule (1) / §3, machine-format bytes (json/yaml/ndjson/timeseries/ # compact/bin) MUST be emitted by the shared cf-gojson / cf-goyaml report-format # crates, never by serde defaults. This crate therefore carries no serde diff --git a/crates/cf-metrics/src/lib.rs b/crates/cf-metrics/src/lib.rs index ee8c6ad5..1f53fb07 100644 --- a/crates/cf-metrics/src/lib.rs +++ b/crates/cf-metrics/src/lib.rs @@ -12,8 +12,8 @@ //! # Serialization //! //! The data types in this crate ([`TimeSeriesPoint`], [`RiskResult`]) appear in -//! MACHINE-format reports, whose bytes are a frozen contract (pinned against the -//! reference implementation by `tests/compat`). Machine-format bytes (json, +//! MACHINE-format reports, whose bytes are a frozen contract (pinned by the +//! report e2e suites, `bins/codefang/tests`). Machine-format bytes (json, //! yaml, ndjson, timeseries, compact, bin) must be produced by the shared //! `cf-gojson` / `cf-goyaml` report-format crates, **not** by raw `serde_json` / //! `serde_yaml` defaults. diff --git a/crates/cf-pathfilter/Cargo.toml b/crates/cf-pathfilter/Cargo.toml index d603238b..960590fd 100644 --- a/crates/cf-pathfilter/Cargo.toml +++ b/crates/cf-pathfilter/Cargo.toml @@ -9,7 +9,7 @@ # This crate emits no machine-format report bytes, so it does NOT depend on the # compat serialization crates cf-gojson / cf-goyaml. Its parity surface is the # boolean include/exclude decision, which selects which files appear in reports -# (pinned by tests/compat). +# (pinned by the report e2e suites in bins/codefang/tests). [package] name = "cf-pathfilter" version.workspace = true diff --git a/crates/cf-pathfilter/build.rs b/crates/cf-pathfilter/build.rs index 4b59d479..f2d8e8ef 100644 --- a/crates/cf-pathfilter/build.rs +++ b/crates/cf-pathfilter/build.rs @@ -4,7 +4,7 @@ //! //! `cf-pathfilter` reproduces `github.com/src-d/enry/v2` `IsVendor`, whose //! decision changes *which* files are analysed and therefore which bytes appear -//! in machine-format reports (pinned by `tests/compat`). The design +//! in machine-format reports. The design //! (`specs/rust-rewrite/DESIGN.md` §2.6) mandates vendoring the **same** data //! tables enry uses, byte-for-byte, instead of hand-translating a generated //! artifact or swapping detectors. diff --git a/crates/cf-pathfilter/src/enry_vendor.rs b/crates/cf-pathfilter/src/enry_vendor.rs index 3f7f4999..11d35df4 100644 --- a/crates/cf-pathfilter/src/enry_vendor.rs +++ b/crates/cf-pathfilter/src/enry_vendor.rs @@ -2,7 +2,7 @@ //! //! enry classification is decision-parity-critical (DESIGN §2.6): the set of //! files included in an analysis selects which bytes appear in machine reports, -//! which `tests/compat` pins against the reference binary. We therefore +//! which the report e2e suites (`bins/codefang/tests`) pins against the reference binary. We therefore //! vendor the **same** regex table enry embeds in its generated //! `data/vendor.go` (itself extracted from GitHub Linguist's `vendor.yml`), //! instead of swapping to a different detector. diff --git a/crates/cf-pathfilter/src/lib.rs b/crates/cf-pathfilter/src/lib.rs index deb69909..ba0a990b 100644 --- a/crates/cf-pathfilter/src/lib.rs +++ b/crates/cf-pathfilter/src/lib.rs @@ -24,7 +24,7 @@ //! //! This crate is decision-parity-critical: its boolean include/exclude result //! selects which files flow into machine reports, whose bytes are pinned -//! against the reference binary by `tests/compat`. Vendor detection +//! by the report e2e suites (`bins/codefang/tests`). Vendor detection //! therefore vendors the same enry `data/vendor.go` regex tables (generated by //! `build.rs` from the pinned enry source, with [`vendor_data`] as the //! checked-in fallback), and the generated-file rule tables below are part of diff --git a/crates/cf-pathfilter/src/vendor_data.rs b/crates/cf-pathfilter/src/vendor_data.rs index 8b68d952..66d648b0 100644 --- a/crates/cf-pathfilter/src/vendor_data.rs +++ b/crates/cf-pathfilter/src/vendor_data.rs @@ -3,7 +3,7 @@ //! # Data parity (DESIGN §2.6) //! //! File-classification decisions change *which* bytes appear in machine reports -//! (pinned by `tests/compat`), so the underlying data tables must match +//! (pinned by the report e2e suites (`bins/codefang/tests`)), so the underlying data tables must match //! the enry library **byte-for-byte**. We therefore vendor the **same** //! regular-expression sources that `github.com/src-d/enry/v2@v2.1.0` compiles //! in `data/vendor.go`, rather than swapping in a behaviourally different diff --git a/crates/cf-pathpolicy/src/lib.rs b/crates/cf-pathpolicy/src/lib.rs index 526ac543..c6807fba 100644 --- a/crates/cf-pathpolicy/src/lib.rs +++ b/crates/cf-pathpolicy/src/lib.rs @@ -4,7 +4,7 @@ //! user-visible options that mirror the CLI flags (`--include-vendored`, //! `--include-generated`, `--extra-excluded-prefixes`). Pure, stateless, //! cross-phase. The decision selects which files feed machine reports, whose -//! bytes are pinned against the reference binary by `tests/compat`. +//! bytes are pinned by the report e2e suites (`bins/codefang/tests`). //! //! Exclusion-rule precedence (part of that contract): //! diff --git a/crates/cf-persist/src/gob.rs b/crates/cf-persist/src/gob.rs index 831f629b..f4cc328f 100644 --- a/crates/cf-persist/src/gob.rs +++ b/crates/cf-persist/src/gob.rs @@ -35,9 +35,9 @@ impl GobCodec { } impl Codec for GobCodec { - fn encode<W: Write, T: Serialize>(&self, mut w: W, state: &T) -> Result<(), PersistError> { - let bytes = bincode::serialize(state).map_err(PersistError::GobEncode)?; - w.write_all(&bytes).map_err(PersistError::Io) + fn encode<W: Write, T: Serialize>(&self, w: W, state: &T) -> Result<(), PersistError> { + // Streams into the writer: no in-memory copy of the encoded state. + bincode::serialize_into(w, state).map_err(PersistError::GobEncode) } fn decode<R: Read, T: DeserializeOwned>( diff --git a/crates/cf-plotpage/Cargo.toml b/crates/cf-plotpage/Cargo.toml index 3e5707ab..2bf27beb 100644 --- a/crates/cf-plotpage/Cargo.toml +++ b/crates/cf-plotpage/Cargo.toml @@ -11,6 +11,6 @@ description = "HTML plot-page renderer for codefang --format plot: themes, go-ec [dependencies] # The chart `option_*` JSON embedded in each page reproduces go-echarts # BaseConfiguration.JSONNotEscaped output and MUST be byte-identical to the -# reference binary's (pinned by tests/compat), so it routes through the +# reference binary's (pinned by the report e2e suites in bins/codefang/tests), so it routes through the # report-format encoder — never serde. cf-gojson = { path = "../cf-gojson" } diff --git a/crates/cf-plotpage/src/echarts.rs b/crates/cf-plotpage/src/echarts.rs index d8aab7ca..df1241f2 100644 --- a/crates/cf-plotpage/src/echarts.rs +++ b/crates/cf-plotpage/src/echarts.rs @@ -1,6 +1,6 @@ //! Minimal go-echarts v2.6.7 model — enough to reproduce, byte for byte, the //! chart `option_*` JSON and `<div>`/`<script>` snippet that `--format plot` -//! pages embed (pinned against the reference binary by `tests/compat`). +//! pages embed (pinned by the report e2e suites (`bins/codefang/tests`)). //! //! # How the reference bytes are produced //! diff --git a/crates/cf-plotpage/src/lib.rs b/crates/cf-plotpage/src/lib.rs index cf2842a6..952ab075 100644 --- a/crates/cf-plotpage/src/lib.rs +++ b/crates/cf-plotpage/src/lib.rs @@ -27,7 +27,7 @@ //! //! All JSON routes through `cf-gojson` (never serde): the chart option JSON //! is report-contract output, byte-compared against the reference binary by -//! `tests/compat`. +//! the report e2e suites (`bins/codefang/tests`). //! //! # Example: build a one-chart page //! diff --git a/crates/cf-plumbing/src/lib.rs b/crates/cf-plumbing/src/lib.rs index 68312945..dbf8c884 100644 --- a/crates/cf-plumbing/src/lib.rs +++ b/crates/cf-plumbing/src/lib.rs @@ -13,7 +13,7 @@ //! on [`LineStats`] exist purely so that *downstream* crates (which DO route //! through the shared report serializers) observe the exact same field names //! and declaration order; the field order below is the byte-identity-relevant -//! invariant (pinned by `tests/compat`). +//! invariant (pinned by the report e2e suites (`bins/codefang/tests`)). //! //! # Bridged identity constants //! diff --git a/crates/cf-quality/Cargo.toml b/crates/cf-quality/Cargo.toml index 9e026bf4..c9e2eeda 100644 --- a/crates/cf-quality/Cargo.toml +++ b/crates/cf-quality/Cargo.toml @@ -8,16 +8,22 @@ repository.workspace = true publish.workspace = true [dependencies] +# Spill encoding of finished history results (cf-commands `spill`). +serde = { workspace = true } # Serialization compat (DESIGN section 2): all machine output goes through these. cf-gojson = { workspace = true } cf-reportutil = { path = "../cf-reportutil" } # Numeric parity for mean/median/P95/max/min/sum (shared statistics kernel). cf-alg-stats = { path = "../cf-alg-stats" } +# The per-file sample is the four static analyzers' own math over the parsed +# UAST (`analyzer::analyze_file`), so the quality numbers are the static +# analyzers' numbers for the same file. +cf-uast-node = { path = "../cf-uast-node" } +cf-complexity = { path = "../cf-complexity" } +cf-halstead = { path = "../cf-halstead" } +cf-comments = { path = "../cf-comments" } +cf-cohesion = { path = "../cf-cohesion" } -# NOTE: the four component analyzers (cf-complexity, cf-halstead, cf-comments, -# cf-cohesion) are consumed only via the `ScalarReport` / `ComponentSet` traits -# in `analyzer.rs`, so they are NOT direct build dependencies yet. They are -# wired in when the framework layer (Consume over cf-plumbing + cf-analyze -# BaseHistoryAnalyzer) is implemented. Listing them now would add unused deps -# (and break the crate whenever those scaffolds lack targets), so they are -# deferred until there are real call sites. +[dev-dependencies] +# Real parsed trees for the per-file sample tests. +cf-uast = { path = "../cf-uast" } diff --git a/crates/cf-quality/src/analyzer.rs b/crates/cf-quality/src/analyzer.rs index de29cf53..2c38fe8f 100644 --- a/crates/cf-quality/src/analyzer.rs +++ b/crates/cf-quality/src/analyzer.rs @@ -1,228 +1,221 @@ -//! The composite quality analyzer. +//! The per-file quality sample: the four static component analyzers run over +//! one parsed file, exactly as the `static/*` surfaces run them. //! -//! Runs the four static component analyzers — complexity, Halstead, comments, -//! cohesion — on each changed file's UAST per commit, recording **scalars only**, -//! and aggregates them order-independently (per-commit results keyed by hash; -//! merging is a no-op). Analyzer ID is `history/quality`. -//! -//! # Component coupling -//! -//! The component analyzers (`cf-complexity`, `cf-halstead`, `cf-comments`, -//! `cf-cohesion`) each expose `Analyzer::new()` + `analyze(&root) -> Result<Report, _>` -//! returning a per-crate report map. This module consumes only the documented -//! scalar keys via the [`ScalarReport`] accessor trait, so it is decoupled from -//! each crate's concrete `Report`/`ReportValue` representation. The framework -//! plumbing (UAST change feed, tick assignment, aggregation) is abstracted -//! behind [`ComponentSet`]. - -use std::collections::BTreeMap; - -use crate::data::TickQuality; - -/// Analyzer identifier. -pub const ID: &str = "history/quality"; - -/// Analyzer description. -pub const DESCRIPTION: &str = - "Tracks complexity, Halstead, comment quality, and cohesion metrics over commit history."; - -/// Estimated bytes of per-commit payload (pre-allocation hint). -pub const ESTIMATED_TC_SIZE: usize = 2 * 1024; - -/// Read-side accessor over a component analyzer's report map. -/// -/// A missing or wrong-typed key yields `0` (the report-accessor convention). -/// Each component crate's report type implements this (or is adapted to it) so -/// the quality analyzer can pull scalars uniformly. -pub trait ScalarReport { - /// Returns the integer value at `key`, truncating floats toward zero; - /// `0` when absent or non-numeric. - fn get_int(&self, key: &str) -> i64; - /// Returns the float value at `key`; `0.0` when absent or non-numeric. - fn get_float(&self, key: &str) -> f64; +//! [`analyze_file`] feeds the file's UAST root AND its source bytes to +//! `cf-complexity` (operator / name / line recovery) and `cf-halstead` +//! (token-less node recovery), so a file's quality numbers are the numbers +//! `static/complexity`, `static/halstead`, `static/comments` and +//! `static/cohesion` report for the same file. The only difference is the +//! discovery depth cap [`FIND_MAX_DEPTH`]. + +use cf_cohesion::report_value::ReportValue; +use cf_gojson::GoValue; +use cf_uast_node::Node; + +/// Discovery depth cap of the component analyzers: only function / comment +/// nodes at depth `<= 10` below the file root are discovered (the static +/// surfaces run uncapped); the analysis of each discovered subtree is +/// unchanged. +pub const FIND_MAX_DEPTH: usize = 10; + +/// One analyzed file's scalars, one per component metric. +#[derive(Debug, Clone, PartialEq)] +pub struct FileSample { + /// Summed cyclomatic complexity (`total_complexity`). + pub complexity: f64, + /// Summed cognitive complexity (`cognitive_complexity`). + pub cognitive: f64, + /// Highest single-function cyclomatic complexity (`max_complexity`). + pub max_complexity: i64, + /// Function count (`total_functions`). + pub functions: i64, + /// Halstead volume of the file's functions (`volume`). + pub halstead_volume: f64, + /// Halstead effort (`effort`). + pub halstead_effort: f64, + /// Halstead delivered bugs (`delivered_bugs`). + pub delivered_bugs: f64, + /// Good-comment ratio (`overall_score`). + pub comment_score: f64, + /// Documented-declaration ratio (`documentation_coverage`). + pub doc_coverage: f64, + /// Cohesion score (`cohesion_score`); `None` for a file without + /// functions — no cohesion data, as in the static analyzer. + pub cohesion: Option<f64>, } -/// The four component analyzers, run per changed file. +/// Runs the four component analyzers over one parsed file (`root`, with its +/// `source` bytes) and returns its [`FileSample`]. /// -/// Implementors invoke each component's `analyze` and return its report (or -/// `None` on error — a component that errors is silently skipped). The -/// [`accumulate_file`] glue records the documented scalar keys from each. -pub trait ComponentSet { - /// The complexity report type (e.g. `cf_complexity::Report`). - type Complexity: ScalarReport; - /// The Halstead report type. - type Halstead: ScalarReport; - /// The comments report type. - type Comments: ScalarReport; - /// The cohesion report type. - type Cohesion: ScalarReport; - /// The UAST node type fed to each component analyzer. - type Node; - - /// Runs the complexity analyzer; `None` signals an analyzer error. - fn analyze_complexity(&self, root: &Self::Node) -> Option<Self::Complexity>; - /// Runs the Halstead analyzer. - fn analyze_halstead(&self, root: &Self::Node) -> Option<Self::Halstead>; - /// Runs the comments analyzer. - fn analyze_comments(&self, root: &Self::Node) -> Option<Self::Comments>; - /// Runs the cohesion analyzer. - fn analyze_cohesion(&self, root: &Self::Node) -> Option<Self::Cohesion>; -} - -/// Accumulates one file's scalars into `tq`. +/// ``` +/// use cf_quality::analyze_file; /// -/// The four components run in the fixed order `complexity, halstead, comments, -/// cohesion`. A component that errored (`None`) contributes nothing. -pub fn accumulate_file<C: ComponentSet>(components: &C, root: &C::Node, tq: &mut TickQuality) { - if let Some(r) = components.analyze_complexity(root) { - tq.complexities.push(r.get_int("total_complexity") as f64); - tq.cognitives.push(r.get_int("cognitive_complexity") as f64); - tq.max_complexities.push(r.get_int("max_complexity")); - tq.functions.push(r.get_int("total_functions")); - } - if let Some(r) = components.analyze_halstead(root) { - tq.halstead_volumes.push(r.get_float("volume")); - tq.halstead_efforts.push(r.get_float("effort")); - tq.delivered_bugs.push(r.get_float("delivered_bugs")); - } - if let Some(r) = components.analyze_comments(root) { - tq.comment_scores.push(r.get_float("overall_score")); - tq.doc_coverages.push(r.get_float("documentation_coverage")); - } - if let Some(r) = components.analyze_cohesion(root) { - tq.cohesion_scores.push(r.get_float("cohesion_score")); +/// let src = b"package a\n\n// Add adds.\nfunc Add(a, b int) int {\n\tif a > b {\n\t\treturn a + b\n\t}\n\treturn b\n}\n"; +/// let root = cf_uast::Parser::new().parse("a.go", src).unwrap(); +/// let sample = analyze_file(&root, src); +/// assert_eq!(sample.functions, 1); +/// assert_eq!(sample.complexity, 2.0); +/// assert_eq!(sample.comment_score, 1.0); +/// assert_eq!(sample.cohesion, Some(1.0)); +/// ``` +#[must_use] +pub fn analyze_file(root: &Node, source: &[u8]) -> FileSample { + let cx_root = cf_complexity::node::Node::from_uast(root); + let cx = cf_complexity::Analyzer.analyze_with( + Some(&cx_root), + &cf_complexity::AnalysisOptions { + source: Some(source), + find_depth: Some(FIND_MAX_DEPTH), + }, + ); + let halstead = cf_halstead::analyze_with_source(root, Some(source)); + let comments = cf_comments::Analyzer::new() + .analyze_with_find_depth(Some(root), Some(FIND_MAX_DEPTH)) + .map_or_else( + |_| (0.0, 0.0), + |report| { + ( + govalue_float(&report, "overall_score"), + govalue_float(&report, "documentation_coverage"), + ) + }, + ); + let cohesion = cf_cohesion::Analyzer::new().analyze_file_capped(root, Some(FIND_MAX_DEPTH)); + + FileSample { + complexity: govalue_int(&cx, "total_complexity") as f64, + cognitive: govalue_int(&cx, "cognitive_complexity") as f64, + max_complexity: govalue_int(&cx, "max_complexity"), + functions: govalue_int(&cx, "total_functions"), + halstead_volume: halstead.volume, + halstead_effort: halstead.effort, + delivered_bugs: halstead.delivered_bugs, + comment_score: comments.0, + doc_coverage: comments.1, + cohesion: cohesion.has_functions().then(|| { + cohesion + .to_report() + .get("cohesion_score") + .and_then(ReportValue::as_float) + .unwrap_or(0.0) + }), } } -/// Builds the per-commit [`TickQuality`] for one commit's changed UAST roots. -/// -/// `roots` are the post-change nodes (callers skip deletions, which have -/// none). Each root is analyzed by all four components. -pub fn consume_commit<C: ComponentSet>(components: &C, roots: &[&C::Node]) -> TickQuality { - let mut tq = TickQuality::new(); - for root in roots { - accumulate_file(components, root, &mut tq); +/// Integer value at `key` in a cf-gojson report map, truncating floats toward +/// zero; `0` when absent or non-numeric. +fn govalue_int(v: &GoValue, key: &str) -> i64 { + match v { + GoValue::Map(m) => match m.get(key) { + Some(GoValue::Int(n)) => *n, + Some(GoValue::Float(f)) => *f as i64, + _ => 0, + }, + _ => 0, } - tq } -/// Folds per-commit results into the canonical `commit_quality` map -/// (the order-independent core of the framework aggregator). -/// -/// Later writes for the same hash overwrite earlier ones (last write wins). -#[must_use] -pub fn fold_commits<I>(per_commit: I) -> BTreeMap<String, TickQuality> -where - I: IntoIterator<Item = (String, TickQuality)>, -{ - let mut out = BTreeMap::new(); - for (hash, tq) in per_commit { - out.insert(hash, tq); +/// Float value at `key`; `0.0` when absent or non-numeric. +fn govalue_float(v: &GoValue, key: &str) -> f64 { + match v { + GoValue::Map(m) => match m.get(key) { + Some(GoValue::Float(f)) => *f, + Some(GoValue::Int(n)) => *n as f64, + _ => 0.0, + }, + _ => 0.0, } - out } #[cfg(test)] mod tests { use super::*; - // A minimal in-memory ScalarReport for testing accumulate/consume. - struct MapReport(BTreeMap<&'static str, f64>); - impl ScalarReport for MapReport { - fn get_int(&self, key: &str) -> i64 { - self.0.get(key).copied().unwrap_or(0.0) as i64 - } - fn get_float(&self, key: &str) -> f64 { - self.0.get(key).copied().unwrap_or(0.0) - } - } + const EPS: f64 = 1e-9; - struct FakeComponents { - ok: bool, - } - impl ComponentSet for FakeComponents { - type Complexity = MapReport; - type Halstead = MapReport; - type Comments = MapReport; - type Cohesion = MapReport; - type Node = (); - - fn analyze_complexity(&self, _: &()) -> Option<MapReport> { - self.ok.then(|| { - MapReport(BTreeMap::from([ - ("total_complexity", 7.0), - ("cognitive_complexity", 3.0), - ("max_complexity", 5.0), - ("total_functions", 2.0), - ])) - }) - } - fn analyze_halstead(&self, _: &()) -> Option<MapReport> { - self.ok.then(|| { - MapReport(BTreeMap::from([ - ("volume", 100.0), - ("effort", 50.0), - ("delivered_bugs", 0.03), - ])) - }) - } - fn analyze_comments(&self, _: &()) -> Option<MapReport> { - self.ok.then(|| { - MapReport(BTreeMap::from([ - ("overall_score", 0.8), - ("documentation_coverage", 0.6), - ])) - }) - } - fn analyze_cohesion(&self, _: &()) -> Option<MapReport> { - self.ok - .then(|| MapReport(BTreeMap::from([("cohesion_score", 0.9)]))) - } - } + /// One documented function with one `if`, one undocumented function + /// sharing no variable with it. + const TWO_FUNCTIONS_GO: &[u8] = b"package main - // Analog of TestAnalyzer_Consume_ReturnsTCWithTickQuality / MultipleFiles. - #[test] - fn consume_records_one_sample_per_file() { - let c = FakeComponents { ok: true }; - let unit = (); - let tq = consume_commit(&c, &[&unit, &unit]); - assert_eq!(tq.files_analyzed(), 2); - assert_eq!(tq.complexities, vec![7.0, 7.0]); - assert_eq!(tq.max_complexities, vec![5, 5]); - assert_eq!(tq.functions, vec![2, 2]); - assert_eq!(tq.delivered_bugs, vec![0.03, 0.03]); - assert_eq!(tq.cohesion_scores, vec![0.9, 0.9]); +// Add returns the sum of a and b. +func Add(a, b int) int { +\tif a > b { +\t\treturn a + b +\t} +\treturn b +} + +func helper() int { +\tx := 1 +\ty := 2 +\treturn x + y +} +"; + + /// A type declaration only. + const NO_FUNCTIONS_GO: &[u8] = + b"package main\n\n// Config holds settings.\ntype Config struct {\n\tName string\n}\n"; + + /// The complexity crate's Python fixture: its UAST carries token-less + /// operator nodes, so the Halstead counts depend on source recovery. + const PY_FIXTURE: &[u8] = include_bytes!("../../cf-complexity/tests/fixtures/cc.py"); + + fn parse(name: &str, src: &[u8]) -> Node { + cf_uast::Parser::new() + .parse(name, src) + .unwrap_or_else(|e| panic!("parse {name}: {e}")) } - // Analog of TestAnalyzer_Consume_EmptyChanges. + /// The sample carries the static analyzers' numbers for the same file. #[test] - fn consume_empty_yields_zero_files() { - let c = FakeComponents { ok: true }; - let tq = consume_commit(&c, &[]); - assert_eq!(tq.files_analyzed(), 0); + fn sample_matches_the_component_analyzers() { + let root = parse("a.go", TWO_FUNCTIONS_GO); + let sample = analyze_file(&root, TWO_FUNCTIONS_GO); + + assert_eq!(sample.functions, 2); + assert_eq!(sample.complexity, 3.0, "{sample:?}"); + assert_eq!(sample.cognitive, 1.0, "{sample:?}"); + assert_eq!(sample.max_complexity, 2); + + let halstead = cf_halstead::analyze_with_source(&root, Some(TWO_FUNCTIONS_GO)); + assert!((sample.halstead_volume - halstead.volume).abs() < EPS); + assert!((sample.halstead_effort - halstead.effort).abs() < EPS); + assert!((sample.delivered_bugs - halstead.delivered_bugs).abs() < EPS); + assert!(sample.halstead_volume > 0.0); + + // One good comment of one; one of two declarations documented. + assert!((sample.comment_score - 1.0).abs() < EPS, "{sample:?}"); + assert!((sample.doc_coverage - 0.5).abs() < EPS, "{sample:?}"); + + let cohesion = cf_cohesion::Analyzer::new().analyze_file(&root); + assert_eq!(sample.cohesion, Some(cohesion.cohesion_score)); } - // A component that errors contributes nothing. + /// The source bytes matter: the Halstead volume recovered from the + /// source is the static analyzer's, not the token-only value. #[test] - fn consume_skips_errored_components() { - let c = FakeComponents { ok: false }; - let unit = (); - let tq = consume_commit(&c, &[&unit]); - assert_eq!(tq.files_analyzed(), 0); - assert!(tq.cohesion_scores.is_empty()); + fn source_bytes_feed_the_token_recovery() { + let root = parse("cc.py", PY_FIXTURE); + let with_source = analyze_file(&root, PY_FIXTURE).halstead_volume; + let token_only = cf_halstead::analyze(&root).volume; + let static_value = cf_halstead::analyze_with_source(&root, Some(PY_FIXTURE)).volume; + assert!((with_source - static_value).abs() < EPS); + assert!( + (with_source - token_only).abs() > EPS, + "the fixture must exercise source recovery: {with_source} vs {token_only}" + ); } + /// A function-free file has no cohesion data and zero function-based + /// scalars; its comment scalars are still the static analyzer's. #[test] - fn fold_overwrites_duplicate_hash() { - let a = TickQuality { - complexities: vec![1.0], - ..TickQuality::default() - }; - let b = TickQuality { - complexities: vec![2.0, 3.0], - ..TickQuality::default() - }; - let out = fold_commits([("h".to_string(), a), ("h".to_string(), b)]); - assert_eq!(out["h"].complexities, vec![2.0, 3.0]); + fn function_free_file_has_no_cohesion_sample() { + let root = parse("c.go", NO_FUNCTIONS_GO); + let sample = analyze_file(&root, NO_FUNCTIONS_GO); + assert_eq!(sample.cohesion, None, "{sample:?}"); + assert_eq!(sample.functions, 0); + assert_eq!(sample.complexity, 0.0); + assert_eq!(sample.halstead_volume, 0.0); + assert!((sample.comment_score - 1.0).abs() < EPS, "{sample:?}"); } } diff --git a/crates/cf-quality/src/data.rs b/crates/cf-quality/src/data.rs index e56ebc26..aab75d86 100644 --- a/crates/cf-quality/src/data.rs +++ b/crates/cf-quality/src/data.rs @@ -1,21 +1,19 @@ -//! Per-tick / per-commit quality data containers. +//! The per-tick quality sample container. //! -//! [`TickQuality`] and [`TickData`] hold per-file scalar samples gathered while -//! consuming commits; summary statistics are computed at output time -//! ([`crate::metrics`]). -//! -//! Order-independence: the composite quality analyzer keys per-commit -//! [`TickQuality`] by commit hash and merges by concatenation, so the result is -//! independent of the order commits are consumed. +//! [`TickQuality`] holds the per-file scalar samples +//! ([`crate::analyzer::FileSample`]) pushed while consuming commits; summary +//! statistics are computed at output time ([`crate::metrics`]). Samples are +//! concatenated, so a tick's statistics do not depend on the order its +//! commits were consumed in. -use std::collections::BTreeMap; +use crate::analyzer::FileSample; -/// Per-file quality metric values for a single tick. +/// Per-file quality metric values for a single tick (or one commit). /// -/// Values are appended per file while consuming a commit; statistics are -/// computed at output time. The vector field order is the declaration order, -/// which is the order [`merge`](TickQuality::merge) concatenates in. -#[derive(Debug, Clone, Default, PartialEq)] +/// One value per analyzed file in every vector except `cohesion_scores`, +/// which holds one value per file WITH functions (a function-free file has no +/// cohesion data). Statistics are computed at output time. +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TickQuality { /// Cyclomatic complexity per file (`total_complexity`). pub complexities: Vec<f64>, @@ -49,6 +47,35 @@ impl TickQuality { Self::default() } + /// Appends one analyzed file's sample. + /// + /// ``` + /// use cf_quality::{FileSample, TickQuality}; + /// + /// let mut tq = TickQuality::new(); + /// tq.push(&FileSample { + /// complexity: 3.0, cognitive: 1.0, max_complexity: 2, functions: 2, + /// halstead_volume: 69.2, halstead_effort: 317.1, delivered_bugs: 0.023, + /// comment_score: 1.0, doc_coverage: 0.5, cohesion: None, + /// }); + /// assert_eq!(tq.files_analyzed(), 1); + /// assert!(tq.cohesion_scores.is_empty(), "no cohesion data for a function-free sample"); + /// ``` + pub fn push(&mut self, sample: &FileSample) { + self.complexities.push(sample.complexity); + self.cognitives.push(sample.cognitive); + self.max_complexities.push(sample.max_complexity); + self.functions.push(sample.functions); + self.halstead_volumes.push(sample.halstead_volume); + self.halstead_efforts.push(sample.halstead_effort); + self.delivered_bugs.push(sample.delivered_bugs); + self.comment_scores.push(sample.comment_score); + self.doc_coverages.push(sample.doc_coverage); + if let Some(cohesion) = sample.cohesion { + self.cohesion_scores.push(cohesion); + } + } + /// Incorporates values from `other` into `self` by concatenation. /// /// Appends each per-file slice in declaration order. An empty `other` is a @@ -96,22 +123,38 @@ impl TickQuality { } } -/// Per-tick aggregated payload. -/// -/// `commit_quality` maps commit hash (lowercase hex) to that commit's -/// [`TickQuality`]. A [`BTreeMap`] is used so that any direct iteration is -/// deterministic; the canonical machine output sorts keys regardless. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct TickData { - /// Maps commit hash (hex) to per-commit [`TickQuality`]. - pub commit_quality: BTreeMap<String, TickQuality>, -} - #[cfg(test)] mod tests { use super::*; - // Mirrors the reference suite's TestTickQuality_Merge. + #[test] + fn push_appends_one_value_per_metric() { + let mut tq = TickQuality::new(); + let with_functions = FileSample { + complexity: 2.0, + cognitive: 1.0, + max_complexity: 2, + functions: 1, + halstead_volume: 25.0, + halstead_effort: 129.0, + delivered_bugs: 0.008, + comment_score: 1.0, + doc_coverage: 1.0, + cohesion: Some(1.0), + }; + let function_free = FileSample { + functions: 0, + cohesion: None, + ..with_functions.clone() + }; + tq.push(&with_functions); + tq.push(&function_free); + assert_eq!(tq.files_analyzed(), 2); + assert_eq!(tq.functions, vec![1, 0]); + assert_eq!(tq.cohesion_scores, vec![1.0]); + assert_eq!(tq.halstead_volumes.len(), 2); + } + #[test] fn merge_concatenates_per_field() { let mut tq1 = TickQuality { diff --git a/crates/cf-quality/src/lib.rs b/crates/cf-quality/src/lib.rs index a6c34219..2b94645e 100644 --- a/crates/cf-quality/src/lib.rs +++ b/crates/cf-quality/src/lib.rs @@ -1,36 +1,26 @@ //! `cf-quality` — the composite **quality** history analyzer (`history/quality`). //! -//! Runs four static component analyzers — complexity, Halstead, comments, and -//! cohesion — on each changed file's UAST per commit, recording **scalars -//! only**, and aggregates them **order-independently** (per-commit results -//! keyed by hash; merging is a no-op). +//! Runs the four static component analyzers — complexity, Halstead, comments, +//! and cohesion — on each analyzed file's UAST per commit, recording one +//! scalar sample per file ([`analyzer::analyze_file`]), and aggregates the +//! samples per tick ([`metrics::compute_all_metrics`]). //! //! # Module map //! -//! * [`data`] — per-tick / per-commit data containers. -//! * [`analyzer`] — consume/accumulate glue over the component analyzers. +//! * [`analyzer`] — the per-file sample over the four component analyzers. +//! * [`data`] — the per-tick sample container. //! * [`metrics`] — per-tick statistics, time-series and aggregate computation. -//! * [`store`] — store record kinds and plot-section titles. -//! * [`serialize`] — report serialization (charts themselves are non-binding -//! cosmetic output). +//! * [`serialize`] — report serialization through [`cf_gojson`] / +//! [`cf_reportutil`] (charts themselves are non-binding cosmetic output). //! -//! # Compatibility +//! # Which files count //! -//! Every machine-format report is serialized through [`cf_gojson`] / -//! [`cf_goyaml`] and the CFB1 envelope via [`cf_reportutil`] (see -//! [`serialize`]), never serde defaults; the bytes are pinned against the -//! reference binary by `tests/compat`. Wrapper structs -//! ([`metrics::TickStats`], [`metrics::TimeSeriesEntry`], -//! [`metrics::AggregateData`], [`metrics::ComputedMetrics`]) emit fields in -//! declaration order honoring `omitempty`; the per-commit summary map is -//! map-origin and byte-sorts its keys. -//! -//! # Numeric parity -//! -//! All statistics (`mean`/`median`/`P95`/`max`/`min`/`sum`) come from -//! [`cf_alg_stats`], the operation-for-operation statistics kernel shared with -//! the reference implementation, so the values flowing into reports are -//! bit-faithful. +//! A file contributes a sample when its UAST parse succeeded; a failed parse +//! contributes nothing. A parsed file without functions contributes its +//! complexity / Halstead / comment scalars (zeros for the function-based +//! ones) but NO cohesion sample — the static cohesion analyzer treats such a +//! file as "no data", and so does quality. A tick in which no file was +//! analyzed is absent from the time series. #![forbid(unsafe_code)] #![warn(missing_docs)] @@ -38,19 +28,10 @@ pub mod analyzer; pub mod data; pub mod metrics; pub mod serialize; -pub mod store; -pub use analyzer::{ - accumulate_file, consume_commit, fold_commits, ComponentSet, ScalarReport, DESCRIPTION, - ESTIMATED_TC_SIZE, ID, -}; -pub use data::{TickData, TickQuality}; +pub use analyzer::{analyze_file, FileSample, FIND_MAX_DEPTH}; +pub use data::TickQuality; pub use metrics::{ - aggregate_commits_to_ticks, commit_summary, compute_all_metrics, compute_tick_stats, - extract_commit_time_series, parse_report_data, AggregateData, CommitSummary, ComputedMetrics, - ReportData, TickBounds, TickStats, TimeSeriesEntry, + commit_summary, compute_all_metrics, compute_tick_stats, AggregateData, CommitSummary, + ComputedMetrics, ReportData, TickBounds, TickStats, TimeSeriesEntry, }; -pub use store::{KIND_AGGREGATE, KIND_TIME_SERIES}; - -/// Crate name, used by smoke tests to confirm the module links. -pub const CRATE_NAME: &str = "cf-quality"; diff --git a/crates/cf-quality/src/metrics.rs b/crates/cf-quality/src/metrics.rs index e8f85d82..b75c0ae9 100644 --- a/crates/cf-quality/src/metrics.rs +++ b/crates/cf-quality/src/metrics.rs @@ -1,17 +1,23 @@ //! Quality statistics: per-tick stats, time-series and aggregate computation. //! //! All numeric routines delegate to [`cf_alg_stats`] (the shared statistics -//! kernel) so that the mean / median / P95 / max / min / sum values flowing -//! into machine reports are computed operation-for-operation as in the -//! reference implementation. +//! kernel). //! -//! # Compatibility +//! # Tick semantics +//! +//! A tick enters the time series and the aggregate only when at least one +//! file was analyzed in it ([`compute_all_metrics`]); the cohesion aggregates +//! (`cohesion_mean_mean`, `min_cohesion`) are taken over the ticks that hold +//! at least one cohesion sample, so `min <= mean` always holds. Within a +//! tick, `cohesion_mean` / `cohesion_min` are `0` when no file has a function +//! (no cohesion data). +//! +//! # Serialization //! //! [`TickStats`], [`TimeSeriesEntry`], [`AggregateData`] and [`ComputedMetrics`] -//! are *wrapper* structs: their fields serialize in **declaration order**, -//! honoring `omitempty` on `start_time` / `end_time`. They are emitted through -//! the fixed-order `GoMap` builder in [`crate::serialize`], never via serde -//! defaults; output bytes are pinned by `tests/compat`. +//! serialize their fields in **declaration order**, honoring `omitempty` on +//! `start_time` / `end_time`, through the fixed-order `GoMap` builder in +//! [`crate::serialize`]. use std::collections::BTreeMap; @@ -87,8 +93,8 @@ pub struct TickStats { /// Computes per-tick statistics. /// -/// Returns the zero value when no files were analyzed (pinned report -/// behaviour for empty ticks). +/// Returns the zero value when no files were analyzed; `cohesion_mean` / +/// `cohesion_min` are `0` when the tick holds no cohesion sample. /// /// ``` /// use cf_quality::data::TickQuality; @@ -215,160 +221,67 @@ pub struct ReportData { pub tick_bounds: BTreeMap<i64, TickBounds>, } -/// Groups per-commit quality into per-tick quality. -/// -/// For every tick, merges the [`TickQuality`] of each of its commit hashes (in -/// the order given by `commits_by_tick`), skipping hashes absent from -/// `commit_quality`. Ticks with no present commits are omitted. Returns an -/// empty map when either input is empty. -/// -/// ``` -/// use cf_quality::data::TickQuality; -/// use cf_quality::metrics::aggregate_commits_to_ticks; -/// use std::collections::BTreeMap; -/// -/// let commit_quality = BTreeMap::from([ -/// ("aaa".to_string(), TickQuality { complexities: vec![1.0], ..Default::default() }), -/// ("bbb".to_string(), TickQuality { complexities: vec![2.0], ..Default::default() }), -/// ]); -/// // Tick 0 references "aaa", "bbb", and an absent "ccc" (skipped). -/// let commits_by_tick = BTreeMap::from([(0i64, vec![ -/// "aaa".to_string(), "bbb".to_string(), "ccc".to_string(), -/// ])]); -/// -/// let per_tick = aggregate_commits_to_ticks(&commit_quality, &commits_by_tick); -/// assert_eq!(per_tick[&0].complexities, vec![1.0, 2.0]); -/// -/// // Either input empty → empty result. -/// assert!(aggregate_commits_to_ticks(&BTreeMap::new(), &commits_by_tick).is_empty()); -/// ``` -#[must_use] -pub fn aggregate_commits_to_ticks( - commit_quality: &BTreeMap<String, TickQuality>, - commits_by_tick: &BTreeMap<i64, Vec<String>>, -) -> BTreeMap<i64, TickQuality> { - let mut result = BTreeMap::new(); - - if commit_quality.is_empty() || commits_by_tick.is_empty() { - return result; - } - - for (&tick, hashes) in commits_by_tick { - let mut merged: Option<TickQuality> = None; - - for hash in hashes { - let Some(cq) = commit_quality.get(hash) else { - continue; - }; - merged.get_or_insert_with(TickQuality::new).merge(cq); - } - - if let Some(m) = merged { - result.insert(tick, m); - } - } - - result -} - -/// Builds [`ReportData`] from the canonical inputs. -/// -/// Aggregates per-commit quality into per-tick quality only when -/// `commit_quality` is non-empty; otherwise `tick_quality` is empty. -#[must_use] -pub fn parse_report_data( - commit_quality: &BTreeMap<String, TickQuality>, - commits_by_tick: &BTreeMap<i64, Vec<String>>, - tick_bounds: BTreeMap<i64, TickBounds>, -) -> ReportData { - let tick_quality = if !commit_quality.is_empty() { - aggregate_commits_to_ticks(commit_quality, commits_by_tick) - } else { - BTreeMap::new() - }; - - ReportData { - tick_quality, - tick_bounds, - } -} - -/// Runs all quality metrics. -/// -/// Iterates ticks in ascending order. The global minimum comment-score and -/// cohesion are seeded to `+∞` and updated only for ticks with -/// `files_analyzed > 0` whose value is strictly smaller; if no such tick -/// exists the field is reset to `0` — this infinity-seed/reset sequence is -/// pinned reference behaviour and must be reproduced exactly. +/// Runs all quality metrics over the ticks that analyzed at least one file, +/// in ascending tick order. The aggregate means are the means of the per-tick +/// values; `min_comment_score` is the minimum over those ticks and +/// `min_cohesion` / `cohesion_mean_mean` are taken over the ticks that hold a +/// cohesion sample (`0` when no tick does). #[must_use] pub fn compute_all_metrics(input: &ReportData) -> ComputedMetrics { - let ticks: Vec<i64> = input.tick_quality.keys().copied().collect(); - let mut time_series = Vec::with_capacity(ticks.len()); - - let mut complexity_medians = Vec::with_capacity(ticks.len()); - let mut complexity_p95s = Vec::with_capacity(ticks.len()); - let mut halstead_medians = Vec::with_capacity(ticks.len()); - let mut comment_means = Vec::with_capacity(ticks.len()); - let mut cohesion_means = Vec::with_capacity(ticks.len()); + let analyzed: Vec<(i64, &TickQuality)> = input + .tick_quality + .iter() + .filter(|(_, tq)| tq.files_analyzed() > 0) + .map(|(tick, tq)| (*tick, tq)) + .collect(); + let mut time_series = Vec::with_capacity(analyzed.len()); + + let mut complexity_medians = Vec::with_capacity(analyzed.len()); + let mut complexity_p95s = Vec::with_capacity(analyzed.len()); + let mut halstead_medians = Vec::with_capacity(analyzed.len()); + let mut comment_means = Vec::with_capacity(analyzed.len()); + let mut comment_mins = Vec::with_capacity(analyzed.len()); + let mut cohesion_means = Vec::with_capacity(analyzed.len()); + let mut cohesion_mins = Vec::with_capacity(analyzed.len()); let mut total_files: i64 = 0; let mut total_bugs: f64 = 0.0; - let mut global_min_comment = f64::INFINITY; - let mut global_min_cohesion = f64::INFINITY; - - for &tick in &ticks { - let tq = &input.tick_quality[&tick]; + for (tick, tq) in analyzed { let ts = compute_tick_stats(tq); - - let mut entry = TimeSeriesEntry { + let bounds = input.tick_bounds.get(&tick).cloned().unwrap_or_default(); + time_series.push(TimeSeriesEntry { tick, + start_time: bounds.start_time, + end_time: bounds.end_time, stats: ts, - ..TimeSeriesEntry::default() - }; - - if let Some(bounds) = input.tick_bounds.get(&tick) { - entry.start_time = bounds.start_time.clone(); - entry.end_time = bounds.end_time.clone(); - } - - time_series.push(entry); + }); complexity_medians.push(ts.complexity_median); complexity_p95s.push(ts.complexity_p95); halstead_medians.push(ts.halstead_vol_median); comment_means.push(ts.comment_score_mean); - cohesion_means.push(ts.cohesion_mean); + comment_mins.push(ts.comment_score_min); + if !tq.cohesion_scores.is_empty() { + cohesion_means.push(ts.cohesion_mean); + cohesion_mins.push(ts.cohesion_min); + } total_files += ts.files_analyzed; total_bugs += ts.delivered_bugs_sum; - - if ts.comment_score_min < global_min_comment && ts.files_analyzed > 0 { - global_min_comment = ts.comment_score_min; - } - if ts.cohesion_min < global_min_cohesion && ts.files_analyzed > 0 { - global_min_cohesion = ts.cohesion_min; - } - } - - if global_min_comment.is_infinite() && global_min_comment > 0.0 { - global_min_comment = 0.0; - } - if global_min_cohesion.is_infinite() && global_min_cohesion > 0.0 { - global_min_cohesion = 0.0; } let aggregate = AggregateData { - total_ticks: ticks.len() as i64, + total_ticks: time_series.len() as i64, total_files_analyzed: total_files, complexity_median_mean: stats::mean(&complexity_medians), complexity_p95_mean: stats::mean(&complexity_p95s), halstead_vol_median_mean: stats::mean(&halstead_medians), total_delivered_bugs: total_bugs, comment_score_mean_mean: stats::mean(&comment_means), - min_comment_score: global_min_comment, + min_comment_score: stats::min(&comment_mins), cohesion_mean_mean: stats::mean(&cohesion_means), - min_cohesion: global_min_cohesion, + min_cohesion: stats::min(&cohesion_mins), }; ComputedMetrics { @@ -377,7 +290,7 @@ pub fn compute_all_metrics(input: &ReportData) -> ComputedMetrics { } } -/// Per-commit summary used by the unified timeseries / drain paths. +/// Per-commit summary used by the `--format timeseries` paths. /// /// Field order here is irrelevant — the machine output for this map is /// *map-origin* and therefore byte-sorts its keys on encode. @@ -408,9 +321,6 @@ pub struct CommitSummary { } /// Computes the per-commit summary for one [`TickQuality`]. -/// -/// Shared by the commit-timeseries and drain paths, which build identical -/// maps. #[must_use] pub fn commit_summary(tq: &TickQuality) -> CommitSummary { let ts = compute_tick_stats(tq); @@ -429,19 +339,6 @@ pub fn commit_summary(tq: &TickQuality) -> CommitSummary { } } -/// Builds the per-commit summary map. -/// -/// Returns an empty map when `commit_quality` is empty. -#[must_use] -pub fn extract_commit_time_series( - commit_quality: &BTreeMap<String, TickQuality>, -) -> BTreeMap<String, CommitSummary> { - commit_quality - .iter() - .map(|(hash, tq)| (hash.clone(), commit_summary(tq))) - .collect() -} - #[cfg(test)] mod tests { use super::*; @@ -450,26 +347,16 @@ mod tests { assert!((a - b).abs() <= d, "expected {b} got {a} (delta {d})"); } - fn cq(pairs: &[(&str, TickQuality)]) -> BTreeMap<String, TickQuality> { - pairs - .iter() - .map(|(h, q)| ((*h).into(), q.clone())) - .collect() - } - - fn ct(pairs: &[(i64, &[&str])]) -> BTreeMap<i64, Vec<String>> { - pairs - .iter() - .map(|(t, hs)| (*t, hs.iter().map(|s| (*s).to_string()).collect())) - .collect() + /// A [`ReportData`] over `(tick, quality)` pairs without bounds. + fn report(ticks: Vec<(i64, TickQuality)>) -> ReportData { + ReportData { + tick_quality: ticks.into_iter().collect(), + tick_bounds: BTreeMap::new(), + } } - const HASH_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const HASH_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - - // Mirrors reference test TestComputeTickStats. #[test] - fn compute_tick_stats_matches_go() { + fn compute_tick_stats_over_five_files() { let tq = TickQuality { complexities: vec![2.0, 4.0, 6.0, 8.0, 10.0], cognitives: vec![1.0, 2.0, 3.0, 4.0, 5.0], @@ -501,7 +388,6 @@ mod tests { assert_eq!(ts.max_complexity, 10); } - // Mirrors reference test TestComputeTickStats_ZeroFiles. #[test] fn compute_tick_stats_zero_files() { let ts = compute_tick_stats(&TickQuality::default()); @@ -510,12 +396,11 @@ mod tests { delta(ts.complexity_median, 0.0, 0.01); } - // Mirrors reference test TestComputeAllMetrics_FromCommitData. #[test] - fn compute_all_metrics_from_commit_data() { - let commit_quality = cq(&[ + fn compute_all_metrics_over_two_ticks() { + let input = report(vec![ ( - HASH_A, + 0, TickQuality { complexities: vec![10.0, 20.0], halstead_volumes: vec![100.0, 200.0], @@ -526,7 +411,7 @@ mod tests { }, ), ( - HASH_B, + 1, TickQuality { complexities: vec![30.0], halstead_volumes: vec![300.0], @@ -537,246 +422,148 @@ mod tests { }, ), ]); - let commits_by_tick = ct(&[(0, &[HASH_A]), (1, &[HASH_B])]); - - let input = parse_report_data(&commit_quality, &commits_by_tick, BTreeMap::new()); let computed = compute_all_metrics(&input); assert_eq!(computed.time_series.len(), 2); - assert_eq!(computed.aggregate.total_ticks, 2); - assert_eq!(computed.aggregate.total_files_analyzed, 3); + let agg = computed.aggregate; + assert_eq!(agg.total_ticks, 2); + assert_eq!(agg.total_files_analyzed, 3); + delta(agg.complexity_median_mean, 22.5, 1e-9); + delta(agg.total_delivered_bugs, 0.6, 1e-9); + delta(agg.min_comment_score, 0.5, 1e-9); + delta(agg.min_cohesion, 0.8, 1e-9); + delta(agg.cohesion_mean_mean, (0.85 + 1.0) / 2.0, 1e-9); } - // Mirrors reference test TestComputeAllMetrics_FromCanonical. #[test] - fn compute_all_metrics_from_canonical() { - let commit_quality = cq(&[( - HASH_A, + fn compute_all_metrics_carries_tick_bounds() { + let mut input = report(vec![( + 3, TickQuality { - complexities: vec![10.0, 20.0], - halstead_volumes: vec![100.0, 200.0], - delivered_bugs: vec![0.1, 0.2], - comment_scores: vec![0.5, 0.7], - cohesion_scores: vec![0.8, 0.9], + complexities: vec![1.0], ..TickQuality::default() }, )]); - let commits_by_tick = ct(&[(0, &[HASH_A])]); - - let input = parse_report_data(&commit_quality, &commits_by_tick, BTreeMap::new()); + input.tick_bounds.insert( + 3, + TickBounds { + start_time: "2024-01-01T00:00:00Z".into(), + end_time: "2024-01-01T12:00:00Z".into(), + }, + ); let computed = compute_all_metrics(&input); - - assert_eq!(computed.time_series.len(), 1); - assert_eq!(computed.aggregate.total_ticks, 1); - assert_eq!(computed.aggregate.total_files_analyzed, 2); + assert_eq!(computed.time_series[0].tick, 3); + assert_eq!(computed.time_series[0].start_time, "2024-01-01T00:00:00Z"); + assert_eq!(computed.time_series[0].end_time, "2024-01-01T12:00:00Z"); } - // Mirrors reference test TestComputeAllMetrics_Empty. #[test] fn compute_all_metrics_empty() { - let input = parse_report_data(&BTreeMap::new(), &BTreeMap::new(), BTreeMap::new()); - let computed = compute_all_metrics(&input); + let computed = compute_all_metrics(&report(Vec::new())); assert!(computed.time_series.is_empty()); - assert_eq!(computed.aggregate.total_ticks, 0); + assert_eq!(computed.aggregate, AggregateData::default()); } - // Mirrors reference test TestComputeAllMetrics_Basic (buildTestQualityReport). #[test] - fn compute_all_metrics_basic() { - let commit_quality = cq(&[ - ( - "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", - TickQuality { - complexities: vec![10.0, 20.0, 30.0], - cognitives: vec![5.0, 10.0, 15.0], - max_complexities: vec![4, 8, 12], - functions: vec![2, 3, 5], - halstead_volumes: vec![100.0, 200.0, 300.0], - halstead_efforts: vec![50.0, 100.0, 150.0], - delivered_bugs: vec![0.1, 0.2, 0.3], - comment_scores: vec![0.5, 0.7, 0.9], - doc_coverages: vec![0.4, 0.6, 0.8], - cohesion_scores: vec![0.8, 0.9, 1.0], - }, - ), - ( - "c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2", - TickQuality { - complexities: vec![15.0, 25.0], - cognitives: vec![7.0, 12.0], - max_complexities: vec![6, 10], - functions: vec![3, 4], - halstead_volumes: vec![150.0, 250.0], - halstead_efforts: vec![75.0, 125.0], - delivered_bugs: vec![0.2, 0.3], - comment_scores: vec![0.6, 0.8], - doc_coverages: vec![0.5, 0.7], - cohesion_scores: vec![0.85, 0.95], - }, - ), - ( - "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", - TickQuality { - complexities: vec![5.0, 15.0, 25.0, 35.0], - cognitives: vec![3.0, 8.0, 13.0, 18.0], - max_complexities: vec![3, 7, 10, 14], - functions: vec![1, 3, 5, 7], - halstead_volumes: vec![50.0, 150.0, 250.0, 350.0], - halstead_efforts: vec![25.0, 75.0, 125.0, 175.0], - delivered_bugs: vec![0.05, 0.15, 0.25, 0.35], - comment_scores: vec![0.4, 0.6, 0.8, 1.0], - doc_coverages: vec![0.3, 0.5, 0.7, 0.9], - cohesion_scores: vec![0.7, 0.8, 0.9, 1.0], - }, - ), - ]); - let commits_by_tick = ct(&[ - (0, &["c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"]), - (1, &["c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2"]), - (2, &["c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"]), - ]); - - let input = parse_report_data(&commit_quality, &commits_by_tick, BTreeMap::new()); - let computed = compute_all_metrics(&input); - - assert!(!computed.time_series.is_empty()); - assert!(computed.aggregate.total_ticks > 0); - assert!(computed.aggregate.total_files_analyzed > 0); - assert!(computed.aggregate.complexity_median_mean > 0.0); - assert!(computed.aggregate.complexity_p95_mean > 0.0); - assert!(computed.aggregate.halstead_vol_median_mean > 0.0); - assert!(computed.aggregate.total_delivered_bugs > 0.0); - } - - // Mirrors reference test TestAggregateCommitsToTicks_* . - #[test] - fn aggregate_single_commit_per_tick() { - let commit_quality = cq(&[ - ( - HASH_A, - TickQuality { - complexities: vec![10.0, 20.0], - ..TickQuality::default() - }, - ), - ( - HASH_B, - TickQuality { - complexities: vec![30.0], - ..TickQuality::default() - }, - ), - ]); - let commits_by_tick = ct(&[(0, &[HASH_A]), (1, &[HASH_B])]); - let result = aggregate_commits_to_ticks(&commit_quality, &commits_by_tick); - assert_eq!(result.len(), 2); - assert_eq!(result[&0].complexities.len(), 2); - assert_eq!(result[&1].complexities.len(), 1); - } - - #[test] - fn aggregate_multiple_commits_per_tick() { - let commit_quality = cq(&[ - ( - HASH_A, - TickQuality { - complexities: vec![10.0], - halstead_volumes: vec![100.0], - ..TickQuality::default() - }, - ), - ( - HASH_B, - TickQuality { - complexities: vec![20.0], - halstead_volumes: vec![200.0], - ..TickQuality::default() - }, - ), - ]); - let commits_by_tick = ct(&[(0, &[HASH_A, HASH_B])]); - let result = aggregate_commits_to_ticks(&commit_quality, &commits_by_tick); - assert_eq!(result.len(), 1); - assert_eq!(result[&0].complexities.len(), 2); - assert_eq!(result[&0].halstead_volumes.len(), 2); - } - - #[test] - fn aggregate_empty() { - let result = aggregate_commits_to_ticks(&BTreeMap::new(), &BTreeMap::new()); - assert!(result.is_empty()); + fn commit_summary_of_two_files() { + let tq = TickQuality { + complexities: vec![10.0, 20.0], + cognitives: vec![5.0, 8.0], + max_complexities: vec![12, 18], + functions: vec![3, 5], + halstead_volumes: vec![100.0, 200.0], + halstead_efforts: vec![50.0, 100.0], + delivered_bugs: vec![0.1, 0.2], + comment_scores: vec![0.5, 0.7], + doc_coverages: vec![0.6, 0.8], + cohesion_scores: vec![0.8, 0.9], + }; + let summary = commit_summary(&tq); + delta(summary.complexity_median, 15.0, 0.01); + delta(summary.cognitive_median, 6.5, 0.01); + assert_eq!(summary.max_complexity, 18); + assert_eq!(summary.functions, 8); + delta(summary.halstead_effort_median, 75.0, 0.01); + delta(summary.delivered_bugs_sum, 0.3, 0.01); + delta(summary.comment_score_min, 0.5, 0.01); + delta(summary.doc_coverage_mean, 0.7, 0.01); + delta(summary.cohesion_min, 0.8, 0.01); + assert_eq!(summary.files_analyzed, 2); } + /// A tick whose commits yielded no analyzable file carries no statistics: + /// it is absent from the time series and never drags an aggregate mean + /// below the aggregate minimum. #[test] - fn aggregate_missing_commit() { - let commit_quality = cq(&[( - HASH_A, + fn zero_file_ticks_are_excluded_from_series_and_aggregate() { + let mut tick_quality = BTreeMap::new(); + tick_quality.insert( + 0, TickQuality { - complexities: vec![10.0], + complexities: vec![2.0], + comment_scores: vec![1.0], + doc_coverages: vec![1.0], + cohesion_scores: vec![1.0], ..TickQuality::default() }, - )]); - let commits_by_tick = ct(&[(0, &[HASH_A, "cccccccccccccccccccccccccccccccccccccccc"])]); - let result = aggregate_commits_to_ticks(&commit_quality, &commits_by_tick); - assert_eq!(result.len(), 1); - assert_eq!(result[&0].complexities.len(), 1); - } - - // Mirrors reference test TestExtractCommitTimeSeries. - #[test] - fn extract_commit_time_series_matches_go() { - let commit_quality = cq(&[ - ( - HASH_A, - TickQuality { - complexities: vec![10.0, 20.0], - cognitives: vec![5.0, 8.0], - max_complexities: vec![12, 18], - functions: vec![3, 5], - halstead_volumes: vec![100.0, 200.0], - halstead_efforts: vec![50.0, 100.0], - delivered_bugs: vec![0.1, 0.2], - comment_scores: vec![0.5, 0.7], - doc_coverages: vec![0.6, 0.8], - cohesion_scores: vec![0.8, 0.9], - }, - ), - ( - HASH_B, - TickQuality { - complexities: vec![30.0], - cognitives: vec![15.0], - max_complexities: vec![25], - functions: vec![7], - halstead_volumes: vec![300.0], - halstead_efforts: vec![150.0], - delivered_bugs: vec![0.3], - comment_scores: vec![0.9], - doc_coverages: vec![0.95], - cohesion_scores: vec![1.0], - }, - ), - ]); - - let result = extract_commit_time_series(&commit_quality); - assert_eq!(result.len(), 2); - - let a = result[HASH_A]; - delta(a.complexity_median, 15.0, 0.01); - assert_eq!(a.max_complexity, 18); - assert_eq!(a.functions, 8); - delta(a.delivered_bugs_sum, 0.3, 0.01); - delta(a.comment_score_min, 0.5, 0.01); - delta(a.cohesion_min, 0.8, 0.01); - assert_eq!(a.files_analyzed, 2); - - let b = result[HASH_B]; - delta(b.complexity_median, 30.0, 0.01); - assert_eq!(b.files_analyzed, 1); + ); + tick_quality.insert(1, TickQuality::default()); + tick_quality.insert( + 2, + TickQuality { + complexities: vec![4.0], + comment_scores: vec![1.0], + doc_coverages: vec![1.0], + cohesion_scores: vec![1.0], + ..TickQuality::default() + }, + ); + let input = ReportData { + tick_quality, + tick_bounds: BTreeMap::new(), + }; + let computed = compute_all_metrics(&input); + let ticks: Vec<i64> = computed.time_series.iter().map(|e| e.tick).collect(); + assert_eq!(ticks, vec![0, 2], "{:?}", computed.time_series); + let agg = computed.aggregate; + assert_eq!(agg.total_ticks, 2); + assert_eq!(agg.total_files_analyzed, 2); + delta(agg.complexity_median_mean, 3.0, 1e-9); + delta(agg.cohesion_mean_mean, 1.0, 1e-9); + delta(agg.min_cohesion, 1.0, 1e-9); + delta(agg.comment_score_mean_mean, 1.0, 1e-9); + assert!(agg.min_cohesion <= agg.cohesion_mean_mean); + assert!(agg.min_comment_score <= agg.comment_score_mean_mean); } + /// Cohesion aggregates are computed over the ticks that have cohesion + /// data: a tick of function-free files (no cohesion sample) neither + /// lowers `cohesion_mean_mean` nor sets `min_cohesion` to 0. #[test] - fn extract_commit_time_series_empty() { - assert!(extract_commit_time_series(&BTreeMap::new()).is_empty()); + fn ticks_without_cohesion_samples_do_not_zero_the_cohesion_aggregate() { + let mut tick_quality = BTreeMap::new(); + tick_quality.insert( + 0, + TickQuality { + complexities: vec![3.0], + cohesion_scores: vec![0.75], + ..TickQuality::default() + }, + ); + tick_quality.insert( + 1, + TickQuality { + complexities: vec![0.0], + ..TickQuality::default() + }, + ); + let input = ReportData { + tick_quality, + tick_bounds: BTreeMap::new(), + }; + let agg = compute_all_metrics(&input).aggregate; + assert_eq!(agg.total_ticks, 2); + delta(agg.cohesion_mean_mean, 0.75, 1e-9); + delta(agg.min_cohesion, 0.75, 1e-9); } } diff --git a/crates/cf-quality/src/serialize.rs b/crates/cf-quality/src/serialize.rs index 25ee0a16..01dedf19 100644 --- a/crates/cf-quality/src/serialize.rs +++ b/crates/cf-quality/src/serialize.rs @@ -1,8 +1,8 @@ //! Report-contract serialization for the quality machine reports. //! //! All machine output is routed through [`cf_gojson`] (the report-format JSON -//! encoder) — never serde defaults; the bytes are pinned against the reference -//! binary by `tests/compat`. The CFB1 "bin" envelope is produced via +//! encoder) — never serde defaults; the bytes are pinned by the CLI-level +//! end-to-end tests. The CFB1 "bin" envelope is produced via //! [`cf_reportutil::encode_binary_envelope`]. //! //! The YAML report path is owned by the `cf-analyze` cross-format conversion diff --git a/crates/cf-quality/src/store.rs b/crates/cf-quality/src/store.rs deleted file mode 100644 index 9d0d5e1b..00000000 --- a/crates/cf-quality/src/store.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Store record kinds and the per-tick / aggregate record set. -//! -//! The store writer streams two record kinds: -//! -//! * `"time_series"` — one [`crate::metrics::TimeSeriesEntry`] per tick, sorted. -//! * `"aggregate"` — a single [`crate::metrics::AggregateData`]. -//! -//! The actual store I/O (begin/write/close, kind discovery) lives in -//! `cf-analyze`'s report store; this module produces the record *payloads* via -//! [`crate::serialize`] and names the kinds. - -use crate::metrics::ComputedMetrics; - -/// Store kind: per-tick time-series records. -pub const KIND_TIME_SERIES: &str = "time_series"; -/// Store kind: the single aggregate record. -pub const KIND_AGGREGATE: &str = "aggregate"; - -/// Plot-section titles emitted when rebuilding sections from the store. -/// -/// These are asserted byte-for-byte by the store round-trip tests; the charts -/// themselves are non-binding cosmetic output. -pub const SECTION_TITLE_COMPLEXITY: &str = "Cyclomatic Complexity Over Time"; -/// Section title for the Halstead chart. -pub const SECTION_TITLE_HALSTEAD: &str = "Halstead Volume Over Time"; -/// Section title for the summary stat grid. -pub const SECTION_TITLE_SUMMARY: &str = "Code Quality Summary"; - -/// Returns the JSON-record payloads to write to the store, in the pinned -/// write order. -/// -/// The first element is the `time_series` kind (a JSON array of entries); the -/// second is the `aggregate` kind (a single object). Each payload is produced -/// by the report-format encoder so a round-trip is byte-stable. -#[must_use] -pub fn store_records(metrics: &ComputedMetrics) -> Vec<(&'static str, ComputedMetricsView<'_>)> { - vec![ - (KIND_TIME_SERIES, ComputedMetricsView::TimeSeries(metrics)), - (KIND_AGGREGATE, ComputedMetricsView::Aggregate(metrics)), - ] -} - -/// A view selecting which part of [`ComputedMetrics`] a store kind serializes. -#[derive(Debug, Clone, Copy)] -pub enum ComputedMetricsView<'a> { - /// The `time_series` slice. - TimeSeries(&'a ComputedMetrics), - /// The `aggregate` object. - Aggregate(&'a ComputedMetrics), -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::metrics::ComputedMetrics; - - #[test] - fn store_records_order_and_kinds() { - let m = ComputedMetrics::default(); - let recs = store_records(&m); - assert_eq!(recs[0].0, KIND_TIME_SERIES); - assert_eq!(recs[1].0, KIND_AGGREGATE); - } - - #[test] - fn section_titles_match_go() { - assert_eq!(SECTION_TITLE_COMPLEXITY, "Cyclomatic Complexity Over Time"); - assert_eq!(SECTION_TITLE_HALSTEAD, "Halstead Volume Over Time"); - assert_eq!(SECTION_TITLE_SUMMARY, "Code Quality Summary"); - } -} diff --git a/crates/cf-renderer/Cargo.toml b/crates/cf-renderer/Cargo.toml index ee01ab36..55906537 100644 --- a/crates/cf-renderer/Cargo.toml +++ b/crates/cf-renderer/Cargo.toml @@ -14,7 +14,7 @@ thiserror = "1" # report-format serialization crates from the design (DESIGN.md §1.1, §2.3: # cf-gojson / cf-goyaml), NEVER serde_json/serde_yaml, so output stays # byte-identical with the reference implementation (pinned by -# tests/compat). The encoder subset this crate needs lives in its +# the report e2e suites in bins/codefang/tests). The encoder subset this crate needs lives in its # `gocompat` module (same GoValue/Encoder API shape). Consolidation plan: # depend on the shared crates and delete `gocompat` once their encoders are # wired through this path: diff --git a/crates/cf-renderer/src/gocompat.rs b/crates/cf-renderer/src/gocompat.rs index 41fbb176..9fb4cffe 100644 --- a/crates/cf-renderer/src/gocompat.rs +++ b/crates/cf-renderer/src/gocompat.rs @@ -2,7 +2,7 @@ //! `cf-gojson` crate (DESIGN.md sec 2.2). //! //! Machine-format report bytes are a frozen contract (pinned by -//! `tests/compat`) and differ from `serde_json` defaults on four points: +//! the report e2e suites (`bins/codefang/tests`)) and differ from `serde_json` defaults on four points: //! map-key ordering, HTML escaping (on by default), float formatting //! (shortest-round-trip `'g'` style with the `1e21` exponent threshold), and //! compact-vs-indent semantics. This module provides the subset the renderer diff --git a/crates/cf-renderer/src/json.rs b/crates/cf-renderer/src/json.rs index 127c4368..461c0cbc 100644 --- a/crates/cf-renderer/src/json.rs +++ b/crates/cf-renderer/src/json.rs @@ -2,7 +2,7 @@ //! //! These structs are the machine model for the static-analysis report. Two //! report-format behaviors are load-bearing here (pinned by -//! `tests/compat`): +//! the report e2e suites (`bins/codefang/tests`)): //! //! 1. **Score-last field ordering.** `score` / `overall_score` is always the //! *last* field emitted in JSON. The diff --git a/crates/cf-renderer/src/lib.rs b/crates/cf-renderer/src/lib.rs index 792b9c99..b123cf26 100644 --- a/crates/cf-renderer/src/lib.rs +++ b/crates/cf-renderer/src/lib.rs @@ -19,8 +19,8 @@ //! //! # Byte identity //! -//! Machine-format report bytes are a frozen contract, pinned against the -//! reference implementation by `tests/compat` (DESIGN.md §1.1/§2). All +//! Machine-format report bytes are a frozen contract, pinned by the report +//! e2e suites (`bins/codefang/tests`; DESIGN.md §1.1/§2). All //! JSON serialization here routes through the [`gocompat`] encoder (map-key //! byte-sorting, HTML escaping on, report-contract float formatting, score-last //! via declaration-ordered objects), never `serde_json`. [`gocompat`] mirrors diff --git a/crates/cf-renderer/src/metrics_output.rs b/crates/cf-renderer/src/metrics_output.rs index b798a877..8e7f3151 100644 --- a/crates/cf-renderer/src/metrics_output.rs +++ b/crates/cf-renderer/src/metrics_output.rs @@ -4,7 +4,7 @@ //! serializable output for the JSON and YAML renderers. The trait returns a //! [`GoValue`](crate::gocompat::GoValue) so serialization routes through the //! report-format byte-compatible encoders rather than `serde_json` (output -//! bytes are pinned by `tests/compat`). +//! bytes are pinned by the report e2e suites (`bins/codefang/tests`)). use crate::gocompat::{Encoder, GoValue}; diff --git a/crates/cf-renderer/src/section_renderer.rs b/crates/cf-renderer/src/section_renderer.rs index 0df9f5d3..2b9d90c2 100644 --- a/crates/cf-renderer/src/section_renderer.rs +++ b/crates/cf-renderer/src/section_renderer.rs @@ -84,10 +84,17 @@ impl SectionRenderer { #[must_use] pub fn render_compact(&self, section: &dyn ReportSection) -> String { let title = terminal::pad_right(§ion.section_title(), COMPACT_TITLE_WIDTH); + let message = section.status_message(); + // Info-only sections carry no score: print the label instead of a + // negative "N/10" bar. + if section.score() < 0.0 { + let empty_bar = terminal::format_empty_bar(COMPACT_BAR_WIDTH); + let label = section.score_label(); + return format!("{title} {empty_bar} {label} {message}"); + } let score_bar = terminal::format_score_bar(section.score(), COMPACT_BAR_WIDTH); let score_color = terminal::color_for_score(section.score()); let score_bar = self.config.colorize(&score_bar, score_color); - let message = section.status_message(); format!("{title} {score_bar} {message}") } @@ -239,7 +246,9 @@ impl SectionRenderer { #[cfg(test)] mod tests { use super::*; - use crate::analyze::{severity, BaseReportSection, DistributionItem, Issue, Metric}; + use crate::analyze::{ + severity, BaseReportSection, DistributionItem, Issue, Metric, SCORE_INFO_ONLY, + }; struct Mock { base: BaseReportSection, @@ -308,6 +317,24 @@ mod tests { } /// Mirrors reference test `TestRenderCompact_*`. + /// An info-only section (score below zero) must print the `Info` label, + /// never a negative `N/10` score. + #[test] + fn render_compact_info_only_section_prints_info_label() { + let r = SectionRenderer::new(80, false, true); + let out = r.render_compact(&Mock::new( + "IMPORTS", + SCORE_INFO_ONLY, + "Found 3 unique imports", + )); + assert!(out.contains("Info"), "missing Info label: {out}"); + assert!( + !out.contains("/10"), + "info section must not carry a score: {out}" + ); + assert!(out.contains("Found 3 unique imports")); + } + #[test] fn render_compact_contents() { let r = SectionRenderer::new(80, false, true); diff --git a/crates/cf-renderer/src/terminal.rs b/crates/cf-renderer/src/terminal.rs index 65baa83a..13efb8f6 100644 --- a/crates/cf-renderer/src/terminal.rs +++ b/crates/cf-renderer/src/terminal.rs @@ -183,6 +183,12 @@ fn clamp_filled(value: f64, bar_width: usize) -> usize { filled as usize } +/// Renders an all-empty bar of `bar_width` cells (used for info-only +/// sections, which have no score to fill the bar with). +pub fn format_empty_bar(bar_width: usize) -> String { + format!("[{}]", BAR_EMPTY.repeat(bar_width)) +} + /// Renders a score as a visual progress bar with an `"N/10"` suffix. pub fn format_score_bar(score: f64, bar_width: usize) -> String { let filled = clamp_filled(score, bar_width); diff --git a/crates/cf-reportutil/src/binary.rs b/crates/cf-reportutil/src/binary.rs index 495aae2a..6d695bc8 100644 --- a/crates/cf-reportutil/src/binary.rs +++ b/crates/cf-reportutil/src/binary.rs @@ -14,7 +14,7 @@ //! HTML-escape ON, no trailing newline (the report-contract JSON encoding). //! Multiple envelopes concatenate back-to-back; the decoder loops while bytes //! remain. See DESIGN.md §2.5. Envelope bytes are pinned by -//! `tests/compat`. +//! the report e2e suites (`bins/codefang/tests`). use cf_gojson::{marshal, GoValue}; diff --git a/crates/cf-reportutil/src/lib.rs b/crates/cf-reportutil/src/lib.rs index 9870654c..74ae793d 100644 --- a/crates/cf-reportutil/src/lib.rs +++ b/crates/cf-reportutil/src/lib.rs @@ -23,7 +23,7 @@ //! [`cf_gojson`] crate's `marshal` (map keys byte-sorted, `<`/`>`/`&` and //! `U+2028`/`U+2029` escaped, no insignificant whitespace, no trailing //! newline). Output bytes are pinned against the reference implementation by -//! `tests/compat`. +//! the report e2e suites (`bins/codefang/tests`). //! //! Cross-type numeric coercion in [`accessors::get_int`] / //! [`accessors::get_float64`] delegates to the shared [`cf_safeconv`] crate. diff --git a/crates/cf-safeconv/src/lib.rs b/crates/cf-safeconv/src/lib.rs index f1d84a6b..1cfcd60e 100644 --- a/crates/cf-safeconv/src/lib.rs +++ b/crates/cf-safeconv/src/lib.rs @@ -23,7 +23,7 @@ //! ([`Number`]) into the requested numeric target type, and [`extract_string`] //! covers the string case; every non-coercible value yields `(zero, false)`. //! These coercion rules are part of the report compatibility contract (pinned -//! by `tests/compat`): a float extracted as an integer truncates toward +//! by the report e2e suites (`bins/codefang/tests`)): a float extracted as an integer truncates toward //! zero, and strings/bools never coerce to numbers. //! //! # Platform note diff --git a/crates/cf-sentiment/Cargo.toml b/crates/cf-sentiment/Cargo.toml index 249e3485..3e8e07ae 100644 --- a/crates/cf-sentiment/Cargo.toml +++ b/crates/cf-sentiment/Cargo.toml @@ -12,7 +12,7 @@ publish.workspace = true # tick over commit history. # # Compatibility: machine-report bytes are pinned against the reference binary by -# tests/compat. The base VADER lexicon is vendored byte-for-byte under +# the report e2e suites in bins/codefang/tests. The base VADER lexicon is vendored byte-for-byte under # data/. [dependencies] diff --git a/crates/cf-sentiment/src/analyzer.rs b/crates/cf-sentiment/src/analyzer.rs index 729b1dfb..c1195961 100644 --- a/crates/cf-sentiment/src/analyzer.rs +++ b/crates/cf-sentiment/src/analyzer.rs @@ -1,20 +1,21 @@ //! Comment extraction, merging, and filtering pipeline. //! //! The pure (UAST-independent) part of the analyzer: grouping comment nodes by -//! line, merging adjacent comments, stripping comment delimiters, and filtering -//! comments down to those suitable for sentiment analysis. +//! line, merging adjacent comments, stripping comment delimiters, filtering +//! comments down to those suitable for sentiment analysis, and the +//! before/after multiset difference ([`new_or_changed`]) that keeps only the +//! comments a commit introduced or edited. //! -//! The UAST-bound pieces (`Consume`, `Fork`, snapshots, the history-analyzer -//! wiring) live in the framework crates; this module provides the deterministic -//! text pipeline plus the analyzer's identity constants and config defaults, -//! all directly testable. +//! The UAST-bound pieces (the history walk, the per-commit tree diff) live in +//! the command layer; this module provides the deterministic text pipeline +//! plus the analyzer's identity constant and the default minimum length, all +//! directly testable. //! -//! # Pattern semantics (report compatibility) +//! # Pattern semantics //! -//! The reference implementation expresses these filters as RE2 patterns. They -//! are reproduced here with hand-written Unicode-class scans (no `regex` crate -//! dependency) so that length/letter-ratio checks use **byte** lengths, exactly -//! as the reference does: +//! The filters are expressed as RE2-style patterns, reproduced here with +//! hand-written Unicode-class scans (no `regex` crate dependency); length and +//! letter-ratio checks use **byte** lengths: //! * `[^\p{L}\p{N}]` — first char must be a Unicode letter or number; //! * `[^\p{L}\p{N}\-_:;,./?!#&%+*=\n \t()]+` — strip runs of disallowed //! characters; @@ -27,31 +28,12 @@ /// Analyzer ID as it appears in reports and on the CLI. pub const ANALYZER_ID: &str = "history/sentiment"; -/// Analyzer description shown in CLI help. -pub const ANALYZER_DESCRIPTION: &str = - "Classifies each new or changed comment per commit as containing positive or negative emotions."; - -/// Minimum comment length below which the configured value is replaced by the -/// default. -pub const MIN_COMMENT_LENGTH_THRESHOLD_HIGH: i64 = 10; - -/// Default minimum comment length. +/// Default minimum comment length in bytes (`--min-comment-len`). pub const DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH: i64 = 20; -/// Default sentiment gap threshold. -pub const DEFAULT_COMMENT_SENTIMENT_GAP: f32 = 0.5; - /// Minimum letters ratio for a comment. pub const COMMENT_LETTERS_RATIO: f32 = 0.6; -/// Estimated bytes of TC payload per commit. -pub const SENTIMENT_AVG_TC_SIZE: i64 = 500; - -/// Config key for the minimum comment length. -pub const CONFIG_MIN_LENGTH: &str = "CommentSentiment.MinLength"; -/// Config key for the sentiment gap. -pub const CONFIG_GAP: &str = "CommentSentiment.Gap"; - /// Comment prefixes stripped before analysis, longest first so `///` matches /// before `//`. pub const COMMENT_PREFIXES: &[&str] = &[ @@ -61,27 +43,9 @@ pub const COMMENT_PREFIXES: &[&str] = &[ /// Comment suffixes stripped from lines. pub const COMMENT_SUFFIXES: &[&str] = &["*/"]; -/// Configuration shared by the analyzer: the configurable fields that affect -/// the text pipeline. -#[derive(Debug, Clone, Copy, Default)] -pub struct Config { - /// Minimum comment byte length. - pub min_comment_length: i64, - /// Sentiment gap threshold. - pub gap: f32, -} - -impl Config { - /// Clamps invalid configuration to defaults. - pub fn validate(&mut self) { - if self.gap < 0.0 || self.gap >= 1.0 { - self.gap = DEFAULT_COMMENT_SENTIMENT_GAP; - } - if self.min_comment_length < MIN_COMMENT_LENGTH_THRESHOLD_HIGH { - self.min_comment_length = DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH; - } - } -} +/// The per-line continuation marker of Javadoc / Doxygen / C block comments +/// (` * text`), stripped after the prefixes. +const BLOCK_CONTINUATION_MARKER: char = '*'; /// Returns true if `c` is a Unicode letter (`\p{L}`). fn is_letter(c: char) -> bool { @@ -135,7 +99,20 @@ fn trim_space(s: &str) -> &str { s.trim_matches(|c: char| c.is_whitespace()) } -/// Removes common comment syntax from each line. +/// Removes common comment syntax from each line: a leading prefix +/// ([`COMMENT_PREFIXES`]), a trailing suffix ([`COMMENT_SUFFIXES`]), then a +/// leading block-continuation `*`; the stripped lines are joined with one +/// space. +/// +/// ``` +/// use cf_sentiment::analyzer::strip_comment_delimiters; +/// +/// assert_eq!(strip_comment_delimiters("// a line\n// another"), "a line another"); +/// assert_eq!( +/// strip_comment_delimiters("/**\n * Javadoc text\n * continues\n */"), +/// "Javadoc text continues" +/// ); +/// ``` #[must_use] pub fn strip_comment_delimiters(text: &str) -> String { let mut lines: Vec<String> = Vec::new(); @@ -153,6 +130,9 @@ pub fn strip_comment_delimiters(text: &str) -> String { trimmed = trim_space(&trimmed[..end]).to_string(); } } + if let Some(rest) = trimmed.strip_prefix(BLOCK_CONTINUATION_MARKER) { + trimmed = trim_space(rest).to_string(); + } lines.push(trimmed); } trim_space(&lines.join(" ")).to_string() @@ -243,8 +223,8 @@ pub fn is_license(s: &str) -> bool { /// Filters merged comments down to those suitable for sentiment analysis. /// -/// `min_length` is compared against the **byte** length (report compatibility: -/// the reference implementation compares byte lengths). +/// `min_length` is compared against the comment's length in **characters** +/// (Unicode scalar values), so the filter treats every script alike. #[must_use] pub fn filter_comments(comments: &[String], min_length: i64) -> Vec<String> { let mut filtered = Vec::with_capacity(comments.len()); @@ -268,7 +248,7 @@ pub fn filter_comments(comments: &[String], min_length: i64) -> Vec<String> { comment = strip_function_names(&comment); comment = strip_filtered_chars(&comment); - if (comment.len() as i64) < min_length { + if (comment.chars().count() as i64) < min_length { continue; } @@ -357,6 +337,40 @@ pub fn merge_comments(nodes: &[CommentNode], min_length: i64) -> Vec<String> { filter_comments(&merged, min_length) } +/// The comments of `after` that are NEW or CHANGED relative to `before`: the +/// multiset difference `after − before`, in `after` order. A comment text +/// present `n` times in `before` cancels its first `n` occurrences in +/// `after`, so a commit that only adds code to a commented file contributes +/// nothing and an edited comment counts once (as new text). +/// +/// ``` +/// use cf_sentiment::analyzer::new_or_changed; +/// +/// let after = vec!["kept".to_string(), "edited text".to_string()]; +/// let before = vec!["kept".to_string(), "old text".to_string()]; +/// assert_eq!(new_or_changed(&after, &before), vec!["edited text".to_string()]); +/// ``` +#[must_use] +pub fn new_or_changed(after: &[String], before: &[String]) -> Vec<String> { + use std::collections::HashMap; + + let mut unchanged: HashMap<&str, usize> = HashMap::new(); + for text in before { + *unchanged.entry(text.as_str()).or_insert(0) += 1; + } + after + .iter() + .filter(|text| match unchanged.get_mut(text.as_str()) { + Some(n) if *n > 0 => { + *n -= 1; + false + } + _ => true, + }) + .cloned() + .collect() +} + /// Returns the UTF-8 byte length of the char starting with lead byte `b`. fn utf8_char_len(b: u8) -> usize { if b < 0x80 { @@ -374,33 +388,27 @@ fn utf8_char_len(b: u8) -> usize { mod tests { use super::*; + /// The minimum length counts characters, so a short comment in a + /// non-ASCII script is filtered like its ASCII counterpart. + #[test] + fn min_length_counts_characters_not_bytes() { + // 14 characters, 26 bytes in UTF-8. + let short_cyrillic = "// Хороший код".to_string(); + assert_eq!(filter_comments(&[short_cyrillic], 20), Vec::<String>::new()); + // 21 characters (and bytes). + let ascii = "// a really great design".to_string(); + assert_eq!( + filter_comments(&[ascii], 20), + vec!["a really great design".to_string()] + ); + } + const MIN_LEN: i64 = 10; fn s(v: &[&str]) -> Vec<String> { v.iter().copied().map(String::from).collect() } - #[test] - // validate() copies a const exactly; an epsilon compare would hide a real clamp bug. - #[allow(clippy::float_cmp)] - fn validate_clamps() { - let mut c = Config { - min_comment_length: 5, - gap: 2.0, - }; - c.validate(); - assert_eq!(c.gap, DEFAULT_COMMENT_SENTIMENT_GAP); - assert_eq!(c.min_comment_length, DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH); - - let mut ok = Config { - min_comment_length: 30, - gap: 0.8, - }; - ok.validate(); - assert_eq!(ok.min_comment_length, 30); - assert!((ok.gap - 0.8).abs() < 1e-6); - } - #[test] fn filter_comments_basic() { // english_accepted @@ -530,6 +538,61 @@ mod tests { fn identity_constants() { assert_eq!(ANALYZER_ID, "history/sentiment"); assert_eq!(DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH, 20); - assert!((DEFAULT_COMMENT_SENTIMENT_GAP - 0.5).abs() < 1e-6); + } + + /// A Javadoc / Doxygen block keeps its text once the ` * ` continuation + /// markers are stripped line by line. + #[test] + fn strip_javadoc_continuation_asterisks() { + let block = "/**\n * This function is wonderful\n * and works every time\n */"; + assert_eq!( + strip_comment_delimiters(block), + "This function is wonderful and works every time" + ); + assert_eq!(strip_comment_delimiters("* bare bullet"), "bare bullet"); + assert_eq!(strip_comment_delimiters(" *"), ""); + // The Javadoc block survives the whole filter pipeline. + let out = filter_comments(&s(&[block]), DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH); + assert_eq!(out.len(), 1, "javadoc block must not be rejected: {out:?}"); + } + + /// Only comments NEW in `after` survive: an unchanged comment is removed + /// once per occurrence in `before`, order of `after` is preserved. + #[test] + fn new_or_changed_is_a_multiset_difference() { + let after = s(&["same text", "new text", "same text", "same text"]); + let before = s(&["same text", "gone text", "same text"]); + assert_eq!( + new_or_changed(&after, &before), + s(&["new text", "same text"]) + ); + assert!(new_or_changed(&[], &before).is_empty()); + assert_eq!(new_or_changed(&after, &[]), after); + } + + /// Comment nodes separated by a non-comment line are two merged comments. + #[test] + fn merge_non_adjacent_lines_split() { + let nodes = vec![ + CommentNode { + start_line: 1, + end_line: 1, + token: "first block".into(), + }, + CommentNode { + start_line: 5, + end_line: 6, + token: "second block".into(), + }, + CommentNode { + start_line: 7, + end_line: 7, + token: "still second".into(), + }, + ]; + assert_eq!( + merge_adjacent_comments(&nodes), + s(&["first block", "second block\nstill second"]) + ); } } diff --git a/crates/cf-sentiment/src/lib.rs b/crates/cf-sentiment/src/lib.rs index 458c174c..bfb53b7b 100644 --- a/crates/cf-sentiment/src/lib.rs +++ b/crates/cf-sentiment/src/lib.rs @@ -11,18 +11,15 @@ //! * [`vader`] — the self-contained VADER scoring engine. //! * [`scorer`] — comment sentiment scoring with multilingual lexicon injection //! and SE-domain neutralizers. -//! * [`metrics`] — per-tick metric computation. +//! * [`metrics`] — per-tick metric computation (only ticks with new or +//! changed comments carry a score). //! * [`model`] — report-bearing types with [`model::ToGoValue`] for byte-stable //! serialization through [`cf_gojson`]. -//! * [`store`] — store record kinds + store-time-series extraction. +//! * [`store`] — the per-tick series the cross-analyzer anomaly path reads. //! -//! # Compatibility -//! -//! Output bytes are pinned against the reference implementation by -//! `tests/compat`. All report serialization routes through [`cf_gojson`] -//! (and `cf-goyaml` for YAML) via [`model::ToGoValue`] — never raw -//! `serde_json`. VADER scores and the multilingual lexicon are reproduced -//! exactly, with the base lexicon vendored byte-for-byte under `data/`. +//! All report serialization routes through [`cf_gojson`] (and `cf-goyaml` for +//! YAML) via [`model::ToGoValue`] — never raw `serde_json`. The VADER base +//! lexicon and emoji table are vendored under `data/`. #![forbid(unsafe_code)] // Deliberate parity casts (usize -> i64 lengths, f64 -> f32 report fields) are @@ -50,10 +47,10 @@ pub use model::{ AggregateData, ComputedMetrics, LowSentimentPeriodData, TimeSeriesData, ToGoValue, TrendData, }; pub use scorer::{compute_sentiment, compute_sentiment_with_options, ScorerOptions}; -pub use store::{KIND_AGGREGATE, KIND_TIME_SERIES, KIND_TREND}; +pub use store::extract_store_time_series; pub use vader::{Sentiment, SentimentIntensityAnalyzer}; -pub use analyzer::ANALYZER_ID; +pub use analyzer::{new_or_changed, ANALYZER_ID, DEFAULT_COMMENT_SENTIMENT_MIN_LENGTH}; /// Crate name, used by smoke tests to confirm the module links. pub const CRATE_NAME: &str = "cf-sentiment"; diff --git a/crates/cf-sentiment/src/metrics.rs b/crates/cf-sentiment/src/metrics.rs index e77f21da..0d527233 100644 --- a/crates/cf-sentiment/src/metrics.rs +++ b/crates/cf-sentiment/src/metrics.rs @@ -4,10 +4,16 @@ //! trend, low-sentiment periods, aggregate), the commit→tick aggregation, and //! [`compute_all_metrics_with_options`]. //! -//! Commit hashes are modeled as their hex strings (the analyzer keys and -//! aggregates by the hash's string form), so the pure computation needs no git -//! dependency. The untyped report-map parsing lives in the `cf-analyze` -//! conversion hub. +//! # Tick semantics +//! +//! A tick carries a score only when at least one of its commits introduced or +//! changed a comment ([`aggregate_commits_to_ticks`]). A comment-free tick +//! has no entry in `emotions_by_tick`: it is absent from the time series, the +//! trend fit and the low-sentiment periods, and it is not classified — its +//! commits still count in [`AggregateData::total_commits`]. +//! +//! Commit hashes are modeled as their hex strings, so the pure computation +//! needs no git dependency. use std::collections::BTreeMap; @@ -72,14 +78,14 @@ pub struct TickBounds { /// /// `commits_by_tick` maps a tick to the hex hashes of its commits; /// `comments_by_commit` (consumed by [`aggregate_commits_to_ticks`]) maps a hex -/// hash to its comments. +/// hash to its new or changed comments. #[derive(Debug, Clone, Default)] pub struct ReportData { - /// Per-tick sentiment scores. + /// Per-tick sentiment scores — only the ticks with at least one comment. pub emotions_by_tick: BTreeMap<i64, f32>, - /// Per-tick comments. + /// Per-tick comments — only the ticks with at least one comment. pub comments_by_tick: BTreeMap<i64, Vec<String>>, - /// Per-tick commit hashes (hex). + /// Per-tick commit hashes (hex) — every tick of the window. pub commits_by_tick: BTreeMap<i64, Vec<String>>, /// Per-tick time bounds. pub tick_bounds: BTreeMap<i64, TickBounds>, @@ -115,7 +121,22 @@ impl ReportData { /// Groups per-commit comments into per-tick comments and emotions. /// -/// Returns empty maps when either input is empty. +/// A tick appears in the returned maps only when its commits carry at least +/// one comment: comment-free ticks are unscored, never scored `0.0`. +/// +/// ``` +/// use std::collections::BTreeMap; +/// use cf_sentiment::metrics::aggregate_commits_to_ticks; +/// +/// let comments = BTreeMap::from([ +/// ("a".to_string(), vec!["great, clean fix".to_string()]), +/// ("b".to_string(), Vec::new()), +/// ]); +/// let commits = BTreeMap::from([(0, vec!["a".to_string()]), (1, vec!["b".to_string()])]); +/// let (by_tick, emotions) = aggregate_commits_to_ticks(&comments, &commits); +/// assert_eq!(by_tick.keys().copied().collect::<Vec<_>>(), vec![0]); +/// assert!(!emotions.contains_key(&1), "a comment-free tick carries no score"); +/// ``` #[must_use] pub fn aggregate_commits_to_ticks( comments_by_commit: &BTreeMap<String, Vec<String>>, @@ -124,20 +145,17 @@ pub fn aggregate_commits_to_ticks( let mut cbt: BTreeMap<i64, Vec<String>> = BTreeMap::new(); let mut ebt: BTreeMap<i64, f32> = BTreeMap::new(); - if comments_by_commit.is_empty() || commits_by_tick.is_empty() { - return (cbt, ebt); - } - for (&tick, hashes) in commits_by_tick { - for hash in hashes { - if let Some(comments) = comments_by_commit.get(hash) { - cbt.entry(tick) - .or_default() - .extend(comments.iter().cloned()); - } + let tick_comments: Vec<String> = hashes + .iter() + .filter_map(|hash| comments_by_commit.get(hash)) + .flat_map(|comments| comments.iter().cloned()) + .collect(); + if tick_comments.is_empty() { + continue; } - let tick_comments = cbt.get(&tick).cloned().unwrap_or_default(); ebt.insert(tick, compute_sentiment(&tick_comments)); + cbt.insert(tick, tick_comments); } (cbt, ebt) @@ -225,7 +243,15 @@ pub fn compute_time_series(input: &ReportData, opts: MetricOptions) -> Vec<TimeS result } -/// Computes the trend. +/// The score range every reported sentiment lies in. +const SCORE_MIN: f32 = 0.0; +/// See [`SCORE_MIN`]. +const SCORE_MAX: f32 = 1.0; + +/// Computes the trend over the scored ticks: the least-squares line through +/// the per-tick scores, evaluated at the first and last scored tick and +/// clamped to `[0, 1]` (a fitted line may overshoot the score range); the +/// change percentage is computed from the clamped endpoints. #[must_use] pub fn compute_trend(input: &ReportData, opts: MetricOptions) -> TrendData { if input.emotions_by_tick.is_empty() { @@ -236,8 +262,9 @@ pub fn compute_trend(input: &ReportData, opts: MetricOptions) -> TrendData { let start_tick = ticks[0]; let end_tick = ticks[ticks.len() - 1]; - let (regression_start, regression_end) = - linear_regression_endpoints(&ticks, &input.emotions_by_tick); + let (fitted_start, fitted_end) = linear_regression_endpoints(&ticks, &input.emotions_by_tick); + let regression_start = fitted_start.clamp(SCORE_MIN, SCORE_MAX); + let regression_end = fitted_end.clamp(SCORE_MIN, SCORE_MAX); let mut change_percent = 0.0; if regression_start > 0.0 { @@ -257,12 +284,9 @@ pub fn compute_trend(input: &ReportData, opts: MetricOptions) -> TrendData { } } -/// Least-squares regression endpoints. -/// -/// The float arithmetic (sums in `f64`, the `n*sumXY - sumX*sumY` form, and the -/// final `f32` casts of `intercept + slope*tick`) is part of the report -/// contract: the reported 32-bit endpoints depend on this exact operation -/// order. +/// Least-squares regression endpoints: the fitted line evaluated at the first +/// and last tick (unclamped; [`compute_trend`] clamps). Sums run in `f64`; +/// the endpoints are cast to the 32-bit report fields. #[must_use] #[allow(clippy::similar_names)] // sum_x / sum_y / sum_xy are canonical regression names pub fn linear_regression_endpoints(ticks: &[i64], emotions: &BTreeMap<i64, f32>) -> (f32, f32) { @@ -344,18 +368,21 @@ pub fn compute_low_sentiment_periods( result } -/// Computes aggregate statistics. +/// Computes aggregate statistics: the classification counts and the mean +/// score over the SCORED ticks, the comment total, and the commit total over +/// EVERY tick of the window (comment-free ticks included). #[must_use] pub fn compute_aggregate(input: &ReportData, opts: MetricOptions) -> AggregateData { let mut agg = AggregateData { total_ticks: input.emotions_by_tick.len() as i64, + total_commits: input + .commits_by_tick + .values() + .map(|commits| commits.len() as i64) + .sum(), ..Default::default() }; - if agg.total_ticks == 0 { - return agg; - } - let mut tick_sentiments: Vec<f32> = Vec::with_capacity(input.emotions_by_tick.len()); for (&tick, &sentiment) in &input.emotions_by_tick { @@ -372,79 +399,20 @@ pub fn compute_aggregate(input: &ReportData, opts: MetricOptions) -> AggregateDa if let Some(comments) = input.comments_by_tick.get(&tick) { agg.total_comments += comments.len() as i64; } - if let Some(commits) = input.commits_by_tick.get(&tick) { - agg.total_commits += commits.len() as i64; - } } - agg.average_sentiment = modal_f32_mean(&tick_sentiments); + agg.average_sentiment = mean_f32(&tick_sentiments); agg } -/// Modal float32 mean over summation orders. -/// -/// Go computes the aggregate average by iterating `emotionsByTick` (a Go map, -/// randomized iteration order) and accumulating in `float32`; the resulting -/// value therefore varies by a few ULPs from run to run with a strongly modal -/// distribution (the most likely rounding outcome dominates). A deterministic -/// port cannot reproduce randomized iteration, so we compute the SAME -/// quantity — the f32 sum-then-divide of the same values — under a -/// deterministically sampled set of permutations and return the modal result, -/// i.e. the value the Go binary is most likely to print. Ties break toward the -/// smaller bit pattern so the result is fully deterministic. -fn modal_f32_mean(values: &[f32]) -> f32 { - const SAMPLES: usize = 201; - - let n = values.len(); - if n == 0 { +/// The arithmetic mean of `values` (accumulated in `f64`), `0.0` when empty. +fn mean_f32(values: &[f32]) -> f32 { + if values.is_empty() { return 0.0; } - let count = n as f32; - - // Cheap exit: if ascending and descending index-order sums agree, the sum - // is order-insensitive at f32 precision and sampling is unnecessary. - let fwd: f32 = values.iter().fold(0.0_f32, |s, &v| s + v); - let rev: f32 = values.iter().rev().fold(0.0_f32, |s, &v| s + v); - if fwd.to_bits() == rev.to_bits() { - return fwd / count; - } - - // Deterministic xorshift64* PRNG with a fixed seed: the output depends only - // on the input values, never on wall clock or address-space randomness. - let mut state: u64 = 0x9E37_79B9_7F4A_7C15; - let mut next = move || { - state ^= state >> 12; - state ^= state << 25; - state ^= state >> 27; - state.wrapping_mul(0x2545_F491_4F6C_DD1D) - }; - - let mut perm: Vec<f32> = values.to_vec(); - let mut tallies: Vec<(u32, u32)> = Vec::new(); // (f32 bits, count) - - for _ in 0..SAMPLES { - // Fisher-Yates shuffle. - for i in (1..n).rev() { - #[allow(clippy::cast_possible_truncation)] - let j = (next() % (i as u64 + 1)) as usize; - perm.swap(i, j); - } - let sum: f32 = perm.iter().fold(0.0_f32, |s, &v| s + v); - let bits = (sum / count).to_bits(); - match tallies.iter_mut().find(|(b, _)| *b == bits) { - Some(entry) => entry.1 += 1, - None => tallies.push((bits, 1)), - } - } - - // Modal value; ties break toward the smaller bit pattern. - let (bits, _) = tallies - .iter() - .copied() - .max_by(|a, b| a.1.cmp(&b.1).then(b.0.cmp(&a.0))) - .unwrap_or((0, 0)); - f32::from_bits(bits) + let sum: f64 = values.iter().map(|&v| f64::from(v)).sum(); + (sum / values.len() as f64) as f32 } #[cfg(test)] @@ -679,6 +647,8 @@ mod tests { commits_by_tick: [ (0, vec!["abc".into()]), (2, vec!["def".into(), "ghi".into()]), + // An unscored tick: its commits count, nothing else. + (3, vec!["jkl".into()]), ] .into_iter() .collect(), @@ -687,7 +657,7 @@ mod tests { let r = compute_aggregate(&input, MetricOptions::default()); assert_eq!(r.total_ticks, 3); assert_eq!(r.total_comments, 3); - assert_eq!(r.total_commits, 3); + assert_eq!(r.total_commits, 4); assert_eq!(r.positive_ticks, 1); assert_eq!(r.neutral_ticks, 1); assert_eq!(r.negative_ticks, 1); @@ -700,6 +670,11 @@ mod tests { let (cbt, ebt) = aggregate_commits_to_ticks(&BTreeMap::new(), &BTreeMap::new()); assert!(cbt.is_empty()); assert!(ebt.is_empty()); + // Commits without comments: ticks exist, none is scored. + let cbt_in: BTreeMap<i64, Vec<String>> = [(0, vec!["a".into()])].into_iter().collect(); + let (cbt, ebt) = aggregate_commits_to_ticks(&BTreeMap::new(), &cbt_in); + assert!(cbt.is_empty()); + assert!(ebt.is_empty()); } #[test] @@ -752,4 +727,85 @@ mod tests { assert_eq!(r.aggregate.total_ticks, 2); assert_eq!(r.aggregate.total_comments, 3); } + + /// A tick whose commits carry no new or changed comment has NO score: it + /// is absent from the time series, never a low-sentiment period, and not + /// counted in the classification totals — while its commits still count. + #[test] + fn comment_free_ticks_carry_no_score() { + let cbc: BTreeMap<String, Vec<String>> = [ + ("a".to_string(), vec!["good work on this".into()]), + ("b".to_string(), Vec::new()), + ("c".to_string(), Vec::new()), + ] + .into_iter() + .collect(); + let cbt: BTreeMap<i64, Vec<String>> = + [(0, vec!["a".into()]), (1, vec!["b".into(), "c".into()])] + .into_iter() + .collect(); + let input = ReportData::from_commit_data(&cbc, cbt, BTreeMap::new()); + assert!(!input.emotions_by_tick.contains_key(&1)); + + let r = compute_all_metrics(&input); + assert_eq!(r.time_series.len(), 1, "{:?}", r.time_series); + assert_eq!(r.time_series[0].tick, 0); + assert!( + r.low_sentiment_periods.is_empty(), + "{:?}", + r.low_sentiment_periods + ); + assert_eq!(r.aggregate.total_ticks, 1); + assert_eq!(r.aggregate.total_commits, 3); + assert_eq!(r.aggregate.total_comments, 1); + assert_eq!( + r.aggregate.positive_ticks + r.aggregate.neutral_ticks + r.aggregate.negative_ticks, + 1 + ); + assert_eq!(r.trend.trend_direction, "stable"); + } + + /// The fitted endpoints stay in `[0, 1]` and the change is computed from + /// the clamped values. + #[test] + fn trend_endpoints_are_clamped_to_unit_interval() { + // Fit over (0, 1.0), (1, 0.0), (2, 0.0): intercept 0.8333, slope -0.5 + // → raw end -0.1667, clamped to 0. + let input = ReportData { + emotions_by_tick: m_f32(&[(0, 1.0), (1, 0.0), (2, 0.0)]), + ..Default::default() + }; + let r = compute_trend(&input, MetricOptions::default()); + assert!((0.0..=1.0).contains(&r.start_sentiment), "{r:?}"); + assert!((0.0..=1.0).contains(&r.end_sentiment), "{r:?}"); + assert!((r.end_sentiment).abs() < FLOAT_DELTA, "{r:?}"); + assert!((r.change_percent - -100.0).abs() < 0.01, "{r:?}"); + + // Fit over (0, 0.9), (1, 0.9), (2, 0.0): intercept 1.05 → start clamped to 1. + let input = ReportData { + emotions_by_tick: m_f32(&[(0, 0.9), (1, 0.9), (2, 0.0)]), + ..Default::default() + }; + let r = compute_trend(&input, MetricOptions::default()); + assert!((r.start_sentiment - 1.0).abs() < FLOAT_DELTA, "{r:?}"); + assert!((r.end_sentiment - 0.15).abs() < FLOAT_DELTA, "{r:?}"); + assert!((r.change_percent - -85.0).abs() < 0.1, "{r:?}"); + } + + /// The aggregate average is the plain arithmetic mean of the scored ticks. + #[test] + fn aggregate_average_is_the_plain_mean() { + let values: Vec<f32> = (0..50).map(|i| 0.1 + (i as f32) * 0.017).collect(); + let input = ReportData { + emotions_by_tick: values + .iter() + .enumerate() + .map(|(i, v)| (i as i64, *v)) + .collect(), + ..Default::default() + }; + let r = compute_aggregate(&input, MetricOptions::default()); + let expected = (values.iter().map(|v| f64::from(*v)).sum::<f64>() / 50.0) as f32; + assert!((r.average_sentiment - expected).abs() < 1e-6, "{r:?}"); + } } diff --git a/crates/cf-sentiment/src/model.rs b/crates/cf-sentiment/src/model.rs index ba3984f0..b63f2c41 100644 --- a/crates/cf-sentiment/src/model.rs +++ b/crates/cf-sentiment/src/model.rs @@ -3,8 +3,8 @@ //! The output structs (`TimeSeriesData`, `TrendData`, `LowSentimentPeriodData`, //! `AggregateData`, `ComputedMetrics`). Each provides //! [`ToGoValue::to_go_value`] so it serializes through [`cf_gojson`] (and -//! `cf-goyaml`) per the report-format compatibility contract (pinned by -//! `tests/compat`). +//! `cf-goyaml`) per the report-format contract (pinned by the CLI-level +//! end-to-end tests). //! //! # Ordering //! @@ -15,11 +15,11 @@ //! # `f32` field precision //! //! Several fields are 32-bit floats in the report contract (`sentiment`, -//! `average_sentiment`, `start_sentiment`, `end_sentiment`). The reference -//! encoder formats such fields with 32-bit shortest precision, which differs -//! from 64-bit shortest. [`f32_float`] reproduces this by rounding the value to -//! its `f32`-shortest decimal before handing it to `cf-gojson`'s 64-bit float -//! formatter, so the rendered digits match the contract. +//! `average_sentiment`, `start_sentiment`, `end_sentiment`) and render with +//! 32-bit shortest precision, which differs from 64-bit shortest. +//! [`f32_float`] rounds the value to its `f32`-shortest decimal before handing +//! it to `cf-gojson`'s 64-bit float formatter, so the rendered digits are the +//! short form (`0.6`, never `0.6000000238418579`). use cf_gojson::{GoMap, GoValue}; @@ -149,7 +149,7 @@ impl LowSentimentPeriodData { m.push("tick", GoValue::Int(self.tick)); m.push("sentiment", f32_float(self.sentiment)); // `comments` has no omitempty. An absent list renders as JSON `null` - // but as YAML `[]` (report-format contract; pinned by tests/compat). + // but as YAML `[]` (report-format contract). if self.comments.is_empty() { m.push( "comments", diff --git a/crates/cf-sentiment/src/scorer.rs b/crates/cf-sentiment/src/scorer.rs index f6c6f2a0..125b3007 100644 --- a/crates/cf-sentiment/src/scorer.rs +++ b/crates/cf-sentiment/src/scorer.rs @@ -1,9 +1,9 @@ //! Comment sentiment scoring. //! -//! Wraps the [VADER engine](crate::vader) with SE-domain neutralizers and -//! comment-length weighting. VADER compound scores feed machine reports, so -//! every float operation here is part of the report contract (pinned by the -//! differential gate). +//! Wraps the [VADER engine](crate::vader) with the software-engineering +//! domain adjustments ([`SE_DOMAIN_NEUTRALIZERS`], [`SE_NEGATIVE_TERMS`] — +//! whole-word matches only) and comment-length weighting, and maps the +//! compound score to `[0, 1]`. use std::sync::OnceLock; @@ -106,72 +106,88 @@ pub fn vader_compound_to_score(compound: f64) -> f32 { score as f32 } -/// SE-domain terms VADER misclassifies, neutralized toward `0`. -const SE_DOMAIN_NEUTRALIZERS: &[&str] = &[ +/// Software-engineering terms the VADER lexicon scores negative although +/// they are neutral in code comments; a whole-word hit pulls the compound +/// toward `0`. The list holds exactly the terms the vendored lexicon knows +/// with a negative valence — a word VADER does not score cannot be +/// misclassified by it, so it is not listed. +pub const SE_DOMAIN_NEUTRALIZERS: &[&str] = &[ "kill", "killed", "killing", - "abort", - "aborted", - "aborting", + "kills", "fatal", "dead", - "terminate", - "terminated", "destroy", + "destroyed", "panic", - "deprecated", "obsolete", - "master", - "execute", "exploit", "conflict", - "revert", + "conflicts", "reject", "rejected", "critical", + "argument", + "arguments", + "block", + "blocked", + "interrupt", + "interrupted", + "trap", + "dump", ]; -/// SE-domain terms that are genuinely negative, with their valence shift. -const SE_NEGATIVE_TERMS: &[(&str, f64)] = &[ +/// Software-engineering terms that signal genuine negativity and that the +/// VADER lexicon does not score, with the compound shift a whole-word (or +/// whole-phrase) hit applies. +pub const SE_NEGATIVE_TERMS: &[(&str, f64)] = &[ ("hack", -0.3), ("hacky", -0.4), ("kludge", -0.5), ("workaround", -0.2), ("technical debt", -0.3), ("spaghetti", -0.4), - ("awful", -0.3), - ("terrible", -0.3), ("nightmare", -0.4), - ("horrible", -0.3), ]; -/// Applies SE-domain adjustment to a VADER compound score. -/// -/// # Ordering note -/// -/// The reference implementation iterates these term sets in nondeterministic -/// (hash-map) order, but the result is order-independent: neutralizer targets -/// are all `0`, so each contributes `(0 - compound) * n_weight` regardless of -/// order, and the negative-term shifts are summed. `adjustment` and `count` -/// therefore match exactly. This implementation uses fixed-order slices, which -/// is numerically identical (sum of the same terms) and fully deterministic. +/// The words of `text`: maximal runs of alphanumeric characters, lower-cased. +fn words(text: &str) -> Vec<String> { + text.split(|c: char| !c.is_alphanumeric()) + .filter(|w| !w.is_empty()) + .map(str::to_lowercase) + .collect() +} + +/// Whether `term` (one or more space-separated words) occurs in `words` as a +/// run of whole words. +fn contains_term(words: &[String], term: &str) -> bool { + let needle: Vec<&str> = term.split(' ').collect(); + words + .windows(needle.len()) + .any(|window| window.iter().zip(&needle).all(|(w, n)| w == n)) +} + +/// Applies the SE-domain adjustment to a VADER compound score: every +/// neutralizer hit contributes `(0 - compound) * n_weight`, every negative +/// term its shift; the mean contribution is added and the result clamped to +/// `[-1, 1]`. Terms match whole words only (`deadline` is not `dead`, +/// `skill` is not `kill`). #[must_use] pub fn apply_se_domain_adjustment_with_weight(text: &str, compound: f64, n_weight: f64) -> f64 { - let lower = text.to_lowercase(); + let words = words(text); let mut adjustment = 0.0; let mut count = 0_i64; for term in SE_DOMAIN_NEUTRALIZERS { - if lower.contains(term) { - // shift is 0.0 for all neutralizers. + if contains_term(&words, term) { adjustment += (0.0 - compound) * n_weight; count += 1; } } for (term, shift) in SE_NEGATIVE_TERMS { - if lower.contains(term) { + if contains_term(&words, term) { adjustment += shift; count += 1; } @@ -186,13 +202,12 @@ pub fn apply_se_domain_adjustment_with_weight(text: &str, compound: f64, n_weigh } /// Returns a sentiment score in `[0,1]` for `comments` using default options. +/// An empty set has no sentiment and yields `0.0`; the metrics layer never +/// scores a comment-free tick (see [`crate::metrics`]). /// /// ``` /// use cf_sentiment::scorer::compute_sentiment; /// -/// // No comments → neutral 0.0. -/// assert_eq!(compute_sentiment(&[]), 0.0); -/// /// // A clearly positive comment scores above the 0.5 neutral midpoint. /// let score = compute_sentiment(&["This is a great, clean fix!".to_string()]); /// assert!(score > 0.5, "score = {score}"); @@ -227,7 +242,7 @@ pub fn compute_sentiment_with_options(comments: &[String], opts: ScorerOptions) let scores = analyzer.polarity_scores(c); let adjusted = apply_se_domain_adjustment_with_weight(c, scores.compound, n_weight); - let weight = comment_weight_with_max(c.len(), avg_len, max_wr); + let weight = comment_weight_with_max(c.chars().count(), avg_len, max_wr); weighted_sum += f64::from(vader_compound_to_score(adjusted)) * weight; total_weight += weight; } @@ -239,7 +254,7 @@ pub fn compute_sentiment_with_options(comments: &[String], opts: ScorerOptions) (weighted_sum / total_weight) as f32 } -/// Mean byte-length of non-empty (trimmed) comments. +/// Mean length, in characters, of non-empty (trimmed) comments. #[must_use] pub fn average_comment_length(comments: &[String]) -> f64 { let mut total = 0_usize; @@ -249,7 +264,7 @@ pub fn average_comment_length(comments: &[String]) -> f64 { if c.is_empty() { continue; } - total += c.len(); + total += c.chars().count(); count += 1; } if count == 0 { @@ -349,6 +364,7 @@ mod tests { "This deprecated API will be removed next version", "Panic handler catches unrecoverable errors", "Execute the shell command with the given arguments", + "Reject the request when the token is critical", ]; for c in cases { let score = compute_sentiment(&s(&[c])); @@ -432,6 +448,17 @@ mod tests { assert!((f64::from(vader_compound_to_score(2.0)) - 1.0).abs() < FLOAT_DELTA); } + /// Weights count characters: an English and a Russian comment of the + /// same length weigh the same, so the tick score is their plain mean. + #[test] + fn length_weighting_counts_characters_across_scripts() { + let en = "great work".to_string(); // 10 characters, 10 bytes + let ru = "ужасный код".to_string(); // 11 characters, 21 bytes + assert!((average_comment_length(std::slice::from_ref(&en)) - 10.0).abs() < FLOAT_DELTA); + assert!((average_comment_length(std::slice::from_ref(&ru)) - 11.0).abs() < FLOAT_DELTA); + assert!((average_comment_length(&[en, ru]) - 10.5).abs() < FLOAT_DELTA); + } + #[test] fn comment_weight() { let max = ScorerOptions::default().max_weight_ratio; @@ -477,4 +504,27 @@ mod tests { assert!((average_comment_length(&s(&[" ", "\t"])) - 1.0).abs() < FLOAT_DELTA); assert!((average_comment_length(&s(&["ab", "abcd", "abcdef"])) - 4.0).abs() < FLOAT_DELTA); } + + /// SE terms match whole words only: `deadline` is not `dead`, `skill` is + /// not `kill`, and `master` is no longer a neutralizer; the multi-word + /// `technical debt` matches as a phrase. + #[test] + fn se_terms_match_whole_words_only() { + let w = NEUTRALIZER_WEIGHT; + for untouched in [ + "we shipped before the deadline", + "this takes real skill to read", + "merged into master this morning", + "the hacker news thread", + ] { + let r = apply_se_domain_adjustment_with_weight(untouched, 0.5, w); + assert!((r - 0.5).abs() < FLOAT_DELTA, "{untouched}: {r}"); + } + let r = apply_se_domain_adjustment_with_weight("kill the process.", -0.6, w); + assert!((r - -0.12).abs() < FLOAT_DELTA, "kill is neutralized: {r}"); + let r = apply_se_domain_adjustment_with_weight("Technical Debt, again", 0.0, w); + assert!(r < 0.0, "technical debt is a negative phrase: {r}"); + let r = apply_se_domain_adjustment_with_weight("a hack, nothing more", 0.0, w); + assert!(r < 0.0, "punctuation does not hide a term: {r}"); + } } diff --git a/crates/cf-sentiment/src/store.rs b/crates/cf-sentiment/src/store.rs index ae3d9c12..1e357381 100644 --- a/crates/cf-sentiment/src/store.rs +++ b/crates/cf-sentiment/src/store.rs @@ -1,55 +1,14 @@ -//! Structured store record kinds and the store-write payload mapping. -//! -//! The store record-kind constants, the record set derived from -//! [`ComputedMetrics`], and the store-based time-series extraction used by the -//! cross-analyzer anomaly path. -//! -//! The actual `ReportWriter`/`ReportReader` I/O lives in the `cf-analyze` store -//! layer; as in `cf-anomaly`, this module exposes the pure record view + -//! extraction logic that a store adapter drives. +//! The per-tick sentiment series as the cross-analyzer anomaly path reads it +//! (`history/anomaly` enriches its report with the other selected analyzers' +//! time series). use crate::metrics::DIM_SENTIMENT; -use crate::model::{AggregateData, ComputedMetrics, TimeSeriesData, TrendData}; +use crate::model::TimeSeriesData; -/// Record kind: per-tick `TimeSeriesData` records (sorted by tick). -pub const KIND_TIME_SERIES: &str = "time_series"; -/// Record kind: the single `TrendData` record. -pub const KIND_TREND: &str = "trend"; -/// Record kind: the single `AggregateData` record. -pub const KIND_AGGREGATE: &str = "aggregate"; - -/// The set of store records derived from computed metrics: the `time_series` -/// slice, the single `trend` record, and the single `aggregate` record. -/// -/// Borrowing keeps this allocation-free; a store-writer adapter (in -/// `cf-analyze`) iterates these and writes each kind through the byte-stable -/// codec. -pub struct StoreRecords<'a> { - /// Per-tick time-series entries (kind [`KIND_TIME_SERIES`]). - pub time_series: &'a [TimeSeriesData], - /// Trend summary (kind [`KIND_TREND`]). - pub trend: &'a TrendData, - /// Aggregate summary (kind [`KIND_AGGREGATE`]). - pub aggregate: &'a AggregateData, -} - -impl<'a> StoreRecords<'a> { - /// Builds the store-record view over `metrics`. - #[must_use] - pub fn from_metrics(metrics: &'a ComputedMetrics) -> Self { - Self { - time_series: &metrics.time_series, - trend: &metrics.trend, - aggregate: &metrics.aggregate, - } - } -} - -/// Tick axis plus named per-tick value dimensions, as extracted from store -/// records for the cross-analyzer anomaly path. +/// Tick axis plus named per-tick value dimensions. pub type StoreTimeSeries = (Vec<i64>, Vec<(String, Vec<f64>)>); -/// Extracts the per-tick sentiment dimension from stored `time_series` records: +/// Extracts the per-tick sentiment dimension from the `time_series` records: /// the tick axis and a single `"sentiment"` dimension of per-tick scores (as /// `f64`). Returns `None` when no time-series records are present. #[must_use] @@ -70,33 +29,6 @@ pub fn extract_store_time_series(time_series: &[TimeSeriesData]) -> Option<Store #[cfg(test)] mod tests { use super::*; - use crate::model::ComputedMetrics; - - #[test] - fn kind_constants_are_stable() { - assert_eq!(KIND_TIME_SERIES, "time_series"); - assert_eq!(KIND_TREND, "trend"); - assert_eq!(KIND_AGGREGATE, "aggregate"); - } - - #[test] - fn store_records_view_matches_metrics() { - let metrics = ComputedMetrics { - time_series: vec![TimeSeriesData { - tick: 7, - ..Default::default() - }], - aggregate: AggregateData { - total_ticks: 1, - ..Default::default() - }, - ..Default::default() - }; - let records = StoreRecords::from_metrics(&metrics); - assert_eq!(records.time_series.len(), 1); - assert_eq!(records.time_series[0].tick, 7); - assert_eq!(records.aggregate.total_ticks, 1); - } #[test] fn extract_store_time_series_empty() { diff --git a/crates/cf-sentiment/src/vader.rs b/crates/cf-sentiment/src/vader.rs index e55679f9..01665235 100644 --- a/crates/cf-sentiment/src/vader.rs +++ b/crates/cf-sentiment/src/vader.rs @@ -1,21 +1,20 @@ //! Self-contained VADER (Valence Aware Dictionary and sEntiment Reasoner) //! scoring engine. //! -//! # Numeric parity (report compatibility) +//! # Fidelity //! //! VADER compound scores are floats that reach machine reports, so this is a -//! bit-faithful implementation of the reference engine, not an approximation -//! (pinned by the differential gate): +//! faithful implementation of the published algorithm (Hutto & Gilbert), not +//! an approximation; the published example sentences score as published +//! (see the tests): //! -//! * The base lexicon and emoji table are **vendored byte-for-byte** -//! (`data/vaderLexicon.txt`, `data/emojiUTF8Lexicon.txt`) and parsed exactly -//! as the reference does (line split, tab split, field 0 = word, field 1 = -//! 64-bit float). -//! * Float arithmetic matches operation-for-operation: the valence sum is a -//! plain left-to-right `f64` sum, and `normalize` uses +//! * The base lexicon and emoji table are **vendored** (`data/vaderLexicon.txt`, +//! `data/emojiUTF8Lexicon.txt`) and parsed as the reference does (line +//! split, tab split, field 0 = word, field 1 = 64-bit float). +//! * The valence sum is a plain left-to-right `f64` sum, and `normalize` uses //! `score / sqrt(score*score + alpha)`. //! * Tokenization splits on single spaces keeping empty tokens, and -//! punctuation stripping / ALLCAPS detection reproduce the reference's +//! punctuation stripping / ALLCAPS detection follow the reference's //! Python-style string semantics. use std::collections::HashMap; @@ -757,4 +756,123 @@ mod tests { assert!(normalize_default(1000.0) <= 1.0); assert!(normalize_default(-1000.0) >= -1.0); } + + const COMPOUND_DELTA: f64 = 1e-4; + + fn compound(text: &str) -> f64 { + SentimentIntensityAnalyzer::new() + .polarity_scores(text) + .compound + } + + fn assert_compound(text: &str, expected: f64) { + let got = compound(text); + assert!( + (got - expected).abs() < COMPOUND_DELTA, + "{text:?}: compound {got}, expected {expected}" + ); + } + + /// The published VADER example sentences (Hutto & Gilbert, the + /// vaderSentiment README) with their published compound scores. + #[test] + fn published_examples_score_as_published() { + for (text, expected) in [ + ("VADER is smart, handsome, and funny.", 0.8316), + ("VADER is smart, handsome, and funny!", 0.8439), + ("VADER is very smart, handsome, and funny.", 0.8545), + ("VADER is VERY SMART, handsome, and FUNNY.", 0.9227), + ("VADER is VERY SMART, handsome, and FUNNY!!!", 0.9342), + ( + "VADER is VERY SMART, uber handsome, and FRIGGIN FUNNY!!!", + 0.9469, + ), + ("VADER is not smart, handsome, nor funny.", -0.7424), + ("The book was good.", 0.4404), + ("At least it isn't a horrible book.", 0.431), + ("The book was only kind of good.", 0.3832), + ( + "The plot was good, but the characters are uncompelling and the dialog is not great.", + -0.7042, + ), + ("Today SUX!", -0.5461), + ("Today only kinda sux! But I'll get by, lol", 0.5249), + ("Make sure you :) or :D today!", 0.8633), + ("Not bad at all", 0.431), + ] { + assert_compound(text, expected); + } + } + + /// `negation_check`: a negator before a lexicon word scales its valence + /// by `N_SCALAR`: good (1.9) → -1.406 → -1.406/√(1.406²+15). + #[test] + fn negation_flips_and_damps_the_valence() { + assert_compound("good", 0.4404); + assert_compound("not good", -0.3412); + // The negator may sit up to three words back. + assert_compound("not very good", -0.3865); + } + + /// `but_check`: valence before `but` is halved, after it is multiplied by + /// 1.5: good 1.9·0.5 + bad −2.5·1.5 = −2.8 → −2.8/√(2.8²+15) = −0.5859; + /// bad −2.5·0.5 + good 1.9·1.5 = 1.6 → 1.6/√(1.6²+15) = 0.3818. + #[test] + fn but_shifts_weight_to_the_second_clause() { + assert_compound("good but bad", -0.5859); + assert_compound("bad but good", 0.3818); + } + + /// `special_idioms_check`: `the shit` overrides shit's −2.6 with +3.0 + /// (3.0/√(9+15) = 0.6124); in `bad ass` BOTH lexicon words (bad −2.5, + /// ass −2.5) are overridden with +1.5 — the idiom is looked up around + /// every scored word — so the sum is 3.0 as well. + #[test] + fn special_idioms_override_the_word_valence() { + assert_compound("this is the shit", 0.6124); + assert_compound("this is shit", -0.5574); + assert_compound("he is a bad ass", 0.6124); + } + + /// `least_check`: `least good` negates, `at least good` does not, and + /// `very least good` keeps the booster: 1.9 + 0.293·0.95 = 2.178 → + /// 2.178/√(2.178²+15) = 0.4902. + #[test] + fn least_negates_unless_preceded_by_at_or_very() { + assert_compound("least good", -0.3412); + assert_compound("at least good", 0.4404); + assert_compound("very least good", 0.4902); + } + + /// `amplify_ep` / `amplify_qm`: `!` adds 0.292 each (at most four), one + /// `?` adds nothing, two or three add 0.18 each, more add 0.96. + #[test] + fn exclamation_and_question_marks_amplify() { + assert_compound("good!", 0.4926); + assert_compound("good!!!!!", 0.6209); + assert_compound("good?", 0.4404); + assert_compound("good??", 0.5040); + assert_compound("good?????", 0.5940); + // Amplification follows the sign of the valence sum. + assert_compound("bad!", -0.5848); + } + + /// ALLCAPS emphasis adds `C_INCR` (0.733) only when SOME words are + /// capitalized (`is_cap_diff`): GOOD 1.9 + 0.733 = 2.633 → 0.5622; + /// BAD −2.5 − 0.733 = −3.233 → −0.6408; an all-caps text gets none. + #[test] + fn allcaps_emphasis_requires_a_capitalization_difference() { + assert_compound("GOOD day", 0.5622); + assert_compound("GOOD DAY", 0.4404); + assert_compound("BAD day", -0.6408); + } + + /// Emoji are replaced by their textual description before scoring. + #[test] + fn emoji_are_replaced_by_their_description() { + assert_compound("Catch utf-8 emoji such as such as 💘 and 💋 and 😁", 0.7003); + // A lone emoji scores exactly as its vendored description. + assert!(compound("😁") > 0.0); + assert!((compound("😁") - compound("beaming face with smiling eyes")).abs() < 1e-12); + } } diff --git a/crates/cf-shotness/Cargo.toml b/crates/cf-shotness/Cargo.toml index a0872288..3256d45f 100644 --- a/crates/cf-shotness/Cargo.toml +++ b/crates/cf-shotness/Cargo.toml @@ -8,13 +8,15 @@ repository.workspace = true publish.workspace = true # cf-shotness — structural hotspots: co-change of DSL-selected UAST entities -# (analyzer id: history/shotness). +# (analyzer id: history/shotness). The live entity state (touch / couple / +# rename / end) and the metrics over its report. # -# All machine-format report serialization routes through cf-gojson; the -# json/yaml/ndjson/timeseries/compact/bin bytes are pinned against the -# reference implementation by tests/compat. NEVER serde_json for output. +# All machine-format report serialization routes through cf-gojson. NEVER +# serde_json for output. [dependencies] +# Spill encoding of finished history results (cf-commands `spill`). +serde = { workspace = true } # Report-format-compatible value model + encoder. The analyzer builds its # report as a GoValue tree; serialization is owned by the report layer. # NEVER serde_json. diff --git a/crates/cf-shotness/src/aggregate.rs b/crates/cf-shotness/src/aggregate.rs deleted file mode 100644 index d3e69a90..00000000 --- a/crates/cf-shotness/src/aggregate.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! Tick aggregation and co-change matrix construction. -//! -//! Per-commit node touches accumulate into per-tick state, coupling pairs are -//! derived from sorted touched-node keys, and the merged per-node data is -//! flattened into the index-keyed `Nodes`/`Counters` report that the metric -//! functions consume. -//! -//! Name-collision semantics ("last-wins") happen upstream during node extraction -//! (see crate docs); here keys are already resolved, and merging is purely -//! additive on counts and coupling counters. - -use std::collections::{BTreeMap, HashMap}; - -use crate::types::{NodeSummary, ReportData}; - -/// Minimum touched nodes to form at least one coupling pair. -pub const MIN_COUPLING_NODES: usize = 2; -/// Divisor in `C(n,2) = n*(n-1)/2`. -pub const COMBINATORIAL_PAIR_DIVISOR: i64 = 2; -/// Cap on touched nodes per commit for coupling map updates. -pub const MAX_COUPLING_NODES: usize = 500; - -/// Per-node accumulation state in a tick. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct NodeShotnessData { - /// Node identity. - pub summary: NodeSummary, - /// Accumulated self-change count. - pub count: i64, - /// Co-change counters keyed by the *other* node's key. - /// - /// A `BTreeMap` is used (rather than a hash map) so iteration during merge - /// and report-building is deterministic; values are order-independent - /// additive counts, so the final report is byte-stable. - pub couples: BTreeMap<String, i64>, -} - -/// Per-tick aggregated node map. -/// -/// Keyed by node key; a `BTreeMap` keeps deterministic ordering for merges and -/// the sorted node order the report builder relies on. -pub type TickNodes = BTreeMap<String, NodeShotnessData>; - -/// Accumulates per-commit node touch deltas into the tick node map. -/// -/// Each touched node's count increments by its per-commit delta (always 1 for -/// a first touch in the commit). -pub fn accumulate_nodes(acc: &mut TickNodes, nodes_touched: &BTreeMap<String, NodeSummary>) { - for (key, summary) in nodes_touched { - let nd = acc.entry(key.clone()).or_insert_with(|| NodeShotnessData { - summary: summary.clone(), - count: 0, - couples: BTreeMap::new(), - }); - nd.count += 1; - } -} - -/// Computes coupling pairs from touched nodes and updates coupling counters. -/// -/// Returns the combinatorial pair count `C(n,2)`. When -/// `n > MAX_COUPLING_NODES` the O(n²) counter updates are skipped (mass -/// refactor — coupling signal is noise) but the pair count is still returned. -/// Touched-node keys are paired in sorted order (an upper-triangle walk). -#[allow(clippy::cast_possible_wrap)] // touched-node counts are small -pub fn compute_coupling_pairs( - acc: &mut TickNodes, - nodes_touched: &BTreeMap<String, NodeSummary>, -) -> i64 { - let n = nodes_touched.len(); - if n < MIN_COUPLING_NODES { - return 0; - } - - let coupling_pairs = n as i64 * (n as i64 - 1) / COMBINATORIAL_PAIR_DIVISOR; - - if n > MAX_COUPLING_NODES { - return coupling_pairs; - } - - // BTreeMap already yields keys sorted ascending. - let keys: Vec<&String> = nodes_touched.keys().collect(); - - for i in 0..keys.len() { - for j in (i + 1)..keys.len() { - let (first, second) = (keys[i], keys[j]); - - if let Some(nd) = acc.get_mut(first) { - *nd.couples.entry(second.clone()).or_insert(0) += 1; - } - if let Some(nd) = acc.get_mut(second) { - *nd.couples.entry(first.clone()).or_insert(0) += 1; - } - } - } - - coupling_pairs -} - -/// Additively merges `src` node data into `dst`: counts add, coupling -/// counters add per key, and new nodes are inserted with copied coupling -/// maps. -pub fn merge_nodes_into(dst: &mut TickNodes, src: &TickNodes) { - for (key, nd) in src { - match dst.get_mut(key) { - None => { - dst.insert(key.clone(), nd.clone()); - } - Some(existing) => { - existing.count += nd.count; - for (ck, cv) in &nd.couples { - *existing.couples.entry(ck.clone()).or_insert(0) += cv; - } - } - } - } -} - -/// Builds the index-keyed `Nodes`/`Counters` report from merged node data. -/// -/// Nodes are ordered by sorted node key; `counters[i][i]` holds node `i`'s -/// self-change count and `counters[i][j]` holds the co-change count with node -/// `j` (only couples whose key resolves to a node in the merged set are -/// kept). -#[must_use] -pub fn build_report_from_merged(merged: &TickNodes) -> ReportData { - let keys: Vec<String> = merged.keys().cloned().collect(); // BTreeMap → sorted. - - let mut reverse_keys: HashMap<&str, usize> = HashMap::with_capacity(keys.len()); - for (i, key) in keys.iter().enumerate() { - reverse_keys.insert(key.as_str(), i); - } - - let mut nodes: Vec<NodeSummary> = Vec::with_capacity(keys.len()); - let mut counters: Vec<HashMap<usize, i64>> = Vec::with_capacity(keys.len()); - - for (i, key) in keys.iter().enumerate() { - let nd = &merged[key]; - nodes.push(nd.summary.clone()); - - let mut counter: HashMap<usize, i64> = HashMap::new(); - counter.insert(i, nd.count); - - for (ck, val) in &nd.couples { - if let Some(&idx) = reverse_keys.get(ck.as_str()) { - counter.insert(idx, *val); - } - } - - counters.push(counter); - } - - ReportData { nodes, counters } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn touched(keys: &[(&str, NodeSummary)]) -> BTreeMap<String, NodeSummary> { - keys.iter() - .map(|(k, s)| ((*k).to_string(), s.clone())) - .collect() - } - - #[test] - fn accumulate_increments_counts() { - let mut acc: TickNodes = BTreeMap::new(); - let foo = NodeSummary::new("Function", "foo", "a.go"); - let t = touched(&[(&foo.key(), foo.clone())]); - accumulate_nodes(&mut acc, &t); - accumulate_nodes(&mut acc, &t); - assert_eq!(acc[&foo.key()].count, 2); - } - - #[test] - fn coupling_pairs_below_min_is_zero() { - let mut acc: TickNodes = BTreeMap::new(); - let foo = NodeSummary::new("Function", "foo", "a.go"); - let t = touched(&[(&foo.key(), foo.clone())]); - accumulate_nodes(&mut acc, &t); - assert_eq!(compute_coupling_pairs(&mut acc, &t), 0); - } - - #[test] - fn coupling_pairs_two_nodes() { - let mut acc: TickNodes = BTreeMap::new(); - let foo = NodeSummary::new("Function", "foo", "a.go"); - let bar = NodeSummary::new("Function", "bar", "a.go"); - let t = touched(&[(&foo.key(), foo.clone()), (&bar.key(), bar.clone())]); - accumulate_nodes(&mut acc, &t); - let pairs = compute_coupling_pairs(&mut acc, &t); - assert_eq!(pairs, 1); - assert_eq!(acc[&foo.key()].couples[&bar.key()], 1); - assert_eq!(acc[&bar.key()].couples[&foo.key()], 1); - } - - #[test] - fn coupling_pairs_three_nodes_is_c_n_2() { - let mut acc: TickNodes = BTreeMap::new(); - let a = NodeSummary::new("F", "a", "x"); - let b = NodeSummary::new("F", "b", "x"); - let c = NodeSummary::new("F", "c", "x"); - let t = touched(&[ - (&a.key(), a.clone()), - (&b.key(), b.clone()), - (&c.key(), c.clone()), - ]); - accumulate_nodes(&mut acc, &t); - assert_eq!(compute_coupling_pairs(&mut acc, &t), 3); // C(3,2) - } - - #[test] - fn build_report_orders_by_sorted_key() { - let mut acc: TickNodes = BTreeMap::new(); - let foo = NodeSummary::new("Function", "foo", "a.go"); - let bar = NodeSummary::new("Function", "bar", "a.go"); - let t = touched(&[(&foo.key(), foo.clone()), (&bar.key(), bar.clone())]); - accumulate_nodes(&mut acc, &t); - compute_coupling_pairs(&mut acc, &t); - - let report = build_report_from_merged(&acc); - assert_eq!(report.nodes.len(), 2); - // Function_bar_a.go < Function_foo_a.go - assert_eq!(report.nodes[0].name, "bar"); - assert_eq!(report.nodes[1].name, "foo"); - assert_eq!(report.counters[0][&0], 1); - assert_eq!(report.counters[1][&1], 1); - assert_eq!(report.counters[0][&1], 1); - assert_eq!(report.counters[1][&0], 1); - } - - #[test] - fn merge_is_additive() { - let foo = NodeSummary::new("Function", "foo", "a.go"); - let bar = NodeSummary::new("Function", "bar", "a.go"); - let mut a: TickNodes = BTreeMap::new(); - let mut b: TickNodes = BTreeMap::new(); - let t = touched(&[(&foo.key(), foo.clone()), (&bar.key(), bar.clone())]); - accumulate_nodes(&mut a, &t); - compute_coupling_pairs(&mut a, &t); - accumulate_nodes(&mut b, &t); - compute_coupling_pairs(&mut b, &t); - - merge_nodes_into(&mut a, &b); - assert_eq!(a[&foo.key()].count, 2); - assert_eq!(a[&foo.key()].couples[&bar.key()], 2); - } -} diff --git a/crates/cf-shotness/src/lib.rs b/crates/cf-shotness/src/lib.rs index fe6efaf4..492b0ada 100644 --- a/crates/cf-shotness/src/lib.rs +++ b/crates/cf-shotness/src/lib.rs @@ -1,19 +1,22 @@ //! cf-shotness — structural hotspots. //! -//! Co-change frequency of DSL-selected UAST entities (functions, classes, …) -//! across commit history. Analyzer id `history/shotness`. +//! Co-change frequency of DSL-selected UAST entities (functions, methods, +//! classes, …) across commit history. Analyzer id `history/shotness`. //! //! # What this crate covers //! -//! - **Node identity & last-wins collision.** [`types::NodeSummary`] derives the -//! canonical key `Type + "_" + Name + "_" + File`. When DSL name extraction -//! yields the same name for multiple nodes the last one wins (resolved by the -//! upstream UAST extraction step; see crate todos). -//! - **Additive merge.** [`aggregate`] accumulates per-commit node touches into -//! per-tick state, derives coupling pairs from sorted touched-node keys, and -//! merges tick/fork state purely additively (counts and coupling counters -//! add). The merged data flattens into the index-keyed `Nodes`/`Counters` -//! report. +//! - **Entity identity.** [`types::NodeSummary`] derives the canonical key +//! `Type + "_" + Name + "_" + File`; the name is the entity's QUALIFIED name +//! (`Class.method`, `Receiver.method`), derived by the walk from the +//! enclosing declarations, so two `String()` methods on different receivers +//! are two entities. +//! - **Live state.** [`state::ShotnessState`] is the one source of truth, +//! built by replaying each commit's [`state::EntityEffect`]s in walk order: +//! a commit touches an entity at most once, the touched entities are +//! coupled pairwise, a file rename moves the entities (counts and couplings +//! preserved), a deleted file or a removed declaration ends them. The report +//! ([`state::ShotnessState::report`]) lists exactly the entities alive at the +//! end of the window as the index-keyed `Nodes`/`Counters` matrix. //! - **Metrics.** [`metrics`] computes hotness, coupling strength //! (`co / max(co, a, b)`), risk classification, and the aggregate summary. //! - **Machine-format report.** [`report`] assembles the @@ -21,20 +24,10 @@ //! the json / yaml / ndjson / timeseries / compact / bin outputs follow the //! report-format contract. All serialization routes through cf-gojson, //! never serde. -//! - **Report store.** [`store`] emits the `node_data` / `aggregate` record -//! stream. -//! -//! # Parity scope -//! -//! Machine formats (json, yaml, ndjson, timeseries, timeseries+ndjson, -//! compact, bin) are byte-identity targets, pinned against the reference -//! implementation by `tests/compat`. Terminal text and plot output are -//! cosmetic and live elsewhere (see crate todos). -pub mod aggregate; pub mod metrics; pub mod report; -pub mod store; +pub mod state; pub mod types; pub use metrics::{ @@ -42,16 +35,20 @@ pub use metrics::{ compute_hotspot_nodes, compute_node_coupling, compute_node_hotness, AggregateData, ComputedMetrics, HotspotNodeData, NodeCouplingData, NodeHotnessData, }; -pub use report::{aggregate_to_value, CommitSummary}; -pub use store::{write_to_store, NodeStoreRecord, ReportWriter, KIND_AGGREGATE, KIND_NODE_DATA}; +pub use state::{ + coupling_pair_count, touched_entities, CommitEffects, EntityEffect, NodeShotness, ShotnessState, +}; pub use types::{NodeSummary, ReportData}; /// Analyzer identifier as used in the report registry and CLI. pub const ANALYZER_ID: &str = "history/shotness"; -/// Default DSL expression for selecting code structures. -pub const DEFAULT_SHOTNESS_DSL_STRUCT: &str = r#"filter(.roles has "Function")"#; -/// Default DSL expression for extracting names. +/// Default DSL expression selecting the tracked code structures: every node +/// carrying the `Function` role anywhere in the tree (top-level functions, +/// class methods, nested functions). +pub const DEFAULT_SHOTNESS_DSL_STRUCT: &str = r#"rfilter(.roles has "Function")"#; +/// Default DSL expression extracting an entity's own name from a selected +/// node (the mapping contract's `props.name`). pub const DEFAULT_SHOTNESS_DSL_NAME: &str = ".props.name"; /// Configuration key for the DSL structure expression. @@ -68,7 +65,7 @@ mod tests { assert_eq!(ANALYZER_ID, "history/shotness"); assert_eq!( DEFAULT_SHOTNESS_DSL_STRUCT, - r#"filter(.roles has "Function")"# + r#"rfilter(.roles has "Function")"# ); assert_eq!(DEFAULT_SHOTNESS_DSL_NAME, ".props.name"); assert_eq!(CONFIG_SHOTNESS_DSL_STRUCT, "Shotness.DSLStruct"); diff --git a/crates/cf-shotness/src/metrics.rs b/crates/cf-shotness/src/metrics.rs index 80c962db..2e59ca08 100644 --- a/crates/cf-shotness/src/metrics.rs +++ b/crates/cf-shotness/src/metrics.rs @@ -23,6 +23,8 @@ pub struct NodeHotnessData { pub coupled_nodes: i64, /// Hotness score normalized to `[0, 1]` against the hottest node. pub hotness_score: f64, + /// The node's declaration (or file) was removed in the window. + pub ended: bool, } /// Coupling between two code nodes. @@ -70,8 +72,10 @@ pub struct AggregateData { pub avg_changes_per_node: f64, /// Average coupling strength across all pairs. pub avg_coupling_strength: f64, - /// Number of nodes at MEDIUM risk or above. + /// Number of live nodes at MEDIUM risk or above. pub hot_nodes: i64, + /// Number of nodes that ended in the window. + pub ended_nodes: i64, } /// All computed metric results. @@ -88,6 +92,9 @@ pub struct ComputedMetrics { pub hotspot_nodes: Vec<HotspotNodeData>, /// Aggregate statistics. pub aggregate: AggregateData, + /// Ended entities were kept (`--shotness-keep-ended`): the report then + /// carries the `ended` and `ended_nodes` fields. + pub ended_tracked: bool, } /// Self-change threshold for HIGH risk. @@ -103,6 +110,11 @@ pub const RISK_LEVEL_MEDIUM: &str = "MEDIUM"; pub const RISK_LEVEL_LOW: &str = "LOW"; /// Looks up `row[idx]`, returning 0 when absent. +/// Whether node `i` ended (a missing flag means live). +fn is_ended(input: &ReportData, i: usize) -> bool { + input.ended.get(i).copied().unwrap_or(false) +} + fn counter_at(row: &HashMap<usize, i64>, idx: usize) -> i64 { row.get(&idx).copied().unwrap_or(0) } @@ -148,6 +160,7 @@ pub fn compute_node_hotness(input: &ReportData) -> Vec<NodeHotnessData> { change_count, coupled_nodes, hotness_score, + ended: is_ended(input, i), }); } @@ -262,6 +275,11 @@ pub fn compute_hotspot_nodes(input: &ReportData) -> Vec<HotspotNodeData> { continue; } + // A hotspot is a risk in the code that exists: ended nodes are not. + if is_ended(input, i) { + continue; + } + let counters = &input.counters[i]; let change_count = counter_at(counters, i); @@ -300,6 +318,7 @@ pub fn compute_aggregate(input: &ReportData) -> AggregateData { let mut total_changes: i64 = 0; let mut total_couplings: i64 = 0; let mut hot_nodes: i64 = 0; + let mut ended_nodes: i64 = 0; let mut strength_sum: f64 = 0.0; let mut pair_count: i64 = 0; @@ -307,7 +326,9 @@ pub fn compute_aggregate(input: &ReportData) -> AggregateData { let self_i = counter_at(counters, i); total_changes += self_i; - if self_i >= HOTSPOT_THRESHOLD_MEDIUM { + if is_ended(input, i) { + ended_nodes += 1; + } else if self_i >= HOTSPOT_THRESHOLD_MEDIUM { hot_nodes += 1; } @@ -337,6 +358,7 @@ pub fn compute_aggregate(input: &ReportData) -> AggregateData { agg.total_changes = total_changes; agg.total_couplings = total_couplings; agg.hot_nodes = hot_nodes; + agg.ended_nodes = ended_nodes; if agg.total_nodes > 0 { agg.avg_changes_per_node = total_changes as f64 / agg.total_nodes as f64; @@ -357,6 +379,7 @@ pub fn compute_all_metrics(input: &ReportData) -> ComputedMetrics { node_coupling: compute_node_coupling(input), hotspot_nodes: compute_hotspot_nodes(input), aggregate: compute_aggregate(input), + ended_tracked: input.ended_tracked, } } @@ -379,6 +402,41 @@ mod tests { pairs.iter().copied().collect() } + /// Two hot entities, the second ended. + fn one_live_one_ended() -> ReportData { + ReportData { + nodes: vec![ + NodeSummary::new("function", "live", "a.go"), + NodeSummary::new("function", "gone", "a.go"), + ], + counters: vec![hm(&[(0, 25), (1, 4)]), hm(&[(0, 4), (1, 30)])], + ended: vec![false, true], + ended_tracked: true, + } + } + + #[test] + fn node_hotness_marks_ended_entities() { + let rows = compute_node_hotness(&one_live_one_ended()); + let flags: Vec<(&str, bool)> = rows.iter().map(|r| (r.name.as_str(), r.ended)).collect(); + assert_eq!(flags, vec![("gone", true), ("live", false)]); + } + + #[test] + fn hotspots_and_hot_nodes_count_only_live_entities() { + let input = one_live_one_ended(); + let names: Vec<String> = compute_hotspot_nodes(&input) + .into_iter() + .map(|h| h.name) + .collect(); + assert_eq!(names, vec!["live"]); + let agg = compute_aggregate(&input); + assert_eq!(agg.hot_nodes, 1); + assert_eq!(agg.ended_nodes, 1); + assert_eq!(agg.total_nodes, 2, "ended entities stay in the report"); + assert_eq!(agg.total_changes, 55); + } + // Mirrors reference test TestNodeHotnessMetric_Empty. #[test] fn compute_node_hotness_empty() { @@ -394,6 +452,8 @@ mod tests { NodeSummary::new("function", "TestFunc2", "file1.go"), ], counters: vec![hm(&[(0, 10), (1, 5)]), hm(&[(0, 5), (1, 20)])], + ended: Vec::new(), + ended_tracked: false, }; let result = compute_node_hotness(&input); assert_eq!(result.len(), 2); @@ -416,6 +476,8 @@ mod tests { NodeSummary::new("", "TestFunc2", ""), ], counters: vec![hm(&[(0, 10)])], + ended: Vec::new(), + ended_tracked: false, }; let result = compute_node_hotness(&input); assert_eq!(result.len(), 1); @@ -442,6 +504,8 @@ mod tests { hm(&[(0, 5), (1, 20), (2, 8)]), hm(&[(0, 2), (1, 8), (2, 5)]), ], + ended: Vec::new(), + ended_tracked: false, }; let result = compute_node_coupling(&input); assert_eq!(result.len(), 3); @@ -466,6 +530,8 @@ mod tests { NodeSummary::new("", "TestFunc2", ""), ], counters: vec![hm(&[(0, 10), (1, 0)]), hm(&[(0, 0), (1, 20)])], + ended: Vec::new(), + ended_tracked: false, }; assert_eq!(compute_node_coupling(&input), vec![]); } @@ -479,6 +545,8 @@ mod tests { NodeSummary::new("function", "TestFunc2", "file2.go"), ], counters: vec![hm(&[(0, 10), (1, 5)]), hm(&[(0, 5), (1, 20)])], + ended: Vec::new(), + ended_tracked: false, }; let result = compute_node_coupling(&input); assert_eq!(result.len(), 1); @@ -516,6 +584,8 @@ mod tests { hm(&[(0, 0), (1, HOTSPOT_THRESHOLD_HIGH)]), hm(&[(0, 0), (1, 0), (2, HOTSPOT_THRESHOLD_MEDIUM)]), ], + ended: Vec::new(), + ended_tracked: false, }; let result = compute_hotspot_nodes(&input); assert_eq!(result.len(), 2); @@ -573,6 +643,8 @@ mod tests { hm(&[(0, HOTSPOT_THRESHOLD_HIGH), (1, 5)]), hm(&[(0, 5), (1, 10)]), ], + ended: Vec::new(), + ended_tracked: false, }; let result = compute_aggregate(&input); assert_eq!(result.total_nodes, 2); @@ -592,6 +664,8 @@ mod tests { NodeSummary::new("", "TestFunc2", ""), ], counters: vec![hm(&[(0, 10), (1, 5)]), hm(&[(0, 5), (1, 10)])], + ended: Vec::new(), + ended_tracked: false, }; let result = compute_aggregate(&input); assert!((result.avg_coupling_strength - 0.5).abs() < 0.01); diff --git a/crates/cf-shotness/src/report.rs b/crates/cf-shotness/src/report.rs index 81aa4d50..d9222d3c 100644 --- a/crates/cf-shotness/src/report.rs +++ b/crates/cf-shotness/src/report.rs @@ -3,13 +3,11 @@ //! The serialized machine output of the shotness analyzer is its //! [`crate::metrics::ComputedMetrics`]. This module assembles //! the ordered [`GoValue`] tree so cf-gojson renders the contractual report -//! bytes (pinned by `tests/compat`): +//! bytes (pinned by the report e2e suites (`bins/codefang/tests`)): //! //! - [`ComputedMetrics`] is struct-origin → keys in declaration order //! (`node_hotness`, `node_coupling`, `hotspot_nodes`, `aggregate`); -//! - each row struct emits its fields in declaration order; -//! - the per-commit timeseries summary is map-origin → keys byte-sorted -//! (`coupling_pairs` before `nodes_touched`). +//! - each row struct emits its fields in declaration order. //! //! All serialization routes through cf-gojson; never serde. @@ -19,31 +17,7 @@ use crate::metrics::{ AggregateData, ComputedMetrics, HotspotNodeData, NodeCouplingData, NodeHotnessData, }; -/// Per-commit summary for timeseries output. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CommitSummary { - /// Number of nodes touched in the commit. - pub nodes_touched: i64, - /// Number of coupling pairs implied by the touched nodes. - pub coupling_pairs: i64, -} - -impl CommitSummary { - /// JSON-friendly map representation. - /// - /// Built as a **map-origin** object so cf-gojson byte-sorts the keys - /// (report-format contract): `coupling_pairs` sorts before - /// `nodes_touched`. - #[must_use] - pub fn to_go_value(&self) -> GoValue { - let mut m = GoMap::new_map(); - m.push("nodes_touched", GoValue::Int(self.nodes_touched)); - m.push("coupling_pairs", GoValue::Int(self.coupling_pairs)); - GoValue::Map(m) - } -} - -fn node_hotness_to_value(h: &NodeHotnessData) -> GoValue { +fn node_hotness_to_value(h: &NodeHotnessData, ended_tracked: bool) -> GoValue { let mut o = GoMap::new_struct(); o.push("name", GoValue::Str(h.name.clone())); o.push("type", GoValue::Str(h.type_.clone())); @@ -51,6 +25,9 @@ fn node_hotness_to_value(h: &NodeHotnessData) -> GoValue { o.push("change_count", GoValue::Int(h.change_count)); o.push("coupled_nodes", GoValue::Int(h.coupled_nodes)); o.push("hotness_score", GoValue::Float(h.hotness_score)); + if ended_tracked { + o.push("ended", GoValue::Bool(h.ended)); + } GoValue::Map(o) } @@ -76,8 +53,7 @@ fn hotspot_to_value(h: &HotspotNodeData) -> GoValue { } /// Builds the [`GoValue`] tree for [`AggregateData`] (struct field order). -#[must_use] -pub fn aggregate_to_value(a: &AggregateData) -> GoValue { +fn aggregate_to_value(a: &AggregateData, ended_tracked: bool) -> GoValue { let mut o = GoMap::new_struct(); o.push("total_nodes", GoValue::Int(a.total_nodes)); o.push("total_changes", GoValue::Int(a.total_changes)); @@ -91,6 +67,9 @@ pub fn aggregate_to_value(a: &AggregateData) -> GoValue { GoValue::Float(a.avg_coupling_strength), ); o.push("hot_nodes", GoValue::Int(a.hot_nodes)); + if ended_tracked { + o.push("ended_nodes", GoValue::Int(a.ended_nodes)); + } GoValue::Map(o) } @@ -106,7 +85,7 @@ impl ComputedMetrics { GoValue::Array( self.node_hotness .iter() - .map(node_hotness_to_value) + .map(|h| node_hotness_to_value(h, self.ended_tracked)) .collect(), ), ); @@ -135,7 +114,10 @@ impl ComputedMetrics { GoValue::Array(self.hotspot_nodes.iter().map(hotspot_to_value).collect()) }, ); - root.push("aggregate", aggregate_to_value(&self.aggregate)); + root.push( + "aggregate", + aggregate_to_value(&self.aggregate, self.ended_tracked), + ); GoValue::Map(root) } @@ -163,19 +145,6 @@ mod tests { ); } - #[test] - fn commit_summary_keys_byte_sorted() { - let cs = CommitSummary { - nodes_touched: 3, - coupling_pairs: 3, - }; - // map-origin: coupling_pairs sorts before nodes_touched. - assert_eq!( - marshal(&cs.to_go_value()), - br#"{"coupling_pairs":3,"nodes_touched":3}"# - ); - } - #[test] fn two_node_report_struct_field_order_and_hottest_first() { let input = ReportData { @@ -184,6 +153,8 @@ mod tests { NodeSummary::new("Function", "foo", "a.go"), ], counters: vec![hm(&[(0, 6), (1, 3)]), hm(&[(1, 10), (0, 3)])], + ended: Vec::new(), + ended_tracked: false, }; let metrics = compute_all_metrics(&input); let s = String::from_utf8(marshal(&metrics.to_go_value())).unwrap(); @@ -193,5 +164,27 @@ mod tests { assert!(s.contains(r#""aggregate":"#)); // Hottest node (foo, change_count 10) sorts first; field order preserved. assert!(s.contains(r#"{"name":"foo","type":"Function","file":"a.go","change_count":10"#)); + assert!( + s.contains(r#""hotness_score":1}"#), + "untracked: no ended field: {s}" + ); + assert!(!s.contains("ended_nodes"), "{s}"); + } + + #[test] + fn a_tracked_report_carries_the_ended_fields() { + let input = ReportData { + nodes: vec![ + NodeSummary::new("Function", "bar", "a.go"), + NodeSummary::new("Function", "foo", "a.go"), + ], + counters: vec![hm(&[(0, 6), (1, 3)]), hm(&[(1, 10), (0, 3)])], + ended: vec![true, false], + ended_tracked: true, + }; + let s = String::from_utf8(marshal(&compute_all_metrics(&input).to_go_value())).unwrap(); + assert!(s.contains(r#""name":"bar","type":"Function","file":"a.go","change_count":6,"coupled_nodes":1,"hotness_score":0.6,"ended":true}"#), "{s}"); + assert!(s.contains(r#""hotness_score":1,"ended":false}"#), "{s}"); + assert!(s.contains(r#""hot_nodes":1,"ended_nodes":1}"#), "{s}"); } } diff --git a/crates/cf-shotness/src/state.rs b/crates/cf-shotness/src/state.rs new file mode 100644 index 00000000..2adb3f9d --- /dev/null +++ b/crates/cf-shotness/src/state.rs @@ -0,0 +1,646 @@ +//! The live entity state of a shotness walk — the ONE source of truth every +//! report is built from — and the per-commit [`EntityEffect`] records it is +//! built from. +//! +//! Entities (functions, methods, classes — whatever the structural DSL +//! selects) are keyed by [`NodeSummary::key`] (`Type_Name_File`). A walk +//! records, per commit, what the commit did to each entity +//! ([`CommitEffects`]): a *touch* (its hunks overlapped the entity's span, or +//! created it), a *move* (a file rename changed the entity's key) and an +//! *end* (the declaration, or its file, was removed). [`ShotnessState::apply`] +//! replays one commit's effects: moves first (keys rewritten, counts and +//! couplings preserved), then touches (one count per entity, the touched +//! entities coupled pairwise), then ends (the entity and every coupling that +//! referenced it are dropped). Replaying every commit in walk order over an +//! empty state yields [`ShotnessState::report`]: exactly the entities alive +//! at the end of the window. + +use std::collections::{BTreeMap, BTreeSet, HashMap}; + +use crate::types::{NodeSummary, ReportData}; + +/// Minimum touched entities in one commit to form a coupling pair. +pub const MIN_COUPLING_NODES: usize = 2; +/// Cap on touched entities per commit for the pairwise coupling update: above +/// it (a mass refactor) the O(n²) update is skipped as noise. +pub const MAX_COUPLING_NODES: usize = 500; +/// Divisor in `C(n,2) = n*(n-1)/2`. +const COMBINATORIAL_PAIR_DIVISOR: i64 = 2; + +/// One entity of the window: its identity, self-change count, co-change +/// counters and whether it has ended. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NodeShotness { + /// Entity identity. + pub summary: NodeSummary, + /// Number of commits that touched the entity. + pub count: i64, + /// Co-change counters keyed by the *other* entity's key (symmetric with + /// the partner's map; a `BTreeMap` keeps report building deterministic). + pub couples: BTreeMap<String, i64>, + /// The declaration (or its file) was removed and not declared again: the + /// entity stays in the report with its history, marked ended. + pub ended: bool, +} + +/// What one commit did to one entity. Keyed in [`CommitEffects`] by the +/// entity's key AFTER any move in that commit. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EntityEffect { + /// The entity's identity after the commit. + pub summary: NodeSummary, + /// The commit's hunks overlapped the entity's span (or created it): one + /// change is counted and the entity is coupled with the commit's other + /// touched entities. + pub touched: bool, + /// The declaration (or its file) was removed: the entity ends after this + /// commit's touches are counted. + pub ended: bool, + /// The entity's key before a file rename in this commit. + pub moved_from: Option<String>, +} + +impl EntityEffect { + /// A touch of `summary`. + #[must_use] + pub fn touched(summary: NodeSummary) -> Self { + Self { + summary, + touched: true, + ended: false, + moved_from: None, + } + } +} + +/// One commit's effects, keyed by entity key — the per-commit record the walk +/// returns and every format reads. +pub type CommitEffects = BTreeMap<String, EntityEffect>; + +/// The entities a commit touched (key → identity), in key order. +pub fn touched_entities(effects: &CommitEffects) -> impl Iterator<Item = (&String, &NodeSummary)> { + effects + .iter() + .filter(|(_, e)| e.touched) + .map(|(k, e)| (k, &e.summary)) +} + +/// The number of coupling pairs `n` touched entities form: `C(n,2)`, zero +/// below [`MIN_COUPLING_NODES`]. +/// +/// ``` +/// use cf_shotness::state::coupling_pair_count; +/// +/// assert_eq!(coupling_pair_count(1), 0); +/// assert_eq!(coupling_pair_count(2), 1); +/// assert_eq!(coupling_pair_count(4), 6); +/// ``` +#[must_use] +#[allow(clippy::cast_possible_wrap)] // touched-entity counts are small +pub fn coupling_pair_count(n: usize) -> i64 { + if n < MIN_COUPLING_NODES { + return 0; + } + let n = n as i64; + n * (n - 1) / COMBINATORIAL_PAIR_DIVISOR +} + +/// The entity set of a walk. By default an entity that ends is dropped with +/// its couplings; a state built with [`ShotnessState::keeping_ended`] keeps +/// it, marked ended (`--shotness-keep-ended`). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ShotnessState { + /// Entity key → entity. + nodes: BTreeMap<String, NodeShotness>, + /// File path → the keys of the LIVE entities declared in it. + files: BTreeMap<String, BTreeSet<String>>, + /// Ended entities stay, marked, instead of being dropped. + keep_ended: bool, +} + +impl ShotnessState { + /// An empty state that drops ended entities. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// An empty state that keeps ended entities with their counts and + /// couplings, marked ended. + #[must_use] + pub fn keeping_ended() -> Self { + Self { + keep_ended: true, + ..Self::default() + } + } + + /// Number of entities, live and ended. + #[must_use] + pub fn len(&self) -> usize { + self.nodes.len() + } + + /// Whether no entity was seen. + #[must_use] + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + + /// The entity under `key`, live or ended. + #[must_use] + pub fn get(&self, key: &str) -> Option<&NodeShotness> { + self.nodes.get(key) + } + + /// The live entities declared in `file`, in key order. + pub fn entities_in(&self, file: &str) -> impl Iterator<Item = &NodeShotness> { + self.files + .get(file) + .into_iter() + .flatten() + .filter_map(|key| self.nodes.get(key)) + } + + /// Applies one commit's effects — moves, then touches (coupled pairwise), + /// then ends — and returns the number of coupling pairs the commit formed. + pub fn apply(&mut self, effects: &CommitEffects) -> i64 { + for effect in effects.values() { + if let Some(from) = &effect.moved_from { + self.move_entity(from, &effect.summary); + } + } + let mut touched: Vec<&str> = Vec::new(); + for (key, effect) in effects { + if effect.touched { + self.touch(&effect.summary); + touched.push(key); + } + } + let pairs = self.record_couplings(&touched); + for (key, effect) in effects { + if effect.ended { + if self.keep_ended { + self.end_entity(key); + } else { + self.drop_entity(key); + } + } + } + pairs + } + + /// Counts one change on `summary`: a new entity is born with count 1, a + /// known one is incremented, an ended one is declared again and keeps + /// its history. + fn touch(&mut self, summary: &NodeSummary) { + let key = summary.key(); + let node = self + .nodes + .entry(key.clone()) + .or_insert_with(|| NodeShotness { + summary: summary.clone(), + count: 0, + couples: BTreeMap::new(), + ended: false, + }); + node.count += 1; + node.ended = false; + self.files + .entry(summary.file.clone()) + .or_default() + .insert(key); + } + + /// Couples every pair of touched entities (both directions) and returns + /// the pair count. Above [`MAX_COUPLING_NODES`] the counters are left + /// untouched (the pair count is still returned). + fn record_couplings(&mut self, touched: &[&str]) -> i64 { + let n = touched.len(); + let pairs = coupling_pair_count(n); + if !(MIN_COUPLING_NODES..=MAX_COUPLING_NODES).contains(&n) { + return pairs; + } + for (i, first) in touched.iter().enumerate() { + for second in &touched[i + 1..] { + self.bump_couple(first, second); + self.bump_couple(second, first); + } + } + pairs + } + + /// Increments `from`'s counter towards `to`. + fn bump_couple(&mut self, from: &str, to: &str) { + if let Some(node) = self.nodes.get_mut(from) { + *node.couples.entry(to.to_string()).or_insert(0) += 1; + } + } + + /// Ends the entity under `key` in a state that keeps ended entities: it + /// leaves its file's live set and is marked ended, keeping its count and + /// couplings. Unknown keys are a no-op. + fn end_entity(&mut self, key: &str) { + let Some(node) = self.nodes.get_mut(key) else { + return; + }; + node.ended = true; + if let Some(keys) = self.files.get_mut(&node.summary.file) { + keys.remove(key); + } + } + + /// Drops the entity under `key` entirely: it and the partners' counters + /// that referenced it. Unknown keys are a no-op. + fn drop_entity(&mut self, key: &str) { + let Some(node) = self.nodes.remove(key) else { + return; + }; + for partner in node.couples.keys() { + if let Some(p) = self.nodes.get_mut(partner) { + p.couples.remove(key); + } + } + if let Some(keys) = self.files.get_mut(&node.summary.file) { + keys.remove(key); + } + } + + /// Moves the entity under `from` to the identity `to`: the key is + /// rewritten, count and couplings preserved, the partners' counters + /// re-pointed. A previous occupant of the destination key (live or + /// ended) is replaced and dropped. + fn move_entity(&mut self, from: &str, to: &NodeSummary) { + let to_key = to.key(); + if from == to_key { + return; + } + let Some(mut node) = self.nodes.remove(from) else { + return; + }; + self.drop_entity(&to_key); + // The dropped occupant may have been a partner; an entity never couples + // with its own key. + node.couples.remove(&to_key); + if let Some(keys) = self.files.get_mut(&node.summary.file) { + keys.remove(from); + } + for partner in node.couples.keys() { + if let Some(p) = self.nodes.get_mut(partner) { + if let Some(count) = p.couples.remove(from) { + p.couples.insert(to_key.clone(), count); + } + } + } + node.summary = to.clone(); + self.files + .entry(to.file.clone()) + .or_default() + .insert(to_key.clone()); + self.nodes.insert(to_key, node); + } + + /// Builds the index-keyed `Nodes`/`Counters` report over every entity, + /// ended ones marked: + /// entities ordered by key, `counters[i][i]` the self-change count and + /// `counters[i][j]` the co-change count with entity `j`. + #[must_use] + pub fn report(&self) -> ReportData { + let index: HashMap<&str, usize> = self + .nodes + .keys() + .enumerate() + .map(|(i, k)| (k.as_str(), i)) + .collect(); + let mut nodes = Vec::with_capacity(self.nodes.len()); + let mut counters = Vec::with_capacity(self.nodes.len()); + let mut ended = Vec::with_capacity(self.nodes.len()); + for (i, node) in self.nodes.values().enumerate() { + nodes.push(node.summary.clone()); + ended.push(node.ended); + let mut counter: HashMap<usize, i64> = HashMap::new(); + counter.insert(i, node.count); + for (partner, count) in &node.couples { + if let Some(&j) = index.get(partner.as_str()) { + counter.insert(j, *count); + } + } + counters.push(counter); + } + ReportData { + nodes, + counters, + ended, + ended_tracked: self.keep_ended, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entity(name: &str, file: &str) -> NodeSummary { + NodeSummary::new("Function", name, file) + } + + /// A commit touching every `entities`. + fn touches(entities: &[NodeSummary]) -> CommitEffects { + entities + .iter() + .map(|e| (e.key(), EntityEffect::touched(e.clone()))) + .collect() + } + + /// A commit moving every entity of `from` to `to`. + fn file_move(state: &ShotnessState, from: &str, to: &str) -> CommitEffects { + state + .entities_in(from) + .map(|n| { + let moved = NodeSummary::new(&n.summary.type_, &n.summary.name, to); + ( + moved.key(), + EntityEffect { + summary: moved, + touched: false, + ended: false, + moved_from: Some(n.summary.key()), + }, + ) + }) + .collect() + } + + /// A commit ending `entities`. + fn ends(entities: &[NodeSummary]) -> CommitEffects { + entities + .iter() + .map(|e| { + ( + e.key(), + EntityEffect { + summary: e.clone(), + touched: false, + ended: true, + moved_from: None, + }, + ) + }) + .collect() + } + + fn count(state: &ShotnessState, key: &str) -> i64 { + state.get(key).map_or(0, |n| n.count) + } + + #[test] + fn a_touch_counts_once_per_commit_and_accumulates() { + let mut state = ShotnessState::new(); + let f = entity("f", "a.go"); + state.apply(&touches(std::slice::from_ref(&f))); + assert_eq!(count(&state, &f.key()), 1); + state.apply(&touches(std::slice::from_ref(&f))); + assert_eq!(count(&state, &f.key()), 2); + assert_eq!(state.len(), 1); + } + + #[test] + fn couplings_are_symmetric_and_additive() { + let mut state = ShotnessState::new(); + let (f, g) = (entity("f", "a.go"), entity("g", "a.go")); + assert_eq!(state.apply(&touches(&[f.clone(), g.clone()])), 1); + state.apply(&touches(&[f.clone(), g.clone()])); + assert_eq!(state.apply(&touches(std::slice::from_ref(&f))), 0); + assert_eq!(state.get(&f.key()).unwrap().couples[&g.key()], 2); + assert_eq!(state.get(&g.key()).unwrap().couples[&f.key()], 2); + assert_eq!(count(&state, &f.key()), 3); + } + + #[test] + fn pair_count_follows_c_n_2_and_the_cap_skips_the_counters() { + let mut state = ShotnessState::new(); + let three: Vec<NodeSummary> = ["a", "b", "c"].iter().map(|n| entity(n, "x")).collect(); + assert_eq!(state.apply(&touches(&three)), 3); + + let many: Vec<NodeSummary> = (0..=MAX_COUPLING_NODES) + .map(|i| entity(&format!("f{i}"), "x")) + .collect(); + let mut big = ShotnessState::new(); + let n = i64::try_from(MAX_COUPLING_NODES + 1).expect("small cap"); + assert_eq!(big.apply(&touches(&many)), n * (n - 1) / 2); + assert!(big + .get(&entity("f0", "x").key()) + .unwrap() + .couples + .is_empty()); + } + + #[test] + fn a_file_move_keeps_counts_and_re_points_couplings() { + let mut state = ShotnessState::new(); + let (f, g, h) = ( + entity("f", "m.go"), + entity("g", "m.go"), + entity("h", "o.go"), + ); + state.apply(&touches(&[f.clone(), g.clone(), h.clone()])); + state.apply(&touches(std::slice::from_ref(&f))); + let moved = file_move(&state, "m.go", "n.go"); + assert_eq!(moved.len(), 2); + state.apply(&moved); + + let (f2, g2) = (entity("f", "n.go"), entity("g", "n.go")); + assert_eq!(state.len(), 3, "entities move, they do not duplicate"); + assert!(state.get(&f.key()).is_none()); + assert_eq!(count(&state, &f2.key()), 2); + assert_eq!(count(&state, &g2.key()), 1); + assert_eq!(state.get(&f2.key()).unwrap().couples[&g2.key()], 1); + assert_eq!(state.get(&h.key()).unwrap().couples[&f2.key()], 1); + assert!(!state.get(&h.key()).unwrap().couples.contains_key(&f.key())); + assert_eq!(state.entities_in("m.go").count(), 0); + + // The moved entities keep accumulating under the new path, and move again. + state.apply(&touches(std::slice::from_ref(&f2))); + assert_eq!(count(&state, &f2.key()), 3); + let moved = file_move(&state, "n.go", "p.go"); + state.apply(&moved); + assert_eq!(count(&state, &entity("f", "p.go").key()), 3); + assert_eq!(state.len(), 3); + } + + #[test] + fn a_move_in_the_same_commit_as_a_touch_counts_under_the_new_key() { + let mut state = ShotnessState::new(); + let f = entity("f", "m.go"); + state.apply(&touches(std::slice::from_ref(&f))); + let f2 = entity("f", "n.go"); + let mut effects = file_move(&state, "m.go", "n.go"); + effects.get_mut(&f2.key()).unwrap().touched = true; + state.apply(&effects); + assert_eq!(count(&state, &f2.key()), 2); + assert!(state.get(&f.key()).is_none()); + } + + #[test] + fn a_move_onto_an_occupied_key_replaces_the_occupant() { + let mut state = ShotnessState::new(); + let (old, incoming) = (entity("f", "b.go"), entity("f", "a.go")); + state.apply(&touches(std::slice::from_ref(&old))); + state.apply(&touches(&[old.clone(), incoming.clone()])); + state.apply(&touches(std::slice::from_ref(&incoming))); + state.apply(&file_move(&state, "a.go", "b.go")); + assert_eq!(state.len(), 1); + assert_eq!(count(&state, &old.key()), 2, "the mover's count survives"); + assert!(state.get(&old.key()).unwrap().couples.is_empty()); + } + + #[test] + fn an_end_drops_the_entity_and_its_couplings() { + let mut state = ShotnessState::new(); + let (f, g, h) = ( + entity("f", "m.go"), + entity("g", "m.go"), + entity("h", "o.go"), + ); + state.apply(&touches(&[f.clone(), g.clone(), h.clone()])); + state.apply(&ends(std::slice::from_ref(&g))); + assert_eq!(state.len(), 2); + assert!(!state.get(&f.key()).unwrap().couples.contains_key(&g.key())); + assert_eq!(state.get(&f.key()).unwrap().couples[&h.key()], 1); + assert_eq!(state.entities_in("m.go").count(), 1); + // Ending twice, or an unknown key, is a no-op. + state.apply(&ends(&[g.clone(), entity("nope", "x")])); + assert_eq!(state.len(), 2); + } + + #[test] + fn a_touch_and_an_end_in_one_commit_count_then_drop() { + let mut state = ShotnessState::new(); + let (f, f2) = (entity("f", "m.go"), entity("f2", "m.go")); + state.apply(&touches(std::slice::from_ref(&f))); + let mut effects = touches(&[f.clone(), f2.clone()]); + effects.get_mut(&f.key()).unwrap().ended = true; + assert_eq!( + state.apply(&effects), + 1, + "the pair is formed before the end" + ); + assert_eq!(state.len(), 1); + assert_eq!(count(&state, &f2.key()), 1); + assert!(state.get(&f2.key()).unwrap().couples.is_empty()); + } + + #[test] + fn a_dropped_entity_declared_again_starts_over() { + let mut state = ShotnessState::new(); + let g = entity("g", "m.go"); + state.apply(&touches(std::slice::from_ref(&g))); + state.apply(&ends(std::slice::from_ref(&g))); + state.apply(&touches(std::slice::from_ref(&g))); + assert_eq!(count(&state, &g.key()), 1); + assert!(!state.report().ended_tracked); + } + + #[test] + fn an_end_keeps_the_entity_and_its_couplings_marked_ended() { + let mut state = ShotnessState::keeping_ended(); + let (f, g, h) = ( + entity("f", "m.go"), + entity("g", "m.go"), + entity("h", "o.go"), + ); + state.apply(&touches(&[f.clone(), g.clone(), h.clone()])); + state.apply(&ends(std::slice::from_ref(&g))); + assert_eq!(state.len(), 3, "an ended entity stays in the report"); + assert!(state.get(&g.key()).unwrap().ended); + assert_eq!(state.get(&f.key()).unwrap().couples[&g.key()], 1); + assert_eq!(state.get(&g.key()).unwrap().couples[&f.key()], 1); + assert_eq!( + state.entities_in("m.go").count(), + 1, + "only live entities belong to their file" + ); + // Ending twice, or an unknown key, is a no-op. + state.apply(&ends(&[g.clone(), entity("nope", "x")])); + assert_eq!(state.len(), 3); + assert_eq!(count(&state, &g.key()), 1); + } + + #[test] + fn a_touch_and_an_end_in_one_commit_count_then_end() { + let mut state = ShotnessState::keeping_ended(); + let (f, f2) = (entity("f", "m.go"), entity("f2", "m.go")); + state.apply(&touches(std::slice::from_ref(&f))); + let mut effects = touches(&[f.clone(), f2.clone()]); + effects.get_mut(&f.key()).unwrap().ended = true; + assert_eq!( + state.apply(&effects), + 1, + "the pair is formed before the end" + ); + assert_eq!(count(&state, &f.key()), 2); + assert!(state.get(&f.key()).unwrap().ended); + assert!(!state.get(&f2.key()).unwrap().ended); + assert_eq!(state.get(&f2.key()).unwrap().couples[&f.key()], 1); + } + + #[test] + fn a_touch_revives_an_ended_entity() { + let mut state = ShotnessState::keeping_ended(); + let g = entity("g", "m.go"); + state.apply(&touches(std::slice::from_ref(&g))); + state.apply(&ends(std::slice::from_ref(&g))); + state.apply(&touches(std::slice::from_ref(&g))); + let node = state.get(&g.key()).unwrap(); + assert!(!node.ended, "declared again"); + assert_eq!(node.count, 2, "its history continues"); + assert_eq!(state.entities_in("m.go").count(), 1); + } + + #[test] + fn report_marks_ended_entities() { + let mut state = ShotnessState::keeping_ended(); + let (f, g) = (entity("f", "m.go"), entity("g", "m.go")); + state.apply(&touches(&[f.clone(), g.clone()])); + state.apply(&ends(std::slice::from_ref(&g))); + let report = state.report(); + assert_eq!(report.nodes, vec![f, g]); + assert_eq!(report.ended, vec![false, true]); + assert!(report.ended_tracked); + assert_eq!( + report.counters[1][&0], 1, + "the ended entity keeps its couplings" + ); + } + + #[test] + fn report_orders_by_key_with_self_and_pair_counters() { + let mut state = ShotnessState::new(); + let (foo, bar) = (entity("foo", "a.go"), entity("bar", "a.go")); + state.apply(&touches(&[foo.clone(), bar.clone()])); + state.apply(&touches(std::slice::from_ref(&foo))); + let report = state.report(); + // Function_bar_a.go < Function_foo_a.go + assert_eq!(report.nodes, vec![bar, foo]); + assert_eq!(report.counters[0][&0], 1); + assert_eq!(report.counters[1][&1], 2); + assert_eq!(report.counters[0][&1], 1); + assert_eq!(report.counters[1][&0], 1); + } + + #[test] + fn touched_entities_lists_only_touches() { + let f = entity("f", "m.go"); + let mut effects = touches(std::slice::from_ref(&f)); + effects.extend(ends(&[entity("g", "m.go")])); + let touched: Vec<&String> = touched_entities(&effects).map(|(k, _)| k).collect(); + assert_eq!(touched, vec![&f.key()]); + } + + #[test] + fn empty_state_reports_nothing() { + let state = ShotnessState::new(); + assert!(state.is_empty()); + assert_eq!(state.report(), ReportData::default()); + } +} diff --git a/crates/cf-shotness/src/store.rs b/crates/cf-shotness/src/store.rs deleted file mode 100644 index 8adad0b2..00000000 --- a/crates/cf-shotness/src/store.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! Report-store record types and writing logic. -//! -//! The store streams one `node_data` record per node (in sorted node-key -//! order) followed by a single `aggregate` record. -//! -//! The store codec itself (the [`ReportWriter`] trait) is defined here as the -//! minimal interface the shotness writer needs; the concrete persistence -//! backend belongs to `cf-analyze`/`cf-persist` (see crate-level todos). -//! Record payloads are exposed as [`GoValue`] trees so whichever backend is -//! wired in serializes them through cf-gojson for byte parity. - -use std::collections::HashMap; - -use cf_gojson::{GoMap, GoValue}; - -use crate::aggregate::{build_report_from_merged, TickNodes}; -use crate::metrics::compute_aggregate; -use crate::report::aggregate_to_value; -use crate::types::NodeSummary; - -/// Store record kind for per-node data. -pub const KIND_NODE_DATA: &str = "node_data"; -/// Store record kind for the aggregate. -pub const KIND_AGGREGATE: &str = "aggregate"; - -/// A single node's summary plus its co-change counter row. -/// -/// Counter keys are node indices into the ordered node list; `counter[self]` -/// is the self-change count. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NodeStoreRecord { - /// Node identity. - pub summary: NodeSummary, - /// Co-change counter row keyed by node index. - pub counter: HashMap<usize, i64>, -} - -impl NodeStoreRecord { - /// Serializes this record to a [`GoValue`] tree. - /// - /// `Summary` is struct-origin (field order: `Type`, `Name`, `File`). - /// `Counter` is an integer-keyed map; the report contract stringifies - /// integer keys and sorts them by their **UTF-8 byte order** (so `"10"` - /// precedes `"2"`, NOT numeric order). We push stringified indices into a - /// map-origin object and let cf-gojson's byte-sort reproduce that ordering - /// exactly — confirmed against the reference encoder. - #[must_use] - pub fn to_go_value(&self) -> GoValue { - let mut summary = GoMap::new_struct(); - summary.push("Type", GoValue::Str(self.summary.type_.clone())); - summary.push("Name", GoValue::Str(self.summary.name.clone())); - summary.push("File", GoValue::Str(self.summary.file.clone())); - - // map-origin: cf-gojson byte-sorts the stringified int keys at encode - // time (report-format contract). - let mut counter = GoMap::new_map(); - for (idx, val) in &self.counter { - counter.push(idx.to_string(), GoValue::Int(*val)); - } - - let mut root = GoMap::new_struct(); - root.push("Summary", GoValue::Map(summary)); - root.push("Counter", GoValue::Map(counter)); - GoValue::Map(root) - } -} - -/// Minimal report-store writer interface used by the shotness store writer. -/// -/// The concrete implementation lives in `cf-analyze` (todo); this trait lets -/// the shotness logic be developed and tested in isolation. -pub trait ReportWriter { - /// Writes a single record of the given kind. The payload is a [`GoValue`] - /// tree (serialized by the backend through cf-gojson for byte parity). - /// - /// # Errors - /// - /// Returns the backend's error message when the record cannot be written. - fn write(&mut self, kind: &str, data: GoValue) -> Result<(), String>; -} - -/// Streams shotness store records from merged per-tick node data. -/// -/// Given the already-merged tick node map, writes one `node_data` record per -/// node (sorted by node key) then a single `aggregate` record. -/// -/// # Errors -/// -/// Returns the underlying [`ReportWriter`] error, prefixed with the failing -/// record kind (`"write <kind>: <err>"` — part of the CLI error-text -/// contract). -pub fn write_to_store<W: ReportWriter>(merged: &TickNodes, w: &mut W) -> Result<(), String> { - let report = build_report_from_merged(merged); - - for i in 0..report.nodes.len() { - let rec = NodeStoreRecord { - summary: report.nodes[i].clone(), - counter: report.counters[i].clone(), - }; - w.write(KIND_NODE_DATA, rec.to_go_value()) - .map_err(|e| format!("write {KIND_NODE_DATA}: {e}"))?; - } - - let agg = compute_aggregate(&report); - w.write(KIND_AGGREGATE, aggregate_to_value(&agg)) - .map_err(|e| format!("write {KIND_AGGREGATE}: {e}"))?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::aggregate::{accumulate_nodes, compute_coupling_pairs}; - use cf_gojson::marshal::marshal; - use std::collections::BTreeMap; - - struct FakeWriter { - records: Vec<(String, GoValue)>, - } - - impl ReportWriter for FakeWriter { - fn write(&mut self, kind: &str, data: GoValue) -> Result<(), String> { - self.records.push((kind.to_string(), data)); - Ok(()) - } - } - - // Mirrors reference test TestWriteToStore_EmptyTicks: empty input → only the aggregate. - #[test] - fn write_to_store_empty_emits_only_aggregate() { - let merged: TickNodes = BTreeMap::new(); - let mut w = FakeWriter { records: vec![] }; - write_to_store(&merged, &mut w).unwrap(); - assert_eq!(w.records.len(), 1); - assert_eq!(w.records[0].0, KIND_AGGREGATE); - } - - #[test] - fn node_store_record_fields() { - let rec = NodeStoreRecord { - summary: NodeSummary::new("Function", "foo", "a.go"), - counter: [(0usize, 5i64), (1usize, 2i64)].into_iter().collect(), - }; - assert_eq!(rec.summary.type_, "Function"); - assert_eq!(rec.summary.name, "foo"); - assert_eq!(rec.counter[&0], 5); - assert_eq!(rec.counter[&1], 2); - } - - // Mirrors reference test TestWriteToStore_RoundTrip (record count + sorted order). - #[test] - fn write_to_store_two_nodes_emits_node_then_aggregate() { - let foo = NodeSummary::new("Function", "foo", "a.go"); - let bar = NodeSummary::new("Function", "bar", "a.go"); - let mut acc: TickNodes = BTreeMap::new(); - let t: BTreeMap<String, NodeSummary> = [(foo.key(), foo.clone()), (bar.key(), bar.clone())] - .into_iter() - .collect(); - accumulate_nodes(&mut acc, &t); - compute_coupling_pairs(&mut acc, &t); - - let mut w = FakeWriter { records: vec![] }; - write_to_store(&acc, &mut w).unwrap(); - - // Two node_data records (sorted: bar then foo) + one aggregate. - assert_eq!(w.records.len(), 3); - assert_eq!(w.records[0].0, KIND_NODE_DATA); - assert_eq!(w.records[1].0, KIND_NODE_DATA); - assert_eq!(w.records[2].0, KIND_AGGREGATE); - - let first = String::from_utf8(marshal(&w.records[0].1)).unwrap(); - assert!(first.contains(r#""Name":"bar""#), "got: {first}"); - } - - // Integer map keys render as strings byte-sorted: "10" precedes "2" - // (report-format contract, confirmed against the reference encoder). - #[test] - fn counter_int_keys_byte_sorted_like_go() { - let rec = NodeStoreRecord { - summary: NodeSummary::new("Function", "foo", "a.go"), - counter: [(0usize, 5i64), (1, 9), (10, 1), (2, 3)] - .into_iter() - .collect(), - }; - let s = String::from_utf8(marshal(&rec.to_go_value())).unwrap(); - assert!( - s.contains(r#""Counter":{"0":5,"1":9,"10":1,"2":3}"#), - "got: {s}" - ); - } -} diff --git a/crates/cf-shotness/src/types.rs b/crates/cf-shotness/src/types.rs index ea54a36b..11acd455 100644 --- a/crates/cf-shotness/src/types.rs +++ b/crates/cf-shotness/src/types.rs @@ -8,13 +8,14 @@ use std::collections::HashMap; /// Identifying information for a code node (UAST entity). /// /// The [`NodeSummary::key`] derivation `Type + "_" + Name + "_" + File` is the -/// canonical node key used for last-wins collision resolution and additive -/// merge across commits. -#[derive(Debug, Clone, PartialEq, Eq, Default)] +/// canonical entity key of the live state; `name` is the qualified name the +/// walk derives (`Class.method`, `Receiver.method`). +#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] pub struct NodeSummary { /// UAST node type (e.g. the grammar type string). pub type_: String, - /// Extracted display name (from the name DSL, or the node token). + /// The qualified entity name (enclosing declarations, then the name DSL + /// result). pub name: String, /// Source file path the node belongs to. pub file: String, @@ -46,24 +47,88 @@ impl NodeSummary { } } -/// Parsed report input for the metric computation stage. +/// The report input of the metric computation stage, built by +/// [`crate::state::ShotnessState::report`]. /// /// `counters[i]` is the co-change row for node `i`; `counters[i][i]` is node /// `i`'s self-change count, and `counters[i][j]` (`j != i`) is the co-change /// count between nodes `i` and `j`. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ReportData { - /// Ordered list of nodes (sorted by node key when produced by the report - /// builder). + /// Ordered list of nodes (sorted by node key). pub nodes: Vec<NodeSummary>, /// Per-node co-change counter rows, parallel to [`ReportData::nodes`]. pub counters: Vec<HashMap<usize, i64>>, + /// Per-node ended flag, parallel to [`ReportData::nodes`] (a missing + /// entry means live). + pub ended: Vec<bool>, + /// Ended entities were kept and marked (`--shotness-keep-ended`); when + /// false they were dropped and the report carries no ended fields. + pub ended_tracked: bool, +} + +impl ReportData { + /// The live entities only, the counters re-pointed at their new indices + /// (a view of the code that exists at the newest commit). + #[must_use] + pub fn live(&self) -> Self { + let is_live = |i: usize| !self.ended.get(i).copied().unwrap_or(false); + let mut new_index = HashMap::new(); + for (old, _) in self.nodes.iter().enumerate().filter(|(i, _)| is_live(*i)) { + new_index.insert(old, new_index.len()); + } + let mut live = Self { + ended_tracked: self.ended_tracked, + ..Self::default() + }; + for (old, node) in self.nodes.iter().enumerate() { + if !new_index.contains_key(&old) { + continue; + } + live.nodes.push(node.clone()); + live.ended.push(false); + live.counters.push( + self.counters + .get(old) + .into_iter() + .flatten() + .filter_map(|(j, count)| Some((*new_index.get(j)?, *count))) + .collect(), + ); + } + live + } } #[cfg(test)] mod tests { use super::*; + /// The live view drops ended nodes and re-points the counters at the + /// remaining nodes' new indices. + #[test] + fn the_live_view_drops_ended_nodes_and_reindexes_counters() { + let n = |name: &str| NodeSummary::new("Function", name, "a.go"); + let row = |pairs: &[(usize, i64)]| pairs.iter().copied().collect::<HashMap<usize, i64>>(); + let report = ReportData { + nodes: vec![n("a"), n("gone"), n("c")], + counters: vec![ + row(&[(0, 3), (1, 2), (2, 1)]), + row(&[(1, 5), (0, 2)]), + row(&[(2, 4), (0, 1)]), + ], + ended: vec![false, true, false], + ended_tracked: true, + }; + let live = report.live(); + assert_eq!(live.nodes, vec![n("a"), n("c")]); + assert_eq!( + live.counters, + vec![row(&[(0, 3), (1, 1)]), row(&[(1, 4), (0, 1)])] + ); + assert_eq!(live.ended, vec![false, false]); + } + #[test] fn node_summary_key_matches_contract_format() { let ns = NodeSummary::new("Function", "foo", "a.go"); diff --git a/crates/cf-textutil/Cargo.toml b/crates/cf-textutil/Cargo.toml index a6779d35..e5d67cff 100644 --- a/crates/cf-textutil/Cargo.toml +++ b/crates/cf-textutil/Cargo.toml @@ -11,7 +11,7 @@ description = "Byte-level text utilities and the canonical report JSON writer fo [dependencies] # Report serialization routes through the shared contract encoder, NEVER # serde_json, so machine-format report bytes stay pinned (DESIGN.md §1.1, §2; -# differential gate: tests/compat). `write_json` wraps `cf-gojson` +# the report e2e suites in bins/codefang/tests). `write_json` wraps `cf-gojson` # directly. cf-gojson = { path = "../cf-gojson" } # Error enums derive Display/Error via thiserror; the error strings are part of diff --git a/crates/cf-textutil/src/json.rs b/crates/cf-textutil/src/json.rs index f642f095..8b5966ac 100644 --- a/crates/cf-textutil/src/json.rs +++ b/crates/cf-textutil/src/json.rs @@ -3,7 +3,7 @@ //! Per `specs/rust-rewrite/DESIGN.md` (§1.1, §2, §3.2 row "json"), report //! serialization MUST route through the shared contract encoder — never raw //! `serde_json` — so machine-format report bytes stay pinned (differential -//! gate: `tests/compat`). +//! report e2e suites, `bins/codefang/tests`). //! //! The shared encoder is the tier-0 crate [`cf_gojson`]. This module is a //! thin wrapper selecting the [`cf_gojson::Encoder`] configuration every diff --git a/crates/cf-textutil/src/lib.rs b/crates/cf-textutil/src/lib.rs index 4c7f0dd2..debe3d70 100644 --- a/crates/cf-textutil/src/lib.rs +++ b/crates/cf-textutil/src/lib.rs @@ -14,7 +14,7 @@ //! [`gocompat`] for the encoder re-exports ([`GoValue`] / [`Encoder`]). //! //! Compatibility: machine-format report bytes are pinned against the reference -//! implementation by the differential gate in `tests/compat`. +//! implementation by the report e2e suites (`bins/codefang/tests`). //! //! # Examples //! diff --git a/crates/cf-typos/Cargo.toml b/crates/cf-typos/Cargo.toml index 56cf1de8..71396b33 100644 --- a/crates/cf-typos/Cargo.toml +++ b/crates/cf-typos/Cargo.toml @@ -6,5 +6,12 @@ rust-version.workspace = true license.workspace = true repository.workspace = true publish.workspace = true +description = "history/typos: typo-fix identifier pair detection over commit diffs and the typos report metrics." [dependencies] +# Spill encoding of finished history results (cf-commands `spill`). +serde = { workspace = true } +# Levenshtein distance for the line prefilter and the identifier-pair bound. +cf-alg-levenshtein = { path = "../cf-alg-levenshtein" } +# The report value every machine format (json/yaml/bin) encodes. +cf-gojson = { path = "../cf-gojson" } diff --git a/crates/cf-typos/src/compat.rs b/crates/cf-typos/src/compat.rs deleted file mode 100644 index b0f42454..00000000 --- a/crates/cf-typos/src/compat.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! Self-contained foundation types: Git hash and a report-contract JSON value -//! + encoder. -//! -//! The integrated pipeline routes report serialization through the shared -//! `cf-gojson` crate (and `cf-gitlib` for the hash type). This module provides -//! the minimal local slice of those contracts so the typos analyzer stands -//! alone and verifiable; consumers convert the local [`GoValue`] to the shared -//! encoder types at the boundary. The value variants and the hash API mirror -//! the shared crates, so a swap is mechanical. - -use std::fmt; - -/// Length of a Git SHA-1 hash in bytes. -pub const HASH_SIZE: usize = 20; - -/// A Git object hash (SHA-1), a 20-byte array. -/// -/// Mirrors `cf_gitlib::Hash`. The zero value is all-zero bytes; -/// [`Hash::string`] renders lowercase hex. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] -pub struct Hash(pub [u8; HASH_SIZE]); - -impl Hash { - /// Returns the lowercase hex string representation. - #[must_use] - pub fn string(&self) -> String { - use std::fmt::Write; - let mut s = String::with_capacity(HASH_SIZE * 2); - for b in &self.0 { - let _ = write!(s, "{b:02x}"); - } - s - } -} - -impl fmt::Display for Hash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.string()) - } -} - -/// A report-contract JSON value. -/// -/// Mirrors `cf_gojson::GoValue` for the variants the typos report uses. Object -/// keys follow the contract's dual-mode rule: -/// -/// * [`GoValue::Map`] — **map-origin**: keys sorted by raw UTF-8 bytes at -/// encode time (dynamic maps). -/// * [`GoValue::Struct`] — **struct-origin**: field declaration order -/// preserved (typed records). -#[derive(Debug, Clone, PartialEq)] -pub enum GoValue { - /// JSON `null`. - Null, - /// Signed integer; rendered with no decimal point. - Int(i64), - /// UTF-8 string. - Str(String), - /// JSON array. - Array(Vec<GoValue>), - /// Map-origin object (sorted keys at encode time). - Map(Vec<(String, GoValue)>), - /// Struct-origin object (declaration order preserved). - Struct(Vec<(String, GoValue)>), -} - -impl GoValue { - /// Convenience constructor for a map-origin object. - #[must_use] - pub fn map<I, K>(entries: I) -> GoValue - where - I: IntoIterator<Item = (K, GoValue)>, - K: Into<String>, - { - GoValue::Map(entries.into_iter().map(|(k, v)| (k.into(), v)).collect()) - } - - /// Serializes to compact JSON, per the report contract (no whitespace, map - /// keys sorted, HTML escaping on, no trailing newline). - #[must_use] - pub fn to_json(&self) -> String { - let mut out = String::new(); - self.write_json(&mut out, None, 0); - out - } - - /// Serializes to two-space-indented JSON, per the report contract's - /// indented mode (a single space after each colon; empty containers - /// collapse to `{}` / `[]`). No trailing newline is added. - #[must_use] - pub fn to_json_indent(&self) -> String { - let mut out = String::new(); - self.write_json(&mut out, Some(" "), 0); - out - } - - fn write_json(&self, out: &mut String, indent: Option<&str>, depth: usize) { - match self { - GoValue::Null => out.push_str("null"), - GoValue::Int(n) => out.push_str(&n.to_string()), - GoValue::Str(s) => write_json_string(s, out), - GoValue::Array(items) => { - write_seq(out, indent, depth, items.iter(), |out, item, ind, d| { - item.write_json(out, ind, d) - }) - } - GoValue::Map(entries) => { - // map-origin: sort keys by raw UTF-8 bytes at encode time. - let mut sorted: Vec<&(String, GoValue)> = entries.iter().collect(); - sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); - write_obj(out, indent, depth, sorted.into_iter()); - } - GoValue::Struct(entries) => { - // struct-origin: preserve declaration order. - write_obj(out, indent, depth, entries.iter()); - } - } - } -} - -/// Writes an array/sequence with optional indentation. -fn write_seq<T, I, F>(out: &mut String, indent: Option<&str>, depth: usize, items: I, mut f: F) -where - I: Iterator<Item = T>, - F: FnMut(&mut String, T, Option<&str>, usize), -{ - let items: Vec<T> = items.collect(); - if items.is_empty() { - out.push_str("[]"); - return; - } - out.push('['); - for (i, item) in items.into_iter().enumerate() { - if i > 0 { - out.push(','); - } - newline_indent(out, indent, depth + 1); - f(out, item, indent, depth + 1); - } - newline_indent(out, indent, depth); - out.push(']'); -} - -/// Writes an object with optional indentation, in the given entry order. -fn write_obj<'a, I>(out: &mut String, indent: Option<&str>, depth: usize, entries: I) -where - I: Iterator<Item = &'a (String, GoValue)>, -{ - let entries: Vec<&(String, GoValue)> = entries.collect(); - if entries.is_empty() { - out.push_str("{}"); - return; - } - out.push('{'); - for (i, (k, v)) in entries.into_iter().enumerate() { - if i > 0 { - out.push(','); - } - newline_indent(out, indent, depth + 1); - write_json_string(k, out); - out.push(':'); - if indent.is_some() { - out.push(' '); // Indent mode: one space after the colon. - } - v.write_json(out, indent, depth + 1); - } - newline_indent(out, indent, depth); - out.push('}'); -} - -/// Emits a newline plus `depth` copies of the indent unit, in indent mode only. -fn newline_indent(out: &mut String, indent: Option<&str>, depth: usize) { - if let Some(unit) = indent { - out.push('\n'); - for _ in 0..depth { - out.push_str(unit); - } - } -} - -/// Writes a report-contract quoted JSON string (HTML escaping on). -fn write_json_string(s: &str, out: &mut String) { - out.push('"'); - for c in s.chars() { - match c { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - '<' => out.push_str("\\u003c"), - '>' => out.push_str("\\u003e"), - '&' => out.push_str("\\u0026"), - '\u{2028}' => out.push_str("\\u2028"), - '\u{2029}' => out.push_str("\\u2029"), - c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)), - c => out.push(c), - } - } - out.push('"'); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hash_hex_zero() { - assert_eq!(Hash::default().string(), "0".repeat(40)); - } - - #[test] - fn hash_hex_nonzero() { - let mut b = [0u8; 20]; - b[0] = 0xab; - b[19] = 0x0f; - let s = Hash(b).string(); - assert!(s.starts_with("ab")); - assert!(s.ends_with("0f")); - assert_eq!(s.len(), 40); - } - - #[test] - fn map_sorts_keys_compact() { - let v = GoValue::map([ - ("typos", GoValue::Array(vec![])), - ("total_typos", GoValue::Int(0)), - ("total_count", GoValue::Int(0)), - ]); - assert_eq!( - v.to_json(), - r#"{"total_count":0,"total_typos":0,"typos":[]}"# - ); - } - - #[test] - fn struct_preserves_order_compact() { - let v = GoValue::Struct(vec![ - ("wrong".to_string(), GoValue::Str("tets".to_string())), - ("correct".to_string(), GoValue::Str("test".to_string())), - ("line".to_string(), GoValue::Int(10)), - ]); - assert_eq!( - v.to_json(), - r#"{"wrong":"tets","correct":"test","line":10}"# - ); - } - - #[test] - fn empty_containers_collapse() { - assert_eq!(GoValue::Array(vec![]).to_json_indent(), "[]"); - assert_eq!(GoValue::Map(vec![]).to_json_indent(), "{}"); - } - - #[test] - fn indent_mode_layout() { - let v = GoValue::Struct(vec![ - ("a".to_string(), GoValue::Int(1)), - ("b".to_string(), GoValue::Int(2)), - ]); - assert_eq!(v.to_json_indent(), "{\n \"a\": 1,\n \"b\": 2\n}"); - } - - #[test] - fn escapes_html() { - // HTML escaping is ON in the report contract: <, >, & become their - // \u00xx escape sequences. - assert_eq!( - GoValue::Str("a<b>&c".to_string()).to_json(), - "\"a\\u003cb\\u003e\\u0026c\"" - ); - } -} diff --git a/crates/cf-typos/src/detect.rs b/crates/cf-typos/src/detect.rs new file mode 100644 index 00000000..08b3100a --- /dev/null +++ b/crates/cf-typos/src/detect.rs @@ -0,0 +1,376 @@ +//! The typo-fix detection rules over one changed file. +//! +//! A history walk hands this module the line diff of a Modify change plus +//! the identifiers each focused line carries (from the UAST), and gets back +//! the `(wrong, correct)` pairs the commit fixed: +//! +//! 1. [`candidate_line_pairs`] aligns a Delete run with the Insert run of the +//! same size that immediately follows it (line *i* of each) and keeps the +//! pairs whose whole lines are within `max_distance` edits — a prefilter +//! that bounds the UAST work to the lines that plausibly changed one +//! token. +//! 2. [`identifier_pair`] diffs the identifier multisets of a candidate pair: +//! the identifiers both lines share cancel, whatever their position, and +//! the pair is a typo fix only when exactly ONE identifier remains on each +//! side, both are non-empty, they differ, and the identifiers themselves +//! are within `max_distance` edits. +//! +//! So `#define N 4` → `#define N 17` is never a typo (`N` cancels, nothing +//! remains), `v := recieve()` → `v := receive()` is (`v` cancels, the pair is +//! two edits apart), and `foo()` → `completelyDifferent()` is not (the pair +//! is far apart — and the line prefilter already rejected it). An +//! identifier is one token: an empty token, or one containing whitespace +//! (the identifier nodes prose mappings such as Markdown produce for a +//! heading), is never part of a pair. + +use std::collections::HashMap; + +use cf_alg_levenshtein::Context; + +/// One run of the line diff between the Before and After sides of a file. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiffOp { + /// `n` lines present on both sides. + Equal(usize), + /// `n` lines present only on the After side. + Insert(usize), + /// `n` lines present only on the Before side. + Delete(usize), +} + +/// A candidate line pair: 0-based indices into the Before and After line +/// vectors a Delete/Insert run pair aligned. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LinePair { + /// Index of the removed line on the Before side. + pub before: usize, + /// Index of the added line on the After side. + pub after: usize, +} + +/// The one identifier a candidate line pair changed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IdentifierChange { + /// The identifier on the Before side (the misspelling). + pub wrong: String, + /// The identifier on the After side (the correction). + pub correct: String, +} + +/// Aligns every Delete run with the Insert run of the same size that +/// immediately follows it and returns the aligned pairs whose whole lines are +/// within `max_distance` edits of each other (a byte-length gap above the +/// bound short-circuits the distance). Indices outside the line vectors are +/// skipped. +/// +/// ``` +/// use cf_alg_levenshtein::Context; +/// use cf_typos::{candidate_line_pairs, DiffOp, LinePair}; +/// +/// let before: Vec<&[u8]> = vec![b"package main", b"func recieve() {}", b"func foo() {}"]; +/// let after: Vec<&[u8]> = vec![b"package main", b"func receive() {}", b"func completelyDifferent() {}"]; +/// let ops = [DiffOp::Equal(1), DiffOp::Delete(2), DiffOp::Insert(2)]; +/// let pairs = candidate_line_pairs(&ops, &before, &after, 4, &mut Context::new()); +/// // Line 1 is two edits away; line 2 is far beyond the bound. +/// assert_eq!(pairs, vec![LinePair { before: 1, after: 1 }]); +/// ``` +#[must_use] +pub fn candidate_line_pairs( + ops: &[DiffOp], + before: &[&[u8]], + after: &[&[u8]], + max_distance: usize, + lctx: &mut Context, +) -> Vec<LinePair> { + let mut pairs = Vec::new(); + let mut cursor_before = 0usize; + let mut cursor_after = 0usize; + let mut pending_delete = 0usize; + for op in ops { + match *op { + DiffOp::Delete(n) => { + cursor_before += n; + pending_delete = n; + } + DiffOp::Insert(n) => { + if n == pending_delete { + for i in 0..n { + let pair = LinePair { + before: cursor_before - n + i, + after: cursor_after + i, + }; + let (Some(lb), Some(la)) = (before.get(pair.before), after.get(pair.after)) + else { + continue; + }; + if lines_within(lb, la, max_distance, lctx) { + pairs.push(pair); + } + } + } + cursor_after += n; + pending_delete = 0; + } + DiffOp::Equal(n) => { + cursor_before += n; + cursor_after += n; + pending_delete = 0; + } + } + } + pairs +} + +/// Whether two raw lines are within `max_distance` edits: the byte-length gap +/// is a lower bound on the distance, so a gap above the bound skips the +/// Levenshtein computation. +fn lines_within(before: &[u8], after: &[u8], max_distance: usize, lctx: &mut Context) -> bool { + if before.len().abs_diff(after.len()) > max_distance { + return false; + } + let sb = String::from_utf8_lossy(before); + let sa = String::from_utf8_lossy(after); + distance(lctx, &sb, &sa) <= max_distance +} + +/// The Levenshtein distance as a count (the context reports an `i32` that is +/// never negative; an impossible negative reads as "infinitely far"). +fn distance(lctx: &mut Context, a: &str, b: &str) -> usize { + usize::try_from(lctx.distance(a, b)).unwrap_or(usize::MAX) +} + +/// The one identifier that changed between the identifier lists of a +/// candidate line pair, or `None` when the pair is not a typo fix. +/// +/// The identifiers are compared as multisets: every identifier present on +/// both sides cancels (position is irrelevant, duplicates cancel one for +/// one). The pair is a typo fix only when exactly one identifier remains on +/// each side, both are single tokens (non-empty, no whitespace), they +/// differ, and their Levenshtein distance is at most `max_distance`. +/// +/// ``` +/// use cf_alg_levenshtein::Context; +/// use cf_typos::{identifier_pair, IdentifierChange}; +/// +/// let mut lctx = Context::new(); +/// let s = |items: &[&str]| items.iter().map(|s| s.to_string()).collect::<Vec<_>>(); +/// +/// // `v := recieve()` → `v := receive()`: `v` cancels, the pair is close. +/// assert_eq!( +/// identifier_pair(&s(&["v", "recieve"]), &s(&["v", "receive"]), 4, &mut lctx), +/// Some(IdentifierChange { wrong: "recieve".into(), correct: "receive".into() }) +/// ); +/// // `#define N 4` → `#define N 17`: the identifier did not change. +/// assert_eq!(identifier_pair(&s(&["N"]), &s(&["N"]), 4, &mut lctx), None); +/// // A rename to something unrelated is not a typo. +/// assert_eq!(identifier_pair(&s(&["foo"]), &s(&["completelyDifferent"]), 4, &mut lctx), None); +/// ``` +#[must_use] +pub fn identifier_pair( + before: &[String], + after: &[String], + max_distance: usize, + lctx: &mut Context, +) -> Option<IdentifierChange> { + let (removed, added) = multiset_difference(before, after); + let [wrong] = removed.as_slice() else { + return None; + }; + let [correct] = added.as_slice() else { + return None; + }; + if !is_identifier_token(wrong) || !is_identifier_token(correct) || wrong == correct { + return None; + } + if distance(lctx, wrong, correct) > max_distance { + return None; + } + Some(IdentifierChange { + wrong: (*wrong).clone(), + correct: (*correct).clone(), + }) +} + +/// Whether a UAST identifier token is one identifier: non-empty and free of +/// whitespace (prose mappings emit identifier nodes spanning whole lines). +fn is_identifier_token(token: &str) -> bool { + !token.is_empty() && !token.chars().any(char::is_whitespace) +} + +/// `(only in before, only in after)` after cancelling the identifiers both +/// lists share, one occurrence for one. Each side keeps its input order. +fn multiset_difference<'a>( + before: &'a [String], + after: &'a [String], +) -> (Vec<&'a String>, Vec<&'a String>) { + let mut counts: HashMap<&str, usize> = HashMap::new(); + for ident in before { + *counts.entry(ident.as_str()).or_insert(0) += 1; + } + let mut added = Vec::new(); + for ident in after { + match counts.get_mut(ident.as_str()) { + Some(n) if *n > 0 => *n -= 1, + _ => added.push(ident), + } + } + let removed = before + .iter() + .filter(|ident| { + let n = counts + .get_mut(ident.as_str()) + .expect("every before identifier was counted"); + if *n > 0 { + *n -= 1; + true + } else { + false + } + }) + .collect(); + (removed, added) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn s(items: &[&str]) -> Vec<String> { + items.iter().map(|i| (*i).to_string()).collect() + } + + fn pair(before: &[&str], after: &[&str], max: usize) -> Option<IdentifierChange> { + identifier_pair(&s(before), &s(after), max, &mut Context::new()) + } + + fn change(wrong: &str, correct: &str) -> Option<IdentifierChange> { + Some(IdentifierChange { + wrong: wrong.to_string(), + correct: correct.to_string(), + }) + } + + #[test] + fn close_pair_is_a_typo_fix() { + assert_eq!( + pair(&["recieve"], &["receive"], 4), + change("recieve", "receive") + ); + } + + #[test] + fn shared_identifiers_cancel_regardless_of_position() { + assert_eq!( + pair(&["v", "recieve"], &["receive", "v"], 4), + change("recieve", "receive") + ); + // Duplicates cancel one for one. + assert_eq!( + pair(&["x", "x", "lenght"], &["x", "length", "x"], 4), + change("lenght", "length") + ); + } + + #[test] + fn unchanged_identifier_is_not_a_typo() { + assert_eq!(pair(&["NUM_CON_TIMES"], &["NUM_CON_TIMES"], 4), None); + assert_eq!(pair(&[], &[], 4), None); + } + + #[test] + fn empty_identifier_is_not_a_typo() { + assert_eq!(pair(&[""], &["x"], 4), None); + assert_eq!(pair(&["x"], &[""], 4), None); + assert_eq!(pair(&[""], &["a"], 4), None); + } + + #[test] + fn tokens_with_whitespace_are_not_identifiers() { + // A Markdown heading's identifier node spans the whole line. + assert_eq!(pair(&["# Hello\n"], &["# Hallo\n"], 4), None); + assert_eq!(pair(&["a b"], &["a c"], 4), None); + } + + #[test] + fn far_pair_is_not_a_typo() { + assert_eq!(pair(&["foo"], &["completelyDifferent"], 4), None); + } + + #[test] + fn pair_distance_is_bounded_by_max_distance() { + // `recieve` → `receive` is two edits: within 2, outside 1. + assert_eq!( + pair(&["recieve"], &["receive"], 2), + change("recieve", "receive") + ); + assert_eq!(pair(&["recieve"], &["receive"], 1), None); + } + + #[test] + fn two_changed_identifiers_are_not_a_typo() { + assert_eq!(pair(&["a", "recieve"], &["b", "receive"], 4), None); + assert_eq!(pair(&["recieve"], &["receive", "extra"], 4), None); + assert_eq!(pair(&["recieve", "extra"], &["receive"], 4), None); + } + + fn lines<'a>(items: &[&'a str]) -> Vec<&'a [u8]> { + items.iter().map(|l| l.as_bytes()).collect() + } + + #[test] + fn aligned_runs_within_the_bound_are_candidates() { + let before = lines(&["a", "func recieve() {}", "x := 1"]); + let after = lines(&["a", "func receive() {}", "x := 2"]); + let ops = [DiffOp::Equal(1), DiffOp::Delete(2), DiffOp::Insert(2)]; + let pairs = candidate_line_pairs(&ops, &before, &after, 4, &mut Context::new()); + assert_eq!( + pairs, + vec![ + LinePair { + before: 1, + after: 1 + }, + LinePair { + before: 2, + after: 2 + } + ] + ); + } + + #[test] + fn unequal_runs_and_far_lines_are_not_candidates() { + let before = lines(&["func foo() {}", "b"]); + let after = lines(&["func completelyDifferent() {}", "b", "c"]); + // A Delete(1) answered by Insert(2) does not align. + let ops = [DiffOp::Delete(1), DiffOp::Insert(2), DiffOp::Equal(1)]; + assert!(candidate_line_pairs(&ops, &before, &after, 4, &mut Context::new()).is_empty()); + // Aligned, but the lines are far apart (and the byte-length gap alone + // already exceeds the bound). + let ops = [DiffOp::Delete(1), DiffOp::Insert(1), DiffOp::Equal(1)]; + assert!(candidate_line_pairs(&ops, &before, &after, 4, &mut Context::new()).is_empty()); + } + + #[test] + fn indices_past_the_line_vectors_are_skipped() { + let before = lines(&["a"]); + let after = lines(&["b"]); + let ops = [DiffOp::Delete(2), DiffOp::Insert(2)]; + let pairs = candidate_line_pairs(&ops, &before, &after, 4, &mut Context::new()); + assert_eq!( + pairs, + vec![LinePair { + before: 0, + after: 0 + }] + ); + } + + #[test] + fn an_equal_run_breaks_the_delete_insert_adjacency() { + let before = lines(&["a", "k"]); + let after = lines(&["k", "b"]); + let ops = [DiffOp::Delete(1), DiffOp::Equal(1), DiffOp::Insert(1)]; + assert!(candidate_line_pairs(&ops, &before, &after, 4, &mut Context::new()).is_empty()); + } +} diff --git a/crates/cf-typos/src/hash.rs b/crates/cf-typos/src/hash.rs new file mode 100644 index 00000000..c4f6eb8e --- /dev/null +++ b/crates/cf-typos/src/hash.rs @@ -0,0 +1,67 @@ +//! The commit hash a typo fix is attributed to. + +use std::fmt; + +/// Length of a Git SHA-1 hash in bytes. +pub const HASH_SIZE: usize = 20; + +/// A Git object hash (SHA-1), a 20-byte array. +/// +/// Mirrors `cf_gitlib::Hash` so the crate stays free of the git dependency; +/// the history walk converts at the boundary (`Hash(hash.0)`). The zero value +/// is all-zero bytes; [`Hash::string`] renders lowercase hex. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Default, + PartialOrd, + Ord, + serde::Serialize, + serde::Deserialize, +)] +pub struct Hash(pub [u8; HASH_SIZE]); + +impl Hash { + /// Returns the lowercase hex string representation. + #[must_use] + pub fn string(&self) -> String { + use std::fmt::Write; + let mut s = String::with_capacity(HASH_SIZE * 2); + for b in &self.0 { + let _ = write!(s, "{b:02x}"); + } + s + } +} + +impl fmt::Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hash_hex_zero() { + assert_eq!(Hash::default().string(), "0".repeat(40)); + } + + #[test] + fn hash_hex_nonzero() { + let mut b = [0u8; 20]; + b[0] = 0xab; + b[19] = 0x0f; + let s = Hash(b).string(); + assert!(s.starts_with("ab")); + assert!(s.ends_with("0f")); + assert_eq!(s.len(), 40); + assert_eq!(Hash(b).to_string(), s); + } +} diff --git a/crates/cf-typos/src/levenshtein.rs b/crates/cf-typos/src/levenshtein.rs deleted file mode 100644 index 073a3e79..00000000 --- a/crates/cf-typos/src/levenshtein.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Self-contained Levenshtein edit distance over Unicode scalar values. -//! -//! Mirrors `cf_alg_levenshtein::Context::distance`, which measures distance in -//! **scalar-value** edits (insert/delete/substitute), each cost 1. The typo -//! detector reuses a [`Context`] for amortized allocation (reusable-row -//! design). -//! -//! Replacing this module with a dependency on `cf-alg-levenshtein` is -//! mechanical (the `Context::distance(&str, &str) -> usize` signature matches). - -/// A reusable Levenshtein computation context. -#[derive(Debug, Default, Clone)] -pub struct Context { - row: Vec<usize>, -} - -impl Context { - /// Creates a new, empty context. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Computes the Levenshtein distance between `a` and `b`. - /// - /// Standard two-row dynamic-programming Levenshtein, operating on Unicode - /// scalar values (`char`), not bytes. - /// - /// ``` - /// use cf_typos::levenshtein::Context; - /// - /// let mut c = Context::new(); - /// assert_eq!(c.distance("kitten", "sitting"), 3); - /// // Distance is measured in scalar-value edits, so a multi-byte char - /// // counts as a single substitution, not its byte length. - /// assert_eq!(c.distance("café", "cafe"), 1); - /// // The same context can be reused across calls. - /// assert_eq!(c.distance("abc", "abd"), 1); - /// ``` - pub fn distance(&mut self, a: &str, b: &str) -> usize { - let ra: Vec<char> = a.chars().collect(); - let rb: Vec<char> = b.chars().collect(); - - if ra.is_empty() { - return rb.len(); - } - if rb.is_empty() { - return ra.len(); - } - - // row[j] = distance between ra[..i] and rb[..j]. - self.row.clear(); - self.row.extend(0..=rb.len()); - - for (i, &ca) in ra.iter().enumerate() { - let mut prev_diag = self.row[0]; // row[0] before this iteration = i - self.row[0] = i + 1; - for (j, &cb) in rb.iter().enumerate() { - let cost = usize::from(ca != cb); - let insertion = self.row[j + 1] + 1; - let deletion = self.row[j] + 1; - let substitution = prev_diag + cost; - prev_diag = self.row[j + 1]; - self.row[j + 1] = insertion.min(deletion).min(substitution); - } - } - - self.row[rb.len()] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn basic_distances() { - let mut c = Context::new(); - assert_eq!(c.distance("", ""), 0); - assert_eq!(c.distance("a", ""), 1); - assert_eq!(c.distance("", "abc"), 3); - assert_eq!(c.distance("kitten", "sitting"), 3); - assert_eq!(c.distance("recieve", "receive"), 2); // transposition = 2 edits - assert_eq!(c.distance("flaw", "lawn"), 2); - } - - #[test] - fn rune_based_not_byte_based() { - let mut c = Context::new(); - // Single multi-byte char replaced by another single char = distance 1. - assert_eq!(c.distance("é", "e"), 1); - assert_eq!(c.distance("café", "cafe"), 1); - } - - #[test] - fn reuse_is_consistent() { - let mut c = Context::new(); - assert_eq!(c.distance("abc", "abd"), 1); - assert_eq!(c.distance("kitten", "sitting"), 3); - assert_eq!(c.distance("abc", "abd"), 1); - } -} diff --git a/crates/cf-typos/src/lib.rs b/crates/cf-typos/src/lib.rs index fef582d3..80ab7737 100644 --- a/crates/cf-typos/src/lib.rs +++ b/crates/cf-typos/src/lib.rs @@ -1,60 +1,44 @@ -//! `cf-typos` — the codefang `history/typos` analyzer. +//! `cf-typos` — the `history/typos` analyzer: typo-fix identifier pairs over +//! commit diffs. //! -//! Detects typo-fix pairs across commit history and computes the typos metric -//! set. The metric computation and report-contract serialization ([`metrics`], -//! [`compat`], [`typos`], [`levenshtein`]) live and are tested here; they back -//! the `run/history_typos.json` binding golden (output bytes are pinned -//! against the reference binary by `tests/compat`). The -//! streaming-pipeline glue (consume/serialize/store/UAST traversal) lives in -//! the consumer: `cf-commands` `handlers/history.rs` drives these modules and -//! `cf-uast-node` directly. +//! A typo fix is ONE identifier that a commit changed into a DIFFERENT, +//! Levenshtein-close identifier on the same line. The crate owns the +//! detection rules ([`detect`]: the diff line-pair alignment and prefilter, +//! then the identifier-multiset diff with the pair-distance bound) and the +//! report metrics ([`metrics`]: `typo_list`, `patterns`, `file_typos`, +//! `aggregate`). The history walk in `cf-commands` supplies the git line +//! diff, the UAST identifiers per line and the commit stamps, and encodes +//! [`metrics_report_value`] through `cf-gojson` / `cf-goyaml` / +//! `cf-reportutil`. +//! +//! ``` +//! use cf_alg_levenshtein::Context; +//! use cf_typos::{identifier_pair, metrics_report_value, Hash, ReportData, Typo}; +//! +//! let mut lctx = Context::new(); +//! let before = vec!["v".to_string(), "recieve".to_string()]; +//! let after = vec!["v".to_string(), "receive".to_string()]; +//! let change = identifier_pair(&before, &after, 4, &mut lctx).expect("a typo fix"); +//! +//! let report = ReportData { +//! typos: vec![Typo { +//! wrong: change.wrong, +//! correct: change.correct, +//! file: "main.go".to_string(), +//! commit: Hash::default(), +//! line: 7, +//! }], +//! }; +//! let json = String::from_utf8(cf_gojson::marshal(&metrics_report_value(&report))).unwrap(); +//! assert!(json.contains(r#""typo_list":[{"wrong":"recieve","correct":"receive","file":"main.go","line":7"#)); +//! ``` -pub mod compat; -pub mod levenshtein; +pub mod detect; +pub mod hash; pub mod metrics; pub mod typos; -pub use compat::{GoValue, Hash}; -pub use metrics::{metrics_report_value, metrics_yaml_value, ReportData}; -pub use typos::Typo; - -/// Crate name, used by smoke tests to confirm the module links. -pub const CRATE_NAME: &str = "cf-typos"; - -/// Serializes the `history/typos` report for the given typos as compact JSON -/// bytes (no trailing newline), matching the `--format json` output of -/// `codefang run --analyzers history/typos`. -/// -/// This is the exact serialization the `run/history_typos.json` golden -/// captures: the metric-set map (byte-sorted keys) marshaled compactly. -/// -/// ``` -/// use cf_typos::report_json; -/// -/// // An empty typo list still produces the full metric-set skeleton. -/// let json = report_json(&[]); -/// assert!(json.contains(r#""total_typos":0"#)); -/// assert!(json.contains(r#""typo_list":[]"#)); -/// // Keys are byte-sorted: `aggregate` precedes `file_typos`. -/// assert!(json.find("aggregate").unwrap() < json.find("file_typos").unwrap()); -/// ``` -#[must_use] -pub fn report_json(typos: &[Typo]) -> String { - let input = ReportData { - typos: typos.to_vec(), - }; - metrics_report_value(&input).to_json() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn empty_report_json_matches_golden() { - assert_eq!( - report_json(&[]), - r#"{"aggregate":{"total_typos":0,"unique_patterns":0,"affected_files":0,"affected_commits":0},"file_typos":[],"patterns":null,"typo_list":[]}"# - ); - } -} +pub use detect::{candidate_line_pairs, identifier_pair, DiffOp, IdentifierChange, LinePair}; +pub use hash::Hash; +pub use metrics::{metrics_report_value, ReportData}; +pub use typos::{deduplicate_typos, Typo, DEFAULT_MAXIMUM_ALLOWED_TYPO_DISTANCE}; diff --git a/crates/cf-typos/src/metrics.rs b/crates/cf-typos/src/metrics.rs index 2940167c..9d47bdc0 100644 --- a/crates/cf-typos/src/metrics.rs +++ b/crates/cf-typos/src/metrics.rs @@ -1,67 +1,76 @@ -//! Typos metrics computation. +//! The `history/typos` report metrics. //! -//! Given the analyzer report (a list of [`Typo`]), computes four metrics — -//! `typo_list`, `patterns`, `file_typos`, `aggregate` — with the pinned sort -//! orders and the "frequency > 1" filter (report contract, pinned by -//! `tests/compat`). +//! Given every typo-fix occurrence a history walk detected ([`ReportData`], +//! chronological), the report carries four metrics: +//! +//! * `typo_list` — every distinct `(wrong, correct)` pair once, attributed +//! to its first fix (file, 1-based line, commit); +//! * `patterns` — the pairs fixed more than once, with their occurrence +//! count, most frequent first; +//! * `file_typos` — occurrences per file, most affected first; +//! * `aggregate` — occurrences, distinct pairs, distinct files, distinct +//! commits. +//! +//! [`metrics_report_value`] is the ONE value every machine format encodes: +//! json marshals it, yaml renders it under the analyzer header, bin wraps the +//! json bytes. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; -use crate::compat::{GoValue, Hash}; -use crate::typos::Typo; +use cf_gojson::{GoMap, GoValue}; -/// Analyzer name used for the metrics-output interface. -pub const ANALYZER_NAME_TYPOS: &str = "typos"; +use crate::hash::Hash; +use crate::typos::{deduplicate_typos, Typo}; -/// Metric name: per-typo list. +/// Metric name: the distinct typo-fix pairs. pub const METRIC_NAME_TYPO_LIST: &str = "typo_list"; -/// Metric name: recurring patterns. +/// Metric name: recurring pairs. pub const METRIC_NAME_PATTERNS: &str = "patterns"; -/// Metric name: per-file typo counts. +/// Metric name: per-file occurrence counts. pub const METRIC_NAME_FILE_TYPOS: &str = "file_typos"; /// Metric name: aggregate summary. pub const METRIC_NAME_AGGREGATE: &str = "aggregate"; -/// Parsed input data for metrics computation. +/// The input of the metrics: every typo-fix occurrence, chronological. #[derive(Debug, Clone, Default)] pub struct ReportData { - /// The typos to compute metrics over. + /// Every occurrence, in walk order (oldest commit first, detection order + /// within a commit). pub typos: Vec<Typo>, } -/// Information about a single typo fix. +/// One `typo_list` entry. /// -/// JSON/YAML field order: wrong, correct, file, line, commit. `commit` is the -/// lowercase hex string form of the hash. +/// Field order: wrong, correct, file, line, commit (lowercase hex). #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypoData { /// Misspelled identifier. pub wrong: String, /// Corrected identifier. pub correct: String, - /// File the fix appeared in. + /// File the first fix appeared in. pub file: String, - /// Zero-based line number. + /// 1-based line of the first fix. pub line: i64, - /// Commit hash (hex string). + /// Commit of the first fix (hex string). pub commit: String, } impl TypoData { - /// Encodes as a struct-origin object (field declaration order preserved). + /// Encodes as a struct-origin object (field declaration order). #[must_use] - pub fn to_govalue(&self) -> GoValue { - GoValue::Struct(vec![ - ("wrong".to_string(), GoValue::Str(self.wrong.clone())), - ("correct".to_string(), GoValue::Str(self.correct.clone())), - ("file".to_string(), GoValue::Str(self.file.clone())), - ("line".to_string(), GoValue::Int(self.line)), - ("commit".to_string(), GoValue::Str(self.commit.clone())), - ]) + pub fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("wrong", GoValue::Str(self.wrong.clone())); + m.push("correct", GoValue::Str(self.correct.clone())); + m.push("file", GoValue::Str(self.file.clone())); + m.push("line", GoValue::Int(self.line)); + m.push("commit", GoValue::Str(self.commit.clone())); + GoValue::Map(m) } } -/// A recurring typo pattern with its frequency. +/// A recurring pair with its occurrence count. /// /// Field order: wrong, correct, frequency. #[derive(Debug, Clone, PartialEq, Eq)] @@ -70,125 +79,112 @@ pub struct TypoPatternData { pub wrong: String, /// Corrected identifier. pub correct: String, - /// Number of occurrences (always > 1, per the patterns filter). + /// Number of occurrences (always > 1). pub frequency: i64, } impl TypoPatternData { /// Encodes as a struct-origin object. #[must_use] - pub fn to_govalue(&self) -> GoValue { - GoValue::Struct(vec![ - ("wrong".to_string(), GoValue::Str(self.wrong.clone())), - ("correct".to_string(), GoValue::Str(self.correct.clone())), - ("frequency".to_string(), GoValue::Int(self.frequency)), - ]) + pub fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("wrong", GoValue::Str(self.wrong.clone())); + m.push("correct", GoValue::Str(self.correct.clone())); + m.push("frequency", GoValue::Int(self.frequency)); + GoValue::Map(m) } } -/// Typo statistics for a single file. +/// Occurrences in a single file. /// /// Field order: file, typo_count, fixed_typos. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FileTypoData { /// File path. pub file: String, - /// Number of typos detected in the file. + /// Number of typo-fix occurrences in the file. pub typo_count: i64, - /// Number of typos fixed (always equal to `typo_count`). + /// Number of fixed typos — every detected occurrence IS a fix, so this + /// equals `typo_count`. pub fixed_typos: i64, } impl FileTypoData { /// Encodes as a struct-origin object. #[must_use] - pub fn to_govalue(&self) -> GoValue { - GoValue::Struct(vec![ - ("file".to_string(), GoValue::Str(self.file.clone())), - ("typo_count".to_string(), GoValue::Int(self.typo_count)), - ("fixed_typos".to_string(), GoValue::Int(self.fixed_typos)), - ]) + pub fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("file", GoValue::Str(self.file.clone())); + m.push("typo_count", GoValue::Int(self.typo_count)); + m.push("fixed_typos", GoValue::Int(self.fixed_typos)); + GoValue::Map(m) } } -/// Summary statistics across all typos. +/// Summary statistics. /// /// Field order: total_typos, unique_patterns, affected_files, /// affected_commits. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AggregateData { - /// Total number of typos. + /// Number of typo-fix occurrences. pub total_typos: i64, - /// Number of distinct `wrong|correct` patterns. + /// Number of distinct `(wrong, correct)` pairs. pub unique_patterns: i64, - /// Number of distinct files affected. + /// Number of distinct files with an occurrence. pub affected_files: i64, - /// Number of distinct commits affected. + /// Number of distinct commits with an occurrence. pub affected_commits: i64, } impl AggregateData { - /// Encodes as a struct-origin object (field declaration order preserved). + /// Encodes as a struct-origin object. #[must_use] - pub fn to_govalue(&self) -> GoValue { - GoValue::Struct(vec![ - ("total_typos".to_string(), GoValue::Int(self.total_typos)), - ( - "unique_patterns".to_string(), - GoValue::Int(self.unique_patterns), - ), - ( - "affected_files".to_string(), - GoValue::Int(self.affected_files), - ), - ( - "affected_commits".to_string(), - GoValue::Int(self.affected_commits), - ), - ]) + pub fn to_go_value(&self) -> GoValue { + let mut m = GoMap::new_struct(); + m.push("total_typos", GoValue::Int(self.total_typos)); + m.push("unique_patterns", GoValue::Int(self.unique_patterns)); + m.push("affected_files", GoValue::Int(self.affected_files)); + m.push("affected_commits", GoValue::Int(self.affected_commits)); + GoValue::Map(m) } } -/// Computes the `typo_list` metric. Preserves input order. +/// The `typo_list` metric: every distinct pair once, attributed to its first +/// occurrence, in first-seen order. #[must_use] pub fn compute_typo_list(input: &ReportData) -> Vec<TypoData> { - input - .typos - .iter() + deduplicate_typos(&input.typos) + .into_iter() .map(|t| TypoData { - wrong: t.wrong.clone(), - correct: t.correct.clone(), - file: t.file.clone(), + wrong: t.wrong, + correct: t.correct, + file: t.file, line: t.line, commit: t.commit.string(), }) .collect() } -/// Computes the `patterns` metric. -/// -/// Counts `wrong|correct` occurrences, keeps only frequency > 1, and sorts by -/// frequency descending. The reference implementation leaves equal-frequency -/// tie order unspecified (unstable sort); this implementation breaks ties by -/// the `wrong|correct` key for determinism. +/// The `patterns` metric: the pairs with more than one occurrence, sorted by +/// frequency descending, then by `wrong` and `correct` ascending. /// /// ``` /// use cf_typos::metrics::{compute_typo_patterns, ReportData}; -/// use cf_typos::typos::Typo; -/// use cf_typos::compat::Hash; +/// use cf_typos::{Hash, Typo}; /// /// let mk = |wrong: &str, correct: &str| Typo { /// wrong: wrong.to_string(), /// correct: correct.to_string(), /// file: "x.go".to_string(), /// commit: Hash::default(), -/// line: 0, +/// line: 1, /// }; /// let input = ReportData { /// typos: vec![ /// mk("recieve", "receive"), -/// mk("recieve", "receive"), // freq 2 -> kept -/// mk("seperate", "separate"), // freq 1 -> filtered out +/// mk("recieve", "receive"), // frequency 2 -> kept +/// mk("seperate", "separate"), // frequency 1 -> filtered out /// ], /// }; /// let patterns = compute_typo_patterns(&input); @@ -198,179 +194,110 @@ pub fn compute_typo_list(input: &ReportData) -> Vec<TypoData> { /// ``` #[must_use] pub fn compute_typo_patterns(input: &ReportData) -> Vec<TypoPatternData> { - let mut counts: BTreeMap<String, i64> = BTreeMap::new(); + let mut counts: BTreeMap<(&str, &str), i64> = BTreeMap::new(); for t in &input.typos { - let key = format!("{}|{}", t.wrong, t.correct); - *counts.entry(key).or_insert(0) += 1; + *counts.entry((&t.wrong, &t.correct)).or_insert(0) += 1; } - let mut result: Vec<TypoPatternData> = counts .into_iter() - .filter(|(_, freq)| *freq > 1) - .filter_map(|(key, freq)| { - key.find('|').map(|i| TypoPatternData { - wrong: key[..i].to_string(), - correct: key[i + 1..].to_string(), - frequency: freq, - }) + .filter(|(_, frequency)| *frequency > 1) + .map(|((wrong, correct), frequency)| TypoPatternData { + wrong: wrong.to_string(), + correct: correct.to_string(), + frequency, }) .collect(); - result.sort_by(|a, b| { b.frequency .cmp(&a.frequency) .then_with(|| a.wrong.cmp(&b.wrong)) .then_with(|| a.correct.cmp(&b.correct)) }); - result } -/// Computes the `file_typos` metric. -/// -/// Counts typos per file and sorts by typo count descending. Ties are broken -/// by file name (ascending) for determinism (the reference sort is unstable). +/// The `file_typos` metric: occurrences per file, sorted by count +/// descending, then by file name ascending. #[must_use] pub fn compute_file_typos(input: &ReportData) -> Vec<FileTypoData> { - let mut file_counts: BTreeMap<String, i64> = BTreeMap::new(); + let mut file_counts: BTreeMap<&str, i64> = BTreeMap::new(); for t in &input.typos { - *file_counts.entry(t.file.clone()).or_insert(0) += 1; + *file_counts.entry(&t.file).or_insert(0) += 1; } - let mut result: Vec<FileTypoData> = file_counts .into_iter() .map(|(file, count)| FileTypoData { - file, + file: file.to_string(), typo_count: count, fixed_typos: count, }) .collect(); - result.sort_by(|a, b| { b.typo_count .cmp(&a.typo_count) .then_with(|| a.file.cmp(&b.file)) }); - result } -/// Computes the `aggregate` metric. +/// The `aggregate` metric. #[must_use] pub fn compute_aggregate(input: &ReportData) -> AggregateData { - use std::collections::HashSet; - - let mut patterns: HashSet<String> = HashSet::new(); - let mut files: HashSet<String> = HashSet::new(); + let mut pairs: HashSet<(&str, &str)> = HashSet::new(); + let mut files: HashSet<&str> = HashSet::new(); let mut commits: HashSet<Hash> = HashSet::new(); - for t in &input.typos { - patterns.insert(format!("{}|{}", t.wrong, t.correct)); - files.insert(t.file.clone()); + pairs.insert((&t.wrong, &t.correct)); + files.insert(&t.file); commits.insert(t.commit); } - AggregateData { total_typos: input.typos.len() as i64, - unique_patterns: patterns.len() as i64, + unique_patterns: pairs.len() as i64, affected_files: files.len() as i64, affected_commits: commits.len() as i64, } } -/// A computed metric result (name + value). -#[derive(Debug, Clone, PartialEq)] -pub struct MetricResult { - /// Metric name. - pub name: String, - /// Metric value. - pub value: GoValue, -} - -/// Runs all typos metrics in the fixed computation order and returns the -/// results: typo_list, patterns, file_typos, aggregate. -#[must_use] -pub fn compute_all_metrics(input: &ReportData) -> Vec<MetricResult> { - vec![ - MetricResult { - name: METRIC_NAME_TYPO_LIST.to_string(), - value: GoValue::Array( - compute_typo_list(input) - .iter() - .map(TypoData::to_govalue) - .collect(), - ), - }, - MetricResult { - name: METRIC_NAME_PATTERNS.to_string(), - // The patterns metric is a nil-when-empty slice in the report - // contract: no pattern survives the "frequency > 1" filter -> - // JSON `null`, NOT `[]`. The `typo_list`/`file_typos` metrics are - // always-present slices and marshal to `[]` when empty — keep - // that asymmetry exact (pinned by the differential gate). - value: { - let patterns = compute_typo_patterns(input); - if patterns.is_empty() { - GoValue::Null - } else { - GoValue::Array(patterns.iter().map(TypoPatternData::to_govalue).collect()) - } - }, - }, - MetricResult { - name: METRIC_NAME_FILE_TYPOS.to_string(), - value: GoValue::Array( - compute_file_typos(input) - .iter() - .map(FileTypoData::to_govalue) - .collect(), - ), - }, - MetricResult { - name: METRIC_NAME_AGGREGATE.to_string(), - value: compute_aggregate(input).to_govalue(), - }, - ] -} - -/// Builds the metrics report value: a **map-origin** object keyed by metric -/// name, so JSON encoding sorts the keys by raw UTF-8 bytes. -/// -/// The metric set serializes as a dynamic map (one entry per metric) with -/// byte-sorted keys. So the top-level order is `aggregate`, `file_typos`, -/// `patterns`, `typo_list` — NOT the computation order. Using -/// [`GoValue::Map`] (sorted at encode time) reproduces that exactly. +/// The report value: a map-origin object keyed by metric name (byte-sorted +/// on encode: `aggregate`, `file_typos`, `patterns`, `typo_list`); every +/// list is present, `[]` when empty. #[must_use] pub fn metrics_report_value(input: &ReportData) -> GoValue { - let metrics = compute_all_metrics(input); - GoValue::Map(metrics.into_iter().map(|m| (m.name, m.value)).collect()) -} - -/// Builds the metrics value for **YAML** serialization. -/// -/// The YAML value tree is the same map as [`metrics_report_value`], but the -/// YAML encoding of a nil-when-empty slice is `[]`, whereas the JSON encoding -/// is `null`. The patterns metric is nil when no pattern repeats, so the only -/// json/yaml shape difference is the empty `patterns` metric: JSON `null` vs -/// YAML `[]`. This builder reproduces the YAML side by promoting an empty -/// `patterns` value ([`GoValue::Null`]) to an empty [`GoValue::Array`]; every -/// other metric is identical to the JSON value. -#[must_use] -pub fn metrics_yaml_value(input: &ReportData) -> GoValue { - let metrics = compute_all_metrics(input); - GoValue::Map( - metrics - .into_iter() - .map(|m| { - let value = if m.name == METRIC_NAME_PATTERNS && m.value == GoValue::Null { - GoValue::Array(Vec::new()) - } else { - m.value - }; - (m.name, value) - }) - .collect(), - ) + let array = |items: Vec<GoValue>| GoValue::Array(items); + let mut m = GoMap::new_map(); + m.insert( + METRIC_NAME_TYPO_LIST, + array( + compute_typo_list(input) + .iter() + .map(TypoData::to_go_value) + .collect(), + ), + ); + m.insert( + METRIC_NAME_PATTERNS, + array( + compute_typo_patterns(input) + .iter() + .map(TypoPatternData::to_go_value) + .collect(), + ), + ); + m.insert( + METRIC_NAME_FILE_TYPOS, + array( + compute_file_typos(input) + .iter() + .map(FileTypoData::to_go_value) + .collect(), + ), + ); + m.insert( + METRIC_NAME_AGGREGATE, + compute_aggregate(input).to_go_value(), + ); + GoValue::Map(m) } #[cfg(test)] @@ -393,6 +320,8 @@ mod tests { Hash(h) } + /// `recieve → receive` fixed on two lines of `a.go` in commit 1, and a + /// second pair in `b.go` in commit 2. fn data() -> ReportData { ReportData { typos: vec![ @@ -404,25 +333,57 @@ mod tests { } #[test] - fn typo_list_preserves_order_and_hex_commit() { + fn typo_list_lists_each_pair_once_at_its_first_fix() { let list = compute_typo_list(&data()); - assert_eq!(list.len(), 3); + assert_eq!(list.len(), 2); assert_eq!(list[0].wrong, "recieve"); + assert_eq!(list[0].line, 10); assert_eq!(list[0].commit, hash(1).string()); - assert_eq!(list[2].wrong, "seperate"); + assert_eq!(list[1].wrong, "seperate"); } #[test] - fn patterns_only_keeps_frequency_gt_one() { + fn patterns_count_every_occurrence_and_keep_frequency_above_one() { let patterns = compute_typo_patterns(&data()); - assert_eq!(patterns.len(), 1); - assert_eq!(patterns[0].wrong, "recieve"); - assert_eq!(patterns[0].correct, "receive"); - assert_eq!(patterns[0].frequency, 2); + assert_eq!( + patterns, + vec![TypoPatternData { + wrong: "recieve".to_string(), + correct: "receive".to_string(), + frequency: 2, + }] + ); + } + + #[test] + fn patterns_sort_by_frequency_then_pair() { + let input = ReportData { + typos: vec![ + typo("b", "bb", "f", 1, hash(1)), + typo("b", "bb", "f", 2, hash(1)), + typo("a", "aa", "f", 3, hash(1)), + typo("a", "aa", "f", 4, hash(1)), + typo("c", "cc", "f", 5, hash(1)), + typo("c", "cc", "f", 6, hash(1)), + typo("c", "cc", "f", 7, hash(1)), + ], + }; + let names: Vec<(String, i64)> = compute_typo_patterns(&input) + .into_iter() + .map(|p| (p.wrong, p.frequency)) + .collect(); + assert_eq!( + names, + vec![ + ("c".to_string(), 3), + ("a".to_string(), 2), + ("b".to_string(), 2) + ] + ); } #[test] - fn file_typos_counts_and_sorts_desc() { + fn file_typos_count_occurrences_and_sort_desc() { let files = compute_file_typos(&data()); assert_eq!(files.len(), 2); assert_eq!(files[0].file, "a.go"); @@ -433,16 +394,17 @@ mod tests { } #[test] - fn aggregate_counts_distinct() { + fn aggregate_counts_occurrences_and_distinct_pairs() { let agg = compute_aggregate(&data()); - assert_eq!(agg.total_typos, 3); - assert_eq!(agg.unique_patterns, 2); - assert_eq!(agg.affected_files, 2); - assert_eq!(agg.affected_commits, 2); - } - - #[test] - fn aggregate_empty() { + assert_eq!( + agg, + AggregateData { + total_typos: 3, + unique_patterns: 2, + affected_files: 2, + affected_commits: 2, + } + ); assert_eq!( compute_aggregate(&ReportData::default()), AggregateData::default() @@ -450,45 +412,27 @@ mod tests { } #[test] - fn all_metrics_order() { - let metrics = compute_all_metrics(&data()); - let names: Vec<&str> = metrics.iter().map(|m| m.name.as_str()).collect(); + fn report_value_json_shape() { + let json = cf_gojson::marshal(&metrics_report_value(&data())); + let json = String::from_utf8(json).unwrap(); assert_eq!( - names, - vec!["typo_list", "patterns", "file_typos", "aggregate"] + json, + concat!( + r#"{"aggregate":{"total_typos":3,"unique_patterns":2,"affected_files":2,"affected_commits":2},"#, + r#""file_typos":[{"file":"a.go","typo_count":2,"fixed_typos":2},{"file":"b.go","typo_count":1,"fixed_typos":1}],"#, + r#""patterns":[{"wrong":"recieve","correct":"receive","frequency":2}],"#, + r#""typo_list":[{"wrong":"recieve","correct":"receive","file":"a.go","line":10,"commit":"0100000000000000000000000000000000000000"},"#, + r#"{"wrong":"seperate","correct":"separate","file":"b.go","line":5,"commit":"0200000000000000000000000000000000000000"}]}"# + ) ); } #[test] - fn metrics_report_value_sorted_keys_and_json() { - // Single typo -> typo_list has one entry, patterns empty (one - // occurrence, filtered out -> nil -> null), file_typos one, aggregate - // totals 1. The metric set is a dynamic map, so keys are byte-sorted: - // aggregate, file_typos, patterns, typo_list. - let input = ReportData { - typos: vec![typo("tets", "test", "main.go", 10, Hash::default())], - }; - let json = metrics_report_value(&input).to_json(); - assert!(json.starts_with("{\"aggregate\":{\"total_typos\":1,")); - assert!(json.contains( - "\"file_typos\":[{\"file\":\"main.go\",\"typo_count\":1,\"fixed_typos\":1}]" - )); - // A single occurrence is filtered (frequency > 1), leaving the nil - // slice -> JSON null, not []. - assert!(json.contains("\"patterns\":null")); - assert!(json.ends_with("\"typo_list\":[{\"wrong\":\"tets\",\"correct\":\"test\",\"file\":\"main.go\",\"line\":10,\"commit\":\"0000000000000000000000000000000000000000\"}]}")); - } - - #[test] - fn metrics_report_value_empty_matches_golden_bytes() { - // The run/history_typos.json golden: zero typos -> the exact 138-byte - // compact JSON the reference binary emits for the empty typos metric - // set. - let json = metrics_report_value(&ReportData::default()).to_json(); + fn empty_report_has_every_list_present() { + let json = cf_gojson::marshal(&metrics_report_value(&ReportData::default())); assert_eq!( - json, - r#"{"aggregate":{"total_typos":0,"unique_patterns":0,"affected_files":0,"affected_commits":0},"file_typos":[],"patterns":null,"typo_list":[]}"# + String::from_utf8(json).unwrap(), + r#"{"aggregate":{"total_typos":0,"unique_patterns":0,"affected_files":0,"affected_commits":0},"file_typos":[],"patterns":[],"typo_list":[]}"# ); - assert_eq!(json.len(), 138); } } diff --git a/crates/cf-typos/src/typos.rs b/crates/cf-typos/src/typos.rs index edac6df4..09e9a1e9 100644 --- a/crates/cf-typos/src/typos.rs +++ b/crates/cf-typos/src/typos.rs @@ -1,85 +1,60 @@ -//! Typo detection data model. -//! -//! The typos analyzer is a **commit history** analyzer: it scans each commit's -//! diffs for line pairs that are within a small Levenshtein distance (default -//! 4) and, when both the removed and added focused lines contain exactly one -//! identifier, records a `(wrong -> correct)` typo-fix pair. +//! The typo-fix occurrence record and its pair-level deduplication. -use crate::compat::Hash; +use std::collections::HashSet; -/// Default maximum Levenshtein distance for typo detection. -pub const DEFAULT_MAXIMUM_ALLOWED_TYPO_DISTANCE: i32 = 4; +use crate::hash::Hash; -/// Configuration key for the maximum Levenshtein distance. -pub const CONFIG_TYPOS_DATASET_MAXIMUM_ALLOWED_DISTANCE: &str = - "TyposDatasetBuilder.MaximumAllowedDistance"; +/// Default maximum Levenshtein distance between the two identifiers of a +/// typo-fix pair (and the line prefilter bound). +pub const DEFAULT_MAXIMUM_ALLOWED_TYPO_DISTANCE: usize = 4; -/// A detected typo-fix pair in source code. +/// One typo-fix occurrence: an identifier a commit changed into a different, +/// Levenshtein-close identifier on the same line. /// -/// Field order (wrong, correct, file, commit, line) is the wire order when a -/// typo list serializes as struct-origin records. -#[derive(Debug, Clone, PartialEq, Eq)] +/// Field order (wrong, correct, file, commit, line) is the wire order of the +/// per-commit ndjson records. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Typo { - /// The misspelled identifier (the "before" token). + /// The misspelled identifier (the Before side). pub wrong: String, - /// The corrected identifier (the "after" token). + /// The corrected identifier (the After side). pub correct: String, - /// Source file (the change's `To.Name`). + /// Source file (the change's After name). pub file: String, /// Commit hash in which the fix appeared. pub commit: Hash, - /// Zero-based line number in the "after" file. + /// 1-based line number of the fix in the After file. pub line: i64, } -/// Aggregated per-tick payload. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct TickData { - /// Detected typos for this tick. - pub typos: Vec<Typo>, -} - -/// Removes duplicate typos, keying on `"wrong|correct"` and preserving -/// first-seen order. -/// -/// The dedup is intentionally only on the `wrong|correct` pair (not -/// file/commit/line) — pinned analyzer behaviour: the first occurrence of a -/// given pair wins and later occurrences are dropped. +/// Keeps the first occurrence of every distinct `(wrong, correct)` pair, in +/// input order — the `typo_list` rule: a pair fixed on several lines or in +/// several commits is listed once, attributed to its chronologically first +/// fix. /// /// ``` -/// use cf_typos::typos::{deduplicate_typos, Typo}; -/// use cf_typos::compat::Hash; +/// use cf_typos::{deduplicate_typos, Hash, Typo}; /// /// let mk = |wrong: &str, file: &str| Typo { /// wrong: wrong.to_string(), /// correct: "receive".to_string(), /// file: file.to_string(), /// commit: Hash::default(), -/// line: 0, +/// line: 1, /// }; -/// // Two entries share the same `wrong|correct` pair but differ in file: -/// // only the first survives. +/// // Two occurrences of the same pair in different files: the first survives. /// let out = deduplicate_typos(&[mk("recieve", "a.go"), mk("recieve", "b.go")]); /// assert_eq!(out.len(), 1); /// assert_eq!(out[0].file, "a.go"); /// ``` #[must_use] pub fn deduplicate_typos(typos: &[Typo]) -> Vec<Typo> { - use std::collections::HashSet; - - let mut seen: HashSet<String> = HashSet::with_capacity(typos.len()); - let mut result: Vec<Typo> = Vec::with_capacity(typos.len()); - - for t in typos { - let key = format!("{}|{}", t.wrong, t.correct); - if seen.contains(&key) { - continue; - } - seen.insert(key); - result.push(t.clone()); - } - - result + let mut seen: HashSet<(&str, &str)> = HashSet::with_capacity(typos.len()); + typos + .iter() + .filter(|t| seen.insert((t.wrong.as_str(), t.correct.as_str()))) + .cloned() + .collect() } #[cfg(test)] @@ -100,13 +75,19 @@ mod tests { fn dedup_keeps_first_seen_pair() { let input = vec![ typo("recieve", "receive", "a.go", 1), - typo("recieve", "receive", "b.go", 2), // dup pair -> dropped + typo("recieve", "receive", "b.go", 2), typo("seperate", "separate", "c.go", 3), + typo("receive", "recieve", "d.go", 4), ]; let out = deduplicate_typos(&input); - assert_eq!(out.len(), 2); - assert_eq!(out[0], typo("recieve", "receive", "a.go", 1)); - assert_eq!(out[1], typo("seperate", "separate", "c.go", 3)); + assert_eq!( + out, + vec![ + typo("recieve", "receive", "a.go", 1), + typo("seperate", "separate", "c.go", 3), + typo("receive", "recieve", "d.go", 4), + ] + ); } #[test] @@ -115,11 +96,7 @@ mod tests { } #[test] - fn defaults_match_contract() { + fn default_distance_is_four() { assert_eq!(DEFAULT_MAXIMUM_ALLOWED_TYPO_DISTANCE, 4); - assert_eq!( - CONFIG_TYPOS_DATASET_MAXIMUM_ALLOWED_DISTANCE, - "TyposDatasetBuilder.MaximumAllowedDistance" - ); } } diff --git a/crates/cf-uast-mappings/Cargo.toml b/crates/cf-uast-mappings/Cargo.toml index e8de703d..6b47965c 100644 --- a/crates/cf-uast-mappings/Cargo.toml +++ b/crates/cf-uast-mappings/Cargo.toml @@ -17,3 +17,9 @@ cf-uast-mapping = { path = "../cf-uast-mapping" } # pattern as a tree_sitter::Query against the wired grammars. cf-uast = { path = "../cf-uast" } tree-sitter = { workspace = true } +# Shape-contract tests (tests/shape_contract.rs) read lowered nodes; the +# schema-vocabulary test (tests/schema_vocabulary.rs) checks every emitted +# type/role against the embedded JSON schema. +cf-uast-node = { path = "../cf-uast-node" } +cf-uast-spec = { path = "../cf-uast-spec" } +serde_json = { workspace = true } diff --git a/crates/cf-uast-mappings/README.md b/crates/cf-uast-mappings/README.md index d9f707ef..211906cc 100644 --- a/crates/cf-uast-mappings/README.md +++ b/crates/cf-uast-mappings/README.md @@ -7,20 +7,18 @@ tree-sitter → UAST mapping table as a `static LanguageMapping` via the analysis path. Provenance: the 68 modules were transpiled mechanically from the Go-era -`.uastmap` DSL corpus and are **equality-gated** against the DSL parser -(`tests/equality_gate.rs` asserts exact `Vec<Rule>` + `LanguageInfo` equality per -language), which is what makes the cutover byte-identical. The vendored -`.uastmap` files in `cf-uast-uastmaps` remain as a frozen snapshot serving the -dev server's text endpoints and the gate; an intentional mapping change here -must update that snapshot or retire the gate — the gate fails on any divergence, -so drift cannot be silent. +`.uastmap` DSL corpus. That corpus and the Go reference are retired, so the +tables are **hand-maintained** and the UAST shape is free to be correct rather +than byte-identical to the old output. Every deliberate shape change is pinned +by a test (see below) and documented in `site/architecture/uast.md` under +"Shape contract" — read that section before editing a rule, because the +analyzers depend on it. ## Adding a language -1. Vendor the tree-sitter grammar and wire it, exactly as before this crate - existed: C sources under `cf-uast/vendor/tree-sitter-<lang>-<ver>/`, a - `build.rs` `GRAMMARS` entry, an `ffi` extern and `match` arm in - `cf-uast/src/languages.rs`. +1. Vendor the tree-sitter grammar and wire it: C sources under + `cf-uast/vendor/tree-sitter-<lang>-<ver>/`, a `build.rs` `GRAMMARS` entry, + an `ffi` extern and `match` arm in `cf-uast/src/languages.rs`. 2. Add `src/<lang>.rs` with a `uast_language!` table (see the syntax below) and a `pub static <LANG>: LanguageMapping`. 3. Register it: one `mod <lang>;` line and one `("<lang>", &<lang>::<LANG>)` @@ -30,6 +28,10 @@ so drift cannot be silent. its grammar (the test fails until you do), then run `cargo test -p cf-uast-mappings` — the conformance test validates every pattern against the linked grammar. +5. Add the language to the operator/token table in `tests/shape_contract.rs` + (`OPERATOR_ROWS`): a snippet with `a + b`, a logical AND, a unary minus, a + plain and a compound assignment. The lowering supplies `props.operator` and + identifier/literal tokens for free in most grammars; the row proves it. ## Rule syntax @@ -50,40 +52,62 @@ pub static T: LanguageMapping = uast_language! { roles: [Assignment], // Role variants children: ["expression_list"], }, - qualified_type ("(qualified_type package: (package_identifier) @pkg)") => { + function_definition ("(function_definition declarator: (function_declarator declarator: (_) @name))") => { + type: Function, + token: capture("name"), + roles: [Function, Declaration], + props: { "name": "@name" }, // @capture | descendant:<kind> | literal + }, + type_spec_struct ("(type_spec)") => { // several rules per node kind: + type: Struct, // first whose `when` passes wins, + roles: [Struct, Declaration], // an unconditional rule is the catch-all + when: ["type.type == \"struct_type\""], + }, + arrow_function_in_declarator ("(arrow_function)") => { type: Synthetic, - token: capture("pkg"), - props: { "kind": "qualified" }, + when: ["parent.type == \"variable_declarator\""], // `parent` steps up }, } }; ``` -`extends:`/`when:` (rule inheritance and conditions) are supported but unused by -the entire corpus. +Prop values resolve as `@capture` (a query capture, falling back to a field or +descendant of that name), `descendant:<kind>` (the first descendant's text), or +a literal string. Condition paths navigate fields / child kinds with `.`, a +terminal `.type` compares the node kind, and a leading `parent` segment steps +to the enclosing tree-sitter node. + +What the lowering adds on top of any table (so a rule need not spell it out): +`props.operator` on operator nodes, source-text tokens on `Identifier` / +`Literal`, leaf `Comment`s with line-exact spans, and the `Else` role on an +`If`'s `alternative` child. See `cf-uast/src/lowering.rs` ("Shape contract"). ## Validation model -- **Compile time:** `type`/`roles` are closed-enum variants (62/61 values from - the corpus); a typo is a compile error. Malformed invocations fail with - diagnostics pinned by the `trybuild` suite in `cf-uast-mapping`. -- **CI time, not compile time:** S-expression **patterns** are string literals; +- **Compile time:** `type`/`roles` are closed-enum variants; a typo is a + compile error. Malformed invocations fail with diagnostics pinned by the + `trybuild` suite in `cf-uast-mapping`. +- **Grammar conformance:** S-expression **patterns** are string literals; `tests/grammar_conformance.rs` compiles every pattern as a `tree_sitter::Query` against each wired grammar (which also proves the node - kind exists). 679 inherited dead rules (anonymous-token / hidden-rule / - version-drift kinds that have never matched, in Go either) are acknowledged in - its allow-list; a dead rule that starts compiling fails the test until its + kind exists). Inherited dead rules (anonymous-token / hidden-rule / + version-drift kinds that have never matched) are acknowledged in its + allow-list; a dead rule that starts compiling fails the test until its entry is removed. -- **Equality gate:** `tests/equality_gate.rs` pins every table against the DSL - snapshot. +- **Shape contract:** `tests/shape_contract.rs` parses snippets through the + real parser and asserts the lowered shape per language plus the + cross-language operator/token table. +- **Schema vocabulary:** `tests/schema_vocabulary.rs` asserts every type and + role any table (or the lowering) emits is a member of the embedded JSON + schema's enums, so `uast validate` accepts `uast parse` output. ## Escape hatch The macro is sugar over plain `MappingRule` literals — any rule (or a whole -module) may be written as `const` data directly. This escape hatch is -load-bearing in the generated corpus: six languages (python, rust, latex, -markdown, markdown_inline, rust_with_rstml) contain a rule named `_`, which -`$name:ident` cannot match, so those modules are emitted in plain-literal form. -`UastType::Other(..)`/`Role::Other(..)` exist for out-of-vocabulary values; -generated code is `Other`-free (asserted by a test), and any hand-written use is -expected to justify itself in review. +module) may be written as `const` data directly. Six languages (python, rust, +latex, markdown, markdown_inline, rust_with_rstml) contain a rule named `_`, +which `$name:ident` cannot match, so those modules are written in +plain-literal form. `UastType::Other(..)`/`Role::Other(..)` exist for +out-of-vocabulary values; the tables are `Other`-free, and any hand-written +use must also appear in the JSON schema's enums (`tests/schema_vocabulary.rs` +rejects anything else) and justify itself in review. diff --git a/crates/cf-uast-mappings/src/c.rs b/crates/cf-uast-mappings/src/c.rs index c529c2f5..b08eff7b 100644 --- a/crates/cf-uast-mappings/src/c.rs +++ b/crates/cf-uast-mappings/src/c.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `c.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `c` — hand-maintained (the system of record). +//! Originally transpiled from the retired `c.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -113,6 +115,7 @@ pub static C: LanguageMapping = uast_language! { type: If, roles: [Condition], children: ["comma_expression", "expression"], + props: { "operator": "?:" }, }, comma_expression => { type: BinaryOp, @@ -157,20 +160,23 @@ pub static C: LanguageMapping = uast_language! { roles: [Type], children: ["expression", "type_descriptor"], }, - function_definition => { + // One Function per definition, named by the declarator's identifier + // through up to two pointer levels (`char **f(void)`); the declarator + // itself is structural, so a definition is never counted twice and a + // prototype (`declaration` > `function_declarator`) is not a Function. + function_definition ("(function_definition declarator: [(function_declarator declarator: (_) @name) (pointer_declarator declarator: (function_declarator declarator: (_) @name)) (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (_) @name)))])") => { type: Function, - token: self, + token: capture("name"), roles: [Function, Declaration], children: ["_declarator", "compound_statement", "type_specifier"], + props: { "name": "@name" }, }, function_declarator => { - type: Function, - roles: [Function, Declaration], + type: Synthetic, children: ["_declarator", "_field_declarator", "_type_declarator", "parameter_list"], }, abstract_function_declarator => { - type: Function, - roles: [Function, Declaration], + type: Synthetic, children: ["_abstract_declarator", "parameter_list"], }, preproc_function_def => { @@ -183,11 +189,6 @@ pub static C: LanguageMapping = uast_language! { roles: [Condition], children: ["else_clause", "parenthesized_expression", "statement"], }, - else_clause => { - type: Block, - token: self, - roles: [Body], - }, for_statement => { type: Loop, token: self, diff --git a/crates/cf-uast-mappings/src/cpp.rs b/crates/cf-uast-mappings/src/cpp.rs index 74d8556c..66f7ce0e 100644 --- a/crates/cf-uast-mappings/src/cpp.rs +++ b/crates/cf-uast-mappings/src/cpp.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `cpp.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `cpp` — hand-maintained (the system of record). +//! Originally transpiled from the retired `cpp.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -164,6 +166,7 @@ pub static CPP: LanguageMapping = uast_language! { type: If, roles: [Condition], children: ["comma_expression", "expression"], + props: { "operator": "?:" }, }, comma_expression => { type: BinaryOp, @@ -211,20 +214,22 @@ pub static CPP: LanguageMapping = uast_language! { roles: [Member], children: ["dependent_name", "destructor_name", "expression", "field_identifier", "qualified_identifier", "template_method"], }, - function_definition => { + // One Function per definition, named by the declarator (`Foo::bar`, + // `operator==`, a plain or field identifier) through pointer and + // reference declarators; the declarator itself is structural. + function_definition ("(function_definition declarator: [(function_declarator declarator: (_) @name) (pointer_declarator declarator: (function_declarator declarator: (_) @name)) (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (_) @name))) (reference_declarator (function_declarator declarator: (_) @name)) (pointer_declarator declarator: (reference_declarator (function_declarator declarator: (_) @name)))])") => { type: Function, + token: capture("name"), roles: [Function, Declaration], children: ["_declarator", "_field_declarator", "compound_statement", "operator_cast", "try_statement", "type_specifier"], + props: { "name": "@name" }, }, function_declarator => { - type: Function, - token: self, - roles: [Function, Declaration], + type: Synthetic, children: ["_declarator", "_field_declarator", "_type_declarator", "parameter_list"], }, abstract_function_declarator => { - type: Function, - roles: [Function, Declaration], + type: Synthetic, children: ["_abstract_declarator", "parameter_list"], }, preproc_function_def => { @@ -365,10 +370,6 @@ pub static CPP: LanguageMapping = uast_language! { roles: [Condition], children: ["condition_clause", "else_clause", "statement"], }, - else_clause => { - type: Block, - roles: [Body], - }, for_statement => { type: Loop, token: self, diff --git a/crates/cf-uast-mappings/src/go.rs b/crates/cf-uast-mappings/src/go.rs index d6952cdb..53d6cf3f 100644 --- a/crates/cf-uast-mappings/src/go.rs +++ b/crates/cf-uast-mappings/src/go.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `go.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `go` — hand-maintained (the system of record). +//! Originally transpiled from the retired `go.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -90,7 +92,8 @@ pub static GO: LanguageMapping = uast_language! { roles: [Continue], }, dec_statement => { - type: Synthetic, + type: UnaryOp, + roles: [Operator], }, default_case => { type: Case, @@ -134,13 +137,14 @@ pub static GO: LanguageMapping = uast_language! { children: ["_type", "field_identifier", "generic_type", "interpreted_string_literal", "qualified_type", "raw_string_literal", "type_identifier"], }, field_declaration_list => { - type: Synthetic, - token: self, + type: Block, + roles: [Body], children: ["field_declaration"], }, + // The init/condition/update clause is structural: `for_statement` is + // the one Loop. for_clause => { - type: Loop, - roles: [Loop], + type: Synthetic, children: ["_expression", "_simple_statement"], }, for_statement => { @@ -160,9 +164,11 @@ pub static GO: LanguageMapping = uast_language! { roles: [Function, Declaration], children: ["_simple_type", "block", "identifier", "parameter_list", "type_parameter_list"], }, + // `func(T) U` in parameter / field / variable position is a type, + // never a Function declaration. function_type => { - type: Function, - roles: [Function, Declaration], + type: TypeAnnotation, + roles: [Type], children: ["_simple_type", "parameter_list"], }, generic_type => { @@ -179,6 +185,7 @@ pub static GO: LanguageMapping = uast_language! { }, if_statement => { type: If, + roles: [Condition], children: ["_expression", "_simple_statement", "block", "if_statement"], }, implicit_length_array_type => { @@ -200,21 +207,17 @@ pub static GO: LanguageMapping = uast_language! { type: Synthetic, }, inc_statement => { - type: Synthetic, - token: self, + type: UnaryOp, + roles: [Operator], }, index_expression => { type: Index, roles: [Index], children: ["_expression"], }, - interface_type => { - type: Interface, - token: self, - roles: [Interface, Declaration], - }, interpreted_string_literal => { type: Literal, + token: self, roles: [Literal], }, keyed_element => { @@ -232,9 +235,9 @@ pub static GO: LanguageMapping = uast_language! { token: self, roles: [Literal], }, + // The `{...}` of a composite literal is a List, not a second Literal. literal_value => { - type: Literal, - roles: [Literal], + type: List, }, map_type => { type: Synthetic, @@ -333,11 +336,6 @@ pub static GO: LanguageMapping = uast_language! { source_file => { type: File, }, - struct_type => { - type: Struct, - token: self, - roles: [Struct, Declaration], - }, type_alias => { type: Synthetic, children: ["_type", "type_identifier"], @@ -385,9 +383,26 @@ pub static GO: LanguageMapping = uast_language! { type: Parameter, roles: [Parameter], }, + // `type X struct{...}` / `type X interface{...}` are the Struct / + // Interface declarations (named through the `name` field); any other + // `type X T` is a TypeDeclaration. The inner `struct_type` / + // `interface_type` nodes are unmapped, so a declaration is never + // counted twice and an anonymous `struct{}` is not a declaration. + type_spec_struct ("(type_spec)") => { + type: Struct, + roles: [Struct, Declaration], + children: ["_type", "type_identifier", "type_parameter_list"], + when: ["type.type == \"struct_type\""], + }, + type_spec_interface ("(type_spec)") => { + type: Interface, + roles: [Interface, Declaration], + children: ["_type", "type_identifier", "type_parameter_list"], + when: ["type.type == \"interface_type\""], + }, type_spec => { - type: List, - token: self, + type: TypeDeclaration, + roles: [Declaration, Type], children: ["_type", "type_identifier", "type_parameter_list"], }, type_switch_statement => { @@ -509,6 +524,7 @@ pub static GO: LanguageMapping = uast_language! { }, imaginary_literal => { type: Literal, + token: self, roles: [Literal], }, import => { @@ -517,6 +533,7 @@ pub static GO: LanguageMapping = uast_language! { }, int_literal => { type: Literal, + token: self, roles: [Literal], }, interface => { @@ -524,10 +541,6 @@ pub static GO: LanguageMapping = uast_language! { token: self, roles: [Interface, Declaration], }, - interpreted_string_literal_content => { - type: Literal, - roles: [Literal], - }, iota => { type: Synthetic, token: self, @@ -559,10 +572,6 @@ pub static GO: LanguageMapping = uast_language! { token: self, roles: [Reference], }, - raw_string_literal_content => { - type: Literal, - roles: [Literal], - }, return => { type: Return, token: self, @@ -570,6 +579,7 @@ pub static GO: LanguageMapping = uast_language! { }, rune_literal => { type: Literal, + token: self, roles: [Literal], }, select => { diff --git a/crates/cf-uast-mappings/src/java.rs b/crates/cf-uast-mappings/src/java.rs index 562f784b..6fa626b3 100644 --- a/crates/cf-uast-mappings/src/java.rs +++ b/crates/cf-uast-mappings/src/java.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `java.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `java` — hand-maintained (the system of record). +//! Originally transpiled from the retired `java.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -131,29 +133,33 @@ pub static JAVA: LanguageMapping = uast_language! { roles: [Parameter], children: ["dimensions", "identifier", "underscore_pattern"], }, + // The caught type list is a type annotation, not a second Catch. catch_type => { - type: Catch, + type: TypeAnnotation, token: self, - roles: [Catch], + roles: [Type], }, + // Bodies are Blocks: the declaration node is the one Class / + // Interface / Enum per declaration. class_body => { - type: Class, - roles: [Class, Declaration], + type: Block, + roles: [Body], }, class_declaration => { type: Class, - token: self, + token: child("identifier"), roles: [Class, Declaration], children: ["class_body", "identifier", "permits", "super_interfaces", "superclass", "type_parameters"], }, class_literal => { - type: Class, - roles: [Class, Declaration], + type: Literal, + token: self, + roles: [Literal], }, compact_constructor_declaration => { - type: Struct, - token: self, - roles: [Struct, Declaration], + type: Method, + token: child("identifier"), + roles: [Function, Declaration, Member], children: ["block", "identifier"], }, constant_declaration => { @@ -161,9 +167,8 @@ pub static JAVA: LanguageMapping = uast_language! { children: ["_unannotated_type", "variable_declarator"], }, constructor_body => { - type: Struct, - token: self, - roles: [Struct, Declaration], + type: Block, + roles: [Body], }, // Constructors are callable members with bodies and complexity of // their own; mapping them as Struct hid every Java constructor from @@ -206,13 +211,12 @@ pub static JAVA: LanguageMapping = uast_language! { children: ["_unannotated_type", "dimensions", "expression", "identifier", "statement", "underscore_pattern"], }, enum_body => { - type: Enum, - roles: [Enum, Declaration], + type: Block, + roles: [Body], }, enum_body_declarations => { - type: Enum, - token: self, - roles: [Enum, Declaration], + type: Block, + roles: [Body], }, enum_constant => { type: EnumMember, @@ -221,13 +225,13 @@ pub static JAVA: LanguageMapping = uast_language! { }, enum_declaration => { type: Enum, - token: self, + token: child("identifier"), roles: [Enum, Declaration], children: ["enum_body", "identifier", "super_interfaces"], }, explicit_constructor_invocation => { - type: Struct, - roles: [Struct, Declaration], + type: Call, + roles: [Call], children: ["argument_list", "primary_expression", "super", "this", "type_arguments"], }, exports_module_directive => { @@ -240,9 +244,8 @@ pub static JAVA: LanguageMapping = uast_language! { type: Synthetic, }, extends_interfaces => { - type: Interface, - token: self, - roles: [Interface, Declaration], + type: List, + roles: [Type], }, field_access => { type: Field, @@ -309,12 +312,12 @@ pub static JAVA: LanguageMapping = uast_language! { token: self, }, interface_body => { - type: Interface, - roles: [Interface, Declaration], + type: Block, + roles: [Body], }, interface_declaration => { type: Interface, - token: self, + token: child("identifier"), roles: [Interface, Declaration], children: ["identifier", "interface_body", "permits", "type_parameters"], }, @@ -350,8 +353,9 @@ pub static JAVA: LanguageMapping = uast_language! { children: ["argument_list", "identifier", "primary_expression", "super", "type_arguments"], }, method_reference => { - type: Method, - roles: [Function, Declaration, Member], + type: MemberAccess, + token: self, + roles: [Member, Reference], }, modifiers => { type: Synthetic, @@ -428,7 +432,8 @@ pub static JAVA: LanguageMapping = uast_language! { roles: [Pattern], }, requires_modifier => { - type: If, + type: Synthetic, + token: self, }, requires_module_directive => { type: Module, @@ -455,7 +460,9 @@ pub static JAVA: LanguageMapping = uast_language! { children: ["identifier", "scoped_identifier"], }, scoped_type_identifier => { - type: If, + type: Identifier, + token: self, + roles: [Name, Type], }, spread_parameter => { type: Spread, @@ -471,16 +478,16 @@ pub static JAVA: LanguageMapping = uast_language! { }, string_literal => { type: Literal, + token: self, roles: [Literal], }, super_interfaces => { - type: Interface, - token: self, - roles: [Interface, Declaration], + type: List, + roles: [Type], }, superclass => { - type: Class, - roles: [Class, Declaration], + type: TypeAnnotation, + roles: [Type], }, switch_block => { type: Block, @@ -496,12 +503,16 @@ pub static JAVA: LanguageMapping = uast_language! { token: self, children: ["parenthesized_expression", "switch_block"], }, + // Every `case` / `default` label is one Case (a branch); the arrow + // form's `switch_rule` is the branch body. switch_label => { - type: Switch, + type: Case, + token: self, + roles: [Branch], }, switch_rule => { - type: Switch, - token: self, + type: Block, + roles: [Body], }, synchronized_statement => { type: Synthetic, @@ -513,8 +524,10 @@ pub static JAVA: LanguageMapping = uast_language! { children: ["primary_expression", "string_literal"], }, ternary_expression => { - type: Synthetic, + type: If, + roles: [Condition], children: ["expression"], + props: { "operator": "?:" }, }, throw_statement => { type: Throw, @@ -603,6 +616,7 @@ pub static JAVA: LanguageMapping = uast_language! { }, binary_integer_literal => { type: Literal, + token: self, roles: [Literal], }, block_comment => { @@ -636,6 +650,7 @@ pub static JAVA: LanguageMapping = uast_language! { }, character_literal => { type: Literal, + token: self, roles: [Literal], }, class => { @@ -654,6 +669,7 @@ pub static JAVA: LanguageMapping = uast_language! { }, decimal_integer_literal => { type: Literal, + token: self, roles: [Literal], }, default => { @@ -710,6 +726,7 @@ pub static JAVA: LanguageMapping = uast_language! { }, hex_floating_point_literal => { type: Literal, + token: self, roles: [Literal], }, hex_integer_literal => { @@ -767,6 +784,7 @@ pub static JAVA: LanguageMapping = uast_language! { }, null_literal => { type: Literal, + token: self, roles: [Literal], }, octal_integer_literal => { diff --git a/crates/cf-uast-mappings/src/javascript.rs b/crates/cf-uast-mappings/src/javascript.rs index 1a18df28..523135a5 100644 --- a/crates/cf-uast-mappings/src/javascript.rs +++ b/crates/cf-uast-mappings/src/javascript.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `javascript.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `javascript` — hand-maintained (the system of record). +//! Originally transpiled from the retired `javascript.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -39,6 +41,17 @@ pub static JAVASCRIPT: LanguageMapping = uast_language! { token: self, roles: [Pattern], }, + // Under a declarator / class field that already lowered to the named + // Function / Method, the function value is structural: one + // function-like node per definition. + arrow_function_in_declarator ("(arrow_function)") => { + type: Synthetic, + when: ["parent.type == \"variable_declarator\""], + }, + arrow_function_in_field ("(arrow_function)") => { + type: Synthetic, + when: ["parent.type == \"field_definition\""], + }, arrow_function => { type: Lambda, roles: [Lambda], @@ -135,12 +148,7 @@ pub static JAVASCRIPT: LanguageMapping = uast_language! { type: Loop, roles: [Loop], children: ["parenthesized_expression", "statement"], - props: { "props": "kind=do-while" }, - }, - else_clause => { - type: Synthetic, - token: self, - roles: [Body], + props: { "kind": "do-while" }, }, empty_statement => { type: Synthetic, @@ -167,16 +175,18 @@ pub static JAVASCRIPT: LanguageMapping = uast_language! { // function detection. field_definition_arrow ("(field_definition)") => { type: Method, - token: self, + token: child("property_identifier"), roles: [Function, Declaration, Member], children: ["computed_property_name", "decorator", "expression", "number", "private_property_identifier", "property_identifier", "string"], + props: { "name": "@property" }, when: ["value.type == \"arrow_function\""], }, field_definition_funcexpr ("(field_definition)") => { type: Method, - token: self, + token: child("property_identifier"), roles: [Function, Declaration, Member], children: ["computed_property_name", "decorator", "expression", "number", "private_property_identifier", "property_identifier", "string"], + props: { "name": "@property" }, when: ["value.type == \"function_expression\""], }, field_definition => { @@ -195,13 +205,13 @@ pub static JAVASCRIPT: LanguageMapping = uast_language! { token: self, roles: [Loop], children: ["array_pattern", "const", "expression", "identifier", "in", "let", "member_expression", "object_pattern", "of", "parenthesized_expression", "sequence_expression", "statement", "subscript_expression", "undefined", "var"], - props: { "props": "kind=for-in" }, + props: { "kind": "for-in" }, }, for_statement => { type: Loop, roles: [Loop], children: ["empty_statement", "expression", "lexical_declaration", "sequence_expression", "statement", "variable_declaration"], - props: { "props": "kind=for" }, + props: { "kind": "for" }, }, formal_parameters => { type: Synthetic, @@ -213,6 +223,14 @@ pub static JAVASCRIPT: LanguageMapping = uast_language! { roles: [Function, Declaration], children: ["formal_parameters", "identifier", "statement_block"], }, + function_expression_in_declarator ("(function_expression)") => { + type: Synthetic, + when: ["parent.type == \"variable_declarator\""], + }, + function_expression_in_field ("(function_expression)") => { + type: Synthetic, + when: ["parent.type == \"field_definition\""], + }, function_expression => { type: Function, token: child("identifier"), @@ -492,7 +510,7 @@ pub static JAVASCRIPT: LanguageMapping = uast_language! { type: Loop, roles: [Loop], children: ["parenthesized_expression", "statement"], - props: { "props": "kind=while" }, + props: { "kind": "while" }, }, with_statement => { type: Synthetic, diff --git a/crates/cf-uast-mappings/src/kotlin.rs b/crates/cf-uast-mappings/src/kotlin.rs index fa8982d1..8bf86f38 100644 --- a/crates/cf-uast-mappings/src/kotlin.rs +++ b/crates/cf-uast-mappings/src/kotlin.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `kotlin.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `kotlin` — hand-maintained (the system of record). +//! Originally transpiled from the retired `kotlin.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -107,9 +109,10 @@ pub static KOTLIN: LanguageMapping = uast_language! { type: BinaryOp, roles: [Operator], }, + // `a shl b` is an infix CALL of `shl`; it has no operator token. infix_expression => { - type: BinaryOp, - roles: [Operator], + type: Call, + roles: [Call], }, range_expression => { type: BinaryOp, @@ -161,17 +164,17 @@ pub static KOTLIN: LanguageMapping = uast_language! { for_statement => { type: Loop, roles: [Loop], - props: { "props": "kind=for" }, + props: { "kind": "for" }, }, while_statement => { type: Loop, roles: [Loop], - props: { "props": "kind=while" }, + props: { "kind": "while" }, }, do_while_statement => { type: Loop, roles: [Loop], - props: { "props": "kind=do-while" }, + props: { "kind": "do-while" }, }, jump_expression => { type: Synthetic, diff --git a/crates/cf-uast-mappings/src/python.rs b/crates/cf-uast-mappings/src/python.rs index f3f4d7b0..6b1be63f 100644 --- a/crates/cf-uast-mappings/src/python.rs +++ b/crates/cf-uast-mappings/src/python.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `python.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `python` — hand-maintained (the system of record). +//! Originally transpiled from the retired `python.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. //! //! Plain-literal form (the `uast_language!` escape hatch); rules forcing it: //! * `_` — rule name is not a macro identifier. @@ -189,13 +191,15 @@ pub static PYTHON: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, + // `and` / `or` / `not` / comparisons are operators (their text + // lands in `props.operator`), not opaque Synthetic nodes. MappingRule { name: "boolean_operator", pattern: "(boolean_operator)", extends: "", - uast_type: UastType::Synthetic, + uast_type: UastType::BinaryOp, token: TokenSource::None, - roles: &[], + roles: &[Role::Operator], children: &["and", "expression", "or"], props: &[], conditions: &[], @@ -281,9 +285,9 @@ pub static PYTHON: LanguageMapping = LanguageMapping { name: "comparison_operator", pattern: "(comparison_operator)", extends: "", - uast_type: UastType::Synthetic, + uast_type: UastType::BinaryOp, token: TokenSource::None, - roles: &[], + roles: &[Role::Operator], children: &["in", "is"], props: &[], conditions: &[], @@ -314,11 +318,11 @@ pub static PYTHON: LanguageMapping = LanguageMapping { name: "conditional_expression", pattern: "(conditional_expression)", extends: "", - uast_type: UastType::Synthetic, - token: TokenSource::SelfText, - roles: &[], + uast_type: UastType::If, + token: TokenSource::None, + roles: &[Role::Condition], children: &[], - props: &[], + props: &[("operator", "if-else")], conditions: &[], }, MappingRule { @@ -459,18 +463,20 @@ pub static PYTHON: LanguageMapping = LanguageMapping { extends: "", uast_type: UastType::If, token: TokenSource::None, - roles: &[], + roles: &[Role::Condition, Role::Else], children: &["block", "expression"], props: &[], conditions: &[], }, + // The else-if convention: `elif` is an `If` child carrying `Else`, + // `else` a `Block` child carrying `Else`. MappingRule { name: "else_clause", pattern: "(else_clause)", extends: "", - uast_type: UastType::Synthetic, - token: TokenSource::SelfText, - roles: &[], + uast_type: UastType::Block, + token: TokenSource::None, + roles: &[Role::Else, Role::Body], children: &["block"], props: &[], conditions: &[], @@ -519,6 +525,20 @@ pub static PYTHON: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, + // A bare string statement (a module / class / function docstring) is + // a documentation comment; its `string` child never lowers (comments + // are leaves). + MappingRule { + name: "expression_statement_docstring", + pattern: "(expression_statement)", + extends: "", + uast_type: UastType::Comment, + token: TokenSource::SelfText, + roles: &[Role::Comment, Role::Doc], + children: &[], + props: &[], + conditions: &["string != \"\""], + }, MappingRule { name: "expression_statement", pattern: "(expression_statement)", @@ -875,9 +895,9 @@ pub static PYTHON: LanguageMapping = LanguageMapping { name: "not_operator", pattern: "(not_operator)", extends: "", - uast_type: UastType::Synthetic, + uast_type: UastType::UnaryOp, token: TokenSource::None, - roles: &[], + roles: &[Role::Operator], children: &["expression"], props: &[], conditions: &[], diff --git a/crates/cf-uast-mappings/src/rust.rs b/crates/cf-uast-mappings/src/rust.rs index e431fe1e..a41f4007 100644 --- a/crates/cf-uast-mappings/src/rust.rs +++ b/crates/cf-uast-mappings/src/rust.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `rust.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `rust` — hand-maintained (the system of record). +//! Originally transpiled from the retired `rust.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. //! //! Plain-literal form (the `uast_language!` escape hatch); rules forcing it: //! * `_` — rule name is not a macro identifier. @@ -233,6 +235,28 @@ pub static RUST: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, + MappingRule { + name: "block_outer_doc_comment", + pattern: "(block_comment)", + extends: "", + uast_type: UastType::Comment, + token: TokenSource::SelfText, + roles: &[Role::Comment, Role::Doc], + children: &[], + props: &[], + conditions: &["outer_doc_comment_marker == \"*\""], + }, + MappingRule { + name: "block_inner_doc_comment", + pattern: "(block_comment)", + extends: "", + uast_type: UastType::Comment, + token: TokenSource::SelfText, + roles: &[Role::Comment, Role::Doc], + children: &[], + props: &[], + conditions: &["inner_doc_comment_marker == \"!\""], + }, MappingRule { name: "block_comment", pattern: "(block_comment)", @@ -240,7 +264,7 @@ pub static RUST: LanguageMapping = LanguageMapping { uast_type: UastType::Comment, token: TokenSource::SelfText, roles: &[Role::Comment], - children: &["doc_comment", "inner_doc_comment_marker", "outer_doc_comment_marker"], + children: &[], props: &[], conditions: &[], }, @@ -249,7 +273,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(boolean_literal)", extends: "", uast_type: UastType::Literal, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Literal], children: &[], props: &[], @@ -409,17 +433,6 @@ pub static RUST: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, - MappingRule { - name: "else_clause", - pattern: "(else_clause)", - extends: "", - uast_type: UastType::Synthetic, - token: TokenSource::SelfText, - roles: &[], - children: &[], - props: &[], - conditions: &[], - }, MappingRule { name: "empty_statement", pattern: "(empty_statement)", @@ -571,7 +584,7 @@ pub static RUST: LanguageMapping = LanguageMapping { token: TokenSource::SelfText, roles: &[Role::Loop], children: &["_expression", "_pattern", "block"], - props: &[("props", "kind=for")], + props: &[("kind", "for")], conditions: &[], }, MappingRule { @@ -761,17 +774,6 @@ pub static RUST: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, - MappingRule { - name: "inner_doc_comment_marker", - pattern: "(inner_doc_comment_marker)", - extends: "", - uast_type: UastType::Comment, - token: TokenSource::SelfText, - roles: &[Role::Comment], - children: &[], - props: &[], - conditions: &[], - }, MappingRule { name: "label", pattern: "(label)", @@ -838,14 +840,38 @@ pub static RUST: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, + // `///` and `//!` are documentation comments (role Doc); every + // comment is a leaf whose span is the comment text only. + MappingRule { + name: "line_outer_doc_comment", + pattern: "(line_comment)", + extends: "", + uast_type: UastType::Comment, + token: TokenSource::SelfText, + roles: &[Role::Comment, Role::Doc], + children: &[], + props: &[], + conditions: &["outer_doc_comment_marker == \"/\""], + }, + MappingRule { + name: "line_inner_doc_comment", + pattern: "(line_comment)", + extends: "", + uast_type: UastType::Comment, + token: TokenSource::SelfText, + roles: &[Role::Comment, Role::Doc], + children: &[], + props: &[], + conditions: &["inner_doc_comment_marker == \"!\""], + }, MappingRule { name: "line_comment", pattern: "(line_comment)", extends: "", uast_type: UastType::Comment, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Comment], - children: &["doc_comment", "inner_doc_comment_marker", "outer_doc_comment_marker"], + children: &[], props: &[], conditions: &[], }, @@ -857,7 +883,7 @@ pub static RUST: LanguageMapping = LanguageMapping { token: TokenSource::SelfText, roles: &[Role::Loop], children: &["block"], - props: &[("props", "kind=loop")], + props: &[("kind", "loop")], conditions: &[], }, MappingRule { @@ -1003,17 +1029,6 @@ pub static RUST: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, - MappingRule { - name: "outer_doc_comment_marker", - pattern: "(outer_doc_comment_marker)", - extends: "", - uast_type: UastType::Comment, - token: TokenSource::SelfText, - roles: &[Role::Comment], - children: &[], - props: &[], - conditions: &[], - }, MappingRule { name: "parameter", pattern: "(parameter)", @@ -1195,7 +1210,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(scoped_type_identifier)", extends: "", uast_type: UastType::Identifier, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Type], children: &["bracketed_type", "crate", "generic_type", "identifier", "metavariable", "scoped_identifier", "self", "super", "type_identifier"], props: &[], @@ -1536,7 +1551,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(unit_expression)", extends: "", uast_type: UastType::Literal, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Literal], children: &[], props: &[], @@ -1671,7 +1686,7 @@ pub static RUST: LanguageMapping = LanguageMapping { token: TokenSource::None, roles: &[Role::Loop], children: &["_expression", "block", "let_chain", "let_condition"], - props: &[("props", "kind=while")], + props: &[("kind", "while")], conditions: &[], }, MappingRule { @@ -1745,7 +1760,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(char_literal)", extends: "", uast_type: UastType::Literal, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Literal], children: &[], props: &[], @@ -1795,17 +1810,6 @@ pub static RUST: LanguageMapping = LanguageMapping { props: &[], conditions: &[], }, - MappingRule { - name: "doc_comment", - pattern: "(doc_comment)", - extends: "", - uast_type: UastType::Comment, - token: TokenSource::SelfText, - roles: &[Role::Comment], - children: &[], - props: &[], - conditions: &[], - }, MappingRule { name: "dyn", pattern: "(dyn)", @@ -1899,7 +1903,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(field_identifier)", extends: "", uast_type: UastType::Identifier, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Name], children: &[], props: &[], @@ -1965,7 +1969,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(identifier)", extends: "", uast_type: UastType::Identifier, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Name], children: &[], props: &[], @@ -2009,7 +2013,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(integer_literal)", extends: "", uast_type: UastType::Literal, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Literal], children: &[], props: &[], @@ -2042,7 +2046,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(literal)", extends: "", uast_type: UastType::Literal, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Literal], children: &[], props: &[], @@ -2218,7 +2222,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(self)", extends: "", uast_type: UastType::Identifier, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Reference], children: &[], props: &[], @@ -2240,7 +2244,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(shorthand_field_identifier)", extends: "", uast_type: UastType::Identifier, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Name], children: &[], props: &[], @@ -2372,7 +2376,7 @@ pub static RUST: LanguageMapping = LanguageMapping { pattern: "(type_identifier)", extends: "", uast_type: UastType::Identifier, - token: TokenSource::None, + token: TokenSource::SelfText, roles: &[Role::Type], children: &[], props: &[], diff --git a/crates/cf-uast-mappings/src/tsx.rs b/crates/cf-uast-mappings/src/tsx.rs index d27c5b10..2b4dc3cc 100644 --- a/crates/cf-uast-mappings/src/tsx.rs +++ b/crates/cf-uast-mappings/src/tsx.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `tsx.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `tsx` — hand-maintained (the system of record). +//! Originally transpiled from the retired `tsx.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -75,6 +77,17 @@ pub static TSX: LanguageMapping = uast_language! { token: self, roles: [Type], }, + // Under a declarator / class field that already lowered to the named + // Function / Method, the function value is structural: one + // function-like node per definition. + arrow_function_in_declarator ("(arrow_function)") => { + type: Synthetic, + when: ["parent.type == \"variable_declarator\""], + }, + arrow_function_in_field ("(arrow_function)") => { + type: Synthetic, + when: ["parent.type == \"public_field_definition\""], + }, arrow_function => { type: Lambda, roles: [Lambda], @@ -213,9 +226,6 @@ pub static TSX: LanguageMapping = uast_language! { roles: [Loop], children: ["parenthesized_expression", "statement"], }, - else_clause => { - type: Synthetic, - }, empty_statement => { type: Synthetic, token: self, @@ -297,6 +307,14 @@ pub static TSX: LanguageMapping = uast_language! { roles: [Function, Declaration], children: ["asserts_annotation", "formal_parameters", "identifier", "statement_block", "type_annotation", "type_parameters", "type_predicate_annotation"], }, + function_expression_in_declarator ("(function_expression)") => { + type: Synthetic, + when: ["parent.type == \"variable_declarator\""], + }, + function_expression_in_field ("(function_expression)") => { + type: Synthetic, + when: ["parent.type == \"public_field_definition\""], + }, function_expression => { type: Function, roles: [Function, Declaration], @@ -574,14 +592,18 @@ pub static TSX: LanguageMapping = uast_language! { // function detection. public_field_definition_arrow ("(public_field_definition)") => { type: Method, + token: child("property_identifier"), roles: [Function, Declaration, Member], children: ["computed_property_name", "decorator", "expression", "number", "private_property_identifier", "property_identifier", "string", "type_annotation"], + props: { "name": "@property" }, when: ["value.type == \"arrow_function\""], }, public_field_definition_funcexpr ("(public_field_definition)") => { type: Method, + token: child("property_identifier"), roles: [Function, Declaration, Member], children: ["computed_property_name", "decorator", "expression", "number", "private_property_identifier", "property_identifier", "string", "type_annotation"], + props: { "name": "@property" }, when: ["value.type == \"function_expression\""], }, public_field_definition => { diff --git a/crates/cf-uast-mappings/src/typescript.rs b/crates/cf-uast-mappings/src/typescript.rs index ce09986d..bf6b56e8 100644 --- a/crates/cf-uast-mappings/src/typescript.rs +++ b/crates/cf-uast-mappings/src/typescript.rs @@ -1,5 +1,7 @@ -//! GENERATED by uastmap2rs — transpiled from `typescript.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `typescript` — hand-maintained (the system of record). +//! Originally transpiled from the retired `typescript.uastmap` DSL; edit the rules +//! here and keep `tests/shape_contract.rs` green. The shape contract every +//! table honours is documented in `site/architecture/uast.md`. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -75,6 +77,17 @@ pub static TYPESCRIPT: LanguageMapping = uast_language! { token: self, roles: [Type], }, + // Under a declarator / class field that already lowered to the named + // Function / Method, the function value is structural: one + // function-like node per definition. + arrow_function_in_declarator ("(arrow_function)") => { + type: Synthetic, + when: ["parent.type == \"variable_declarator\""], + }, + arrow_function_in_field ("(arrow_function)") => { + type: Synthetic, + when: ["parent.type == \"public_field_definition\""], + }, arrow_function => { type: Lambda, roles: [Lambda], @@ -213,9 +226,6 @@ pub static TYPESCRIPT: LanguageMapping = uast_language! { roles: [Loop], children: ["parenthesized_expression", "statement"], }, - else_clause => { - type: Synthetic, - }, empty_statement => { type: Synthetic, token: self, @@ -297,6 +307,14 @@ pub static TYPESCRIPT: LanguageMapping = uast_language! { roles: [Function, Declaration], children: ["asserts_annotation", "formal_parameters", "identifier", "statement_block", "type_annotation", "type_parameters", "type_predicate_annotation"], }, + function_expression_in_declarator ("(function_expression)") => { + type: Synthetic, + when: ["parent.type == \"variable_declarator\""], + }, + function_expression_in_field ("(function_expression)") => { + type: Synthetic, + when: ["parent.type == \"public_field_definition\""], + }, function_expression => { type: Function, roles: [Function, Declaration], @@ -574,14 +592,18 @@ pub static TYPESCRIPT: LanguageMapping = uast_language! { // function detection. public_field_definition_arrow ("(public_field_definition)") => { type: Method, + token: child("property_identifier"), roles: [Function, Declaration, Member], children: ["computed_property_name", "decorator", "expression", "number", "private_property_identifier", "property_identifier", "string", "type_annotation"], + props: { "name": "@property" }, when: ["value.type == \"arrow_function\""], }, public_field_definition_funcexpr ("(public_field_definition)") => { type: Method, + token: child("property_identifier"), roles: [Function, Declaration, Member], children: ["computed_property_name", "decorator", "expression", "number", "private_property_identifier", "property_identifier", "string", "type_annotation"], + props: { "name": "@property" }, when: ["value.type == \"function_expression\""], }, public_field_definition => { diff --git a/crates/cf-uast-mappings/tests/schema_vocabulary.rs b/crates/cf-uast-mappings/tests/schema_vocabulary.rs new file mode 100644 index 00000000..d5f6dcd0 --- /dev/null +++ b/crates/cf-uast-mappings/tests/schema_vocabulary.rs @@ -0,0 +1,56 @@ +//! `uast validate` must accept everything `uast parse` can emit: every UAST +//! type and role any mapping table produces — plus the roles the lowering +//! adds on its own — is a member of the embedded JSON schema's `NodeType` / +//! `Role` enums. Guards the schema against drifting behind the tables. + +use std::collections::BTreeSet; + +use serde_json::Value; + +/// Roles the lowering attaches without a table rule +/// (`cf_uast::lowering`, shape contract). +const LOWERING_ROLES: &[&str] = &[cf_uast_node::ROLE_ELSE]; + +fn schema_enum(schema: &Value, definition: &str) -> BTreeSet<String> { + schema["definitions"][definition]["enum"] + .as_array() + .unwrap_or_else(|| panic!("schema definition {definition} has an enum")) + .iter() + .map(|v| v.as_str().expect("enum member is a string").to_string()) + .collect() +} + +#[test] +fn schema_enums_cover_every_emitted_type_and_role() { + let schema: Value = serde_json::from_str(cf_uast_spec::schema()).expect("schema is JSON"); + let types = schema_enum(&schema, "NodeType"); + let roles = schema_enum(&schema, "Role"); + + let mut missing: Vec<String> = Vec::new(); + for (stem, mapping) in cf_uast_mappings::ALL { + for rule in mapping.rules { + let t = rule.uast_type.as_str(); + if !types.contains(t) { + missing.push(format!("{stem}/{}: type {t}", rule.name)); + } + for r in rule.roles { + if !roles.contains(r.as_str()) { + missing.push(format!("{stem}/{}: role {}", rule.name, r.as_str())); + } + } + } + } + for r in LOWERING_ROLES { + if !roles.contains(*r) { + missing.push(format!("lowering: role {r}")); + } + } + missing.sort(); + missing.dedup(); + assert!( + missing.is_empty(), + "schema enums lack {} emitted value(s):\n{}", + missing.len(), + missing.join("\n") + ); +} diff --git a/crates/cf-uast-mappings/tests/shape_contract.rs b/crates/cf-uast-mappings/tests/shape_contract.rs new file mode 100644 index 00000000..1b8c880d --- /dev/null +++ b/crates/cf-uast-mappings/tests/shape_contract.rs @@ -0,0 +1,664 @@ +//! Per-language SHAPE contract: each test parses a small snippet through the +//! real parser (`cf_uast::Parser`, grammars + tables linked) and asserts the +//! lowered shape the analyzers depend on — one declaration node per +//! declaration, `name` on every declaration, `operator` on every operator +//! node, source-text tokens on identifiers and literals, the else-if chain +//! convention and leaf comments. `specs/bugs/BUG-uast-mapping-shapes.md` +//! lists the defects each assertion closes; `site/architecture/uast.md` +//! documents the contract itself. + +use cf_uast::{Node, Parser}; + +/// The `props` key carrying an operator node's operator text. +const PROP_OPERATOR: &str = "operator"; +/// The `props` key carrying a declaration's declared name. +const PROP_NAME: &str = "name"; +/// The role marking an `If`'s alternative branch (else / else-if). +const ROLE_ELSE: &str = "Else"; +/// The role marking a documentation comment. +const ROLE_DOC: &str = "Doc"; +/// The role carried by conditional nodes (`If`, ternaries). +const ROLE_CONDITION: &str = "Condition"; +/// The ternary operator spelling in C-family languages. +const TERNARY_C_FAMILY: &str = "?:"; +/// The ternary operator spelling in Python (`x if c else y`). +const TERNARY_PYTHON: &str = "if-else"; + +fn parse(filename: &str, source: &str) -> Node { + Parser::new() + .parse(filename, source.as_bytes()) + .unwrap_or_else(|e| panic!("{filename}: parse failed: {e}")) +} + +fn of_type<'a>(root: &'a Node, node_type: &str) -> Vec<&'a Node> { + root.find(|n| n.node_type == node_type) +} + +fn names(nodes: &[&Node]) -> Vec<String> { + nodes + .iter() + .map(|n| n.props.get(PROP_NAME).cloned().unwrap_or_default()) + .collect() +} + +fn operator(n: &Node) -> &str { + n.props.get(PROP_OPERATOR).map_or("", String::as_str) +} + +fn has_role(n: &Node, role: &str) -> bool { + n.roles.iter().any(|r| r == role) +} + +/// Asserts that a node of `node_type` with `operator` text `op` exists. +fn assert_operator(root: &Node, node_type: &str, op: &str, lang: &str) { + let found = of_type(root, node_type) + .into_iter() + .any(|n| operator(n) == op); + assert!( + found, + "{lang}: expected a {node_type} with operator {op:?}; got {:?}", + of_type(root, node_type) + .iter() + .map(|n| operator(n).to_string()) + .collect::<Vec<_>>() + ); +} + +/// The function-like node types a complexity analyzer counts. +const FUNCTION_LIKE: &[&str] = &["Function", "Method", "Lambda"]; + +fn function_like(root: &Node) -> Vec<&Node> { + root.find(|n| FUNCTION_LIKE.contains(&n.node_type.as_str())) +} + +// ---------------------------------------------------------------- C / C++ + +#[test] +fn c_one_function_per_definition_named_by_declarator() { + let src = "int f(int a) { return a; }\nchar *g(void) { return 0; }\nstatic int **h(int x);\n"; + let root = parse("t.c", src); + let fns = of_type(&root, "Function"); + assert_eq!(names(&fns), ["f", "g"], "one Function per definition"); + for f in &fns { + assert_eq!( + f.token, f.props[PROP_NAME], + "token is the name, not the body" + ); + assert!( + f.find(|n| n.node_type == "Function").len() == 1, + "no nested Function inside {}", + f.props[PROP_NAME] + ); + } +} + +#[test] +fn c_pointer_depth_and_ternary_operator() { + let src = "int **pp(void) { return 0; }\nint t(int a) { return a ? 1 : 2; }\n"; + let root = parse("t.c", src); + assert_eq!(names(&of_type(&root, "Function")), ["pp", "t"]); + let ternary = of_type(&root, "If"); + assert_eq!(ternary.len(), 1); + assert_eq!(operator(ternary[0]), TERNARY_C_FAMILY); + assert!(has_role(ternary[0], ROLE_CONDITION)); +} + +#[test] +fn c_else_if_chain_is_if_under_if_with_else_role() { + let src = "void f(int a, int b) { if (a) { x(); } else if (b) { y(); } else { z(); } }\n"; + let root = parse("t.c", src); + assert_else_if_chain(&root, "c"); +} + +#[test] +fn cpp_qualified_and_reference_declarators_are_named() { + let src = "int Foo::bar(int a) { return a; }\nint& ref() { static int x; return x; }\nint t(int a) { return a ? 1 : 2; }\n"; + let root = parse("t.cpp", src); + assert_eq!(names(&of_type(&root, "Function")), ["Foo::bar", "ref", "t"]); + assert_operator(&root, "If", TERNARY_C_FAMILY, "cpp"); +} + +#[test] +fn cpp_else_if_chain() { + let src = "void f(int a, int b) { if (a) { x(); } else if (b) { y(); } else { z(); } }\n"; + assert_else_if_chain(&parse("t.cpp", src), "cpp"); +} + +// ---------------------------------------------------------------- Go + +#[test] +fn go_function_type_parameter_is_not_a_function() { + let src = "package p\n\nfunc apply(f func(int) int, x int) int { return f(x) }\n"; + let root = parse("t.go", src); + assert_eq!(names(&of_type(&root, "Function")), ["apply"]); + assert!( + !of_type(&root, "TypeAnnotation").is_empty(), + "func(int) int lowers to a TypeAnnotation" + ); +} + +#[test] +fn go_three_clause_for_is_one_loop() { + let src = "package p\n\nfunc f(n int) {\n\tfor i := 0; i < n; i++ {\n\t}\n\tfor _, x := range []int{1} {\n\t\t_ = x\n\t}\n}\n"; + let root = parse("t.go", src); + assert_eq!( + of_type(&root, "Loop").len(), + 2, + "one Loop per for statement" + ); +} + +#[test] +fn go_literals_and_identifiers_carry_tokens() { + let src = "package p\n\nvar a = 42\nvar b = 1.5\nvar c = \"s\"\nvar d = 'r'\nvar e = 2i\n"; + let root = parse("t.go", src); + let tokens: Vec<&str> = of_type(&root, "Literal") + .iter() + .map(|n| n.token.as_str()) + .collect(); + assert_eq!(tokens, ["42", "1.5", "\"s\"", "'r'", "2i"]); + assert!(of_type(&root, "Identifier") + .iter() + .all(|n| !n.token.is_empty())); +} + +#[test] +fn go_named_type_declarations_carry_names() { + let src = "package p\n\ntype Config struct {\n\tA int\n}\n\ntype Shape interface {\n\tArea() float64\n}\n\ntype ID int\n"; + let root = parse("t.go", src); + let structs = of_type(&root, "Struct"); + assert_eq!(names(&structs), ["Config"]); + assert_eq!(of_type(structs[0], "Field").len(), 1, "fields stay inside"); + assert_eq!(names(&of_type(&root, "Interface")), ["Shape"]); + assert_eq!(names(&of_type(&root, "TypeDeclaration")), ["ID"]); +} + +#[test] +fn go_else_if_chain_and_inc_dec() { + let src = "package p\n\nfunc f(a, b bool, i int) {\n\tif a {\n\t\tx()\n\t} else if b {\n\t\ty()\n\t} else {\n\t\tz()\n\t}\n\ti++\n\ti--\n}\n"; + let root = parse("t.go", src); + assert_else_if_chain(&root, "go"); + assert_operator(&root, "UnaryOp", "++", "go"); + assert_operator(&root, "UnaryOp", "--", "go"); +} + +// ---------------------------------------------------------------- Rust + +#[test] +fn rust_identifiers_and_literals_carry_tokens() { + let src = "fn f(x: i32) -> bool { let y = x + 1; let s = \"s\"; let c = 'c'; let b = true; y > 2.5 as i32 }\n"; + let root = parse("t.rs", src); + let idents: Vec<&str> = of_type(&root, "Identifier") + .iter() + .map(|n| n.token.as_str()) + .collect(); + assert!( + idents.contains(&"y") && idents.contains(&"x") && !idents.contains(&""), + "identifiers: {idents:?}" + ); + let lits: Vec<&str> = of_type(&root, "Literal") + .iter() + .map(|n| n.token.as_str()) + .collect(); + for want in ["1", "\"s\"", "'c'", "true", "2.5"] { + assert!( + lits.contains(&want), + "literal {want:?} missing from {lits:?}" + ); + } +} + +#[test] +fn rust_doc_comments_are_flat_single_line_doc_comments() { + let src = "//! Crate doc.\n\n/// Doc for f.\n/// Second line.\nfn f() {}\n// plain\nfn g() {}\n/** block doc */\nfn h() {}\n"; + let root = parse("t.rs", src); + let comments = of_type(&root, "Comment"); + let tokens: Vec<&str> = comments.iter().map(|n| n.token.as_str()).collect(); + assert_eq!( + tokens, + [ + "//! Crate doc.", + "/// Doc for f.", + "/// Second line.", + "// plain", + "/** block doc */" + ] + ); + for c in &comments { + assert!(c.children.is_empty(), "comments are leaves: {:?}", c.token); + let pos = c.pos.expect("comment has a position"); + assert_eq!( + pos.start_line, pos.end_line, + "span is the comment line: {:?}", + c.token + ); + } + let docs: Vec<bool> = comments.iter().map(|n| has_role(n, ROLE_DOC)).collect(); + assert_eq!(docs, [true, true, true, false, true]); +} + +#[test] +fn rust_else_if_chain() { + let src = "fn f(a: bool, b: bool) { if a { x() } else if b { y() } else { z() } }\n"; + assert_else_if_chain(&parse("t.rs", src), "rust"); +} + +// ---------------------------------------------------------------- Java + +#[test] +fn java_switch_cases_single_catch_ternary_and_literals() { + let src = "class A {\n int f(int r) {\n int t = r > 0 ? 1 : 2;\n try { r = r / t; } catch (ArithmeticException e) { r = 0; }\n switch (r) { case 1: r = 10; break; case 2: r = 20; break; default: r = \"s\".length(); }\n return r;\n }\n}\n"; + let root = parse("A.java", src); + assert_eq!( + of_type(&root, "Switch").len(), + 1, + "the switch statement only" + ); + assert_eq!(of_type(&root, "Case").len(), 3, "case 1, case 2, default"); + let catches = of_type(&root, "Catch"); + assert_eq!(catches.len(), 1, "one Catch per catch clause"); + let ternary = of_type(&root, "If"); + assert_eq!(ternary.len(), 1); + assert_eq!(operator(ternary[0]), TERNARY_C_FAMILY); + let lits: Vec<&str> = of_type(&root, "Literal") + .iter() + .map(|n| n.token.as_str()) + .collect(); + for want in ["0", "1", "2", "10", "20", "\"s\""] { + assert!( + lits.contains(&want), + "literal {want:?} missing from {lits:?}" + ); + } +} + +#[test] +fn java_one_class_per_declaration_with_name() { + let src = "/** Javadoc for A. */\npublic class A {\n /** Javadoc for foo. */\n public int foo() { return 1; }\n public int bar() { return 2; }\n}\ninterface I { void m(); }\nenum E { X, Y }\n"; + let root = parse("A.java", src); + assert_eq!(names(&of_type(&root, "Class")), ["A"]); + assert_eq!(names(&of_type(&root, "Interface")), ["I"]); + assert_eq!(names(&of_type(&root, "Enum")), ["E"]); + assert_eq!(names(&of_type(&root, "Method")), ["foo", "bar", "m"]); + assert_eq!(function_like(&root).len(), 3); +} + +#[test] +fn java_else_if_chain() { + let src = "class A { void f(boolean a, boolean b) { if (a) { x(); } else if (b) { y(); } else { z(); } } }\n"; + assert_else_if_chain(&parse("A.java", src), "java"); +} + +// ---------------------------------------------------------------- Python + +#[test] +fn python_boolean_and_comparison_operators() { + let src = "e = a and b\nf = a or b\ng = not a\nh = a < b\ni = a not in b\nj = x if c else y\n"; + let root = parse("t.py", src); + assert_operator(&root, "BinaryOp", "and", "python"); + assert_operator(&root, "BinaryOp", "or", "python"); + assert_operator(&root, "UnaryOp", "not", "python"); + assert_operator(&root, "BinaryOp", "<", "python"); + assert_operator(&root, "BinaryOp", "not in", "python"); + assert_operator(&root, "If", TERNARY_PYTHON, "python"); + assert!(of_type(&root, "Synthetic") + .iter() + .all(|n| !n.token.contains(" and ") && !n.token.contains(" or "))); +} + +#[test] +fn python_elif_else_structure_is_recoverable() { + let src = "if a:\n x()\nelif b:\n y()\nelse:\n z()\n"; + let root = parse("t.py", src); + let outer = of_type(&root, "If") + .into_iter() + .find(|n| !has_role(n, ROLE_ELSE)) + .expect("the outer if"); + let elif = outer + .children + .iter() + .find(|c| c.node_type == "If") + .expect("elif lowers to an If child of the if"); + assert!(has_role(elif, ROLE_ELSE) && has_role(elif, ROLE_CONDITION)); + let else_block = outer + .children + .iter() + .find(|c| c.node_type == "Block" && has_role(c, ROLE_ELSE)) + .expect("else lowers to a Block child with role Else"); + assert!(has_role(else_block, "Body")); +} + +#[test] +fn python_docstrings_lower_to_doc_comments() { + let src = "\"\"\"Module doc.\"\"\"\n\nclass K:\n \"\"\"Class doc.\"\"\"\n\n def m(self):\n \"\"\"Method doc.\"\"\"\n s = \"not a doc\"\n return s\n"; + let root = parse("t.py", src); + let docs = of_type(&root, "Comment"); + let tokens: Vec<&str> = docs.iter().map(|n| n.token.as_str()).collect(); + assert_eq!( + tokens, + [ + "\"\"\"Module doc.\"\"\"", + "\"\"\"Class doc.\"\"\"", + "\"\"\"Method doc.\"\"\"" + ] + ); + for d in &docs { + assert!(has_role(d, "Comment") && has_role(d, ROLE_DOC)); + assert!(d.children.is_empty(), "docstrings are leaf comments"); + } + assert_eq!( + of_type(&root, "Literal") + .iter() + .map(|n| n.token.as_str()) + .collect::<Vec<_>>(), + ["\"not a doc\""], + "a string used as a value stays a Literal" + ); + let method = &of_type(&root, "Function")[0]; + assert_eq!(method.props[PROP_NAME], "m"); + assert_eq!( + of_type(method, "Comment").len(), + 1, + "the docstring sits inside its function" + ); +} + +// ---------------------------------------------------------------- JavaScript / TypeScript + +#[test] +fn javascript_else_if_chain_and_operators() { + let src = "function f(a, b) {\n if (a) { x(); } else if (b) { y(); } else { z(); }\n let c = a && b || !a;\n c = 1; c += 2; c++;\n}\n"; + let root = parse("t.js", src); + assert_else_if_chain(&root, "javascript"); + assert_operator(&root, "BinaryOp", "&&", "javascript"); + assert_operator(&root, "BinaryOp", "||", "javascript"); + assert_operator(&root, "UnaryOp", "!", "javascript"); + assert_operator(&root, "Assignment", "=", "javascript"); + assert_operator(&root, "Assignment", "+=", "javascript"); + assert_operator(&root, "UnaryOp", "++", "javascript"); + assert_eq!(names(&of_type(&root, "Function")), ["f"]); +} + +#[test] +fn javascript_named_arrow_is_one_function() { + let src = "const arrow = (x) => { if (x) { return 1; } return 0; };\nconst fe = function named() { return 2; };\nclass C { handler = () => 1; }\n[1].map((v) => v * 2);\n"; + let root = parse("t.js", src); + let fns = function_like(&root); + let summary: Vec<(String, String)> = fns + .iter() + .map(|n| { + ( + n.node_type.clone(), + n.props.get(PROP_NAME).cloned().unwrap_or_default(), + ) + }) + .collect(); + assert_eq!( + summary, + [ + ("Function".into(), "arrow".into()), + ("Function".into(), "fe".into()), + ("Method".into(), "handler".into()), + ("Lambda".into(), String::new()), + ] + ); + for f in &fns { + assert_eq!( + function_like(f).len(), + 1, + "no nested function-like node under {:?}", + f.props + ); + } + let arrow = fns[0]; + assert_eq!( + of_type(arrow, "If").len(), + 1, + "the body stays inside the named function" + ); +} + +#[test] +fn typescript_else_if_chain_and_named_arrow() { + let src = "function f(a: boolean, b: boolean): void {\n if (a) { x(); } else if (b) { y(); } else { z(); }\n}\nconst g = (x: number): number => x + 1;\nclass C { h = (v: number) => v; }\n"; + let root = parse("t.ts", src); + assert_else_if_chain(&root, "typescript"); + let summary: Vec<(String, String)> = function_like(&root) + .iter() + .map(|n| { + ( + n.node_type.clone(), + n.props.get(PROP_NAME).cloned().unwrap_or_default(), + ) + }) + .collect(); + assert_eq!( + summary, + [ + ("Function".into(), "f".into()), + ("Function".into(), "g".into()), + ("Method".into(), "h".into()), + ] + ); + assert_operator(&root, "BinaryOp", "+", "typescript"); + let tsx = parse("t.tsx", "const g = (x: number) => <div>{x}</div>;\n"); + assert_eq!( + function_like(&tsx).len(), + 1, + "tsx: one function-like node for a named arrow" + ); +} + +#[test] +fn loop_kind_props_are_real_props() { + let js = parse( + "t.js", + "for (;;) {} while (a) {} do {} while (a); for (k in o) {}\n", + ); + let kinds: Vec<String> = of_type(&js, "Loop") + .iter() + .map(|n| n.props.get("kind").cloned().unwrap_or_default()) + .collect(); + assert_eq!(kinds, ["for", "while", "do-while", "for-in"]); + assert!(of_type(&js, "Loop") + .iter() + .all(|n| !n.props.contains_key("props"))); + let rs = parse("t.rs", "fn f() { for x in 0..1 {} while a {} loop {} }\n"); + let kinds: Vec<String> = of_type(&rs, "Loop") + .iter() + .map(|n| n.props.get("kind").cloned().unwrap_or_default()) + .collect(); + assert_eq!(kinds, ["for", "while", "loop"]); + let kt = parse( + "t.kt", + "fun f() {\n for (x in xs) {}\n while (a) {}\n do {} while (a)\n}\n", + ); + let kinds: Vec<String> = of_type(&kt, "Loop") + .iter() + .map(|n| n.props.get("kind").cloned().unwrap_or_default()) + .collect(); + assert_eq!(kinds, ["for", "while", "do-while"]); +} + +// ---------------------------------------------------------------- shared assertions + +/// The else-if convention: the outer `If` has an `If` child carrying `Else` +/// (the else-if link), and that inner `If` has a `Block` child carrying +/// `Else` (the final else); neither is wrapped in a `Synthetic`/`Block`. +fn assert_else_if_chain(root: &Node, lang: &str) { + let ifs = of_type(root, "If"); + assert_eq!( + ifs.len(), + 2, + "{lang}: an if/else-if/else chain is two If nodes" + ); + let outer = ifs[0]; + assert!( + !has_role(outer, ROLE_ELSE), + "{lang}: the outer if is not an else branch" + ); + let link = outer + .children + .iter() + .find(|c| c.node_type == "If") + .unwrap_or_else(|| { + panic!( + "{lang}: else-if must be a direct If child; got {:?}", + kinds(outer) + ) + }); + assert!( + has_role(link, ROLE_ELSE), + "{lang}: the else-if If carries Else" + ); + assert!( + has_role(link, ROLE_CONDITION), + "{lang}: the else-if If keeps Condition" + ); + let else_block = link + .children + .iter() + .find(|c| c.node_type == "Block" && has_role(c, ROLE_ELSE)) + .unwrap_or_else(|| { + panic!( + "{lang}: else must be a direct Block child with Else; got {:?}", + kinds(link) + ) + }); + assert!(has_role(else_block, "Body")); + let bodies = outer + .children + .iter() + .filter(|c| c.node_type == "Block" && !has_role(c, ROLE_ELSE)) + .count(); + assert_eq!( + bodies, 1, + "{lang}: exactly one consequence Block on the outer if" + ); +} + +fn kinds(n: &Node) -> Vec<(String, Vec<String>)> { + n.children + .iter() + .map(|c| (c.node_type.clone(), c.roles.clone())) + .collect() +} + +// ---------------------------------------------------------------- cross-language operator contract + +/// One row per language: a snippet containing `a + b`, a logical AND, a +/// unary minus, a plain assignment and a compound assignment. +struct OperatorRow { + file: &'static str, + source: &'static str, + and: &'static str, +} + +const OPERATOR_ROWS: &[OperatorRow] = &[ + OperatorRow { + file: "t.go", + source: "package p\n\nfunc f(a, b int) int {\n\tc := a + b\n\tc = 1\n\tc += 2\n\td := -a\n\te := a > 0 && b > 0\n\t_, _ = d, e\n\treturn c\n}\n", + and: "&&", + }, + OperatorRow { + file: "t.py", + source: "c = a + b\nc = 1\nc += 2\nd = -a\ne = a and b\n", + and: "and", + }, + OperatorRow { + file: "t.js", + source: "let c = a + b; c = 1; c += 2; let d = -a; let e = a && b;\n", + and: "&&", + }, + OperatorRow { + file: "t.ts", + source: "let c = a + b; c = 1; c += 2; let d = -a; let e = a && b;\n", + and: "&&", + }, + OperatorRow { + file: "t.rs", + source: "fn f(a: i32, b: i32) { let mut c = a + b; c = 1; c += 2; let d = -a; let e = a > 0 && b > 0; }\n", + and: "&&", + }, + OperatorRow { + file: "A.java", + source: "class A { void f(int a, int b) { int c = a + b; c = 1; c += 2; int d = -a; boolean e = a > 0 && b > 0; } }\n", + and: "&&", + }, + OperatorRow { + file: "t.c", + source: "int f(int a, int b) { int c = a + b; c = 1; c += 2; int d = -a; int e = a > 0 && b > 0; return c; }\n", + and: "&&", + }, + OperatorRow { + file: "t.cpp", + source: "int f(int a, int b) { int c = a + b; c = 1; c += 2; int d = -a; bool e = a > 0 && b > 0; return c; }\n", + and: "&&", + }, + OperatorRow { + file: "A.cs", + source: "class A { void F(int a, int b) { int c = a + b; c = 1; c += 2; int d = -a; bool e = a > 0 && b > 0; } }\n", + and: "&&", + }, + OperatorRow { + file: "t.rb", + source: "c = a + b\nc = 1\nc += 2\nd = -a\ne = a && b\n", + and: "&&", + }, + OperatorRow { + file: "t.php", + source: "<?php\n$c = $a + $b; $c = 1; $c += 2; $d = -$a; $e = $a && $b;\n", + and: "&&", + }, + OperatorRow { + file: "t.kt", + source: "fun f(a: Int, b: Int) { var c = a + b; c = 1; c += 2; val d = -a; val e = a > 0 && b > 0 }\n", + and: "&&", + }, + OperatorRow { + file: "t.swift", + source: "func f(a: Int, b: Int) { var c = a + b; c = 1; c += 2; let d = -a; let e = a > 0 && b > 0 }\n", + and: "&&", + }, +]; + +#[test] +fn every_language_exposes_operator_text_on_operator_nodes() { + for row in OPERATOR_ROWS { + let root = parse(row.file, row.source); + assert_operator(&root, "BinaryOp", "+", row.file); + assert_operator(&root, "BinaryOp", row.and, row.file); + assert_operator(&root, "UnaryOp", "-", row.file); + assert_operator(&root, "Assignment", "=", row.file); + assert_operator(&root, "Assignment", "+=", row.file); + for t in ["BinaryOp", "UnaryOp", "Assignment"] { + for n in of_type(&root, t) { + assert!( + !operator(n).is_empty(), + "{}: a {t} without operator: {:?}", + row.file, + n.token + ); + } + } + } +} + +#[test] +fn every_language_tokens_identifiers_and_literals() { + for row in OPERATOR_ROWS { + let root = parse(row.file, row.source); + for t in ["Identifier", "Literal"] { + let nodes = of_type(&root, t); + assert!(!nodes.is_empty(), "{}: no {t} nodes", row.file); + for n in nodes { + assert!( + !n.token.is_empty(), + "{}: token-less {t} at {:?}", + row.file, + n.pos + ); + } + } + } +} diff --git a/crates/cf-uast-node/Cargo.toml b/crates/cf-uast-node/Cargo.toml index 2e219f3f..eef2dbd2 100644 --- a/crates/cf-uast-node/Cargo.toml +++ b/crates/cf-uast-node/Cargo.toml @@ -10,7 +10,7 @@ publish = false # The canonical UAST node / tree representation. The keystone export is # `Node::to_map`, whose key ordering is part of the report-format contract # (DESIGN.md §2.2: Node.ToMap is the canonical map-origin object; pinned by -# tests/compat). All `to_map` serialization is built on the shared +# the report e2e suites in bins/codefang/tests). All `to_map` serialization is built on the shared # `cf-gojson` crate (the tier-0 report-format JSON encoder). Nothing here uses # serde_json for the serialization path. diff --git a/crates/cf-uast-node/src/dsl/field_access.rs b/crates/cf-uast-node/src/dsl/field_access.rs index 0366b832..bef0ec23 100644 --- a/crates/cf-uast-node/src/dsl/field_access.rs +++ b/crates/cf-uast-node/src/dsl/field_access.rs @@ -1,9 +1,10 @@ //! Field-access strategies for the DSL. //! //! A field name resolves a node to a (possibly empty) set of nodes. Built-in -//! fields are `children`, `token`, `id`, `roles`, `type`, `first`, `last`; any -//! other name falls back to a property lookup (yielding a literal node holding -//! the property value). +//! fields are `children`, `token`, `id`, `roles`, `type`, `props`, `first`, +//! `last`; any other name falls back to a property lookup (yielding a literal +//! node holding the property value), so `.props.name` and `.name` both read +//! the `name` property. use crate::node::Node; use std::collections::HashMap; @@ -31,6 +32,7 @@ impl FieldAccessStrategyRegistry { strategies.insert("id", id_strategy); strategies.insert("roles", roles_strategy); strategies.insert("type", type_strategy); + strategies.insert("props", props_strategy); strategies.insert("first", first_strategy); strategies.insert("last", last_strategy); Self { strategies } @@ -117,6 +119,16 @@ fn type_strategy(node: &Node) -> Vec<Node> { } } +/// `props` → one carrier node holding the node's properties, so a chained +/// `.props.<key>` resolves `<key>` through the property fallback (one literal +/// per present key, nothing for an absent one). +fn props_strategy(node: &Node) -> Vec<Node> { + vec![Node { + props: node.props.clone(), + ..Node::default() + }] +} + /// `first` → the first child, if any. fn first_strategy(node: &Node) -> Vec<Node> { node.children.first().cloned().into_iter().collect() @@ -183,6 +195,15 @@ mod tests { assert!(r.access(&tree(), "missing").is_empty()); } + #[test] + fn props_field_chains_into_a_property() { + let ex = FieldAccessExecutor::new(); + let carrier = ex.execute(&tree(), "props"); + assert_eq!(carrier.len(), 1); + assert_eq!(ex.execute(&carrier[0], "lang")[0].token, "go"); + assert!(ex.execute(&carrier[0], "missing").is_empty()); + } + #[test] fn type_field_empty_when_no_type() { let r = FieldAccessStrategyRegistry::new(); diff --git a/crates/cf-uast-node/src/dsl/lowering.rs b/crates/cf-uast-node/src/dsl/lowering.rs index 37d477a1..0f886dd1 100644 --- a/crates/cf-uast-node/src/dsl/lowering.rs +++ b/crates/cf-uast-node/src/dsl/lowering.rs @@ -3,14 +3,16 @@ use crate::dsl::field_access::FieldAccessExecutor; use crate::dsl::operators::{ - apply_builtin, apply_filter, apply_map, apply_reduce, compare_values, extract_string_value, + apply_builtin, apply_filter, apply_map, apply_reduce, apply_rfilter, apply_rmap, + compare_values, with_string_value, }; use crate::dsl::runtime::{DslError, QueryFn}; use crate::node::Node; use crate::types::{DslLiteral, DslNode}; -/// A boxed transformation from a node slice to an owned node set. -type NodeSetFn = Box<dyn Fn(&[Node]) -> Vec<Node>>; +/// A boxed transformation from a node slice to an owned node set — the same +/// shape as a [`QueryFn`], named for the sub-expressions that build one. +type NodeSetFn = QueryFn; /// Lowers a DSL AST node into a [`QueryFn`]. Node kinds that cannot appear as /// a query root (bare literals and comparisons) return an error. @@ -22,58 +24,67 @@ pub fn lower_node(ast: &DslNode) -> Result<QueryFn, DslError> { match ast { DslNode::Map(expr) => { let field_fn = lower_map_expr(expr); - Ok(Box::new(move |nodes: Vec<Node>| { - apply_map(&nodes, field_fn.as_ref()) + Ok(Box::new(move |nodes: &[Node]| { + apply_map(nodes, field_fn.as_ref()) })) } DslNode::Filter(expr) => { let predicate = lower_filter_expr(expr); - Ok(Box::new(move |nodes: Vec<Node>| { - apply_filter(&nodes, predicate.as_ref()) + Ok(Box::new(move |nodes: &[Node]| { + apply_filter(nodes, predicate.as_ref()) })) } DslNode::Reduce(expr) => { // A non-call reduce body is identity (reference-implementation // behavior); a call-expr body runs as a fold over the whole slice. let body = lower_map_expr(expr); - Ok(Box::new(move |nodes: Vec<Node>| { - apply_reduce(&nodes, body.as_ref()) + Ok(Box::new(move |nodes: &[Node]| { + apply_reduce(nodes, body.as_ref()) })) } - DslNode::Field(_) => { - let f = lower_field_access(ast); - Ok(Box::new(move |nodes: Vec<Node>| f(&nodes))) - } + DslNode::Field(_) => Ok(lower_field_access(ast)), DslNode::Call { name, args } => { let name = name.clone(); let args = args.clone(); - Ok(Box::new(move |nodes: Vec<Node>| { - apply_builtin(&name, &nodes, &args) + Ok(Box::new(move |nodes: &[Node]| { + apply_builtin(&name, nodes, &args) })) } DslNode::Pipeline(stages) => { - let stages = stages.clone(); - Ok(Box::new(move |nodes: Vec<Node>| { - apply_pipeline_stages(&stages, nodes) + let stage_fns: Vec<QueryFn> = + stages.iter().filter_map(|s| lower_node(s).ok()).collect(); + Ok(Box::new(move |nodes: &[Node]| { + apply_pipeline_stages(&stage_fns, nodes) + })) + } + // The recursive stages: the same predicate / expression as `filter` / + // `map`, applied to every node of each input's subtree in pre-order + // (the input itself first). + DslNode::RFilter(expr) => { + let predicate = lower_filter_expr(expr); + Ok(Box::new(move |nodes: &[Node]| { + apply_rfilter(nodes, predicate.as_ref()) + })) + } + DslNode::RMap(expr) => { + let f = lower_map_expr(expr); + Ok(Box::new(move |nodes: &[Node]| { + apply_rmap(nodes, f.as_ref()) })) } - // RMap/RFilter lower to identity. - DslNode::RMap(_) | DslNode::RFilter(_) => Ok(Box::new(|nodes: Vec<Node>| nodes)), DslNode::Literal(_) | DslNode::Comparison { .. } => { Err(DslError::UnknownNodeType(ast.type_name().to_string())) } } } -/// Applies pipeline stages sequentially. A stage that fails to lower is -/// skipped (reference-implementation behavior; pinned by the differential -/// gate). -fn apply_pipeline_stages(stages: &[DslNode], nodes: Vec<Node>) -> Vec<Node> { - let mut current = nodes; +/// Applies the lowered pipeline stages sequentially. A stage that failed to +/// lower was dropped at lowering time (reference-implementation behavior; +/// pinned by the differential gate). +fn apply_pipeline_stages(stages: &[QueryFn], nodes: &[Node]) -> Vec<Node> { + let mut current = nodes.to_vec(); for stage in stages { - if let Ok(stage_fn) = lower_node(stage) { - current = stage_fn(current); - } + current = stage(¤t); } current } @@ -82,10 +93,7 @@ fn apply_pipeline_stages(stages: &[DslNode], nodes: Vec<Node>) -> Vec<Node> { /// access, a call lowers to a builtin, anything else is identity. fn lower_map_expr(expr: &DslNode) -> NodeSetFn { match expr { - DslNode::Field(_) => { - let f = lower_field_access(expr); - Box::new(move |nodes: &[Node]| f(nodes)) - } + DslNode::Field(_) => lower_field_access(expr), DslNode::Call { name, args } => { let name = name.clone(); let args = args.clone(); @@ -96,14 +104,16 @@ fn lower_map_expr(expr: &DslNode) -> NodeSetFn { } /// Lowers field access into a function over a node slice: applies each field -/// name in sequence (chained `.a.b`). +/// name in sequence (chained `.a.b`). The executor is built once here, not +/// per evaluated node — a recursive query evaluates its stages on every node +/// of the tree. fn lower_field_access(expr: &DslNode) -> NodeSetFn { let fields = match expr { DslNode::Field(fields) => fields.clone(), _ => Vec::new(), }; + let executor = FieldAccessExecutor::new(); Box::new(move |nodes: &[Node]| { - let executor = FieldAccessExecutor::new(); let mut current = nodes.to_vec(); for field in &fields { let mut next = Vec::new(); @@ -128,6 +138,15 @@ fn lower_filter_expr(expr: &DslNode) -> Box<dyn Fn(&Node) -> bool> { // left side is a single `[a b]`-bracketed literal, which the // single-field `.roles` access never produces (it yields one literal // per role). + // `.roles has Value` is the structural-selection hot path: membership + // in the role list, with no node materialized per role. + DslNode::Comparison { lhs, op, rhs } + if op == "has" + && matches!(lhs.as_ref(), DslNode::Field(f) if f.len() == 1 && f[0] == "roles") => + { + let rhs_val = literal_or_field_value(rhs); + Box::new(move |n: &Node| !rhs_val.is_empty() && n.roles.contains(&rhs_val)) + } DslNode::Comparison { lhs, op, rhs } if op == "has" => { let left_fn = lower_field_access(lhs); let rhs_val = literal_or_field_value(rhs); @@ -138,13 +157,12 @@ fn lower_filter_expr(expr: &DslNode) -> Box<dyn Fn(&Node) -> bool> { let op = op.clone(); let rhs_val = literal_or_field_value(rhs); Box::new(move |n: &Node| { - let left = extract_string_value(n, &field); - compare_values(&left, &rhs_val, &op) + with_string_value(n, &field, |left| compare_values(left, &rhs_val, &op)) }) } DslNode::Field(fields) => { let field = fields.first().cloned().unwrap_or_default(); - Box::new(move |n: &Node| !extract_string_value(n, &field).is_empty()) + Box::new(move |n: &Node| with_string_value(n, &field, |v| !v.is_empty())) } _ => Box::new(|_n: &Node| true), } diff --git a/crates/cf-uast-node/src/dsl/mod.rs b/crates/cf-uast-node/src/dsl/mod.rs index 357af438..fdde9144 100644 --- a/crates/cf-uast-node/src/dsl/mod.rs +++ b/crates/cf-uast-node/src/dsl/mod.rs @@ -1,7 +1,9 @@ //! The UAST query DSL: parsing, lowering, and execution. //! //! [`parse`] is a hand-written recursive-descent parser implementing the query -//! grammar directly. The grammar is small and unambiguous: +//! grammar directly; [`Query`] compiles a query once and runs it against any +//! number of roots; [`Node::find_dsl`] is the one-shot form. The grammar is +//! small and unambiguous: //! //! ```text //! Query <- Pipeline EOT @@ -20,6 +22,17 @@ //! CompareOp <- "==" / "!=" / "<=" / ">=" / "<" / ">" //! Literal <- StringLiteral / NumberLiteral / BoolLiteral //! ``` +//! +//! Stage semantics: `filter(expr)` keeps the input nodes satisfying `expr` and +//! `map(expr)` applies `expr` to each input node; both look at the input set +//! only (a `filter(...)` root starts from the root's DIRECT children). +//! `rfilter(expr)` and `rmap(expr)` are their recursive forms: they apply the +//! same predicate / expression to every node of each input's subtree in +//! pre-order (the input itself first), so `rfilter(.roles has "Function")` +//! finds class methods and nested functions that `filter(...)` never sees. +//! Field access resolves `children`, `token`, `id`, `roles`, `type`, `props`, +//! `first`, `last`, and any other name as a property lookup +//! (`.props.name` ≡ `.name`). mod field_access; mod lowering; @@ -29,7 +42,7 @@ mod runtime; pub use field_access::{FieldAccessExecutor, FieldAccessStrategyRegistry}; pub use parser::{parse, ParseError}; -pub use runtime::{lower, DslError, QueryFn}; +pub use runtime::{lower, DslError, Query, QueryFn}; use crate::node::Node; @@ -55,51 +68,22 @@ impl Node { /// assert!(root.find_dsl("").is_err()); /// ``` /// + /// Compile the query once with [`Query::compile`] when it runs against + /// many trees. + /// /// # Errors /// /// Returns an error for an empty query (`"query string is empty"`), a parse /// failure, or a lowering failure. The error strings are part of the CLI /// compatibility contract. pub fn find_dsl(&self, query: &str) -> Result<Vec<Self>, DslError> { - if query.is_empty() { - return Err(DslError::EmptyQuery); - } - let ast = parse(query).map_err(DslError::Parse)?; - let initial = self.determine_initial_input(&ast); - let runtime = lower(&ast)?; - Ok(runtime(initial)) - } - - /// Chooses the initial input set for a query: a `filter(...)` root and - /// pipelines start from the children, except a pipeline whose first stage - /// is exactly `map(.children)`, which starts from the node itself; any - /// other root also starts from the node itself. - fn determine_initial_input(&self, ast: &crate::types::DslNode) -> Vec<Self> { - use crate::types::DslNode; - match ast { - DslNode::Filter(_) => self.children.clone(), - DslNode::Pipeline(stages) => { - if stages.is_empty() { - return self.children.clone(); - } - if let DslNode::Map(expr) = &stages[0] { - if let DslNode::Field(fields) = expr.as_ref() { - if fields.len() == 1 && fields[0] == "children" { - return vec![self.clone()]; - } - } - return self.children.clone(); - } - self.children.clone() - } - _ => vec![self.clone()], - } + Ok(Query::compile(query)?.run(self)) } } #[cfg(test)] mod tests { - use crate::dsl::DslError; + use crate::dsl::{DslError, Query}; use crate::node::Node; fn sample_tree() -> Node { @@ -110,6 +94,36 @@ mod tests { root } + /// A declaration node carrying the `Function` role and `props.name`. + fn declaration(node_type: &str, roles: &[&str], name: &str) -> Node { + let mut n = Node::with_token(node_type, ""); + n.roles = roles.iter().map(|r| (*r).to_string()).collect(); + n.props.insert("name".to_string(), name.to_string()); + n + } + + /// `File → Class K { Method km } , Function top`: one nested and one + /// top-level function-role node. + fn nested_tree() -> Node { + let mut root = Node::with_token("File", ""); + let mut class = declaration("Class", &["Class", "Declaration"], "K"); + class.add_child(declaration( + "Method", + &["Function", "Declaration", "Member"], + "km", + )); + root.add_child(class); + root.add_child(declaration("Function", &["Function", "Declaration"], "top")); + root + } + + fn names(nodes: &[Node]) -> Vec<String> { + nodes + .iter() + .map(|n| n.props.get("name").cloned().unwrap_or_default()) + .collect() + } + #[test] fn empty_query_errors() { let root = sample_tree(); @@ -131,4 +145,76 @@ mod tests { // map(.children) over [root] yields root's children, then filter. assert!(out.is_ok()); } + + #[test] + fn filter_sees_only_the_direct_children() { + let out = nested_tree() + .find_dsl(r#"filter(.roles has "Function")"#) + .expect("query"); + assert_eq!(names(&out), vec!["top"]); + } + + #[test] + fn rfilter_sees_every_descendant_in_pre_order() { + let out = nested_tree() + .find_dsl(r#"rfilter(.roles has "Function")"#) + .expect("query"); + assert_eq!(names(&out), vec!["km", "top"]); + } + + #[test] + fn rfilter_includes_the_root_when_it_matches() { + let mut outer = declaration("Function", &["Function", "Declaration"], "outer"); + outer.add_child(declaration( + "Function", + &["Function", "Declaration"], + "inner", + )); + let out = outer + .find_dsl(r#"rfilter(.roles has "Function")"#) + .expect("query"); + assert_eq!(names(&out), vec!["outer", "inner"]); + } + + #[test] + fn rmap_applies_the_expression_to_every_descendant() { + let out = nested_tree().find_dsl("rmap(.props.name)").expect("query"); + let tokens: Vec<&str> = out.iter().map(|n| n.token.as_str()).collect(); + assert_eq!(tokens, vec!["K", "km", "top"]); + } + + #[test] + fn props_field_resolves_a_property_to_one_literal() { + let top = declaration("Function", &["Function"], "top"); + let out = top.find_dsl(".props.name").expect("query"); + assert_eq!(out.len(), 1); + assert_eq!(out[0].token, "top"); + let unnamed = Node::with_token("Function", ""); + assert!(unnamed.find_dsl(".props.name").expect("query").is_empty()); + } + + #[test] + fn rfilter_pipeline_starts_from_the_root() { + let out = nested_tree() + .find_dsl(r#"rfilter(.roles has "Function") | map(.props.name)"#) + .expect("query"); + let tokens: Vec<&str> = out.iter().map(|n| n.token.as_str()).collect(); + assert_eq!(tokens, vec!["km", "top"]); + } + + #[test] + fn compiled_query_runs_against_many_roots() { + let query = Query::compile(r#"rfilter(.roles has "Function")"#).expect("compile"); + assert_eq!(names(&query.run(&nested_tree())), vec!["km", "top"]); + assert_eq!(query.run(&sample_tree()).len(), 0); + } + + #[test] + fn compile_rejects_a_malformed_query() { + assert!(matches!( + Query::compile("rfilter("), + Err(DslError::Parse(_)) + )); + assert!(matches!(Query::compile(""), Err(DslError::EmptyQuery))); + } } diff --git a/crates/cf-uast-node/src/dsl/operators.rs b/crates/cf-uast-node/src/dsl/operators.rs index 246109db..14d74e5c 100644 --- a/crates/cf-uast-node/src/dsl/operators.rs +++ b/crates/cf-uast-node/src/dsl/operators.rs @@ -23,14 +23,46 @@ pub fn apply_reduce(nodes: &[Node], f: &dyn Fn(&[Node]) -> Vec<Node>) -> Vec<Nod f(nodes) } +/// Applies a recursive filter: every node of each input's subtree (the input +/// itself first, then its descendants in pre-order) that satisfies +/// `predicate` is kept, in that order. +pub fn apply_rfilter(nodes: &[Node], predicate: &dyn Fn(&Node) -> bool) -> Vec<Node> { + let mut result = Vec::new(); + for n in nodes { + n.visit_pre_order(|d| { + if predicate(d) { + result.push(d.clone()); + } + }); + } + result +} + +/// Applies a recursive map: runs `f` on every node of each input's subtree +/// (pre-order, the input itself first) and concatenates the results. +pub fn apply_rmap(nodes: &[Node], f: &dyn Fn(&[Node]) -> Vec<Node>) -> Vec<Node> { + let mut result = Vec::new(); + for n in nodes { + n.visit_pre_order(|d| result.extend(f(std::slice::from_ref(d)))); + } + result +} + /// Extracts a string value from a node by field name: `type`/`token`/`id` are /// built-ins, else a prop lookup. pub fn extract_string_value(n: &Node, field: &str) -> String { + with_string_value(n, field, str::to_owned) +} + +/// Runs `f` on the node's string value for `field` (see +/// [`extract_string_value`]) without copying it — the per-node predicate +/// path of a recursive query. +pub fn with_string_value<R>(n: &Node, field: &str, f: impl FnOnce(&str) -> R) -> R { match field { - "type" => n.node_type.clone(), - "token" => n.token.clone(), - "id" => String::from_utf8_lossy(&n.id).into_owned(), - _ => n.props.get(field).cloned().unwrap_or_default(), + "type" => f(&n.node_type), + "token" => f(&n.token), + "id" => f(&String::from_utf8_lossy(&n.id)), + _ => f(n.props.get(field).map_or("", String::as_str)), } } @@ -216,4 +248,30 @@ mod tests { fn unknown_builtin_passes_through() { assert_eq!(apply_builtin("nope", &nodes(), &[]).len(), 3); } + + /// `outer { inner { leaf } }`. + fn nested() -> Node { + let mut inner = Node::with_token("Function", "inner"); + inner.add_child(Node::with_token("Identifier", "leaf")); + let mut outer = Node::with_token("Function", "outer"); + outer.add_child(inner); + outer + } + + #[test] + fn rfilter_walks_the_subtree_in_pre_order() { + let out = apply_rfilter(&[nested()], &|n| n.node_type == "Function"); + let tokens: Vec<&str> = out.iter().map(|n| n.token.as_str()).collect(); + assert_eq!(tokens, vec!["outer", "inner"]); + assert!(apply_filter(&[nested()], &|n| n.token == "inner").is_empty()); + } + + #[test] + fn rmap_concatenates_over_the_subtree() { + let out = apply_rmap(&[nested()], &|ns| { + ns.iter().map(|n| Node::literal(n.token.clone())).collect() + }); + let tokens: Vec<&str> = out.iter().map(|n| n.token.as_str()).collect(); + assert_eq!(tokens, vec!["outer", "inner", "leaf"]); + } } diff --git a/crates/cf-uast-node/src/dsl/runtime.rs b/crates/cf-uast-node/src/dsl/runtime.rs index e904781c..948c9431 100644 --- a/crates/cf-uast-node/src/dsl/runtime.rs +++ b/crates/cf-uast-node/src/dsl/runtime.rs @@ -1,12 +1,13 @@ -//! DSL runtime entry points and error type. +//! DSL runtime entry points: the compiled [`Query`], the error type and the +//! executable stage function type. use crate::dsl::lowering::lower_node; -use crate::dsl::parser::ParseError; +use crate::dsl::parser::{parse, ParseError}; use crate::node::Node; use crate::types::DslNode; -/// An executable query: maps an input node set to an output node set. -pub type QueryFn = Box<dyn Fn(Vec<Node>) -> Vec<Node>>; +/// An executable stage: maps an input node set to an output node set. +pub type QueryFn = Box<dyn Fn(&[Node]) -> Vec<Node>>; /// Errors produced while parsing/lowering/running a DSL query. /// @@ -33,3 +34,77 @@ pub enum DslError { pub fn lower(ast: &DslNode) -> Result<QueryFn, DslError> { lower_node(ast) } + +/// Where a query starts relative to the root it runs against. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum InitialInput { + /// The root's direct children (`filter(...)` roots and most pipelines). + Children, + /// The root itself (recursive stages, field access, builtins). + Root, +} + +/// Chooses the initial input set for a query: a `filter(...)` root and +/// pipelines start from the children, except a pipeline whose first stage is +/// `map(.children)`, a recursive `rfilter(...)` / `rmap(...)` stage, which +/// start from the root itself; any other root also starts from the root. +fn initial_input(ast: &DslNode) -> InitialInput { + match ast { + DslNode::Filter(_) => InitialInput::Children, + DslNode::Pipeline(stages) => match stages.first() { + Some(DslNode::Map(expr)) if is_children_field(expr) => InitialInput::Root, + Some(DslNode::RFilter(_) | DslNode::RMap(_)) => InitialInput::Root, + _ => InitialInput::Children, + }, + _ => InitialInput::Root, + } +} + +/// Whether `expr` is exactly the field access `.children`. +fn is_children_field(expr: &DslNode) -> bool { + matches!(expr, DslNode::Field(fields) if fields.len() == 1 && fields[0] == "children") +} + +/// A DSL query parsed and lowered once, runnable against any number of roots +/// (the per-file hot path of the history analyzers compiles the query once per +/// commit instead of once per parsed tree). +pub struct Query { + initial: InitialInput, + run: QueryFn, +} + +impl Query { + /// Parses and lowers `query`. + /// + /// # Errors + /// + /// [`DslError::EmptyQuery`] for an empty string, [`DslError::Parse`] for a + /// grammar violation, [`DslError::UnknownNodeType`] for a root kind that + /// cannot be lowered (a bare literal or comparison). + pub fn compile(query: &str) -> Result<Self, DslError> { + if query.is_empty() { + return Err(DslError::EmptyQuery); + } + let ast = parse(query).map_err(DslError::Parse)?; + let initial = initial_input(&ast); + let run = lower(&ast)?; + Ok(Self { initial, run }) + } + + /// Runs the query against `root`, returning the matching nodes (cloned). + #[must_use] + pub fn run(&self, root: &Node) -> Vec<Node> { + match self.initial { + InitialInput::Children => (self.run)(&root.children), + InitialInput::Root => (self.run)(std::slice::from_ref(root)), + } + } +} + +impl std::fmt::Debug for Query { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Query") + .field("initial", &self.initial) + .finish_non_exhaustive() + } +} diff --git a/crates/cf-uast-node/src/lib.rs b/crates/cf-uast-node/src/lib.rs index b944f79e..8f6616d7 100644 --- a/crates/cf-uast-node/src/lib.rs +++ b/crates/cf-uast-node/src/lib.rs @@ -6,7 +6,7 @@ //! byte-identity-critical export is [`Node::to_map`], whose output is serialized //! into machine reports — its keys (`children`, `id`, `pos`, `props`, `roles`, //! `token`, `type`) must emit in raw-UTF-8 byte order (report-format contract; -//! pinned by `tests/compat`). To guarantee that, [`Node::to_map`] returns a +//! pinned by the report e2e suites (`bins/codefang/tests`)). To guarantee that, [`Node::to_map`] returns a //! [`GoValue`] built from a map-origin [`GoMap`] (sort-on-encode), never //! `serde_json`. See DESIGN.md §2.2. //! diff --git a/crates/cf-uast-node/src/node.rs b/crates/cf-uast-node/src/node.rs index 4f3f1524..db94e2e7 100644 --- a/crates/cf-uast-node/src/node.rs +++ b/crates/cf-uast-node/src/node.rs @@ -189,6 +189,10 @@ pub mod roles { pub const ROLE_DOC: &str = "Doc"; /// `"Comment"`. pub const ROLE_COMMENT: &str = "Comment"; + /// `"Else"` — the alternative branch of an `If` (an else-if `If` or an + /// else `Block`); added by the lowering from the tree-sitter + /// `alternative` field. + pub const ROLE_ELSE: &str = "Else"; /// `"Attribute"`. pub const ROLE_ATTRIBUTE: &str = "Attribute"; /// `"Annotation"`. diff --git a/crates/cf-uast-node/src/tomap.rs b/crates/cf-uast-node/src/tomap.rs index 2e2f1caf..a28a7c8c 100644 --- a/crates/cf-uast-node/src/tomap.rs +++ b/crates/cf-uast-node/src/tomap.rs @@ -2,7 +2,7 @@ //! representation. //! //! The report-format contract requires map keys to serialize in raw-UTF-8 byte -//! order (pinned by `tests/compat`). To guarantee that, `to_map` returns a +//! order (pinned by the report e2e suites (`bins/codefang/tests`)). To guarantee that, `to_map` returns a //! [`cf_gojson::GoValue`] whose objects are built with //! [`cf_gojson::GoMap::from_map`] (sort-on-encode). The full key set a node can //! emit is `children, id, pos, props, roles, token, type`, which in byte order @@ -14,7 +14,7 @@ use cf_gojson::{GoMap, GoValue}; impl Node { /// Converts this node (and its subtree) into a [`GoValue`] whose /// serialization is part of the report-format contract (pinned by - /// `tests/compat`). + /// the report e2e suites (`bins/codefang/tests`)). /// /// Map-origin objects are used throughout so keys byte-sort on encode. The /// field-population rules are frozen: diff --git a/crates/cf-uast-spec/src/uast-schema.json b/crates/cf-uast-spec/src/uast-schema.json index 58e226e1..578f089a 100644 --- a/crates/cf-uast-spec/src/uast-schema.json +++ b/crates/cf-uast-spec/src/uast-schema.json @@ -98,7 +98,16 @@ "Match", "Return", "Break", - "Continue" + "Continue", + "Case", + "Cast", + "Defer", + "Else", + "EnumMember", + "Export", + "For", + "Method", + "Switch" ] }, "NodeType": { @@ -163,6 +172,11 @@ "Comprehension", "Pattern", "Match", + "Annotation", + "Defer", + "Export", + "MemberAccess", + "TypeDeclaration", "Synthetic" ] } diff --git a/crates/cf-uast/build.rs b/crates/cf-uast/build.rs index c9b4d0d1..f4796974 100644 --- a/crates/cf-uast/build.rs +++ b/crates/cf-uast/build.rs @@ -4,8 +4,8 @@ //! verbatim from `go-sitter-forest` at the **exact** grammar revision the //! reference build links (see each vendored `grammar.json`). Because //! tree-sitter node kinds and spans flow into machine report bytes (DESIGN -//! §5), using the identical grammar revision is what keeps `uast parse -//! --format json` byte-identical (pinned by tests/compat). +//! §5), using one pinned grammar revision is what keeps `uast parse +//! --format json` stable across builds. //! //! Each grammar exports a `tree_sitter_<lang>()` C entry point (some ship an //! external `scanner.c` as well). All sources compile into a single static diff --git a/crates/cf-uast/src/lib.rs b/crates/cf-uast/src/lib.rs index 2667b3f9..1f0f1461 100644 --- a/crates/cf-uast/src/lib.rs +++ b/crates/cf-uast/src/lib.rs @@ -26,7 +26,7 @@ //! binary remains the first end-to-end-shippable artifact (DESIGN §1.1). //! //! Compatibility: parse trees and report bytes are pinned against the -//! reference implementation by `tests/compat`. +//! reference implementation by the report e2e suites (`bins/codefang/tests`). //! //! # Byte-identity //! diff --git a/crates/cf-uast/src/lowering.rs b/crates/cf-uast/src/lowering.rs index 3a1f4763..772c9651 100644 --- a/crates/cf-uast/src/lowering.rs +++ b/crates/cf-uast/src/lowering.rs @@ -17,14 +17,61 @@ //! is produced; with none, it is dropped. //! - **IDs.** Stable IDs are assigned in a later pass (the `uast parse` //! command calls `assign_stable_ids`); this lowering leaves `id` empty. +//! +//! # Shape contract (language-agnostic guarantees) +//! +//! Documented for analyzer authors in `site/architecture/uast.md`; every +//! mapping table inherits them without spelling them out: +//! +//! - **Operator text.** A mapped node gets `props.operator` from, in order: +//! the rule's own `operator` prop; a tree-sitter field named `operator`, +//! `op` or `operation`; the space-joined repeated `operators` field; and — +//! for `BinaryOp` / `UnaryOp` / `Assignment` nodes only — the first +//! anonymous child token (`=`, `not`, `++`, …). See [`OPERATOR_FIELDS`]. +//! - **Source-text tokens.** `Identifier` and `Literal` nodes whose rule +//! names no token source carry the node's source text. +//! - **Leaf comments.** `Comment` nodes never have children and their span +//! and token never include the trailing line terminator. +//! - **Else role.** Children lowered from an `If`'s `alternative` field carry +//! the `Else` role, so an else-if link (`If` under `If`) and an else branch +//! (`Block` under `If`) are recognisable without a wrapper node. +//! - **Prop values.** `@capture` resolves a query capture, `descendant:<kind>` +//! the first descendant's text, anything else is a literal. +//! - **Conditions.** A dotted condition path may start with `parent` to +//! inspect the enclosing tree-sitter node (`parent.type == "..."`). use std::collections::HashMap; use std::sync::Arc; use cf_uast_mapping::{PatternMatcher, Rule}; -use cf_uast_node::{Node, Positions}; +use cf_uast_node::{ + Node, Positions, ROLE_ELSE, UAST_ASSIGNMENT, UAST_BINARY_OP, UAST_COMMENT, UAST_IDENTIFIER, + UAST_IF, UAST_LITERAL, UAST_UNARY_OP, +}; use tree_sitter::{Node as TsNode, Query, Tree}; +/// The `props` key carrying an operator node's operator text. +const PROP_OPERATOR: &str = "operator"; +/// Tree-sitter field names that hold a node's operator token, in lookup +/// order (`operator`: most grammars; `op`: tree-sitter-swift; `operation`: +/// tree-sitter-swift prefix expressions). +const OPERATOR_FIELDS: &[&str] = &["operator", "op", "operation"]; +/// The repeated field tree-sitter-python's `comparison_operator` uses for a +/// chain (`a < b <= c`); its values are space-joined. +const OPERATORS_FIELD: &str = "operators"; +/// UAST types whose operator text may fall back to the first anonymous child. +const OPERATOR_TYPES: &[&str] = &[UAST_BINARY_OP, UAST_UNARY_OP, UAST_ASSIGNMENT]; +/// UAST types whose token defaults to the node's source text. +const SOURCE_TEXT_TYPES: &[&str] = &[UAST_IDENTIFIER, UAST_LITERAL]; +/// The tree-sitter field holding an `If`'s else / else-if branch. +const FIELD_ALTERNATIVE: &str = "alternative"; +/// The condition-path segment that steps to the tree-sitter parent. +const PARENT_SEGMENT: &str = "parent"; +/// Prop-value prefix selecting the first descendant of a kind. +const PROP_DESCENDANT_PREFIX: &str = "descendant:"; +/// Prop-value prefix selecting a query capture. +const PROP_CAPTURE_PREFIX: char = '@'; + /// A mapping [`Rule`] with its inheritance already merged and its pattern /// already compiled. /// @@ -250,7 +297,13 @@ impl<'a> Lowering<'a> { return self.finish_unmapped_node(root, node_type, mapped_children); }; - let children = self.process_children(root, Some(resolved)); + // Comments are leaves (shape contract): a doc comment's marker/text + // sub-nodes would otherwise lower to nested Comments. + let children = if resolved.rule.uast_spec.r#type == UAST_COMMENT { + Vec::new() + } else { + self.process_children(root, Some(resolved)) + }; if self.should_skip_node(root, Some(resolved)) { return None; @@ -285,6 +338,7 @@ impl<'a> Lowering<'a> { fn process_children(&self, root: TsNode<'_>, mapping_rule: Option<&ResolvedRule>) -> Vec<Node> { let count = root.named_child_count(); let mut children = Vec::with_capacity(count); + let alternatives = Self::alternative_child_ids(root, mapping_rule); // Index-based `named_child(idx)`, NOT a cursor walk and NOT the // cursor-backed `named_children` iterator: only per-index construction // assigns production aliases correctly (extras keep their raw kind) — @@ -295,7 +349,12 @@ impl<'a> Lowering<'a> { }; if !self.should_exclude_child(child, mapping_rule) { let child_ctx = self.derive_parent_context(root, mapping_rule); - if let Some(canonical) = self.to_canonical_node(child, &child_ctx) { + if let Some(mut canonical) = self.to_canonical_node(child, &child_ctx) { + if alternatives.contains(&child.id()) + && !canonical.roles.iter().any(|r| r == ROLE_ELSE) + { + canonical.roles.push(ROLE_ELSE.to_string()); + } children.push(canonical); } } @@ -303,6 +362,22 @@ impl<'a> Lowering<'a> { children } + /// The tree-sitter ids of an `If` node's `alternative`-field children + /// (else / else-if branches); empty for every other rule type. + /// + /// Identity is compared by [`TsNode::id`] rather than by index so the + /// per-index named-child construction above stays untouched. + fn alternative_child_ids(root: TsNode<'_>, mapping_rule: Option<&ResolvedRule>) -> Vec<usize> { + let is_if = mapping_rule.is_some_and(|r| r.rule.uast_spec.r#type == UAST_IF); + if !is_if { + return Vec::new(); + } + let mut cursor = root.walk(); + root.children_by_field_name(FIELD_ALTERNATIVE, &mut cursor) + .map(|n| n.id()) + .collect() + } + /// The context string a child sees: the parent rule's UAST type, or the /// parent's raw node type when unmapped. fn derive_parent_context( @@ -458,7 +533,8 @@ impl<'a> Lowering<'a> { /// Resolves a dotted condition path against `root` and returns the value /// to compare: the resolved node's alias-aware type for a terminal /// `.type` segment, its source text otherwise. `None` when any segment - /// fails to resolve (the condition is then false). + /// fails to resolve (the condition is then false). A `parent` segment + /// steps to the enclosing tree-sitter node (`parent.type == "pair"`). fn resolve_field_path(&self, root: TsNode<'_>, path: &str) -> Option<String> { let mut segments: Vec<&str> = path.split('.').collect(); let terminal_is_type = segments.last() == Some(&"type"); @@ -468,7 +544,11 @@ impl<'a> Lowering<'a> { let mut node = root; for seg in &segments { - node = self.resolve_path_segment(node, seg)?; + node = if *seg == PARENT_SEGMENT { + node.parent()? + } else { + self.resolve_path_segment(node, seg)? + }; } if terminal_is_type { @@ -541,9 +621,14 @@ impl<'a> Lowering<'a> { let mut props: HashMap<String, String> = HashMap::new(); self.extract_properties(root, rule, &mut props); self.extract_name(root, rule, &mut props); + self.extract_operator(root, rule, &mut props); let pos = self.extract_positions(root); - let token_text = self.extract_token_text(root, rule); + let uast_type = rule.uast_spec.r#type.as_str(); + let mut token_text = self.extract_token_text(root, rule); + if token_text.is_empty() && SOURCE_TEXT_TYPES.contains(&uast_type) { + token_text = self.extract_node_text(root); + } let mut node = Node::new( Vec::new(), @@ -559,9 +644,92 @@ impl<'a> Lowering<'a> { // spec resolves to a `fields.name`/`text` source. self.extract_token(root, rule, &mut node); + if uast_type == UAST_COMMENT { + self.trim_comment_span(&mut node); + } + node } + /// Fills `props["operator"]` for operator nodes (shape contract): an + /// explicit rule prop wins; then the grammar's operator field(s); then, + /// for [`OPERATOR_TYPES`], the first anonymous child token. + fn extract_operator(&self, root: TsNode<'_>, rule: &Rule, props: &mut HashMap<String, String>) { + if props.contains_key(PROP_OPERATOR) { + return; + } + let from_fields = self.operator_from_fields(root); + let text = if from_fields.is_some() { + from_fields + } else if OPERATOR_TYPES.contains(&rule.uast_spec.r#type.as_str()) { + self.first_anonymous_child_text(root) + } else { + None + }; + if let Some(text) = text.filter(|t| !t.is_empty()) { + props.insert(PROP_OPERATOR.to_string(), text); + } + } + + /// The operator text held by one of [`OPERATOR_FIELDS`], or the + /// space-joined values of the repeated [`OPERATORS_FIELD`]. + fn operator_from_fields(&self, root: TsNode<'_>) -> Option<String> { + for field in OPERATOR_FIELDS { + if let Some(op) = root.child_by_field_name(field) { + return Some(self.extract_node_text(op)); + } + } + let mut cursor = root.walk(); + let chain: Vec<String> = root + .children_by_field_name(OPERATORS_FIELD, &mut cursor) + .map(|n| self.extract_node_text(n)) + .collect(); + if chain.is_empty() { + None + } else { + Some(chain.join(" ")) + } + } + + /// The text of the node's first anonymous (unnamed) child — the operator + /// keyword or symbol of grammars that give it no field (`=`, `not`, `++`). + fn first_anonymous_child_text(&self, root: TsNode<'_>) -> Option<String> { + let mut cursor = root.walk(); + let first = root.children(&mut cursor).find(|c| !c.is_named()); + first.map(|c| self.extract_node_text(c)) + } + + /// Drops the trailing line terminator(s) from a `Comment`'s span and + /// token: tree-sitter-rust's doc-comment token swallows the newline, so + /// the raw span ended at column 1 of the NEXT line. + fn trim_comment_span(&self, node: &mut Node) { + let Some(pos) = node.pos.as_mut() else { + return; + }; + let (start, end) = ( + usize::try_from(pos.start_offset).unwrap_or(usize::MAX), + usize::try_from(pos.end_offset).unwrap_or(usize::MAX), + ); + if start > end || end > self.source.len() { + return; + } + let text = &self.source[start..end]; + let kept = text.len() - trailing_line_terminators(text); + if kept == text.len() { + return; + } + let trimmed = &text[..kept]; + pos.end_offset = (start + kept) as u64; + let newlines = trimmed.iter().filter(|&&b| b == b'\n').count() as u64; + pos.end_line = pos.start_line + newlines; + pos.end_col = match trimmed.iter().rposition(|&b| b == b'\n') { + Some(last_nl) => (kept - last_nl - 1) as u64 + 1, + None => pos.start_col + kept as u64, + }; + let token_kept = node.token.trim_end_matches(['\n', '\r']).len(); + node.token.truncate(token_kept); + } + /// The rule's roles, verbatim. fn extract_roles(&self, rule: &Rule) -> Vec<String> { rule.uast_spec.roles.clone() @@ -610,31 +778,18 @@ impl<'a> Lowering<'a> { } } - /// Resolves one prop value: `@capture`, `descendant:<type>`, or a direct - /// child type. + /// Resolves one prop value: `@capture` (a query capture), + /// `descendant:<kind>` (the first descendant's text), or a literal. fn extract_property_value(&self, root: TsNode<'_>, prop: &str) -> String { - if let Some(capture) = prop.strip_prefix('@') { + if let Some(capture) = prop.strip_prefix(PROP_CAPTURE_PREFIX) { if !capture.is_empty() { return self.extract_capture_text(root, capture); } } - if let Some(after) = prop.strip_prefix("descendant:") { + if let Some(after) = prop.strip_prefix(PROP_DESCENDANT_PREFIX) { return self.find_descendant_token(root, after); } - self.extract_direct_child_property(root, prop) - } - - /// The text of the first named child whose type equals `prop`. - fn extract_direct_child_property(&self, root: TsNode<'_>, prop: &str) -> String { - let count = root.named_child_count(); - for idx in 0..count { - if let Some(child) = root.named_child(idx) { - if self.node_type(child) == prop { - return self.extract_node_text(child); - } - } - } - String::new() + prop.to_string() } /// Post-construction token override (see [`Self::create_mapped_node`]). @@ -851,6 +1006,14 @@ impl<'a> Lowering<'a> { } } +/// The number of trailing `\n` / `\r` bytes in `text`. +fn trailing_line_terminators(text: &[u8]) -> usize { + text.iter() + .rev() + .take_while(|&&b| b == b'\n' || b == b'\r') + .count() +} + /// The first descendant (pre-order, including self) of the given type. fn find_descendant_by_type<'tree>(node: TsNode<'tree>, typ: &str) -> Option<TsNode<'tree>> { if node.kind() == typ { diff --git a/crates/cf-uast/tests/lowering_contract.rs b/crates/cf-uast/tests/lowering_contract.rs new file mode 100644 index 00000000..52e6d33a --- /dev/null +++ b/crates/cf-uast/tests/lowering_contract.rs @@ -0,0 +1,178 @@ +//! The language-agnostic lowering contract (`site/architecture/uast.md`, +//! "Shape contract"): behaviors the lowering guarantees for EVERY language +//! regardless of what its mapping table says — operator text on operator +//! nodes, source-text tokens on identifiers and literals, leaf comments with +//! line-exact spans, and the `Else` role on an `If`'s alternative branch. +//! +//! Each test drives the real parser over an embedded language chosen because +//! its table does NOT spell the behavior out, so a green test proves the +//! lowering supplies it. + +use cf_uast::{Node, Parser}; + +const PROP_OPERATOR: &str = "operator"; +const ROLE_ELSE: &str = "Else"; + +fn parse(filename: &str, source: &str) -> Node { + Parser::new() + .parse(filename, source.as_bytes()) + .unwrap_or_else(|e| panic!("{filename}: parse failed: {e}")) +} + +fn of_type<'a>(root: &'a Node, node_type: &str) -> Vec<&'a Node> { + root.find(|n| n.node_type == node_type) +} + +fn operators(root: &Node, node_type: &str) -> Vec<String> { + of_type(root, node_type) + .iter() + .map(|n| n.props.get(PROP_OPERATOR).cloned().unwrap_or_default()) + .collect() +} + +#[test] +fn identifiers_and_literals_default_to_their_source_text() { + // Kotlin's table declares `identifier` / `integer_literal` without a + // token source; the lowering fills in the source text anyway. + let root = parse("t.kt", "val x = 1\nval s = \"s\"\n"); + let idents: Vec<&str> = of_type(&root, "Identifier") + .iter() + .map(|n| n.token.as_str()) + .collect(); + assert!(idents.contains(&"x") && idents.contains(&"s"), "{idents:?}"); + let lits: Vec<&str> = of_type(&root, "Literal") + .iter() + .map(|n| n.token.as_str()) + .collect(); + assert_eq!(lits, ["1", "\"s\""]); +} + +#[test] +fn operator_text_comes_from_the_operator_field() { + let root = parse("t.go", "package p\n\nvar x = a + b\nvar y = -a\n"); + assert_eq!(operators(&root, "BinaryOp"), ["+"]); + assert_eq!(operators(&root, "UnaryOp"), ["-"]); +} + +#[test] +fn operator_text_comes_from_the_op_field() { + // tree-sitter-swift names the field `op`. + let root = parse("t.swift", "let x = a + b\n"); + assert_eq!(operators(&root, "BinaryOp"), ["+"]); +} + +#[test] +fn chained_comparison_operators_are_space_joined() { + // tree-sitter-python's `comparison_operator` carries a repeated + // `operators` field; the chain `a < b <= c` reports both. + let root = parse("t.py", "x = a < b <= c\n"); + assert_eq!(operators(&root, "BinaryOp"), ["< <="]); +} + +#[test] +fn operator_text_falls_back_to_the_anonymous_operator_token() { + // Rust's `assignment_expression` has no `operator` field; `=` is an + // anonymous child. + let root = parse("t.rs", "fn f() { c = 1; }\n"); + assert_eq!(operators(&root, "Assignment"), ["="]); + // Python's `not_operator` likewise. + let root = parse("t.py", "x = not a\n"); + assert_eq!(operators(&root, "UnaryOp"), ["not"]); +} + +#[test] +fn explicit_literal_operator_props_win() { + // The C ternary has no single operator token; its table pins `?:`. + let root = parse("t.c", "int f(int a) { return a ? 1 : 2; }\n"); + assert_eq!(operators(&root, "If"), ["?:"]); +} + +#[test] +fn comments_are_leaves_with_line_exact_spans() { + // tree-sitter-rust's doc comments nest marker/text children and swallow + // the line terminator; the lowering flattens and trims them, for LF and + // CRLF alike. + for (src, want_token) in [ + ("/// Doc.\nfn f() {}\n", "/// Doc."), + ("/// Doc.\r\nfn f() {}\r\n", "/// Doc."), + ("//! Inner.\n\nfn f() {}\n", "//! Inner."), + ] { + let root = parse("t.rs", src); + let comments = of_type(&root, "Comment"); + assert_eq!(comments.len(), 1, "{src:?}"); + let c = comments[0]; + assert_eq!(c.token, want_token); + assert!(c.children.is_empty()); + let pos = c.pos.expect("position"); + assert_eq!((pos.start_line, pos.end_line), (1, 1), "{src:?}"); + assert_eq!( + pos.end_col, + u64::try_from(want_token.len()).expect("fits") + 1 + ); + assert_eq!( + pos.end_offset, + u64::try_from(want_token.len()).expect("fits") + ); + } +} + +#[test] +fn alternative_field_children_carry_the_else_role() { + // Go and Java have no `else_clause` node: the alternative is a direct + // field of the if statement, so the role must come from the lowering. + let go = parse( + "t.go", + "package p\n\nfunc f(a, b bool) {\n\tif a {\n\t} else if b {\n\t} else {\n\t}\n}\n", + ); + assert_else_roles(&go); + let java = parse( + "A.java", + "class A { void f(boolean a, boolean b) { if (a) {} else if (b) {} else {} } }\n", + ); + assert_else_roles(&java); +} + +fn assert_else_roles(root: &Node) { + let ifs = of_type(root, "If"); + assert_eq!(ifs.len(), 2); + let outer = ifs[0]; + assert!(!outer.roles.iter().any(|r| r == ROLE_ELSE)); + let with_else: Vec<&str> = outer + .children + .iter() + .filter(|c| c.roles.iter().any(|r| r == ROLE_ELSE)) + .map(|c| c.node_type.as_str()) + .collect(); + assert_eq!( + with_else, + ["If"], + "the else-if link is the only Else child of the outer if" + ); + let inner = ifs[1]; + let with_else: Vec<&str> = inner + .children + .iter() + .filter(|c| c.roles.iter().any(|r| r == ROLE_ELSE)) + .map(|c| c.node_type.as_str()) + .collect(); + assert_eq!( + with_else, + ["Block"], + "the final else is the only Else child of the inner if" + ); + let dup = inner + .children + .iter() + .filter(|c| c.roles.iter().filter(|r| *r == ROLE_ELSE).count() > 1) + .count(); + assert_eq!(dup, 0, "Else is never duplicated"); +} + +#[test] +fn parent_conditions_select_rules_by_enclosing_node() { + // `arrow_function` lowers to a Lambda on its own, but is transparent + // under the declarator that already lowered to a named Function. + let root = parse("t.js", "const f = () => 1;\ng(() => 2);\n"); + assert_eq!(of_type(&root, "Lambda").len(), 1); + assert_eq!(of_type(&root, "Function").len(), 1); +} diff --git a/crates/cf-version/README.md b/crates/cf-version/README.md index 12ea5d8a..afa0344d 100644 --- a/crates/cf-version/README.md +++ b/crates/cf-version/README.md @@ -15,26 +15,23 @@ consumed by `option_env!`: | Field | Env (first non-empty wins) | Default | | ------- | --------------------------------------- | ----------- | -| Version | `CF_VERSION`, `GIT_VERSION` | `dev` | -| Commit | `CF_COMMIT`, `GIT_COMMIT` | `none` | +| Version | `CF_VERSION`, `GIT_VERSION` | crate version | +| Commit | `CF_COMMIT`, `GIT_COMMIT`, `GIT_HASH`, the checkout's `HEAD` | `unknown` | | Date | `CF_DATE`, `SOURCE_DATE_EPOCH` (→RFC3339 UTC) | `unknown` | -A plain `cargo build` with no env produces `dev` / `none` / `unknown`. +A plain `cargo build` with no env reports the crate version, the commit `HEAD` points at (or `unknown` outside a git checkout), and an `unknown` build date. `SOURCE_DATE_EPOCH` keeps the `built:` date reproducible so version goldens -are stable. The banner format is a frozen CLI contract, pinned against the -reference binary by `tests/compat`. +are stable. The banner format is a CLI contract, pinned by the `codefang` +and `uast` end-to-end tests. ## Usage -With no build-time injection the defaults (`dev` / `none` / `unknown`) are used. -This exact behavior is pinned by the crate doctest and unit tests on -[`banner`](src/lib.rs): +The banner always has the same shape, whatever was injected: ```rust -assert_eq!( - cf_version::banner("codefang"), - "codefang dev (commit: none, built: unknown)", -); +let line = cf_version::banner("codefang"); +assert!(line.starts_with(&format!("codefang {} (commit: ", cf_version::VERSION))); +assert!(line.ends_with(&format!(", built: {})", cf_version::DATE))); ``` Build with injected metadata: diff --git a/crates/cf-version/build.rs b/crates/cf-version/build.rs index 57df402c..f1a34112 100644 --- a/crates/cf-version/build.rs +++ b/crates/cf-version/build.rs @@ -2,18 +2,28 @@ //! //! The build script reads environment variables and re-exports them as //! `cargo:rustc-env` values that the library picks up with `option_env!`. -//! When an env var is absent the library falls back to the frozen defaults, -//! so a plain `cargo build` with no env produces `dev` / `none` / `unknown`. +//! When an input is absent the library falls back to its defaults: the +//! crate version, `unknown` commit, `unknown` date. //! //! Recognized inputs (first non-empty wins per field): //! Version: `CF_VERSION`, then `GIT_VERSION` -//! Commit: `CF_COMMIT`, then `GIT_COMMIT` +//! Commit: `CF_COMMIT`, then `GIT_COMMIT`, then `GIT_HASH`, then the +//! commit `HEAD` of the enclosing checkout, read straight from the +//! `.git` directory (worktrees and packed refs included; no `git` +//! process is spawned) //! Date: `CF_DATE`, then `SOURCE_DATE_EPOCH` (epoch seconds -> RFC3339 UTC) //! +//! No wall-clock time is ever embedded. +//! //! `SOURCE_DATE_EPOCH` support makes `built:` reproducible (per DESIGN.md //! §2.8): goldens pin it so the printed `built` date is stable. use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Hex digits of the commit the binaries report. +const SHORT_HASH_LEN: usize = 12; /// Seconds in a (non-leap) day. const SECS_PER_DAY: i64 = 86_400; @@ -42,7 +52,12 @@ fn first_env(keys: &[&str]) -> Option<String> { fn main() { // Re-run when any recognized input changes. let version = first_env(&["CF_VERSION", "GIT_VERSION"]); - let commit = first_env(&["CF_COMMIT", "GIT_COMMIT"]); + let commit = first_env(&["CF_COMMIT", "GIT_COMMIT"]).or_else(|| { + first_env(&["GIT_HASH"]) + .map(|h| h.trim().to_owned()) + .or_else(head_commit) + .map(|h| h.chars().take(SHORT_HASH_LEN).collect()) + }); // Date: explicit CF_DATE wins; otherwise derive RFC3339 UTC from // SOURCE_DATE_EPOCH for reproducible builds. @@ -90,3 +105,70 @@ fn civil_from_days(z: i64) -> (i64, u32, u32) { let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; // [1, 12] (if m <= 2 { y + 1 } else { y }, m, d) } + +/// Asks cargo to rebuild when `path` changes (only for existing files: a +/// missing path would force a rebuild every time). +fn watch(path: &Path) { + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } +} + +/// The commit `HEAD` of the enclosing checkout points at. +fn head_commit() -> Option<String> { + let manifest = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR")?); + let git_dir = find_git_dir(&manifest)?; + let common = common_dir(&git_dir); + let head_path = git_dir.join("HEAD"); + watch(&head_path); + let head = fs::read_to_string(&head_path).ok()?; + let head = head.trim(); + let Some(reference) = head.strip_prefix("ref: ") else { + return is_hash(head).then(|| head.to_owned()); + }; + for dir in [&git_dir, &common] { + let loose = dir.join(reference); + if loose.is_file() { + watch(&loose); + let hash = fs::read_to_string(&loose).ok()?; + let hash = hash.trim(); + return is_hash(hash).then(|| hash.to_owned()); + } + } + let packed = common.join("packed-refs"); + watch(&packed); + fs::read_to_string(&packed).ok()?.lines().find_map(|line| { + let (hash, name) = line.split_once(' ')?; + (name == reference && is_hash(hash)).then(|| hash.to_owned()) + }) +} + +/// The `.git` directory of the checkout containing `start`: a `.git` +/// directory, or the `gitdir:` a worktree's `.git` file names. +fn find_git_dir(start: &Path) -> Option<PathBuf> { + for dir in start.ancestors() { + let dot_git = dir.join(".git"); + if dot_git.is_dir() { + return Some(dot_git); + } + if dot_git.is_file() { + let text = fs::read_to_string(&dot_git).ok()?; + let target = text.trim().strip_prefix("gitdir: ")?; + return Some(dir.join(target)); + } + } + None +} + +/// The directory holding the shared refs of a worktree (`commondir`), or +/// `git_dir` itself. +fn common_dir(git_dir: &Path) -> PathBuf { + fs::read_to_string(git_dir.join("commondir")) + .map(|rel| git_dir.join(rel.trim())) + .unwrap_or_else(|_| git_dir.to_path_buf()) +} + +/// True for a full hexadecimal object id. +fn is_hash(s: &str) -> bool { + s.len() >= SHORT_HASH_LEN && s.bytes().all(|b| b.is_ascii_hexdigit()) +} diff --git a/crates/cf-version/src/lib.rs b/crates/cf-version/src/lib.rs index 6a476838..a8355cc0 100644 --- a/crates/cf-version/src/lib.rs +++ b/crates/cf-version/src/lib.rs @@ -13,13 +13,15 @@ //! `Version`/`Commit`/`Date` are injected at build time (DESIGN.md §2.8) //! through a build script (`build.rs`) that reads environment variables and //! re-exports them as `rustc-env` entries; the library reads those with -//! [`option_env!`], falling back to the defaults `dev` / `none` / `unknown` -//! when unset. A plain `cargo build` with no environment therefore produces -//! exactly `dev` / `none` / `unknown`. +//! [`option_env!`], falling back to the crate version, `unknown` and `unknown` +//! when unset. Inside a git checkout the build script also finds the commit +//! `HEAD` points at, so a plain `cargo build` reports the crate version and +//! that commit. //! //! Recognized build-time inputs (see `build.rs`): //! - `Version`: `CF_VERSION`, then `GIT_VERSION` -//! - `Commit`: `CF_COMMIT`, then `GIT_COMMIT` +//! - `Commit`: `CF_COMMIT`, then `GIT_COMMIT`, then `GIT_HASH`, then the +//! checkout's `HEAD` //! - `Date`: `CF_DATE`, then `SOURCE_DATE_EPOCH` (epoch → RFC3339 UTC) //! //! `SOURCE_DATE_EPOCH` support keeps the `built:` date reproducible so the @@ -30,10 +32,10 @@ use std::fmt::Write as _; -/// Default version when none is injected. -pub const DEFAULT_VERSION: &str = "dev"; -/// Default commit when none is injected. -pub const DEFAULT_COMMIT: &str = "none"; +/// Default version when none is injected: the workspace crate version. +pub const DEFAULT_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Default commit when none is injected and the build is not in a checkout. +pub const DEFAULT_COMMIT: &str = "unknown"; /// Default build date when none is injected. pub const DEFAULT_DATE: &str = "unknown"; /// Default git hash of the running binary. There is no build-time injection @@ -45,7 +47,7 @@ pub const DEFAULT_BINARY: i64 = 0; /// The build version string. /// /// Injected via `CF_VERSION` / `GIT_VERSION` at build time; defaults to -/// [`DEFAULT_VERSION`] (`"dev"`). +/// [`DEFAULT_VERSION`] (the crate version). pub const VERSION: &str = match option_env!("CF_VERSION_INJECTED") { Some(v) => v, None => DEFAULT_VERSION, @@ -54,7 +56,7 @@ pub const VERSION: &str = match option_env!("CF_VERSION_INJECTED") { /// The build commit hash. /// /// Injected via `CF_COMMIT` / `GIT_COMMIT` at build time; defaults to -/// [`DEFAULT_COMMIT`] (`"none"`). +/// [`DEFAULT_COMMIT`] (`"unknown"`). pub const COMMIT: &str = match option_env!("CF_COMMIT_INJECTED") { Some(v) => v, None => DEFAULT_COMMIT, @@ -86,14 +88,11 @@ pub const BINARY_GIT_HASH: &str = DEFAULT_BINARY_GIT_HASH; /// # Examples /// /// ``` -/// // With no build-time injection, the defaults are used: -/// assert_eq!( -/// cf_version::banner("codefang"), -/// "codefang dev (commit: none, built: unknown)" -/// ); +/// use cf_version::{banner, COMMIT, DATE, VERSION}; +/// /// assert_eq!( -/// cf_version::banner("uast"), -/// "uast dev (commit: none, built: unknown)" +/// banner("uast"), +/// format!("uast {VERSION} (commit: {COMMIT}, built: {DATE})") /// ); /// ``` #[must_use] @@ -102,7 +101,7 @@ pub fn banner(name: &str) -> String { } /// Returns the `codefang version` line — the banner plus a trailing newline -/// (frozen CLI contract, pinned by `tests/compat`). +/// (CLI contract, pinned by the binaries' end-to-end tests). #[must_use] pub fn codefang_version_line() -> String { let mut s = banner("codefang"); @@ -111,7 +110,7 @@ pub fn codefang_version_line() -> String { } /// Returns the `uast version` line — the banner plus a trailing newline -/// (frozen CLI contract, pinned by `tests/compat`). +/// (CLI contract, pinned by the binaries' end-to-end tests). #[must_use] pub fn uast_version_line() -> String { let mut s = banner("uast"); @@ -176,10 +175,10 @@ mod tests { #[test] fn defaults_are_frozen() { - // Frozen defaults: Version="dev", Commit="none", Date="unknown", + // Defaults: the crate version, Commit="unknown", Date="unknown", // BinaryGitHash="<unknown>", Binary=0. - assert_eq!(DEFAULT_VERSION, "dev"); - assert_eq!(DEFAULT_COMMIT, "none"); + assert_eq!(DEFAULT_VERSION, env!("CARGO_PKG_VERSION")); + assert_eq!(DEFAULT_COMMIT, "unknown"); assert_eq!(DEFAULT_DATE, "unknown"); assert_eq!(DEFAULT_BINARY_GIT_HASH, "<unknown>"); assert_eq!(DEFAULT_BINARY, 0); @@ -204,16 +203,22 @@ mod tests { #[test] fn banner_with_no_injection_uses_defaults() { - // In the test build no CF_*_INJECTED env is set, so the consts default. - assert_eq!(VERSION, "dev"); - assert_eq!(COMMIT, "none"); - assert_eq!(DATE, "unknown"); + // With no injected version the crate version is reported; the commit + // is the checkout's HEAD (a hex id) or `unknown` outside a checkout. + assert_eq!(VERSION, env!("CARGO_PKG_VERSION")); + assert!( + COMMIT == DEFAULT_COMMIT || COMMIT.bytes().all(|b| b.is_ascii_hexdigit()), + "{COMMIT}" + ); assert_eq!(BINARY_GIT_HASH, "<unknown>"); assert_eq!( banner("codefang"), - "codefang dev (commit: none, built: unknown)" + format!("codefang {VERSION} (commit: {COMMIT}, built: {DATE})") + ); + assert_eq!( + banner("uast"), + format!("uast {VERSION} (commit: {COMMIT}, built: {DATE})") ); - assert_eq!(banner("uast"), "uast dev (commit: none, built: unknown)"); } #[test] diff --git a/cspell.json b/cspell.json index 3cffd610..de547bb4 100644 --- a/cspell.json +++ b/cspell.json @@ -2,7 +2,9 @@ "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "version": "0.2", "language": "en", - "files": ["**/*.md"], + "files": [ + "**/*.md" + ], "ignorePaths": [ "node_modules/**", "build/**", @@ -11,129 +13,137 @@ ".github/styles/**", "CHANGELOG.md" ], - "dictionaries": ["en_US", "softwareTerms", "go", "filetypes"], + "dictionaries": [ + "en_US", + "softwareTerms", + "go", + "filetypes" + ], "words": [ - "codefang", - "Sumatoshi", - "hercules", "abandonware", - "burndown", - "couples", - "shotness", - "Halstead", - "LCOM", - "cohesion", "anomaly", - "uast", - "UAST", - "libgit", - "libgit2", - "treesitter", - "enry", - "linguist", - "goreleaser", - "mkdocs", - "godoc", - "pkgsite", - "gosec", - "golangci", - "deadcode", - "filestats", - "hibernate", - "blob", - "blobs", - "monorepo", - "OTEL", - "otel", - "opentelemetry", - "MCP", - "mcp", - "stdio", - "stdout", - "stdin", - "JSONC", - "jsonc", - "lychee", - "lycheeignore", - "markdownlint", - "cspell", - "Vale", - "Diataxis", - "Diátaxis", - "MADR", - "Nygard", - "OpenSSF", - "CHAOSS", - "DCO", - "semver", - "changelog", - "vettool", - "cgo", - "CGO", - "pprof", - "goroutine", - "goroutines", - "tick", - "ticks", - "devs", - "LRU", - "hibernatable", - "timeseries", "Babelfish", "bblfsh", + "blob", + "blobs", + "burndown", "busfactor", "Cabe", "CFLAGS", + "cgo", + "CGO", + "changelog", + "CHAOSS", "Checkpointable", "checkpointing", + "codefang", + "cohesion", "contextcheck", + "couples", "cpuprofile", + "cspell", + "DCO", + "deadcode", + "devs", + "Diataxis", + "Diátaxis", + "Dmytro", + "dmytrogajewski", "embedders", + "enry", "Errorf", + "filestats", "frds", + "Gajewski", "gitlib", "Gitolite", "GOBIN", + "godoc", "gogc", + "golangci", "GOMAXPROCS", "GOMEMLIMIT", "GOPATH", + "goroutine", + "goroutines", + "gosec", "Greenplum", + "Halstead", "heapprofile", + "hercules", + "hibernatable", + "hibernate", "hotspot", "hotspots", "Inuse", "iosafety", + "javax", + "JSONC", + "jsonc", + "kotlinx", "langpath", - "ldflags", + "LCOM", "LDFLAGS", + "ldflags", + "libgit", + "libgit2", "libgomp", + "licence", + "linguist", + "LRU", + "lychee", + "lycheeignore", "maat", + "MADR", "malloc", "mapx", + "markdownlint", + "MCP", + "mcp", "metaprogramming", + "mkdocs", + "monorepo", + "numpy", + "Nygard", + "OpenSSF", + "opentelemetry", + "OTEL", + "otel", "parentbased", "pathspec", "phpcs", + "pkgsite", + "pprof", "rubocop", "safeconv", "sandboxed", "scalafix", + "semver", + "shotness", "Skiena", "sloglint", "spancheck", "stddev", + "stdin", + "stdio", + "stdlib", + "stdout", + "Sumatoshi", "testdata", "textutil", + "tick", + "ticks", + "timeseries", "traceidratio", "traceparent", "tracestate", "treap", + "treesitter", + "UAST", + "uast", + "Vale", "venv", - "licence", - "Dmytro", - "dmytrogajewski", - "Gajewski", + "vettool", "Yourdon" ], "ignoreRegExpList": [ diff --git a/docs/adr/0002-two-binary-split-uast-codefang.md b/docs/adr/0002-two-binary-split-uast-codefang.md index ed7b1fe1..2934c680 100644 --- a/docs/adr/0002-two-binary-split-uast-codefang.md +++ b/docs/adr/0002-two-binary-split-uast-codefang.md @@ -27,7 +27,7 @@ The original `hercules` was a monolith. Codefang has two distinct jobs: turning Chosen option: "Two binaries — `uast` and `codefang`", because parsing and analysis are separable responsibilities with different consumers, and a pipe-friendly split lets each tool stay small while still composing into a pipeline. -`uast` (`bins/uast/`) turns source code into UAST and exposes `parse`, `query`, `diff`, `explore`, `analyze`, and `server`. `codefang` (`bins/codefang/`) consumes UASTs for static analysis or Git repositories for history analysis via `run`, `render`, and related commands. Both CLIs are thin entrypoints over the shared workspace crates (`cf-uast*` for parsing, `cf-commands` and the analyzer crates for analysis), so the split is at the command surface, not a duplication of core logic. The canonical composition is `codefang run -a static/* --format json /path/to/repo`; `uast parse` can also produce a UAST for inspection or for piping into other tools. +`uast` (`bins/uast/`) turns source code into UAST and exposes `parse`, `query`, `diff`, `explore`, `analyze`, `validate`, and `mapping`. `codefang` (`bins/codefang/`) consumes UASTs for static analysis or Git repositories for history analysis via `run` (plus `version` and `completion`). Both CLIs are thin entrypoints over the shared workspace crates (`cf-uast*` for parsing, `cf-commands` and the analyzer crates for analysis), so the split is at the command surface, not a duplication of core logic. The canonical composition is `codefang run -a static/* --format json /path/to/repo`; `uast parse` can also produce a UAST for inspection or for piping into other tools. ### Consequences diff --git a/llms-full.txt b/llms-full.txt index 7b98fb8d..d80b7a9e 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -14,167 +14,159 @@ </p> [![CI](https://github.com/Sumatoshi-tech/codefang/actions/workflows/ci.yml/badge.svg)](https://github.com/Sumatoshi-tech/codefang/actions/workflows/ci.yml) -[![Go Reference](https://pkg.go.dev/badge/github.com/Sumatoshi-tech/codefang.svg)](https://pkg.go.dev/github.com/Sumatoshi-tech/codefang) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -**The heavy lifter for your codebase.** +**The heavy lifter for your codebase — deep code analysis through structure and history.** -Codefang is a comprehensive code analysis platform that understands your code deeply—not just as text, but as structure (AST) and history (Git). Whether you're tracking technical debt, analyzing developer churn, or feeding context to an AI, Codefang does the heavy lifting so you don't have to. +Codefang understands your code as structure (an abstract syntax tree) and as +history (Git), not just as text. It ships two binaries: `uast` parses source +into a Universal Abstract Syntax Tree across 60+ languages, and `codefang` runs +static and history analyzers over that structure and over Git repositories. ---- - -## 1. Preface - -For detailed technical documentation, architectural decisions, and algorithm deep-dives, please visit our **[Documentation](https://sumatoshi-tech.github.io/codefang/)**. - -So, you've stumbled upon Codefang. You might be thinking, *"Wait, didn't `src-d` build something like this years ago?"* - -Yes, they did. And it was glorious. But like many great empires, `src-d` faded into the annals of history, and the original `hercules` was left gathering dust in the digital attic. - -**This is not that project.** - -This is a **reincarnation**. We took the core philosophy, stripped out the obsolete parts, and rebuilt it with a modern engine. It's not a drop-in replacement; it's a spiritual successor with a gym membership and a PhD in Abstract Syntax Trees. +For deep dives — architecture, per-analyzer references, the metric models — see +the [documentation site](https://sumatoshi-tech.github.io/codefang/) (built from +`site/` with MkDocs Material). --- -## 2. Historical context +## What it does -Once upon a time, there was a company called **source{d}**. They pioneered "Mining Software Repositories" on a massive scale. Their crown jewel was `hercules`, a tool that could chew through git logs faster than you can say `git blame`. +- **UAST parsing** — `uast` turns source code into one normalized tree shape, + so a single analyzer works across Go, Python, JavaScript, Rust, Java, C++ and + more (Tree-sitter under the hood). +- **Static analysis** — complexity, cohesion, comments, clones, composition, + Halstead, and imports, computed from the UAST. +- **History analysis** — burndown, developers, couples, file-history, imports, + quality, sentiment, shotness, typos, and anomaly, computed by walking Git + history through libgit2. +- **Stable machine output** — `--format json` (and `yaml`, `ndjson`, + `timeseries`, `bin`, `text`, `compact`, `plot`). The machine formats are a + stable contract, pinned by CLI-level end-to-end tests (see Testing below). -It gave us insights like: -- **Burndown Charts**: How much code from 2015 is still surviving today? -- **Coupling**: If I change `A.go`, does `B.go` always change with it? +## Why -When `src-d` ceased operations, the tool became abandonware. But the need to understand code didn't go away. - -We revived the project with a new mission: **Combine history with structure.** -The old `hercules` was great at Git history. Codefang adds **UAST (Universal Abstract Syntax Tree)** powers, allowing it to understand the *meaning* of the code across 60+ languages, not just the diffs. +The original `hercules` mined Git history at scale but understood code only as +diffs. Codefang keeps the history mining and adds a structural layer (the UAST), +so one analyzer reasons about meaning across many languages. The project is now +a Rust workspace; the original Go implementation has been removed. --- -## 3. Quickstart - -Enough history class. Let's see some muscles. +## Install / build -### Installation +Codefang is a single Rust workspace rooted at the repository root. You need a +recent stable Rust toolchain and a C toolchain (a C/C++ compiler and CMake) — +the `git2` crate builds a vendored copy of **libgit2** from +`third_party/libgit2`, so no system libgit2 is required. -You'll need Go installed. Then, just grab the binaries: +The quickest path — clone, then one `make install` that builds both binaries +and puts them on your `PATH` (`~/.cargo/bin`): -```sh -# The brain: Code analysis and history mining -go install github.com/Sumatoshi-tech/codefang/cmd/codefang@latest - -# The eyes: Universal parser (supports 60+ languages) -go install github.com/Sumatoshi-tech/codefang/cmd/uast@latest +```bash +git clone --recurse-submodules https://github.com/Sumatoshi-tech/codefang.git +cd codefang +make install ``` -### Let's flex - -Codefang follows the UNIX philosophy: small tools, joined by pipes. - -**1. Analyze complexity (Static Analysis):** -Parse your code and pipe it into the analyzer. +`make install` also initializes the submodule, so if you cloned without +`--recurse-submodules` it still works. Run `make help` to see the other targets +(`build`, `test`, `clean`). -```sh -# How messy is my main.go? -uast parse main.go | codefang analyze -a complexity +Prefer plain cargo, or just want the binaries in `target/release/` without +touching `PATH`? Use `make build` (equivalently +`cargo build --release -p codefang -p uast`) and invoke them by full path. -# ...or my entire codebase? -uast parse **/*.go | codefang analyze -a complexity -``` - -**2. Analyze history (Git Forensics):** -See who knows what, and how code is aging. +Check the install: -```sh -# Generate a burndown chart (lines surviving over time) -codefang history -a burndown . +```console +codefang version ``` -**3. Find the experts:** -Who actually wrote the code that's running in production? - -```sh -codefang history -a devs --head . +```console +uast version ``` --- -## 4. Architecture & design decisions - -We made a few bold choices in the rewrite. +## Usage -### The split: `uast` vs `codefang` +`uast` and `codefang` are separate tools (see [ADR-0002](docs/adr/0002-two-binary-split-uast-codefang.md)). -The original `hercules` was a monolith. We split it in two: +Parse a source file into a UAST: -1. **`uast` (The Parser)**: This tool focuses on one thing—turning source code into a standardized tree structure. It uses **Tree-sitter** under the hood to support practically every language you care about (Go, Python, JS, Rust, C++, Java, and even COBOL... probably). -2. **`codefang` (The Analyzer)**: This tool consumes the data. It takes UASTs for static analysis or Git repositories for history analysis. - -### Why UAST? - -Most linters are language-specific. `eslint` for JS, `golangci-lint` for Go. Codefang uses a **Universal** AST. This means we can write a single "Complexity Analyzer" and it immediately works for Python, Go, and TypeScript. +```console +uast parse main.rs +``` -### Pluggable analysis +Run a static analyzer over a checkout (UAST is built internally per file): -Analyzers are modular. You want to measure "Sentiment of Code Comments"? There's an analyzer for that (`sentiment`). You want to find "Typos in Variable Names"? There's one for that (`typos`). +```console +codefang run --analyzers static/complexity --format json --head /path/to/repo +``` ---- +Run a history analyzer by walking Git history (use `--limit` to cap commits): -## 5. Codefang as an AI agent tool +```console +codefang run --analyzers history/burndown --format json --limit 50 /path/to/repo +``` -Don't just use AI to write code. Use Codefang to verify it. By giving `codefang` and `uast` to your AI agent as tools (via MCP or shell), you create a self-correcting quality loop. +List every analyzer ID: -**1. The Self-Correcting Coder** -* **Scenario:** Agent generates a new function or refactors a file. -* **Action:** Agent runs `codefang analyze -a complexity` on the new code. -* **Outcome:** If complexity scores are high (e.g., > 15), the agent self-reflects: *"This is too complex. I need to simplify logic before showing it to the user."* -* **Result:** Clean, maintainable code *before* it even hits the PR. +```console +codefang run --list-analyzers +``` -**2. Architectural Context Injection** -* **Scenario:** You ask the agent about a high-level architectural change. -* **Problem:** "Context Window Exceeded" or the agent hallucinates file relationships. -* **Solution:** Agent runs `uast parse` to get the AST structure or `codefang analyze -a imports` to map the dependency graph. It learns the system architecture without reading every single line of text. +Analyzer IDs are `static/<name>` and `history/<name>`; `-a` accepts globs +(`static/*`, `history/*`, `*`). -**3. Risk-Aware Refactoring** -* **Scenario:** Agent is asked to refactor a legacy module. -* **Action:** Agent runs `codefang history -a couples`. -* **Insight:** *"Warning: Changing `User.go` usually breaks `Billing.go` 80% of the time."* -* **Result:** Agent proactively checks related files for regressions, preventing bugs that a simple text-based analysis would miss. +--- -**4. Style & Consistency Enforcement** -* **Scenario:** "Make this look like the rest of the project." -* **Action:** Agent analyzes `cohesion` and `comments` metrics of existing high-quality files to set a baseline. -* **Result:** Agent generates code that matches the *structural* quality standards of your specific repo, not just generic language syntax. +## Testing ---- +Three gates, all runnable locally and all required to be green: -## 6. Maintainers +```bash +make test # the workspace test suite (unit, integration, CLI-level e2e) +make lint # clippy across the workspace; every warning is an error +make deadcode # fails on any unreachable private item +``` -The dumbbells don't rack themselves. This project is kept in fighting shape by: +The analyzer crates carry unit tests for their metric math; `bins/codefang/tests` +drives the real binary over hermetic fixture repositories and source trees and +pins the machine-format bytes end to end. -- **Dmytro Gajewski** +The CLI examples in this README and in `docs/` are themselves executed against +the built Rust binary by the `doc-examples` test crate +(`cargo test -p doc-examples`), so the commands shown here are runnable, not +illustrative. --- -## 7. Contributing +## Documentation -Spotted a bug? Want to teach Codefang a new trick? PRs are very welcome. - -Read the [Contributing guide](CONTRIBUTING.md) before you start—it covers the build, the test gauntlet, commit conventions, and how to open a PR that gets merged instead of bounced. +- [Architecture overview](site/architecture/overview.md) and the + [UAST system](site/architecture/uast.md). +- Per-analyzer references under [`site/analyzers/`](site/analyzers/) and the + metric models under [`site/explanation/`](site/explanation/). +- Architecture Decision Records under [`docs/adr/`](docs/adr/). +- Per-crate Rust API docs: `cargo doc --open`. --- -## 8. License +## Contributing -Codefang is released under the [Apache-2.0](LICENSE) license. Use it, fork it, ship it—just keep the notice intact. +PRs are welcome. Read the [Contributing guide](CONTRIBUTING.md) for the build, +the test gates, and commit conventions. Architecturally significant changes are +recorded as ADRs (see [ADR-0001](docs/adr/0001-record-architecture-decisions.md)). ---- +## Security -### Ready to lift? +Report vulnerabilities privately through GitHub's "Report a vulnerability" +button on the Security tab. See the [Security policy](SECURITY.md). -Check out the [full documentation](https://sumatoshi-tech.github.io/codefang/) for deep dives, or just start piping commands and see what breaks. +## License -*Happy Mining!* +Codefang is released under the [Apache-2.0](LICENSE) license. --- @@ -253,10 +245,10 @@ codefang run -a static/complexity --format text . =============================== static/complexity: - internal/framework/runner.go - Function RunPipeline -- Cyclomatic: 12 Cognitive: 8 - Function initWorkers -- Cyclomatic: 4 Cognitive: 3 - cmd/codefang/commands/run.go + crates/cf-framework/src/runner.rs + Function run_pipeline -- Cyclomatic: 12 Cognitive: 8 + Function init_workers -- Cyclomatic: 4 Cognitive: 3 + crates/cf-commands/src/run.rs Function run -- Cyclomatic: 9 Cognitive: 7 ``` @@ -303,7 +295,7 @@ column shows how many of those lines survive into subsequent windows. ## 4. Developer analysis -See per-developer contribution statistics with a single commit snapshot: +See per-developer contribution statistics for the latest commit: ```bash codefang run -a history/devs --head --format yaml . @@ -312,24 +304,37 @@ codefang run -a history/devs --head --format yaml . ??? example "Expected output (truncated)" ```yaml - devs: - developers: - - name: "Alice" - commits: 142 - added: 12400 - removed: 3200 - changed: 8900 - - name: "Bob" - commits: 87 - added: 6300 - removed: 1800 - changed: 4100 + codefang (v2): + version: 0 + hash: <unknown> + history/devs: + aggregate: + total_commits: 1 + total_lines_added: 2 + total_lines_removed: 0 + total_developers: 1 + active_developers: 1 + developers: + - id: 0 + name: alice + email: alice@example.com + commits: 1 + lines_added: 2 + lines_removed: 0 + lines_changed: 0 + net_lines: 2 + languages: + - language: Go + added: 2 + removed: 0 + changed: 0 ``` !!! info "The `--head` flag" - `--head` tells the history pipeline to analyze only the HEAD commit's - state rather than walking the full history. It is much faster and useful - for snapshot metrics like developer totals. + `--head` analyzes exactly one commit — `HEAD`, diffed against its first + parent — and ignores `--limit` and `--since`. It is the same window + `--limit 1` selects, so the two produce identical reports. Use it for a + fast snapshot of the latest change; drop it to walk the full history. --- @@ -371,32 +376,26 @@ invocation: Generate a self-contained HTML report with interactive charts: ```bash -codefang run -a history/burndown,history/devs --format plot . +codefang run -a history/burndown,history/devs --format plot -o report . ``` -This writes an HTML page to **stdout**. Redirect it to a file and open in your -browser: +`-o` names the output **directory** (required with `--format plot`). It +receives `index.html`, one page per analyzer (`burndown.html`, `devs.html`) +and `report.json`. Open the index in your browser: ```bash -codefang run -a history/burndown --format plot . > burndown.html -open burndown.html # macOS -xdg-open burndown.html # Linux +xdg-open report/index.html # Linux +open report/index.html # macOS ``` --- -## 7. Pipe into AI (MCP integration) - -Codefang ships a built-in **Model Context Protocol** server so AI agents can -query analysis results programmatically: - -```bash -codefang mcp serve -``` +## 7. Pipe into AI -This starts a JSON-RPC server that tools like Claude Desktop, Cursor, and -other MCP-compatible clients can connect to. See the -[MCP Integration](../integrations/mcp.md) guide for configuration details. +Any `--format json` output is machine-friendly and can be fed to an LLM or an +agent. Codefang also provides a **Model Context Protocol** server (the `cf-mcp` +crate) so agents can query analysis results programmatically; see the +[MCP Integration](../integrations/mcp.md) guide for how to wire it up. !!! note "AI-ready output" Any `--format json` output can also be fed directly to an LLM for @@ -472,15 +471,17 @@ generated noise. codefang run -a '*' --extra-excluded-prefixes '.venv/,target/,build/' --format json . ``` -5. Constrain memory on a large monorepo so the streaming pipeline auto-tunes - worker count and spill thresholds rather than consuming all available RAM: +5. Bound the history window on a large monorepo — there is no memory or + worker knob; a history run costs what its commit window costs. Limit the + range with `--since` or `--limit`, skip merge side branches with + `--first-parent`, and drop mass-change commits (vendoring dumps, + generated-code regenerations) with `--max-changes-per-commit`: ```bash - codefang run -a 'history/*' --memory-budget 4GB --workers 4 --format json --silent . + codefang run -a 'history/*' --limit 5000 --first-parent --format json --silent . ``` -6. If you need only a subset of the history, limit the commit range with - `--since` or `--limit` to keep the run bounded: +6. If you need only a period of the history, select it with `--since`: ```bash codefang run -a 'history/devs' --since 2025-01-01 --format json . @@ -584,12 +585,14 @@ detects issues. echo "${{ steps.codefang.outputs.report }}" ``` -5. If a large repository runs out of memory in CI, constrain it with a memory - budget by running the binary directly inside the job, or in Docker: +5. If a large repository runs out of memory or time in CI, bound the history + window — `--limit`, `--since`, `--first-parent`, + `--max-changes-per-commit` — by running the binary directly inside the + job, or in Docker: ```bash docker run --rm -v "$(pwd):/workspace:ro" \ - codefang run -a 'history/*' --memory-budget 2GiB --format json --silent /workspace + codefang run -a 'history/*' --limit 2000 --format json --silent /workspace ``` ## Result @@ -665,9 +668,9 @@ you can see how much code from each era still survives at the latest sample. the visual form of the same matrix: ```bash - codefang run -a history/burndown --format plot . > burndown.html - xdg-open burndown.html # Linux - open burndown.html # macOS + codefang run -a history/burndown --format plot -o burndown . + xdg-open burndown/index.html # Linux + open burndown/index.html # macOS ``` 6. Extract just the tick axis for scripting or a custom plot: @@ -736,12 +739,12 @@ checking out a working tree. -type d > /tmp/repo_paths.txt ``` -4. Run a single bare repo with a memory budget so the streaming pipeline stays - bounded on large histories: +4. Run a single bare repo; bound the commit window with `--limit` on large + histories: ```bash codefang run /data/extract/repositories/@hashed/ab/cd/abcdef.git \ - -a 'history/*' --memory-budget 4GiB --workers 4 --format json --silent + -a 'history/*' --limit 5000 --format json --silent ``` 5. For periodic scans, analyze only new commits with `--since` rather than @@ -786,6 +789,12 @@ agent can run code analysis as a tool. launches MCP servers over stdio). - For the `codefang_history` tool, an absolute path to a local Git repository. +!!! warning "Not available in this release" + No `codefang` subcommand launches the MCP server yet: `codefang mcp` + fails with `unrecognized subcommand 'mcp'`. The server and its tools are + implemented in the `cf-mcp` library crate (see [MCP server](../integrations/mcp.md)); + these steps describe the integration once a launcher ships. + ## Steps 1. Confirm the MCP server starts. It communicates over stdio using JSON-RPC, the @@ -878,28 +887,45 @@ Static analyzers parse source files into a UAST and compute metrics in a single | Analyzer | ID | Description | |---|---|---| -| [Complexity](complexity.md) | `complexity` | Cyclomatic complexity, cognitive complexity, nesting depth | -| [Cohesion](cohesion.md) | `cohesion` | LCOM4 class cohesion, method-field usage graphs | -| [Halstead](halstead.md) | `halstead` | Program length, vocabulary, volume, difficulty, effort | -| [Comments](comments.md) | `comments` | Documentation coverage, comment placement quality | -| [Imports](imports.md) | `imports` | Import and dependency analysis | -| [Clones](clones.md) | `clones` | Cross-file duplicate function detection via MinHash + LSH | +| [Complexity](complexity.md) | `static/complexity` | Cyclomatic complexity, cognitive complexity, nesting depth | +| [Cohesion](cohesion.md) | `static/cohesion` | LCOM over function–variable usage, function cohesion | +| [Halstead](halstead.md) | `static/halstead` | Program length, vocabulary, volume, difficulty, effort | +| [Comments](comments.md) | `static/comments` | Documentation coverage, comment placement quality | +| [Imports](imports.md) | `static/imports` | Import and dependency analysis | +| [Clones](clones.md) | `static/clones` | Cross-file duplicate function detection via MinHash + LSH | +| [Composition](composition.md) | `static/composition` | File-type breakdown: source, docs, config, vendor, generated, dotfile, image, binary, other | ### Running static analyzers -Pipe a UAST through `codefang analyze`: +Point `codefang run` at a file or a directory tree; the UAST is built +internally per file: ```bash # Single file, single analyzer -uast parse main.go | codefang analyze -a complexity +codefang run -a static/complexity main.go -# Single file, all static analyzers -uast parse main.go | codefang analyze +# Entire directory tree, single analyzer +codefang run -a static/complexity ./src/ -# Entire directory tree -codefang analyze -a complexity ./src/ +# Entire directory tree, all static analyzers +codefang run -a 'static/*' ./src/ ``` +Every static analyzer reads the **same** walk of the tree, so +`-a 'static/*'` reports exactly the numbers the single runs report: the +walk visits directories in lexical order (skipping `.git`, cache-tagged +directories and symlinked directories), admits the files the UAST parser +supports and `--languages` keeps, applies the vendor / generated / +`--extra-excluded-prefixes` policy to the root-relative path, reads each +file once (files over 64 MiB and unreadable files are skipped with one +stderr warning each), and applies the generated-content marker +(`DO NOT EDIT`, `Code generated`, … in the first 512 bytes) unless +`--include-generated`. `static/composition` classifies every file instead, +vendored and generated included. `--per-file` adds per-file entries to +`--format json` only. See the +[CLI reference](../guide/cli-reference.md#the-static-walk) for the full +contract. + --- ## History analyzers @@ -941,7 +967,7 @@ You can select analyzers using glob patterns with the `-a` flag: ```bash # All static analyzers -codefang analyze -a 'static/*' ./src/ +codefang run -a 'static/*' ./src/ # All history analyzers codefang run -a 'history/*' . @@ -954,30 +980,30 @@ codefang run -a '*' . ## Output formats -All analyzers support multiple output formats via the `-f` flag: +All analyzers support multiple output formats via the `--format` flag: === "JSON" ```bash - codefang run -a history/devs -f json . + codefang run -a history/devs --format json . ``` === "YAML" ```bash - codefang run -a history/devs -f yaml . + codefang run -a history/devs --format yaml . ``` === "Plot" ```bash - codefang run -a history/burndown -f plot . + codefang run -a history/burndown --format plot . ``` === "Binary" ```bash - codefang run -a history/burndown -f binary . + codefang run -a history/burndown --format binary . ``` !!! tip "Combining analyzers" @@ -992,7 +1018,9 @@ All analyzers support multiple output formats via the `-f` flag: Codefang ships two binaries: **`codefang`**, the analyzer engine, and **`uast`**, the Universal Abstract Syntax Tree parser. This page is the exhaustive reference for -every command, sub-command, and flag. +every command, sub-command, and flag. Every flag listed by `codefang run --help` +is documented here and is read by the pipeline; a flag that is accepted but +has no effect is a bug. --- @@ -1001,6 +1029,17 @@ every command, sub-command, and flag. The main analysis binary. It runs static and history analyzers against source code and Git repositories. +#### Global flags + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--verbose` | `-v` | `bool` | `false` | Print run details on stderr: `progress: config file: <path>` (or `none`) and `progress: analyzers: <resolved ids>` | +| `--quiet` (alias `--silent`) | `-q` | `bool` | `false` | Suppress the `progress:` lines on stderr; warnings and errors still print. `-v` and `-q` together are a usage error. | +| `--profile` | | `bool` | `false` | Memory watchdog: logs `MEM t=<s> RSS=<MiB> threads=<n>` to stderr at start and every 2 s, writes `/proc/self/maps` to `$TMPDIR/maps_baseline.txt` at start and `maps_spike_<MiB>MiB.txt` on the first RSS spike above 4096 MiB (Linux) | + +Global flags are accepted before or after the subcommand +(`codefang -q run …` ≡ `codefang run -q …`). + ### `codefang run` The primary entry point. Runs selected static and/or history analyzers on a @@ -1016,7 +1055,8 @@ If `path` is omitted the current directory (or the value of `--path`) is used. | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--analyzers` | `-a` | `[]string` | `nil` | Analyzer IDs or glob patterns. Comma-separated. | +| `--analyzers` | `-a` | `[]string` | all | Analyzer IDs or glob patterns. Comma-separated. Omitted ⇒ every analyzer. | +| `--list-analyzers` | | `bool` | `false` | Print every analyzer ID (one per line) and exit | Analyzer IDs follow a `<category>/<name>` convention. Globs are supported: @@ -1040,8 +1080,8 @@ codefang run -a '*' . ??? info "Available Analyzer IDs" **Static analyzers:** - `static/complexity`, `static/comments`, `static/halstead`, - `static/cohesion`, `static/imports` + `static/clones`, `static/cohesion`, `static/comments`, `static/complexity`, + `static/composition`, `static/halstead`, `static/imports` **History analyzers:** `history/anomaly`, `history/burndown`, `history/couples`, @@ -1053,61 +1093,93 @@ codefang run -a '*' . | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--format` | | `string` | `json` | Output format: `json`, `text`, `compact`, `yaml`, `plot`, `bin`, `timeseries` | -| `--verbose` | `-v` | `bool` | `false` | Enable detailed output (RSS/memory stats in progress logs, full static report details) | -| `--silent` | | `bool` | `false` | Suppress progress output on stderr | -| `--no-color` | | `bool` | `false` | Disable colored static output | +| `--format` | | `string` | `json` | Output format: `json`, `yaml`, `bin`, `text`, `compact`, `ndjson`, `timeseries`, `plot` | +| `--ndjson` | | `bool` | `false` | With `--format timeseries`: emit one JSON line per commit instead of the merged document. With any other format the run fails with `Error: invalid --ndjson: requires --format timeseries, got --format <f>`. | +| `--output` | `-o` | `string` | `""` | Write the report to this file (created or truncated) instead of stdout. With `--format plot` it is the output directory for the HTML pages, and required. | +| `--per-file` | `-F` | `bool` | `false` | Add one `files` entry per analyzed file to every static section. **`--format json` only**: with any other format the flag is ignored and the run prints `--per-file applies to --format json only; ignored for <format>` on stderr (once). | +| `--no-color` | | `bool` | `false` | Disable ANSI color in `text` / `compact` output (same as `NO_COLOR=1`) | + +Which formats a selection accepts: + +| Selection | Accepted formats | +|-----------|------------------| +| static only | `json`, `yaml`, `bin`, `text`, `compact`, `plot` | +| history only | `json`, `yaml`, `bin`, `text`, `compact`, `ndjson`, `timeseries` (+ `--ndjson`), `plot` | +| mixed static + history | `json`, `yaml`, `bin`, `ndjson`, `timeseries`, `plot` | + +`text` is supported by the six history analyzers with a text renderer +(sentiment, shotness, burndown, couples, devs, file-history); selecting any +other history analyzer with `--format text` fails with +`serialization error for <Name>: unsupported format: text`. ```bash # Human-readable table codefang run -a 'static/*' --format text . +# One summary line per history analyzer +codefang run -a 'history/*' --format compact . + # Interactive HTML charts -codefang run -a 'history/*' --format plot . +codefang run -a 'history/*' --format plot --output ./report . + +# The report in a file instead of stdout +codefang run -a static/complexity --format json -o complexity.json . # Unified time-series JSON codefang run -a 'history/devs,history/sentiment' --format timeseries . ``` -#### Path & input flags +!!! note "Several analyzers, one document" + A `json` or `yaml` run that selects more than one analyzer — a mixed + static+history selection, or several history analyzers — always emits + **one** document: -| Flag | Short | Type | Default | Description | -|------|-------|------|---------|-------------| -| `--path` | `-p` | `string` | `.` | Folder or repository path to analyze | -| `--input` | | `string` | `""` | Input report path for cross-format conversion | -| `--input-format` | | `string` | `auto` | Input format: `auto`, `json`, `bin` | + ```json + { + "version": "codefang.run.v1", + "metadata": { "...": "run provenance" }, + "analyzers": [ + { "id": "history/devs", "mode": "history", "report": { "...": "the devs report" } }, + { "id": "history/burndown", "mode": "history", "report": { "...": "the burndown report" } } + ] + } + ``` -!!! tip "Format Conversion" + A single analyzer emits its own report as the document root. Several + **static** analyzers in `json`/`text` merge their sections into one report + with an `overall_score`. - Use `--input` to convert a previously generated report to a different - format without re-running analysis: +#### Path flags - ```bash - codefang run -a 'history/*' --format bin . > report.bin - codefang run -a 'history/*' --input report.bin --format plot - ``` +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--path` | `-p` | `string` | `.` | Folder or repository path to analyze (the positional `path` wins when both are given) | #### Git history flags | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--limit` | `int` | `0` | Analyze at most N commits — the N **newest** (`0` = no limit) | -| `--since` | `string` | `""` | Only analyze commits at/after this time or revision | -| `--first-parent` | `bool` | `false` | Follow only first parent of merge commits | -| `--head` | `bool` | `false` | Analyze only HEAD commit (ignores `--limit` and `--since`) | +| `--limit` | `int` | `0` | Analyze at most N commits — the N **newest** (`0` = no limit). Negative values are rejected. | +| `--since` | `string` | `""` | Only analyze commits **authored** at/after this time or revision | +| `--first-parent` | `bool` | `false` | Follow only the first parent of merge commits | +| `--head` | `bool` | `false` | Analyze only the HEAD commit (ignores `--limit` and `--since`) | +| `--tick-size` | `int` | `24` | Width of one tick in hours (must be `> 0`). Burndown bands, devs activity, anomaly windows and `commits_by_tick` are bucketed by tick. | +| `--max-changes-per-commit` | `int` | `0` | Drop commits whose raw tree diff exceeds this many changes (`0` = default 10000; negative values are rejected). Dropped commits are invisible to every analyzer; burndown re-inserts an affected file whole the next time it is touched. Raise this on monorepos with legitimate large commits. | These flags bound the **history** phase only — static analyzers read the -working tree, so neither one affects them. +working tree, so none of them affects a static run. The window is selected **newest-first** (`--limit 10` = the ten most recent commits, like `git log -n 10`) and then handed to the analyzers in chronological oldest-first order, because ticks, burndown line survival and author identity all depend on that order. `--limit` and `--since` compose: -`--since` picks the window, `--limit` caps its newest end. +`--since` picks the window, `--limit` caps its newest end. `--head` is the +single-commit window `[HEAD]` — the same window `--limit 1` selects, so the two +produce byte-identical reports for every analyzer and format. The `--since` flag accepts these forms, in precedence order: -- **Go duration**: `24h`, `168h` (7 days), `1h30m` — units are `h`/`m`/`s` +- **Duration**: `24h`, `168h` (one week), `1h30m` — units are `h`/`m`/`s` only. There is **no** `d` or `w` unit and `m` means **minutes**, so a week is `168h`, not `7d` - **Date**: `2025-01-01` (midnight UTC) @@ -1115,8 +1187,13 @@ The `--since` flag accepts these forms, in precedence order: - **Git revision**: `v1.2.0`, `HEAD~100`, or a bare SHA — that commit's author time becomes the cutoff +The cutoff compares the commit's **author** time, and every commit authored +at/after it is kept regardless of its position in the committer-time walk +(a commit authored after the cutoff but committed before it is still +included; one authored before the cutoff but committed after it is not). + ```bash -# Last 7 days only +# Everything authored in the last 168h (one week) codefang run -a history/devs --since 168h . # Since a specific date @@ -1127,6 +1204,9 @@ codefang run -a history/couples --since v1.2.0 . # Only the latest 500 commits codefang run -a history/couples --limit 500 . + +# Hourly ticks +codefang run -a history/devs --tick-size 1 --format timeseries . ``` !!! note "Empty windows and bad values" @@ -1136,31 +1216,72 @@ codefang run -a history/couples --limit 500 . neither a time form nor a resolvable revision fails immediately with `Error: cannot resolve --since "<value>"`, empty stdout, exit 1. -!!! note "Burndown and `--first-parent`" - - The burndown analyzer automatically enables `--first-parent` when selected. - This is required for correct line-tracking across merge commits. +!!! note "Burndown always walks first-parent" + The burndown analyzer tracks line survival along the main line of + development, so **its own** walk always follows the first parent of merge + commits, whatever `--first-parent` says. This never leaks into the other + analyzers: selecting burndown next to devs, couples or file-history leaves + their walks exactly as the user's `--first-parent` flag configures them. + +!!! note "Renames" + The tree diff detects renames (libgit2 `find_similar`, the `git diff -M` + default of 50 % similarity). A pure `git mv` is one change whose old and + new paths differ and whose blob is unchanged, so devs reports 0 added / + 0 removed lines for it and burndown, couples, file-history and shotness + keep the file's identity across the move. + +!!! note "Deterministic output" + A history report is a pure function of the repository and the flags. It + does not depend on the host's CPU count, the wall clock, or the number of + threads the walk happens to use. #### Language filtering | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--languages` | `[]string` | `[all]` | Restrict analysis to the given Linguist languages; comma-separated. `all` (default) disables the filter. Applies to **both** history and static phases. | - -**History phase** — the filter is pushed down into libgit2's `pathspec` -at the tree-diff stage, so non-matching files are skipped before the -diff crosses the cgo boundary. On a polyglot repo a narrow filter can -reduce wall time by 30–40 %. The Go-side language check still runs as -the authoritative pass for content-disambiguated extensions (`.h`, -`.pl`, `.m`, `.r`). - -**Static phase** — the filter is applied at the directory walker -(`matchesLanguageGlobs`) before the UAST parser or raw-file analyzers -see the file. It's path-based only: the parser's own language router -remains the final authority for how a matched file is parsed (e.g. a -`.h` under `--languages c++` is still parsed as C). Both phases read -from the same `langpath.Globs` helper, so the flag value has one -meaning across `-a 'static/*'`, `-a 'history/*'`, and `-a '*'` runs. +| `--languages` | `[]string` | `[all]` | Restrict analysis to the given Linguist languages; comma-separated. `all` (default), an absent flag and an explicit empty value (`--languages ""`) all disable the filter. Applies to **both** history and static phases with one meaning. In the static phase each language resolves to its Linguist filename globs, matched against the file's **basename** (`*.go`, `*.py`, `Dockerfile`, …). | + +**History phase** — each changed file is classified with the full enry +cascade over its basename **and** its blob content, so ambiguous extensions +(`.h`, `.m`, `.pl`, `.r`) and extensionless scripts (shebang) resolve +correctly; changes whose language is not in the set are dropped before any +analyzer sees them. + +**Static phase** — the filter is applied by the ONE directory walk every +static analyzer shares, before the UAST parser or raw-file analyzers see the +file: the language globs are matched against the basename, so the parser's +own language router remains the final authority for how a matched file is +parsed. Both phases read the same `langpath` tables, so the flag value has +one meaning across `-a 'static/*'`, `-a 'history/*'`, and `-a '*'` runs. + +#### The static walk + +Every `static/*` analyzer reads the same walk of the path you pass +(a directory or a single file): + +- directories are visited in lexical order, depth-first; `.git` and any + directory carrying a `CACHEDIR.TAG` (Cargo's `target/`) are skipped; + symlinked directories are never followed (a symlink loop cannot recurse + and the walk cannot leave the root); symlinks to regular files are read; +- a file is admitted when the UAST parser supports its extension, its + basename matches `--languages`, and its **root-relative** path passes the + vendor / generated / `--extra-excluded-prefixes` policy; +- each admitted file is read **once**; a file larger than **64 MiB** or one + that cannot be read is skipped with one stderr line + (`warning: skipped <path>: larger than 64 MiB` / `: unreadable`) for the + whole run, however many analyzers are selected, and counted once in the + end-of-run `progress: skipped N unreadable/oversized/unparseable files` + line; +- the generated-content marker (`DO NOT EDIT`, `Code generated`, + `@generated`, … in the first 512 bytes) is applied to the bytes just read + unless `--include-generated`; +- a single-file path analyzes that one file and stamps its **name** + (`a.go`, never `.`) on every per-file entry and issue location. + +`static/composition` is the designed exception: it exists to report +vendored / generated / binary shares, so it classifies **every** regular +file the walk reaches (reading only the first 8 KiB of each); only +`--languages` and `--extra-excluded-prefixes` narrow it. Language names are [Linguist keys](https://github.com/github/linguist/blob/master/lib/linguist/languages.yml) and common aliases resolve automatically: @@ -1169,13 +1290,14 @@ and common aliases resolve automatically: # Canonical names (any case, whitespace is trimmed) codefang run -a 'history/devs' --languages go,python,typescript . -# Aliases resolve via enry +# Aliases resolve via the Linguist tables codefang run -a 'history/devs' --languages golang,js,ts . -# Unknown language fails fast at configure time instead of silently -# returning an empty report: +# Unknown language fails fast instead of silently returning an empty report: codefang run -a 'history/devs' --languages notalang . -# → Error: failed to configure TreeDiff: tree-diff pathspec: unknown language: "notalang" +# → Error: history --languages: unknown language: "notalang" +codefang run -a 'static/complexity' --languages notalang . +# → Error: static --languages: unknown language: "notalang" ``` Filename-only languages (e.g. `Dockerfile`, `Makefile`) are also supported: @@ -1184,9 +1306,6 @@ Filename-only languages (e.g. `Dockerfile`, `Makefile`) are also supported: codefang run -a 'history/devs' --languages dockerfile . ``` -See `specs/optimize-lang/PROPOSAL.md` for the architecture and acceptance-gate -numbers. - #### Vendor & generated exclusion By default, Codefang excludes **vendored dependencies** and **auto-generated @@ -1196,11 +1315,13 @@ single-language analyser (`go vet`, `eslint`, `ruff`, `rubocop`, `scalafix`, | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--include-vendored` | `bool` | `false` | Re-include vendored dependencies (detected by enry / Linguist) in analysis. Cross-language: covers `vendor/`, `node_modules/`, `third_party/`, `testdata/`, minified bundles, and more. | -| `--include-generated` | `bool` | `false` | Re-include auto-generated files in analysis. Covers `*.pb.go`, `zz_generated_*.go`, `*_pb2.py`, `*.min.js`, and any file whose first 512 bytes contain a generated-file marker (`DO NOT EDIT`, `Code generated`, etc.). | -| `--extra-excluded-prefixes` | `[]string` | `[]` | Additional UNIX path prefixes to exclude on top of enry heuristics (e.g. `".venv/,target/,build/"`). | +| `--include-vendored` | `bool` | `false` | Re-include vendored dependencies (detected by the Linguist vendor patterns over the **root-relative** path) in analysis. Cross-language: covers `vendor/`, `node_modules/`, `third_party/`, `testdata/`, minified bundles, and more. `static/composition` counts vendored files regardless. | +| `--include-generated` | `bool` | `false` | Re-include auto-generated files in analysis. Covers `*.pb.go`, `zz_generated_*.go`, `*_pb2.py`, `*.min.js` (path heuristics) **and** any file whose first 512 bytes contain a generated-file marker (`DO NOT EDIT`, `Code generated`, `@generated`, …) — the static walk reads every admitted file and applies the marker to it; the history filter applies it to the blob header. `static/composition` counts generated files regardless. | +| `--extra-excluded-prefixes` | `[]string` | `[]` | Additional UNIX path prefixes to exclude on top of the built-in heuristics (e.g. `".venv/,target/,build/"`), matched against the **root-relative** path (`gen/` excludes `<root>/gen/…` whatever the root spelling). Always excludes, even with the include flags set, and narrows `static/composition` too. | -All three flags apply **identically** to both static and history phases. +All three flags apply **identically** to both static and history phases: the +history change filter reads the blob header for the generated-file marker +exactly as the static walk reads the bytes of each admitted file. ```bash # default: your own code only @@ -1209,126 +1330,149 @@ codefang run -a '*' . # include vendored deps (node_modules/, vendor/, …) codefang run -a '*' --include-vendored . -# restore pre-codefang-2026-04 behaviour (include everything) +# include everything codefang run -a '*' --include-vendored --include-generated . -# skip extras that enry doesn't know about (e.g. Python venv, Rust target/) +# skip extras the heuristics do not know about (e.g. Python venv, Rust target/) codefang run -a '*' --extra-excluded-prefixes '.venv/,target/' . ``` -!!! warning "Breaking change in 2026-04" - - Earlier versions of Codefang analysed vendored and generated files by - default (they needed the confusingly-named `--skip-blacklist=true` to be - excluded). Starting from 2026-04, defaults flip: vendor / generated - are **excluded by default**. To restore the old behaviour: - - ```bash - codefang run ... --include-vendored --include-generated - ``` - - The deprecated `--skip-blacklist` and `--blacklisted-prefixes` flags - still work with a cobra deprecation warning and will be removed in - the next minor release. Map them to: - - - `--skip-blacklist` → no-op (the new default already excludes) - - `--blacklisted-prefixes X,Y` → `--extra-excluded-prefixes X,Y` - -See `specs/exclude-vendored/PROPOSAL.md` for the full cross-phase design -and `specs/frds/FRD-20260419-exclude-vendored.md` for implementation -details. +!!! warning "Removed spellings" + The hidden `--skip-blacklist` and `--blacklisted-prefixes` flags were + accepted and ignored; they are removed and fail as unknown arguments. + Drop `--skip-blacklist` (the default already excludes) and write + `--blacklisted-prefixes X,Y` as `--extra-excluded-prefixes X,Y`. -#### Pipeline tuning flags +#### Developer identity | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--workers` | `int` | `0` | Parallel workers (`0` = CPU count via `GOMAXPROCS`) | -| `--buffer-size` | `int` | `0` | Internal pipeline channel size (`0` = `workers * 2`) | -| `--commit-batch-size` | `int` | `0` | Commits per processing batch (`0` = default 100) | -| `--blob-cache-size` | `string` | `""` | Max blob cache size (e.g. `256MB`, `1GB`; empty = 1 GB) | -| `--diff-cache-size` | `int` | `0` | Max diff cache entries (`0` = default 10000) | -| `--blob-arena-size` | `string` | `""` | Memory arena for blob loading (e.g. `4MB`; empty = 4 MB) | -| `--memory-budget` | `string` | `""` | Memory budget for auto-tuning (e.g. `512MB`, `2GB`). Applies to both history and static phases — auto-derives worker count, spill thresholds, and Go memory limit | +| `--people-dict` | `path` | `""` | File with one person per line, `|`-separated identity tokens (names and emails, matched case-insensitively). With a dictionary, an author matching no line is reported as `<unmatched>`. An unreadable file is an error. | +| `--exact-signatures` | `bool` | `false` | Match the whole `name <email>` signature instead of merging authors that share a name **or** an email. Yields considerably more identities. | +| `--anonymize` | `bool` | `false` | Replace developer names in every report with `Developer-A`, `Developer-B`, … | -```bash -# Large repository with constrained memory -codefang run -a 'history/*' --workers 4 --memory-budget 2GB . - -# Static analysis with memory budget (auto-tunes workers and spill thresholds) -codefang run -a 'static/*' --memory-budget 512MB . +By default an author is identified by **either** the lowercased email or the +lowercased name, and a hit on one token registers the other: `alice <a@x>`, +`Alice Smith <a@x>` and `Alice Smith <a@y>` are one developer. One identity +detector serves every history analyzer, so devs, burndown, couples, +file-history and anomaly agree on author ids. -# High-throughput with large caches -codefang run -a 'history/*' --blob-cache-size 2GB --diff-cache-size 50000 . ``` - -#### GC tuning flags - -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--gogc` | `int` | `0` | GC target percentage (`0` = Go default 100) | -| `--ballast-size` | `string` | `"0"` | GC ballast allocation (`0` = disabled) | - -#### Checkpoint & resume flags - -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--checkpoint` | `bool` | `true` | Enable checkpointing for crash recovery | -| `--checkpoint-dir` | `string` | `""` | Checkpoint directory (default: `~/.codefang/checkpoints`) | -| `--resume` | `bool` | `true` | Resume from checkpoint if available | -| `--clear-checkpoint` | `bool` | `false` | Clear existing checkpoint before run | +# people.txt +Alice|alice@example.com|alice@other.org +Bob|bob@example.com +``` ```bash -# Disable checkpointing entirely -codefang run -a history/burndown --checkpoint=false . +codefang run -a history/devs --people-dict people.txt . +codefang run -a history/devs --anonymize --format text . +``` -# Resume a previously interrupted run -codefang run -a 'history/*' --resume . +#### Analyzer options -# Start fresh, clearing old checkpoint data -codefang run -a 'history/*' --clear-checkpoint . -``` +| Flag | Type | Default | Applies to | Description | +|------|------|---------|------------|-------------| +| `--typos-max-distance` | `int` | `4` | `history/typos` | Maximum Levenshtein distance between the two identifiers of a typo-fix pair (also bounds the whole-line prefilter); must be `> 0` | +| `--granularity` | `int` | `30` | `history/burndown` | Ticks per age band of the survival matrix (must be `> 0`; `Error: invalid --granularity: must be > 0, got <v>`) | +| `--sampling` | `int` | `30` | `history/burndown` | Ticks per sample row of the survival matrix (must be `> 0`; independent of `--granularity`) | +| `--burndown-files` | `bool` | `false` | `history/burndown` | Per-file survival (`file_survival`, `tracked_files`, ndjson `FileDeltas`); with `--burndown-people` also per-file ownership | +| `--burndown-people` | `bool` | `false` | `history/burndown` | Per-developer survival and the interaction matrix (`developer_survival`, `interactions`, `tracked_developers`, ndjson `PeopleDeltas` / `MatrixDeltas`, the text "Top Developers" section) | +| `--anomaly-window` | `int` | `20` | `history/anomaly` | Trailing window size in ticks for the rolling mean and standard deviation (must be `>= 2`; a shorter history uses every earlier tick) | +| `--anomaly-threshold` | `float` | `2.0` | `history/anomaly` | Z-score threshold, in standard deviations, above which a tick is flagged (must be `> 0`) | +| `--empty-commits` | `bool` | `false` | `history/devs` | Count commits whose filtered diff is empty (trivial merges, `--allow-empty` commits, commits touching only excluded paths) as commits with zero lines | +| `--shotness-dsl-struct` | `string` | `rfilter(.roles has "Function")` | `history/shotness` | UAST DSL query selecting the tracked declarations anywhere in each file (`filter(...)` would see the top level only); validated before the walk, an invalid expression is a flag error | +| `--shotness-dsl-name` | `string` | `.props.name` | `history/shotness` | UAST DSL query extracting a declaration's own name (applied to selected nodes and their enclosing declarations); validated before the walk | +| `--min-comment-len` | `int` | `20` | `history/sentiment` | Minimum comment length in characters to analyze; `0` disables the length filter, a negative value is rejected (`Error: invalid --min-comment-len: …`) | -#### Profiling & debug flags +#### Configuration | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--profile` | `bool` | `false` | Start pprof HTTP server on `localhost:6060` and memory watchdog (heap dumps on RSS spikes) | -| `--cpuprofile` | `string` | `""` | Write CPU profile to file | -| `--heapprofile` | `string` | `""` | Write heap profile to file | -| `--debug-trace` | `bool` | `false` | Enable 100% OpenTelemetry trace sampling | +| `--config` | `string` | `""` | Configuration file. Without it `./.codefang.yaml`, then `$HOME/.codefang.yaml` is read when present. An explicit file that cannot be read is an error. | -`--profile` is a **global flag** (available on all subcommands). It enables: - -- A pprof HTTP server on `localhost:6060` for live heap/goroutine/CPU inspection -- A memory watchdog that logs RSS every 2s and dumps heap profiles to `/tmp` when RSS exceeds 4 GiB +Every key of the file — and its `CODEFANG_*` environment variable — sets +the **default** of one flag; the precedence is explicit flag > environment +> config file > compiled default. `--config /dev/null` runs with the +compiled defaults only. Keys nothing reads are ignored with one +`warning: ignoring unsupported config keys: …` line. See +[Configuration](configuration.md) for the key → flag table. ```bash -# CPU profile a large run -codefang run -a 'history/*' --cpuprofile cpu.prof . +codefang run -a history/anomaly --config ./ci.codefang.yaml . +``` -# Live pprof + memory watchdog -codefang run -a 'static/*' --profile --path ~/sources/kubernetes +No checkpoint store, incremental cache or trace exporter is linked into the +binary, so the Go-era `--checkpoint`, `--checkpoint-dir`, `--resume`, +`--clear-checkpoint`, `--cache-dir`, `--no-cache` and `--debug-trace` flags +are removed; passing one fails as an unknown argument. A run always analyzes +the whole requested window — bound it with `--limit` / `--since`. + +#### Errors + +Every user error prints `Error: <message>` to stderr, leaves stdout empty and +exits 1: + +| Situation | Message | +|-----------|---------| +| the path does not exist | `path not found: <path>` | +| a history analyzer on a directory that is not a git repository | `cannot open repository: <path>` | +| an unknown `--format` | `unsupported format: <value>` | +| a known format the selection cannot render (e.g. static-only `ndjson`, mixed `compact`) | `format <value> is not supported for <analyzer>` | +| an unknown analyzer id | `unknown analyzer id: <id>` | +| `--limit`, `--tick-size`, `--max-changes-per-commit`, `--granularity`, `--sampling`, `--typos-max-distance`, `--anomaly-window`, `--anomaly-threshold`, `--min-comment-len` out of range | `invalid --<flag>: must be … , got <value>` | +| `--ndjson` without `--format timeseries` | `invalid --ndjson: requires --format timeseries, got --format <f>` | +| `--verbose` with `--quiet` | `--verbose and --quiet cannot be used together` | +| an unreadable `--config` file | `cannot read --config <path>: …` | +| a malformed config file or an out-of-range key | `invalid config <path>: <key> must be …, got <value>` (or the parser's message naming the key and line) | +| a malformed `CODEFANG_*` variable | `invalid environment variable <NAME>: …` | +| `-o` cannot be created | `cannot write --output <path>: …` | +| an unresolvable `--since` | `cannot resolve --since "<value>": …` | +| an unknown `--languages` token | `static --languages: unknown language: "<token>"` / `history --languages: …` | +| an unreadable `--people-dict` | `cannot read --people-dict <path>: …` | +| an analyzer produced nothing for an otherwise valid run | `no report produced for <analyzer> --format <format>` | + +A history `--format text` selection that includes an analyzer without a text +renderer writes the header and the section name first, then fails with +`serialization error for <Name>: unsupported format: text`. + +### `codefang version` + +Prints `codefang <version> (commit: <hash>, built: <date>)`: + +- `<version>` — the crate version (or `CF_VERSION` / `GIT_VERSION` set at + build time); +- `<hash>` — the commit the binary was built from: `CF_COMMIT` / + `GIT_COMMIT` / `GIT_HASH` set at build time, else the `HEAD` of the + checkout it was built in, else `unknown`; +- `<date>` — `CF_DATE` / `SOURCE_DATE_EPOCH` set at build time, else + `unknown` (the build never reads the wall clock). -# Full debug tracing -codefang run -a 'history/*' --debug-trace . +```bash +codefang version ``` ---- +The former `codefang render <store-dir>` subcommand only ever printed +"not available" — there is no stored-report format to render from — and is +removed; `codefang run --format plot -o <dir>` writes the HTML pages +directly. -### `codefang mcp` +### `codefang completion <shell>` -Start a Model Context Protocol (MCP) server on stdio transport. This exposes -Codefang analysis capabilities as tools that AI agents can discover and invoke. +Generates the shell-completion script for `bash`, `fish`, `powershell` or +`zsh` on stdout, from the live command tree (every flag on this page). +`--no-descriptions` leaves the help texts out of the script. ```bash -codefang mcp [flags] +codefang completion bash > ~/.local/share/bash-completion/completions/codefang ``` -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--debug` | `bool` | `false` | Enable debug logging to stderr | +--- + +### MCP server -The MCP server exposes the following tools: +The Model Context Protocol server lives in the `cf-mcp` crate. It exposes +Codefang analysis capabilities as tools that AI agents can discover and invoke +over stdio: | Tool | Description | |------|-------------| @@ -1336,20 +1480,18 @@ The MCP server exposes the following tools: | `codefang_history` | Git history analysis (burndown, couples, devs, sentiment, etc.) | | `uast_parse` | Parse source code into Universal AST | -```bash -# Start MCP server for agent integration -codefang mcp - -# With debug logging -codefang mcp --debug -``` +The crate is a library: the `codefang` binary has no `mcp` subcommand in +this release. See the [MCP Integration](../integrations/mcp.md) guide for the +tool reference. --- ## `uast` The UAST binary parses source files into a Universal Abstract Syntax Tree and -provides querying, diffing, and exploration utilities. +provides querying, diffing, and exploration utilities. The root command has +no flags of its own; every flag below belongs to its subcommand and is read by +it (`uast <command> --help` lists the same set). ### `uast parse` @@ -1363,16 +1505,19 @@ uast parse [files...] [flags] |------|-------|------|---------|-------------| | `--language` | `-l` | `string` | `""` | Force language detection override | | `--output` | `-o` | `string` | `""` | Output file (default: stdout) | -| `--format` | `-f` | `string` | `json` | Output format: `json`, `compact`, `tree` | -| `--progress` | `-p` | `bool` | `false` | Show progress for multiple files | +| `--format` | `-f` | `string` | `json` | Output format: `json` (pretty), `compact` (one line per input), `none` (parse only) | | `--all` | | `bool` | `false` | Parse all source files in the codebase recursively | +With no files (and no `--all`), or for a `-` file argument, the source is read +from stdin and parsed as Go unless `-l` names the language. With `-o`, every +input's document is written to the one file, in input order. + ```bash # Parse a single file uast parse main.go -# Parse all Go files with progress -uast parse -p *.go +# Parse all Go files, one compact document per line +uast parse -f compact *.go # Force language detection uast parse -l go main.c @@ -1399,15 +1544,24 @@ uast query <expression> [files...] [flags] | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--input` | `-i` | `string` | `""` | Input file (UAST JSON or source code) | -| `--output` | `-o` | `string` | `""` | Output file (default: stdout) | +| `--input` | `-i` | `string` | `""` | Input file (UAST JSON or source code), queried before any positional files | +| `--output` | `-o` | `string` | `""` | Output file (default: stdout); every input's result is written to it, in input order | | `--format` | `-f` | `string` | `json` | Output format: `json`, `compact`, `count` | -| `--interactive` | `-t` | `bool` | `false` | Interactive query mode | +| `--interactive` | `-t` | `bool` | `false` | Interactive query mode over one file (`--input`, else the single positional file, else source read from stdin) | + +With neither files nor `--input`, or for a `-` file argument, the query runs +over a UAST JSON tree read from stdin. ```bash -# Find all functions +# Find the top-level functions (filter looks at the file's direct children) uast query "filter(.type == 'Function')" main.go +# Find every function-role node anywhere in the tree, methods included +uast query 'rfilter(.roles has "Function")' main.go + +# The name of every declaration in the tree +uast query "rmap(.props.name)" main.go + # Find exported items uast query "filter(.roles has 'Exported')" *.go @@ -1417,6 +1571,9 @@ uast query "reduce(count)" main.go # Query from piped UAST JSON uast parse main.go | uast query "filter(.type == 'Call')" - +# The same query over an --input file +uast query "filter(.type == 'Call')" -i main.go + # Count format uast query -f count "filter(.type == 'Function')" main.go @@ -1426,14 +1583,29 @@ uast query -t main.go ??? info "DSL Syntax Reference" + A query is a pipeline of stages joined by `|`. `filter` and `map` look + at their input set only — a `filter(...)` at the start of a query runs + over the root's **direct children** — while `rfilter` and `rmap` apply + the same predicate / expression to every node of each input's subtree in + pre-order (the input itself first). + | Expression | Description | |-----------|-------------| - | `filter(.type == "Function")` | Filter by node type | - | `filter(.type == "Call")` | Find function calls | - | `filter(.type == "Identifier")` | Find identifiers | - | `filter(.type == "Literal")` | Find literal values | - | `filter(.roles has "Exported")` | Filter by role annotation | - | `reduce(count)` | Count matching nodes | + | `filter(.type == "Function")` | Direct children of the root whose type is `Function` | + | `rfilter(.type == "Call")` | Every function call anywhere in the tree | + | `rfilter(.type == "Identifier")` | Every identifier | + | `rfilter(.roles has "Function")` | Every function-role node (functions and methods) | + | `filter(.roles has "Exported")` | Direct children carrying a role | + | `rfilter(.type == "Class") \| map(.props.name)` | The names of the classes | + | `rmap(.props.name)` | The `name` property of every node that has one | + | `map(.children)` | The children of each input node | + | `reduce(count)` | Count the matching nodes | + + Field access resolves `children`, `token`, `id`, `roles`, `type`, + `props`, `first`, `last`; any other name reads that property + (`.props.name` ≡ `.name`). Comparisons: `==`, `!=`, `<`, `<=`, `>`, + `>=`, `has` (membership in `roles`). Builtins: `contains`, + `startsWith`, `endsWith`, `has`, `count`, `first`, `last`, `sort`. --- @@ -1495,63 +1667,95 @@ Available interactive commands: --- -### `uast server` +### `uast analyze` -Start an HTTP development server that provides UAST parsing and querying -via a REST API. +Quick analysis shortcut that parses and analyzes source files in one step. ```bash -uast server [flags] +uast analyze [files...] [flags] ``` | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--port` | `-p` | `string` | `8080` | Port to listen on | -| `--static` | `-s` | `string` | `""` | Directory to serve static files from | +| `--output` | `-o` | `string` | `""` | Output file (default: stdout) | +| `--format` | `-f` | `string` | `text` | Output format: `text`, `json`, `html` | ```bash -# Start on default port -uast server +# Analyze a single file +uast analyze main.go + +# Analyze all Go files as JSON +uast analyze -f json *.go -# Custom port with static file serving -uast server -p 3000 -s ./web +# Generate an HTML report +uast analyze -o report.html -f html *.go ``` -**API Endpoints:** +--- + +### `uast validate` -| Method | Path | Description | -|--------|------|-------------| -| `POST` | `/api/parse` | Parse source code to UAST | -| `POST` | `/api/query` | Query a UAST with DSL expression | -| `GET` | `/api/mappings` | List available language mappings | -| `GET` | `/api/mappings/<name>` | Get a specific language mapping | +Validate a UAST JSON document against the UAST JSON Schema. Exit codes: `0` +valid, `1` the document violates the schema, `2` an error (unreadable input, +malformed JSON, unreadable schema). + +```bash +uast validate <file.json|-> [flags] +``` + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--schema` | | `string` | `""` | Path to a UAST JSON schema; empty validates against the schema embedded in the binary | + +```bash +# Validate a parsed tree against the embedded schema +uast parse -o tree.json main.go +uast validate tree.json +``` --- -### `uast analyze` +### `uast mapping` -Quick analysis shortcut that parses and analyzes source files in one step. +Grammar helpers for UAST mapping authors: summarize and classify a +tree-sitter `node-types.json`, generate a `.uastmap` DSL skeleton from it, or +dump the raw tree-sitter tree of source files. ```bash -uast analyze [files...] [flags] +uast mapping [files...] [flags] ``` | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--output` | `-o` | `string` | `""` | Output file (default: stdout) | -| `--format` | `-f` | `string` | `text` | Output format: `text`, `json`, `html` | +| `--node-types` | | `string` | `""` | Path to `node-types.json` (required unless `--show-treesitter`) | +| `--format` | | `string` | `text` | Output format of the node-types summary: `text`, `json` | +| `--generate` | | `bool` | `false` | Print a `.uastmap` DSL skeleton generated from `node-types.json` | +| `--show-treesitter` | | `bool` | `false` | Print the raw tree-sitter tree (named nodes, positions, byte ranges, text) of each input file | +| `--language` | | `string` | `""` | Tree-sitter language name (e.g. `go`): required by `--show-treesitter`, written into the `--generate` header | +| `--extensions` | | `string` | `""` | Comma-separated file extensions written into the `--generate` header | ```bash -# Analyze a single file -uast analyze main.go +# Classify a grammar's node types +uast mapping --node-types node-types.json --format json -# Analyze all Go files as JSON -uast analyze -f json *.go +# Generate a mapping skeleton +uast mapping --node-types node-types.json --generate --language go --extensions .go -# Generate an HTML report -uast analyze -o report.html -f html *.go +# Dump the tree-sitter tree of a file +uast mapping --show-treesitter --language go main.go ``` +--- + +### `uast completion <shell>` + +Generates the completion script for `bash`, `zsh`, `fish` or `powershell` on +stdout, covering every subcommand and flag listed on this page. + +### `uast version` + +Prints `uast <version> (commit: <hash>, built: <date>)`. + --- @@ -1560,9 +1764,9 @@ uast analyze -o report.html -f html *.go # Understanding complexity metrics This page explains the mental model behind the complexity analyzer: what -each metric means, why the analyzer measures three dimensions, and how -cyclomatic and cognitive complexity differ. For configuration keys and the -output schema, see the [Complexity reference](../analyzers/complexity.md). +each metric means, the exact counting rules, how cyclomatic and cognitive +complexity differ, and how the rules are validated. For the output schema +and the thresholds, see the [Complexity reference](../analyzers/complexity.md). --- @@ -1570,20 +1774,23 @@ output schema, see the [Complexity reference](../analyzers/complexity.md). The complexity analyzer measures three dimensions of code complexity from source code: **cyclomatic complexity**, **cognitive complexity**, and -**nesting depth**. It operates on the UAST representation of your source -files. +**nesting depth**, plus the **lines of code** of each function. It operates +on the UAST representation of your source files, with the file's source text +at hand so operators, identifiers and line counts can be recovered where a +language mapping does not carry them. ### Cyclomatic complexity (McCabe, 1976) -Counts the number of linearly independent paths through a function's control flow graph. - -For Go code, the analyzer follows the same practical counting model used by `gocyclo`: +Counts the number of linearly independent paths through a function's +control-flow graph. The same rules apply to every language: - Base score is `1` -- `if`, `for` / `range` add `+1` -- Non-default `case` adds `+1` -- Each `&&` and `||` adds `+1` -- `default` case does **not** add complexity +- `+1` per `if`, `else if` / `elif`, and conditional (ternary) expression +- `+1` per loop (`for`, `while`, `do`, `loop`, `for … in`, `range`) +- `+1` per non-default `case` / match arm (`default`, `_`, `else` and + Python's `case _:` add nothing) +- `+1` per `catch` / `except` +- `+1` per logical operator (`&&`, `||`, `and`, `or`) !!! info "Interpretation" - **1-10**: Simple, low risk @@ -1593,29 +1800,143 @@ For Go code, the analyzer follows the same practical counting model used by `goc ### Cognitive complexity (SonarSource, 2017) -Measures how difficult code is for a human to read and understand. Unlike cyclomatic complexity, it penalizes nested structures more heavily and rewards linear sequences of logic. +Measures how difficult code is for a human to read and understand. Unlike +cyclomatic complexity, it penalizes nested structures more heavily and +rewards linear sequences of logic. + +- **Structural increments**: `if`, conditional expressions, + `switch`/`match`, loops and `catch` each add `1` **plus the current + nesting level**, and raise the nesting level for what they contain. +- **Flat increments**: `else` and `else if` add `1` each and never a nesting + penalty; an `else if` chain stays at the level of its first `if`. +- **Sequences of logical operators**: `+1` per run of like operators — + `a && b && c` is one, `a && b || c` is two. A sequence is counted once + wherever it appears (conditions, returns, assignments, call arguments). +- **Jumps to a label**: `break label`, `continue label` and `goto` add `1`; + a plain `break`/`continue` adds nothing. +- **Direct recursion**: a call to the function itself adds `1`. +- **Nesting only**: lambdas and anonymous function literals (closures, + callbacks) add nothing themselves but raise the nesting level for what + they contain. `try` adds nothing; its `catch` is a structural increment. +- The function's own body is nesting level 0 — including an arrow or + function-expression wrapper such as `const f = (x) => { … }`. +- A **named nested function** (Python `def` inside `def`, an inner `fn`, + `const g = function () {}`) is its own unit: it is reported separately and + excluded from the enclosing function's metrics. -Key differences from cyclomatic complexity: +### Nesting depth -- Nesting increments add to the score (deeper nesting = higher penalty) -- `else if` adds structural complexity, but avoids extra nesting penalty vs deeply nested `if` -- Logical operator sequences account for readability, not just path count -- Direct recursion adds a penalty +The maximum depth of nested control-flow blocks within a function: each +`if`/`else` chain (one level for the whole chain), loop, `switch`/`match` +and `try`/`catch` statement (one level for the whole statement) opens a +level. Conditional expressions and lambdas are not blocks and do not count. +Deep nesting is a strong signal for refactoring. + +### Lines of code and density + +Lines of code are the non-blank lines of the function's span that carry +something other than comment text (comment-only lines and blank lines are +skipped; a trailing comment does not blank a code line). The lines of a +nested named function belong to that function. `complexity_density` is +cyclomatic complexity divided by lines of code. + +--- + +## Counting rules + +### What is a function + +A `Function`/`Method` node with a body: a block, an expression body, or an +anonymous function/lambda it wraps (JS declarations). Declarations without a +body — C prototypes and declarators, Go function types and interface +methods, Rust trait signatures, Java abstract and interface methods — are +never reported. Anonymous function literals inside a function belong to +that function (they raise nesting); at the top level they are reported as +`anonymous` only when the mapping exposes them as functions. + +Names come from the mapping's `name` property, else from the `Name`-role +identifier (through a C declarator), falling back to the identifier's source +text; a multi-line token is never a name. + +### Worked example + +```go +func decisions(a, b int, xs []int) int { + r := 0 + if a > 0 && b > 0 { // cyclomatic +1 (if) +1 (&&) | cognitive +1 (if) +1 (&& sequence) + r++ + } else if a < 0 || b < 0 { // cyclomatic +1 (if) +1 (||) | cognitive +1 (else if) +1 (|| sequence) + r-- + } else { // | cognitive +1 (else) + r = 2 + } + for _, x := range xs { // cyclomatic +1 | cognitive +1 + r += x + } + switch r { // | cognitive +1 + case 1: // cyclomatic +1 + r = 10 + case 2: // cyclomatic +1 + r = 20 + default: // nothing + r = 0 + } + return r +} +``` -### Nesting depth +Cyclomatic `1 + 7 = 8`, cognitive `7`, nesting depth `1` (nothing is nested +inside another block), lines of code `22`. + +Nesting changes the cognitive score but not the cyclomatic one: + +```go +func nested(a, b, c bool) int { + if a { // cognitive +1 (nesting 0) + if b { // cognitive +2 (nesting 1) + if c { // cognitive +3 (nesting 2) + return 1 + } + } + } + return 0 +} +``` + +Cyclomatic `4`, cognitive `6`, nesting depth `3`. + +### Operator recovery -Tracks the maximum depth of nested control structures within each function. Deep nesting is a strong signal for refactoring. +A binary operator is read from the node's `operator` property when the +language mapping provides one, else from the node's own token when that is +a bare operator symbol, else from the source text between the two operands. +Python's `and`/`or` are recognised through the same source-text rule even +where the mapping lowers them to a generic node. --- -## Validation against golden implementations +## Validation -The complexity analyzer is continuously validated against battle-tested Go references: +The counting rules are pinned by hand-computed fixtures, not by another +tool's output: -- **Cyclomatic parity target:** `gocyclo` (v0.6.0) -- **Cognitive parity target:** `gocognit` (v1.2.1) +- `crates/cf-complexity/tests/fixtures/` holds Go, Python, JavaScript, Rust, + Java and C sources (else-if chains, nested `else`, boolean sequences, + `if` with an init statement, `try`/`catch`, lambdas and closures, + `switch` with several cases, `default`/`_` arms, recursion, labelled + jumps, nested functions, declarations without bodies, generics). Every + function's cyclomatic, cognitive, nesting and line count is asserted + from the parsed source (`crates/cf-complexity/tests/fixtures.rs`). +- `bins/codefang/tests/complexity_e2e.rs` drives the real binary over the + same sources and asserts the exact numbers in `json`, `yaml`, `bin` and + `text`, that `--per-file` entries sum to the aggregate, that every format + survives 20 000 nesting levels, and that output is deterministic. +- Unit tests pin each rule on hand-built trees (`crates/cf-complexity/src`). -For stabilization, we run a controlled methodology sample through both references and `codefang`, then align discrepancies until parity is reached. +One expectation is recorded but not yet enforced: Java `switch` arms and +the Java ternary are lowered by the present mapping in a shape the analyzer +cannot count (`java_decisions_method` is `#[ignore]`d until the UAST mapping +contract lands); every other Java construct is covered. --- @@ -1631,15 +1952,16 @@ For stabilization, we run a controlled methodology sample through both reference ## Limitations - **Language coverage**: Only languages supported by the UAST parser are analyzed. Unsupported files are silently skipped. -- **Generated code**: The analyzer does not distinguish hand-written code from generated code. Consider excluding generated directories. +- **Mapping fidelity**: the analyzer counts what the language mapping exposes. Where a mapping lowers a construct to a generic node (with the present mapping: Java `switch` arms and the Java ternary), that construct is not counted until the mapping is fixed. +- **Generated code**: The analyzer does not distinguish hand-written code from generated code. Consider excluding generated directories (the default path policy already skips vendored and generated paths). - **Macros and metaprogramming**: Complexity within macros or template metaprogramming may not be fully captured, since the UAST represents the source as written, not as expanded. -- **Cognitive complexity model**: The cognitive complexity scoring follows the SonarSource specification. Other tools may use slightly different weightings. +- **Cognitive complexity model**: The cognitive complexity scoring follows the SonarSource specification. Other tools may use slightly different weightings (for example, `gocognit` does not count `goto`). --- ## See also -- [Complexity reference](../analyzers/complexity.md) — configuration keys and output schema. +- [Complexity reference](../analyzers/complexity.md) — output schema and thresholds. - [Quick start](../getting-started/quickstart.md) — run your first analysis. @@ -1650,79 +1972,165 @@ For stabilization, we run a controlled methodology sample through both reference # Understanding cohesion metrics This page explains the mental model behind the cohesion analyzer: what -LCOM-HS measures, how the variable-sharing ratio captures communicational -cohesion, and how to read the scores. For configuration keys and the output -schema, see the [Cohesion reference](../analyzers/cohesion.md). +counts as a variable, what LCOM-HS measures, how the variable-sharing ratio +captures communicational cohesion, how scores are combined across files, and +how to read them. For the output schema, see the +[Cohesion reference](../analyzers/cohesion.md). --- ## What it measures -The cohesion analyzer computes **LCOM-HS (Henderson-Sellers)** and **variable -sharing ratio** metrics to identify files and modules with low internal -cohesion. Low cohesion indicates functions that are poorly related to each -other — a strong signal for refactoring. +The cohesion analyzer computes **LCOM-HS (Henderson-Sellers)** per file and a +**variable sharing ratio** per function to identify files whose functions are +poorly related to each other — a strong signal for refactoring. + +Both metrics are built on one relation: *which function uses which +variable*. Everything below follows from how that relation is extracted. + +### What counts as a variable + +A function *uses* a variable when its body contains an identifier that reads +or writes it. The analyzer works lexically on the UAST — no symbol +resolution — with these rules: + +| Counted | Not counted | +|---|---| +| Local variables (`x := 1`, `let m = …`, `const total = …`) — by the declared **name**, never the whole statement | Type names (`int`, `string`, `Config`, `i32`, `Self`, `number`) | +| Parameters (`cfg`, `items`, `n`) | The function's own name and its return-type annotation | +| Fields and attributes (`s.a` counts `a`; `self.x` counts `x`; `this.b` counts `b`) | Callees: `helper()`, `string(v)`, the `Println` in `fmt.Println(x)`, the `Validate` in `cfg.Validate()` | +| The object of a member access (`cfg` in `cfg.Name`, `fmt` in `fmt.Println`) | The receiver: `self`, `this`, and a Go receiver such as `s` in `func (s *S) M()` | +| Variables of a closure, attributed to the enclosing function | Literals (`1`, `"x"`, `nil`, `true`) | + +Each function's variables form a **set**: using `x` three times is one use of +`x`. + +!!! note "Lexical means lexical" + `fmt` in `fmt.Println(x)` is indistinguishable from a variable without + symbol resolution, so it counts. Two different variables that share a + name across functions count as the same variable. Both are documented + limitations, not bugs. ### LCOM-HS (lack of cohesion of methods — Henderson-Sellers) -LCOM-HS is the industry-standard cohesion metric used by NDepend, JArchitect, and CppDepend. +LCOM-HS is the industry-standard cohesion metric used by NDepend, JArchitect, +and CppDepend. -**Formula:** +**Formula (per file):** -``` +```text LCOM = 1 - sum(mA) / (m × a) ``` Where: -- `m` = number of functions in the file -- `a` = number of distinct variables across all functions -- `mA` = for each variable, the count of functions that reference it -- `sum(mA)` = sum of access counts across all variables +- `m` = number of functions in the file **that use at least one variable** +- `a` = number of distinct variables across those functions +- `mA` = for each variable, the number of functions that use it +- `sum(mA)` = the number of (function, variable) usage pairs -**Range:** 0.0 (perfect cohesion) to 1.0 (no cohesion). +**Range:** 0.0 (perfect cohesion: every function uses every variable) to +1.0 (no cohesion: no two functions share a variable). -!!! info "Interpretation" - - **LCOM ≤ 0.3** (green): Excellent cohesion — functions share most variables. - - **LCOM ≤ 0.6** (yellow): Moderate cohesion — some functions may be loosely related. - - **LCOM > 0.6** (red): Poor cohesion — functions share few variables and may belong in separate files. +Functions that use no variables are not part of the usage matrix (there is +nothing they could share), so trivial helpers never penalize a file. When +fewer than two functions use a variable — a single-function file, or a file +of parameterless helpers — there is nothing to compare, and `LCOM = 0`. + +**Worked example** — `f` uses `{x, y}`, `g` uses `{x}`: + +```text +m = 2, a = 2 ({x, y}), sum(mA) = 2 (x) + 1 (y) = 3 +LCOM = 1 - 3 / (2 × 2) = 0.25 +``` ### Cohesion score -A convenience inversion of LCOM-HS: `cohesion_score = 1.0 - LCOM`. Higher is better. +A convenience inversion of LCOM-HS: `cohesion_score = 1.0 - LCOM`. Higher is +better. It drives the section score (`6/10` means `0.6`) and the summary +message: -| Cohesion Score | Assessment | +| Cohesion score | Summary | |---|---| -| ≥ 0.7 | Excellent | -| ≥ 0.4 | Good | -| ≥ 0.3 | Fair | -| < 0.3 | Poor | +| ≥ 0.7 | Excellent overall cohesion across all analyzed code | +| ≥ 0.4 | Good overall cohesion with room for improvement | +| ≥ 0.3 | Fair overall cohesion - consider refactoring some functions | +| < 0.3 | Poor overall cohesion - significant refactoring recommended | ### Function-level cohesion (variable sharing ratio) -For each function, the analyzer computes what fraction of its variables are shared with at least one other function in the same file. This measures **communicational cohesion** (Yourdon-Constantine level 5). +For each function, the analyzer computes what fraction of its variables is +shared with **at least one other function in the same file**. This measures +**communicational cohesion** (Yourdon-Constantine level 5). **Formula:** -``` -function_cohesion = shared_vars / total_unique_vars +```text +function_cohesion = shared_vars / unique_vars ``` Where: -- `shared_vars` = variables in this function that also appear in at least one other function -- `total_unique_vars` = total distinct variables in this function -- Functions with no variables receive a score of 1.0 (trivial, no penalty) +- `unique_vars` = the function's distinct variables +- `shared_vars` = those of them that at least one *other* function of the + file also uses (a repeat inside the same function is not sharing) +- Functions with no variables score 1.0 (trivial, no penalty) +- Every function of a file with fewer than two variable-using functions + scores 1.0 (nothing to compare against) **Range:** 0.0 (completely isolated function) to 1.0 (all variables shared). -| Function Cohesion | Assessment | +| Function cohesion | Assessment | |---|---| | ≥ 0.6 | Excellent — function shares most variables with the module | | ≥ 0.4 | Good — reasonable sharing with room for improvement | | ≥ 0.3 | Fair — consider refactoring | | < 0.3 | Poor — function is isolated from the module | +Functions below **Excellent** are listed as issues, worst first. The +distribution counts every function in exactly one of the four buckets, so it +always sums to **Total Functions**. + +In the worked example above, `f` shares `x` but not `y` (`0.5`, Good) and `g` +shares its only variable (`1.0`, Excellent). + +### Across files + +A variable is a per-file notion, so LCOM is never recomputed over a whole +tree. Instead: + +- **LCOM Score** = the function-weighted mean of the per-file LCOM values (a + file with five functions weighs five times a file with one); +- **Cohesion Score** = `1 - LCOM Score`; +- **Avg Cohesion** = the mean per-function cohesion over every function; +- **Total Functions** = every function found. Same-named functions are kept + (methods are reported as `Type.name`, e.g. `A.String` and `B.String`). + +Files without functions — struct-only files, empty files, Markdown — carry +**no cohesion data**: they are excluded from every denominator. A tree in +which no file has a function reports `No cohesion data available` with score +`0/10` (and exit code 0). Use `--per-file` to see each file's own LCOM. + +--- + +## Per-language notes + +- **Go** — `type_identifier` nodes (`int`, `Config`) are typed and excluded; + receivers are excluded and name the method (`S.M1`); closures (`func() + {…}` literals) belong to the enclosing function. +- **Python** — `self` is excluded; methods are qualified by their class + (`C.m1`); decorators are outside the function. Type annotations are plain + identifiers in Python's UAST and therefore count. +- **JavaScript / TypeScript** — `this` is excluded; class methods are + qualified (`A.m1`); TypeScript type annotations are excluded. +- **Java** — `this` is excluded; methods, including constructors, are + qualified by their class (`A.m1`, `A.A`); primitive types are excluded. +- **Rust** — `self` is excluded; `impl` blocks do not qualify method names + (`m1`, not `S.m1`). Identifier nodes in the Rust mapping carry no token; + their names are recovered from the enclosing function's source span. +- Any language supported by the UAST works; results are best where variable + declarations and function definitions are explicit. + --- ## Use cases @@ -1743,9 +2151,9 @@ Where: ## Limitations -- **Language scope**: Works with any language supported by the UAST. Best results with languages that have explicit variable declarations and function definitions. +- **Lexical names**: The analyzer uses identifier spellings. Different variables with the same name across functions count as shared; package qualifiers (`fmt`) count as variables. +- **Constructors and initializers**: A constructor that initializes every field (`S { a: 1, b: 2 }`, `this.a = …`) uses every field and ties the other methods together, raising the file's cohesion. - **Accessor methods**: Simple getters/setters may inflate cohesion since each touches only one shared field. -- **Variable naming**: The analyzer uses lexical variable names. Different variables with the same name across functions will be counted as shared. - **Single-function files**: Files with only one function always receive perfect cohesion (LCOM = 0.0, cohesion = 1.0) since there are no other functions to compare against. - **Trivial functions**: Functions with no variables receive a cohesion score of 1.0 to avoid penalizing simple utility functions. @@ -1753,7 +2161,7 @@ Where: ## See also -- [Cohesion reference](../analyzers/cohesion.md) — configuration keys and output schema. +- [Cohesion reference](../analyzers/cohesion.md) — output schema and thresholds. - [Quick start](../getting-started/quickstart.md) — run your first analysis. @@ -1764,19 +2172,19 @@ Where: # Understanding Halstead metrics This page explains the mental model behind the Halstead analyzer: what -operators and operands are, which derived metrics matter most, and how the -counting policy keeps results stable across languages. For configuration keys -and the output schema, see the [Halstead reference](../analyzers/halstead.md). +operators and operands are, how codefang finds them in the UAST, how a +codebase is aggregated, and which derived metrics matter most. For the +output schema and the exact thresholds, see the +[Halstead reference](../analyzers/halstead.md). --- ## What it measures -The Halstead analyzer computes **Halstead complexity metrics** (1977) based on -operator and operand counts extracted from the UAST. These metrics provide an -objective, quantitative assessment of program size and complexity. - -The Halstead model treats a program as a sequence of **operators** (keywords, symbols, function calls) and **operands** (variables, literals, constants). From these counts, it derives: +The Halstead model (1977) treats a program as a sequence of **operators** +(symbols and keywords that *do* something: `+`, `:=`, `return`, `if`, a +call's parentheses, a member access) and **operands** (the things they act +on: variables, literals, names). From four counts it derives: | Metric | Formula | Description | |---|---|---| @@ -1786,49 +2194,134 @@ The Halstead model treats a program as a sequence of **operators** (keywords, sy | **N2** | -- | Total number of operands | | **Vocabulary** (n) | n1 + n2 | Distinct tokens used | | **Length** (N) | N1 + N2 | Total tokens in the program | -| **Volume** (V) | N * log2(n) | Information content in bits | -| **Difficulty** (D) | (n1/2) * (N2/n2) | Error proneness | -| **Effort** (E) | D * V | Mental effort to implement | +| **Volume** (V) | N · log2(n) | Information content in bits | +| **Difficulty** (D) | (n1/2) · (N2/n2) | Error proneness | +| **Effort** (E) | D · V | Mental effort to implement | | **Time** (T) | E / 18 | Estimated implementation time (seconds) | | **Bugs** (B) | V / 3000 | Estimated delivered bugs | !!! info "Key insight" - **Volume** measures the size of the implementation. **Difficulty** captures how error-prone it is. Their product, **Effort**, is the best single number for comparing overall complexity. + **Volume** measures the size of the implementation. **Difficulty** + captures how error-prone it is. Their product, **Effort**, is the best + single number for comparing overall complexity. -### Counting policy +### A worked example -Codefang computes Halstead metrics from UAST with a lexical-first policy: - -- Counts lexical operands (`Identifier`, `Literal`, `Field`) and operator nodes/roles. -- Excludes structural wrappers and declaration-only artifacts from operand counts. -- Uses explicit operator properties when available, then token extraction fallback. +```go +func hal(a int, b int) int { + c := a + b + return c * 2 +} +``` -This improves stability across languages and avoids pseudo-operands like structural `Parameter` nodes. +| | Tokens | Count | +|---|---|---| +| operators | `:=`, `+`, `*`, `return` | n1 = 4, N1 = 4 | +| operands | `a`, `b`, `c`, `c`, `2` | n2 = 4, N2 = 5 | + +n = 8, N = 9, V = 9 · log2(8) = **27**, D = (4/2) · (5/4) = **2.5**, +E = **67.5**, T = 3.75 s, B = 0.009. The function's name, its parameters +and its return type are its *signature* — they are declared, not operated +on — so `a` and `b` count once each (their uses in the body), not three +times. + +--- + +## Counting policy + +Codefang classifies UAST nodes inside each function body with one table +(the full version is in the [reference](../analyzers/halstead.md#classification-table)): + +- **Operands** are leaf `Identifier` and `Literal` nodes, named by their + text. A quoted string is one operand with its quotes; a qualified name + such as `fmt.Println` is two operands (`fmt`, `Println`) plus the + member-access operator `.`. +- **Operators** are, in order of preference: the node's explicit + `operator` property when the language mapping exposes it; the fixed + operator of calls (`()`), indexing (`[]`) and slicing (`[:]`); otherwise + the node's **residue** — its source text with every child blanked out. + For `c := a + b` the declaration's residue is ` := ` and the binary + expression's is ` + `; for `return c * 2` it is `return `; for `r++` it + is `++`. Residue is tokenized into symbol runs (always operators) and + words (operators only when they are keywords: `return`, `if`, `for`, + `let`, `int`, `new`, …). Brackets, commas, semicolons and a lone `:` are + punctuation, not operators; the conditional `? :` counts as one `?:`. +- A leaf identifier with the `Type` role inside a body (`int` in + `int c = 0;`, `var x int`) is a type-specifier **operator**, following + the classic convention for declaration keywords. +- **Skipped**: the function's signature (name, parameters, modifiers, + return type), parameter subtrees anywhere, comments and docstrings. +- **Closures and lambdas** belong to the function that contains them (the + same rule the complexity analyzer uses); nested *named* functions and + methods are separate records. + +Text is recovered from the file bytes by byte span, so languages whose +mappings leave literals or identifiers without a token (Rust, Java, Go +literals) are counted correctly. When the analyzer runs without the file +(the `history/quality` path), it falls back to the node's own verbatim +token or the enclosing function's token; a node whose text cannot be +recovered is left uncounted — a node is never counted under its type name, +and a statement's text is never an operand. + +### Aggregation: union, not average + +A file, and the whole codebase, is aggregated as the **union of its +functions' multisets**: an operator used in ten functions is *one* +distinct operator (n1) with its occurrences summed (N1); the derived +measures are then computed from the four union counts exactly as for a +single function. Alongside the union, the report carries per-function +statistics — mean and maximum volume, difficulty and effort, and the +number and share of **high-effort functions** (effort ≥ 10000). + +The status message, the health score and the section score are keyed off +that share: a codebase where fewer than 5% of functions are high-effort +is "well-structured", up to 15% "acceptable", up to 30% "consider +refactoring", beyond that "significant refactoring recommended"; the +health score falls linearly from 100 through 90, 70 and 40 at those +boundaries to 0 when every function is high-effort. Note that the *union* +difficulty of a large codebase grows with code size (every operand reuse +raises `N2 / n2`), which is why the health score is based on the +per-function distribution rather than on the union D or E. --- ## Use cases -- **Effort estimation**: Use the Effort metric to compare the relative complexity of different modules or features. -- **Bug prediction**: The Bugs metric provides a rough upper bound on expected defects. Modules with high estimated bugs warrant more thorough testing. -- **Code review guidance**: Functions with high Difficulty scores are more error-prone and deserve extra scrutiny. -- **Language comparison**: Halstead metrics allow cross-language comparisons since they are based on abstract operator/operand counts. +- **Effort estimation**: compare the Effort of modules or features to rank + them by the mental work they represent. +- **Bug prediction**: the Bugs metric is a rough upper bound on expected + defects; functions with high estimated bugs warrant more thorough testing. +- **Code review guidance**: functions with high Difficulty are more + error-prone and deserve extra scrutiny; `--per-file` shows which files + concentrate them. +- **Language comparison**: the counts are abstract operator / operand + counts, so trends compare across languages within one toolchain. --- ## Limitations -- **Cross-tool variance**: Language-specific tools (for example, Python- or ESTree-only analyzers) can differ in tokenization and counting policy. Compare trends within one toolchain rather than mixing absolute numbers across tools. -- **Operator classification**: UAST-based classification targets cross-language consistency, not byte-for-byte parity with each language parser. -- **Estimation accuracy**: The Bugs and Time formulas are empirical approximations from the 1970s. Treat them as relative indicators, not precise predictions. -- **Macro expansion**: Halstead metrics count tokens as written, not as expanded. Heavy use of macros or code generation can skew results. -- **Comments excluded**: Comments and whitespace are excluded from Halstead counts (by design). +- **Cross-tool variance**: language-specific tools (for example, Python- + or ESTree-only analyzers) differ in tokenization and counting policy. + Compare trends within one toolchain rather than mixing absolute numbers + across tools. +- **Mapping coverage**: the counts are as good as the UAST mapping of the + language. A construct the mapping lowers to a bare wrapper without spans + is not counted; keyword operators come from a fixed whitelist, so an + unlisted keyword is dropped rather than mislabelled. +- **Estimation accuracy**: the Bugs and Time formulas are empirical + approximations from the 1970s. Treat them as relative indicators, not + precise predictions. +- **Macro expansion**: Halstead metrics count tokens as written, not as + expanded. Heavy use of macros or code generation can skew results. +- **Comments excluded**: comments and whitespace are excluded from + Halstead counts (by design). --- ## See also -- [Halstead reference](../analyzers/halstead.md) — configuration keys and output schema. +- [Halstead reference](../analyzers/halstead.md) — classification table, thresholds and output schema. - [Quick start](../getting-started/quickstart.md) — run your first analysis. @@ -1839,61 +2332,153 @@ This improves stability across languages and avoids pseudo-operands like structu # Understanding documentation coverage This page explains the mental model behind the comments analyzer: what -documentation coverage means, how the analyzer distinguishes comment types, -and what a healthy comment density looks like. For configuration keys and the -output schema, see the [Comments reference](../analyzers/comments.md). +documentation coverage means, how a comment is attached to a declaration, and +how the score is built. For the output schema, see the +[Comments reference](../analyzers/comments.md). --- ## What it measures -The comments analyzer evaluates **documentation coverage** by examining comment -presence, placement, and density across your source code. It identifies -functions and classes that lack documentation and measures overall -documentation quality. +The comments analyzer looks at two things in every parsed file: -### Documentation coverage +- **Documentation coverage** — the share of declarations (functions, methods, + classes, interfaces, structs) that carry a doc comment. +- **Comment placement** — where each comment sits: directly above a + declaration, inside a body, or attached to nothing. -The percentage of public functions, methods, and classes that have an associated documentation comment. A comment is "associated" when it immediately precedes the declaration. +Both are summed over the whole analyzed tree: coverage is +*documented declarations / all declarations* and the good-comment ratio is +*good comments / all comments*. A file without declarations (a Markdown +README, a file of constants) adds nothing to either denominator, so it cannot +dilute the result. -### Comment placement quality +### Which comments document a declaration -Not all comments are equally useful. The analyzer distinguishes: +A comment block documents a declaration when it **ends on the line directly +above the declaration**. There may be no blank line in between. Attribute and +decorator lines (`#[derive(...)]`, `@dataclass`, annotations) belong to the +declaration, so a doc comment above them still counts. -- **Doc comments**: Comments immediately before a function/class declaration -- **Inline comments**: Comments within function bodies -- **Orphan comments**: Comments not associated with any code structure +=== "Go" -### Comment density + ```go + // Add adds. ← above Add: documents it + func Add(a, b int) int { + // inline comment ← inline (inside Add) + return a + b + } -The ratio of comment lines to total lines of code. Provides a quick overall measure of how well-documented a file is. + // Mul multiplies. ← unassociated: a blank line separates it + + func Mul(a, b int) int { return a * b } + ``` + +=== "Rust" + + ```rust + /// Documents foo. ← above foo + #[inline] ← attribute lines belong to the declaration + pub fn foo() -> i32 { 1 } + + //! Crate docs ← unassociated unless directly above an item + ``` + +=== "Python" -!!! info "Healthy ratios" - A comment density between **15-30%** is generally considered healthy. Below 10% suggests under-documentation; above 40% may indicate stale or redundant comments. + ```python + # documents K ← above K, through the decorator line + @dataclass + class K: + """Class docstring.""" ← docstring: documents K + + # documents m ← above m (inside K's span, but directly above m) + def m(self): ... + ``` + +=== "Java" + + ```java + /** Javadoc for A. */ ← above A + public class A { + /** Javadoc for foo. */ ← above foo + public int foo() { + return 1; // trailing ← inline (inside foo) + } + } + ``` + +Adjacent comment lines form one block, so a multi-line `//` comment counts as +several comments that share one placement. Comment text is not analyzed: +`// do the thing` documents a function as well as a paragraph does. + +### Placement categories + +| Placement | Meaning | Counts as | +|---|---|---| +| `above` | Ends on the line directly above a declaration (attribute lines included) | good — documents the declaration | +| `inline` | Lies inside a declaration's span; attributed to the innermost one | bad | +| `unassociated` | Everything else: file headers, license blocks, comments after the last declaration, comments separated from a declaration by a blank line | bad | + +### Score and status + +The section score is the good-comment ratio, shown as `N/10`, and the status +is its threshold label: + +| Good ratio | Status | +|---|---| +| `>= 0.8` | Excellent comment quality and placement | +| `>= 0.6` | Good comment quality with room for improvement | +| `>= 0.4` | Fair comment quality - consider improving placement | +| below | Poor comment quality - significant improvement needed | + +A tree with neither comments nor declarations reports `No comments found`. +Coverage is reported alongside the score (`Doc Coverage`) and drives the +Documented / Undocumented distribution and the issue list; it is not folded +into the score. --- ## Use cases -- **Documentation enforcement**: Require minimum documentation coverage in CI/CD pipelines. -- **Onboarding assessment**: Measure how well-documented a codebase is before new team members join. -- **API surface quality**: Ensure all exported/public functions have doc comments. -- **Stale comment detection**: Identify files with unusually high comment density that may contain outdated comments. +- **Documentation enforcement**: require a minimum `Doc Coverage` in CI. +- **Onboarding assessment**: measure how well-documented a codebase is before + new team members join. +- **API surface quality**: the issue list names every undocumented + declaration with its file; `--per-file` breaks it down per file. +- **Placement hygiene**: a low good ratio with high coverage means most + comments live inside bodies or float between declarations. --- ## Limitations -- **Comment quality**: The analyzer measures comment *presence* and *placement*, not comment *quality*. A comment saying `// do the thing` counts the same as a thorough description. -- **Language conventions**: Documentation comment conventions vary by language (e.g., `///` in Rust, `"""` in Python, `//` in Go). The UAST normalizes these, but edge cases in less common languages may be missed. -- **Auto-generated docs**: Comments generated by tools (e.g., protobuf stubs) are counted the same as hand-written documentation. -- **Multilingual comments**: Comment text is not analyzed for language quality or correctness. +- **Comment quality**: presence and placement are measured, not content. +- **Docstrings**: a Python docstring (the string that is the first statement + of a `def` / `class` body) is a `Comment` with the `Doc` role in the UAST + and documents its declaration (placement `docstring`). A string literal + that appears later in a body is an `inline` comment of that body, and a + module docstring is `unassociated`. +- **Declaration names**: a declaration's name is its UAST `name` property or + its name identifier; a declaration the mapping leaves nameless — an + anonymous function expression, for instance — is reported as `unknown` + (a nameless class / struct / interface node is not counted at all). + Same-named declarations are tracked separately, so two `String` methods on + different receivers are documented independently. +- **Unparsable files**: a file with a supported extension that the parser + cannot handle (Markdown) is listed as an empty file with zero counts. +- **Auto-generated docs**: comments generated by tools (protobuf stubs) count + like hand-written ones. +- **Attribute roles**: attribute / decorator lines bridge the gap only when + the language mapping gives them the `Attribute` role (Rust attributes and + Python decorators do). --- ## See also -- [Comments reference](../analyzers/comments.md) — configuration keys and output schema. +- [Comments reference](../analyzers/comments.md) — output schema and + definitions. - [Quick start](../getting-started/quickstart.md) — run your first analysis. @@ -1905,26 +2490,37 @@ The ratio of comment lines to total lines of code. Provides a quick overall meas This page explains the mental model behind the imports analyzer: what it measures in static and history modes, and how the history-mode aggregation -pipeline works. For configuration keys and the output schema, see the -[Imports reference](../analyzers/imports.md). +pipeline works. For the per-language extraction rules, the classification +table and the output schema, see the [Imports reference](../analyzers/imports.md). --- ## What it measures The imports analyzer extracts **import and dependency information** from source -code using UAST parsing. It operates in both static mode (single-file analysis) -and history mode (tracking import usage per developer over time). +code using UAST parsing. It operates in both static mode (per-file analysis +aggregated over a tree) and history mode (tracking import usage per developer +over time). ### Static mode -- **Import list**: All imports/dependencies declared in each file -- **Language detection**: Automatically detects the language and normalizes import paths -- **Dependency graph**: Maps which files depend on which packages +- **Import targets, not statements**: each file contributes the modules, + packages or files its import statements name — `strings` for Go's + `str "strings"`, `react` for `import { useState } from 'react'`, + `std::collections::HashMap` and `std::collections::HashSet` for one Rust + `use` with a brace group. Aliases, specifier lists and keywords are never + imports. Each target counts once per file. +- **Usage across files**: the aggregate reports every unique import with the + number of files importing it, so the most-shared dependencies surface + first. +- **Classification by the importing file's language**: `relative`, + `stdlib` or `external`, using per-language standard-library tables (a Go + `errors` and a Python `datetime` are both stdlib; a JavaScript `react` is + external). ### History mode -Tracks import usage across Git history, producing a per-developer, per-language, per-tick breakdown of which dependencies each developer introduces or modifies. This reveals: +Tracks import usage across Git history, producing a per-developer, per-language, per-tick breakdown of which dependencies each developer introduces or modifies: every commit's changed files (the After version of each Insert/Modify) are parsed, their imports are extracted with the file's language, and each import is counted under the commit's author, the file's language and the commit's tick. This reveals: - **Dependency adoption timeline**: When new libraries were introduced - **Developer expertise signals**: Which developers work with which dependencies @@ -1932,15 +2528,26 @@ Tracks import usage across Git history, producing a per-developer, per-language, --- -## Architecture (history mode) +## How extraction works + +The extractor walks the parsed tree with one rule per language (Go, Python, +JavaScript/TypeScript, Rust, Java, C/C++, C#, PHP, Ruby, Kotlin) and a +generic rule for everything else. A rule recognizes an import node by its +shape — a statement token to parse, a quoted literal child, an identifier +child, or a `require`/`import` call — and once a node matches, its subtree +is never visited again. That consuming walk is what keeps nested +specifier nodes (`React`, `{ useState, useEffect }`, `* as ns`) out of the +result. + +--- -The imports history analyzer follows the **TC/Aggregator pattern**: +## Architecture (history mode) -1. **Consume phase**: For each commit, `Consume()` extracts imports from changed files via parallel UAST parsing and returns them as `TC{Data: []ImportEntry}`. Each `ImportEntry` carries a language and import path. The analyzer retains no per-commit state; only the UAST parser is kept as working state. -2. **Aggregation phase**: An `imports.Aggregator` collects TCs into a 4-level `Map` (author -> language -> import -> tick -> count) using `SpillStore[Map]`. The `AuthorID` and `Tick` from each TC index the entries correctly. -3. **Serialization phase**: `SerializeTICKs()` merges all tick data back into the full `Map` with metadata (`author_index`, `tick_size`), then delegates to `Serialize()` for JSON, YAML, binary, or HTML plot output. +The map and its report live in the `cf-imports` crate; the history walk in `cf-commands` supplies the per-commit inputs: -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +1. **Per-commit product**: for every Insert/Modify change the walk parses the After blob (files the parser supports, up to 256 KiB) and runs `cf_imports::extract_imports` with the parser's language for the file and the blob bytes — the same extractor and per-language rules as `static/imports`. The product is the commit's list of `(language, import)` entries, a pure function of the commit, computed in parallel across cores. +2. **Chronological reduce**: the shared history core stamps every commit with its author id and tick in oldest-first order; `add_entries_to_map` folds each commit's entries into the 4-level map `author -> language -> import -> tick -> count`, which merges additively. +3. **Report**: `cf_imports::HistoryReport` carries the tick size, the author index and the map keyed by developer label; its one value tree is what json, yaml and bin encode, and `aggregate_import_counts` / `top_imports` derive the plot's top-20 bars from the same map. --- @@ -1956,17 +2563,20 @@ This separation enables streaming output, budget-aware memory spilling, and deco ## Limitations -- **Language support**: Only languages with UAST parser support are analyzed. Unsupported file types are silently skipped. -- **Dynamic imports**: Runtime or dynamic imports (e.g., Python's `importlib.import_module()`, JavaScript's `import()`) are not detected since they are not present in the static UAST. +- **Language support**: Only languages with UAST parser support are analyzed. Unsupported file types are silently skipped. Languages without a dedicated rule use the generic statement rule, which covers the common `import` / `from … import` / `use` forms. +- **Dynamic imports**: JavaScript `import('m')` and `require('m')` are detected only when the argument is a string literal; computed module names (`require(name)`) and Python's `importlib.import_module()` are not present as imports in the UAST. +- **Re-exports**: `export { x } from './m'` is not an import. +- **Kotlin**: the Kotlin grammar lowers imports without tokens, so the statement is read back from the file bytes; both the static and the history path provide them. - **Transitive dependencies**: Only direct imports are reported. The analyzer does not resolve transitive dependency trees. -- **File size threshold**: Files exceeding `MaxFileSize` (default 1 MB) are skipped to avoid excessive memory usage during parallel extraction. -- **History mode overhead**: History mode creates a UAST parser per fork, which increases memory usage. Tune `Goroutines` based on available memory. +- **Blob size threshold**: In history mode, blobs above 256 KiB are not parsed. +- **Counts are per change, not per file**: history mode counts the imports of every Insert/Modify change's After version, so a file rewritten in several commits contributes its imports once per commit. +- **History mode overhead**: Every changed file of every commit is parsed; one UAST parser per worker thread is reused across the commits that thread processes. --- ## See also -- [Imports reference](../analyzers/imports.md) — configuration keys and output schema. +- [Imports reference](../analyzers/imports.md) — extraction rules, classification, configuration keys and output schema. - [Quick start](../getting-started/quickstart.md) — run your first analysis. @@ -1978,72 +2588,132 @@ This separation enables streaming output, budget-aware memory spilling, and deco This page explains the mental model behind the clone detection analyzer: what a clone pair is, the clone-type taxonomy, and why the two-stage MinHash + LSH -design keeps cross-file detection fast. For configuration keys and the output -schema, see the [Clones reference](../analyzers/clones.md). +design keeps cross-file detection fast. For the output schema and the metric +definitions, see the [Clones reference](../analyzers/clones.md). --- ## What it measures The clone detection analyzer finds **duplicate and near-duplicate functions** -across your entire codebase using MinHash signatures and Locality-Sensitive -Hashing (LSH). It operates on the UAST representation and detects clones -**cross-file** — not just within a single file. +across your entire codebase. It operates on the UAST representation and +detects clones **cross-file** — not just within a single file. ### Clone pairs -A clone pair is two functions whose AST structure is similar enough to be considered duplicated code. Each pair includes a **similarity score** (0.0–1.0) and a **clone type** classification. +A clone pair is two functions whose AST structure is similar enough to be +considered duplicated code. Each pair carries a **similarity** (the exact +Jaccard index of the two functions' shingle sets, 0.5–1.0) and a **clone +type**. ### Clone types -| Type | Similarity | Description | +The classification follows the standard taxonomy (Roy & Cordy) and is decided +from two fingerprints per function — one over its *structure* (the sequence of +AST node types) and one over its *tokens* (the identifiers, literals and +operators at the leaves) — plus the exact shingle similarity: + +| Type | Rule | Similarity | |---|---|---| -| **Type-1 (Exact)** | = 1.0 | Identical AST structure and tokens | -| **Type-2 (Renamed)** | ≥ 0.8 | Identical structure, different variable/function names | -| **Type-3 (Near-miss)** | ≥ 0.5 | Similar but modified structure | +| **Type-1 (Exact)** | identical structure **and** identical tokens | 1.0 | +| **Type-2 (Renamed)** | identical structure, different identifiers / literals | 1.0 | +| **Type-3 (Near-miss)** | different structure, shingle Jaccard ≥ 0.5 | 0.5 – 1.0 | + +So a function copied verbatim is Type-1; the same copy with every variable +renamed, a constant changed or an operator swapped is Type-2; a copy with a +statement added or removed is Type-3 with a similarity that says how much of +the structure survived. Comments are ignored entirely: a copy that only +gained comments is still Type-1. ### Clone ratio -The ratio of detected clone pairs to total functions, computed globally across all files: +The share of functions that take part in at least one clone pair, computed +globally across all files: -``` -clone_ratio = total_clone_pairs / total_functions +```text +clone_ratio = functions in ≥ 1 pair / total_functions ``` +It is bounded to `[0, 1]` (a pair-count ratio is not: three copies of one +function form three pairs). Both the status message and the `N/10` score are +keyed off it: + !!! info "Interpretation" - - **0.0** (green): No duplication detected - - **≤ 0.1** (yellow): Low duplication — few clone pairs - - **≤ 0.3** (red): Moderate duplication — consider refactoring - - **> 0.3**: High duplication — significant refactoring recommended + - **0.0** — `No code clones detected`, 10/10 + - **≤ 0.1** — `Low duplication` + - **≤ 0.3** — `Moderate duplication - consider refactoring clone pairs` + - **> 0.3** — `High duplication - significant refactoring recommended` + + The score is `1 − clone_ratio`, so the message and the score always agree. --- ## Methodology -### Stage 1: per-file signature extraction +### Stage 1: per-file profiles and signatures For each source file, the analyzer: -1. **Parses** the file into a UAST and finds all function/method nodes. -2. **Shingles** each function by performing a pre-order traversal of the AST subtree and extracting sliding windows of `k` consecutive node types (k=5 by default). For example, a function with nodes `[Function, Block, Assignment, Identifier, Call, Return]` produces shingles like `Function|Block|Assignment|Identifier|Call` and `Block|Assignment|Identifier|Call|Return`. -3. **MinHash** compresses each function's shingle set into a fixed-size signature of 128 hash values. Two functions with identical structure produce identical signatures; similar functions produce similar signatures. - -Signatures are exported per-file along with function names. At this stage, no clone detection occurs — comparison is deferred to the aggregation stage. +1. **Parses** the file into a UAST and finds every function, method and + closure (UAST `Lambda`). Closures are named after their enclosing function + and ordinal (`Handle.func#2`), or `func#N` at file level. +2. **Profiles** each function: a pre-order traversal of its subtree (comment + nodes skipped) yields the node-type sequence, a *structure* fingerprint + over that sequence, and a *token* fingerprint over the leaf + `(type, token)` pairs. Functions with fewer than 20 nodes are counted but + not profiled further. +3. **Shingles** the type sequence into sliding windows of `k = 5` consecutive + node types. For example, `[Function, Block, Assignment, Identifier, Call, + Return]` produces `Function|Block|Assignment|Identifier|Call` and + `Block|Assignment|Identifier|Call|Return`. Each shingle is hashed; the + sorted, deduplicated hash set is kept — it is the exact set the next stage + verifies against. +4. **MinHash** compresses that shingle set into a fixed-size signature of 128 + hash values. + +No comparison happens at this stage; it is deferred to the aggregation stage. ### Stage 2: cross-file clone detection After all files have been processed, the aggregator: -1. **Qualifies** function names with their source file path (`path/to/file.go::FuncName`) so that same-named functions across files are distinguishable. -2. **Builds a global LSH index** (16 bands, 8 rows per band) from all collected signatures. LSH hashes each signature's bands into buckets — functions whose signatures share any bucket are candidate clones. -3. **Queries** the LSH index for each function, retrieves candidates above the similarity threshold (0.5), computes exact Jaccard similarity from the full MinHash signatures, and classifies each pair by clone type. -4. **Computes metrics** globally: `clone_ratio = total_pairs / total_functions`. - -This two-stage design means clone detection works across file boundaries. A controller duplicated in `pkg/a/handler.go` and `pkg/b/handler.go` is detected even though each file is parsed independently. +1. **Qualifies** function names with their source file path + (`path/to/file.go::FuncName`) so that same-named functions across files + are distinguishable. +2. **Builds a global LSH index** (32 bands × 4 rows) from all signatures. + LSH hashes each signature's bands into buckets — functions whose + signatures share any bucket are candidate clones. With these parameters a + pair with Jaccard `s` becomes a candidate with probability + `1 − (1 − s⁴)³²`: ≈ 0.87 at the 0.5 threshold, > 0.99 at 0.64, and + < 0.06 at 0.2. (The previous 16 × 8 layout had a recall of 0.06 at 0.5, + which is why near-miss pairs went unreported.) +3. **Verifies every candidate pair exactly**: equal structure fingerprints + mean similarity 1.0 — Type-1 if the token fingerprints match, Type-2 + otherwise; different structure means the exact Jaccard of the two shingle + sets, reported as Type-3 when it reaches 0.5 and dropped otherwise. The + `similarity` in the report is therefore always exact, never the MinHash + estimate, and the lower S-curve never adds false positives. +4. **Counts** every pair, keeps the best 1000 (similarity descending, then + name) as the detail list, and computes `clone_ratio` from the functions + that appear in at least one pair. + +Every unordered pair is enumerated exactly once (each function is queried for +the candidates that come after it in the index), so the work is linear in the +number of candidate pairs and nothing is allocated per pair: 4000 identical +functions — 8 million Type-1 pairs — are counted in a few seconds. + +This two-stage design means clone detection works across file boundaries. A +controller duplicated in `pkg/a/handler.go` and `pkg/b/handler.go` is detected +even though each file is parsed independently. ### Why MinHash + LSH? -Pairwise comparison of N functions is O(N²). For a project with 100K functions this is infeasible. MinHash compresses each function into a 1 KB signature (128 hashes × 8 bytes), and LSH reduces candidate pair generation to near-linear time by only comparing functions that hash into the same bucket. The combination provides probabilistic guarantees on recall while keeping the analysis fast. +Pairwise comparison of N functions is O(N²). For a project with 100K +functions this is infeasible. MinHash compresses each function into a 1 KB +signature (128 hashes × 8 bytes), and LSH reduces candidate pair generation to +near-linear time by only comparing functions that hash into the same bucket. +The exact verification step then makes the result precise: LSH only decides +*which* pairs are looked at, never what is reported about them. --- @@ -2060,7 +2730,7 @@ Pairwise comparison of N functions is O(N²). For a project with 100K functi ## Methodology references - **MinHash**: Broder, A. Z. (1997). *On the resemblance and containment of documents*. Proceedings of the Compression and Complexity of Sequences. MinHash estimates Jaccard similarity between sets using compact fixed-size signatures. -- **LSH (Locality-Sensitive Hashing)**: Indyk, P. & Motwani, R. (1998). *Approximate nearest neighbors: towards removing the curse of dimensionality*. STOC. Banded LSH reduces candidate pair generation from O(N²) to near-linear time. +- **LSH (Locality-Sensitive Hashing)**: Indyk, P. & Motwani, R. (1998). *Approximate nearest neighbors: towards removing the curse of dimensionality*. STOC. Banded LSH reduces candidate pair generation from O(N²) to near-linear time. - **AST-based clone detection**: Jiang, L. et al. (2007). *DECKARD: Scalable and accurate tree-based detection of code clones*. ICSE. Tree-based shingling captures structural similarity independent of identifier names. - **Clone taxonomy**: Roy, C. K. & Cordy, J. R. (2007). *A survey on software clone detection research*. The Type-1/Type-2/Type-3 classification follows the standard clone taxonomy. @@ -2069,10 +2739,10 @@ Pairwise comparison of N functions is O(N²). For a project with 100K functi ## Limitations - **Language scope**: Works with any language supported by the UAST parser. Unsupported files are silently skipped. -- **Structural only**: Detection is based on AST node types, not token values. Two functions with identical structure but completely different semantics (e.g., add vs multiply with the same nesting shape) may appear as clones. -- **Minimum function size**: Functions smaller than the shingle window (5 AST nodes) produce no shingles and are excluded from clone detection. They are still counted in `total_functions`. -- **Probabilistic recall**: LSH is a probabilistic algorithm. Clone pairs with similarity just above the threshold (0.5) may occasionally be missed. High-similarity pairs (Type-1 and Type-2) have near-perfect recall. -- **Generated code**: The analyzer does not distinguish hand-written code from generated code. Consider excluding generated directories. +- **Structural similarity**: Type-3 similarity is based on AST node types, not token values. Two functions with the same nesting shape but different semantics (e.g. add vs multiply) are reported as Type-2 clones, which is what the taxonomy defines. +- **Minimum function size**: Functions with fewer than 20 UAST nodes (comments excluded) are counted in `total_functions` but never paired. +- **Probabilistic candidate recall**: LSH is a probabilistic candidate stage. A pair whose similarity is exactly at the 0.5 threshold is proposed with probability ≈ 0.87; pairs at 0.64 and above are proposed practically always, and Type-1/Type-2 pairs always. Every proposed pair is verified exactly, so the stage never produces false positives. +- **Generated code**: The analyzer does not distinguish hand-written code from generated code; generated files are excluded by the default path policy (`--include-generated` adds them). - **No Type-4 clones**: Semantic clones (different structure, same behavior) are not detected. The analyzer operates purely on syntactic structure. --- @@ -2090,45 +2760,67 @@ Pairwise comparison of N functions is O(N²). For a project with 100K functi # Understanding code burndown This page explains the mental model behind the burndown analyzer: what code -survival means, the per-file and per-developer breakdowns, and how the -line-tracking algorithm works. For configuration keys and the output schema, -see the [Burndown reference](../analyzers/burndown.md). +survival means, how the per-file and per-developer breakdowns are defined, +and how the line-tracking algorithm works. For the flags and the output +schema, see the [Burndown reference](../analyzers/burndown.md). --- ## What it measures -The burndown analyzer tracks **code survival over time** by following every -line of code through Git history. It produces burndown charts showing how code -written at different points in time persists, is modified, or is deleted. -Optionally, it tracks per-file and per-developer breakdowns. +The burndown analyzer follows every line of code through Git history and +answers one question per time band: **of the lines written in this band, +how many are still alive at each later sample?** The result is the classic +burndown chart — stacked areas showing how much code from each era remains. ### Global code survival -A time-series matrix where each row is a sampling point and each column is an age band. The value at `[sample][band]` is the number of lines that were last edited during that band and still survive at that sample point. +A matrix where each row is a sample and each column is an age band. The +value at `[sample][band]` is the number of lines last written in that band +that still exist at the sample. Three figures summarise it: -This matrix produces the classic **burndown chart**: stacked area plots showing how much code from each era remains. +- **peak lines** — the sum over bands of each band's maximum, i.e. the lines + ever written that lived until at least one sample boundary; +- **current lines** — the live lines at the last sample; +- **survival rate** — `current / peak`, never above 100 %. -### Per-file burndown +A line that is written and deleted inside one sampling period never reaches +a sample boundary and is invisible to the matrix (it still shows up in the +per-commit `LinesAdded` / `LinesRemoved` of the ndjson and timeseries +formats). -When `--burndown-files` is enabled, the analyzer produces a separate survival matrix for each file, enabling file-level burndown charts. +### Per-file survival (`--burndown-files`) -### Per-developer burndown +The same matrix per file, reduced to `current_lines`, `peak_lines` and +`survival_rate`. Files are keyed by their path at the end of the window: +renames are followed (a pure `git mv` keeps the file's history), and a file +deleted inside the window stays in the list with zero current lines. +Binary files are never tracked. -When `--burndown-people` is enabled, the analyzer tracks which developer last edited each line. This reveals: +### Per-developer survival and interactions (`--burndown-people`) -- **Developer survival rates**: How much of each developer's code persists -- **Interaction matrix**: Which developers modify each other's code +Every line also remembers who wrote it. This gives: -### File ownership +- **developer survival** — per developer, the lines they wrote that are + still alive (`current_lines`), the lines they ever wrote that reached a + sample (`peak_lines`) and the ratio. Every developer the shared identity + detector knows appears, even with zeros; +- **interactions** — the overwrite matrix: one entry per `(author, + modifier)` pair counting the lines written by `author` that `modifier` + deleted or replaced inside the window. `is_self_modify` marks a developer + rewriting their own code. -When both `--burndown-files` and `--burndown-people` are enabled, the analyzer computes per-file ownership by iterating the live line segments in each file's internal tree. Each segment stores a packed `[author|tick]` value, from which the author ID is extracted to produce a `file -> author -> line_count` mapping. +### File ownership (both flags) -!!! note "Ownership requires developer tracking" - File ownership data is only available when `--burndown-people` is enabled. Without developer tracking, no author information is stored in the line segments, so file ownership will be empty. +With both flags on, each file carries `ownership` — live lines per +developer id — and the top owner (most lines; the smaller id on a tie) with +its percentage of the file's owned lines. A deleted file has an empty +ownership map and no top owner. -!!! warning "Memory usage" - Per-file and per-developer tracking significantly increases memory usage. For repositories with more than 100k commits, consider enabling hibernation (on by default). +!!! note "Memory" + Per-file tracking keeps one sparse history per path ever tracked; + per-developer tracking keeps one per developer. Both are small compared + to the line treaps the global curve needs anyway. --- @@ -2136,53 +2828,117 @@ When both `--burndown-files` and `--burndown-people` are enabled, the analyzer c ### Algorithm overview -1. **Commit traversal**: Commits are processed sequentially in topological order. For each commit, the analyzer diffs the parent tree against the current tree to find inserted, deleted, and modified lines. - -2. **Line tracking**: Each line in every file is tracked using an augmented balanced binary tree (treap). Each tree node stores a contiguous range of lines with a packed value encoding `[author_id | creation_tick]`. When lines are inserted or deleted, the tree is split and merged to maintain the correct line-to-value mapping. - -3. **Sparse history accumulation**: At each commit, the analyzer records deltas in a sparse history structure: `map[currentTick]map[creationTick]lineCountDelta`. This captures how many lines created at `creationTick` are alive at `currentTick`. - -4. **Tick aggregation**: Sparse deltas from individual commits are accumulated into tick-level snapshots by a streaming aggregator. At each sampling boundary, the aggregator emits a `TickResult` containing the full sparse history state. +1. **First-parent walk.** Burndown always follows the first parent, whatever + `--first-parent` says, because line survival is a property of the main + line of development. A merge commit is consumed as the change set it + introduces on that line: the lines it deletes are debited at the merge's + own tick, the lines it brings in are credited to it. The user's + `--first-parent` flag is never forced on any other analyzer. + +2. **Window base.** In a `--limit`, `--since` or `--head` window the tracked + state is seeded from the window's base tree — the first in-window + commit's parent — at tick 0 before the first commit is applied. A + commit's `LinesAdded` / `LinesRemoved` are therefore the lines it really + changed (the same delta devs reports for that commit), and the base + lines form band 0 of the global curve as "older than the window" code + with no author. A window starting at a root commit has no base. + +3. **Line tracking.** Each tracked file is an implicit treap of line + segments. A segment's value packs `(author, tick)`: the tick field is + exactly as wide as the window's last tick needs and the author takes + the remaining bits of the 32-bit value (without `--burndown-people` the + whole value is the tick), so no tick is ever reserved and a 45-year or + an hourly history runs like any other. Every edit splits and merges the + treap in `O(log N)`. + +4. **Deltas.** Each `File::update` reports `(current, previous, delta)`: + an insertion `(now, now, +n)`, a deletion `(now, written, −n)` per + deleted segment. The per-commit reduce turns the reports into the + commit's sparse global history (`current tick → written tick → delta`), + the per-file and per-developer histories (keyed by the author of the + affected lines) and the interaction cells (a negative report whose + author differs from, or equals, the committing developer). A path that + is re-inserted — a length mismatch after a dropped commit, a text file + turning binary and back — is debited first, so no line is ever counted + twice or leaked. + +5. **Dense matrix.** The sparse histories are laid out on the sampling + grid: band `j` holds the lines written in ticks `[j·granularity, + (j+1)·granularity)`, sample `i` is the state after the last tick of + `[i·sampling, (i+1)·sampling)`, and samples with no recorded tick repeat + the previous row. The grid runs to the newest commit's tick, so the last + sample is always the end of the window. + +6. **Figures.** Peak, current and survival per matrix; ownership from the + live segments of each file at the end of the walk. -5. **Dense matrix conversion**: The sparse history is converted to a dense matrix (`DenseHistory`) via `groupSparseHistory()`. This involves: - - **Tick normalization**: Map arbitrary tick values to sorted indices - - **Granularity grouping**: Collapse adjacent creation ticks into age bands of width `granularity` - - **Forward-fill**: For each age band, carry forward the last known value across sampling gaps to produce a complete matrix +### Key parameters -6. **Metrics computation**: The dense matrix is used to compute aggregate statistics (survival rate, peak lines, current lines), per-developer survival, and the interaction matrix. +- **`--granularity`** — ticks per age band. Wider bands, fewer columns. +- **`--sampling`** — ticks per sample. Wider periods, fewer rows; a + non-positive value means the default (30), and the two are independent. +- **`--tick-size`** — hours per tick (24 by default). All commits inside one + tick share a band. +- **`--max-changes-per-commit`** — applied exactly as devs applies it: a + commit whose raw tree diff exceeds the cap is dropped before the ticks + and identities are assigned, so burndown and devs see the same commit + set. A dropped commit's edits are never applied; the next commit that + touches an affected file finds its tracked state out of sync, debits it + and re-inserts the file whole at that commit's tick. -### Key parameters +### Age labels -- **Granularity** controls the width of each age band (in ticks). Higher values produce fewer, wider bands. -- **Sampling** controls how frequently snapshots are taken (in ticks). Higher values reduce the number of data points. -- **Tick size** defaults to a 24-hour window. All commits within the same day share one tick. +The text report groups the last sample's bands into `< 1 month`, `1-3 +months`, `3-6 months`, `6-12 months` and `> 12 months` by the band's age at +the end of the window: band `i` of `n` is `(n − i) · granularity · +tick_hours / 24 / 30` months old, so the last band is the youngest. The plot +labels its series the same way (`3mo`, `2y`, …) unless the history spans two +calendar years, in which case the bands are regrouped by year. --- ## Use cases -- **Project health monitoring**: Track the overall code survival rate. A declining rate may indicate churn or instability. -- **Developer contribution analysis**: Understand whose code persists and who rewrites existing code. -- **Code age visualization**: Generate burndown charts showing how much ancient code remains in the codebase. -- **Refactoring impact**: Measure how much code a refactoring effort actually replaced. -- **Team dynamics**: The interaction matrix reveals collaboration patterns -- who reviews and modifies whose code. +- **Project health monitoring**: track the overall survival rate. A falling + rate means the code base is being rewritten faster than it grows. +- **Developer contribution analysis**: `developer_survival` shows whose code + persists; `interactions` shows who rewrites whose code. +- **Code age visualization**: the burndown chart shows how much ancient code + remains. +- **Refactoring impact**: run with `--since` on the refactoring window and + read how much of the base tree (band 0) the window replaced. +- **File-level hot spots**: `file_survival` ranks files by surviving lines; + a low survival rate with a high peak is a file that keeps being rewritten. --- ## Limitations -- **Sequential processing**: Burndown tracks cumulative per-line state across all commits and must process commits sequentially. It cannot be parallelized across commits (though per-file processing within a commit is parallelized via goroutines). -- **Memory intensive**: Every line in every file is tracked throughout history. Large repositories (100k+ commits, 10k+ files) can require several GB of RAM. Use hibernation options to manage memory. -- **Binary files excluded**: Binary files are automatically skipped since they cannot be meaningfully diff'd line-by-line. -- **Rename tracking**: File renames are tracked using Git's rename detection. If Git does not detect a rename (e.g., content changed significantly), the file appears as a deletion + insertion. -- **Tick resolution**: The default 24-hour tick means that all commits within the same day share one tick. Sub-day granularity is not supported. -- **File ownership without developer tracking**: File ownership data requires `--burndown-people`. Without it, the `file_survival` array will have empty ownership maps. +- **Sequential processing**: line survival is cumulative state, so commits + are applied one after another (only the per-commit tree diffs used for + the cap are computed in parallel). +- **Memory**: every live line of every tracked file is a treap segment. + Large repositories need memory in proportion to their line count. +- **Binary files excluded**: a binary blob is never tracked; a text file + turning binary is debited at that commit. +- **Rename tracking**: renames come from Git's similarity detection. A move + with heavy edits appears as a deletion plus an insertion. +- **Tick resolution**: all commits inside one tick share a band; sub-tick + ordering inside a band is not represented. +- **Unknown authors**: lines older than the window (the seeded base tree) + have no author. They count in the global curve and in per-file survival, + but not in developer survival, interactions or ownership. +- **Identity capacity**: with `--burndown-people` the developer id shares + the 32-bit segment value with the tick. A window of `T` ticks leaves + `32 − ⌈log₂(T + 1)⌉` bits for developers — 2²³ for a 500-tick window, + 2¹⁷ for a 45-year daily one. Ids beyond that fold into the "unknown" + slot. --- ## See also -- [Burndown reference](../analyzers/burndown.md) — configuration keys and output schema. +- [Burndown reference](../analyzers/burndown.md) — flags and output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. @@ -2194,7 +2950,7 @@ When both `--burndown-files` and `--burndown-people` are enabled, the analyzer c This page explains the mental model behind the developers analyzer: what per-developer statistics it computes, how the bus factor is derived, and how -the aggregation pipeline is structured. For configuration keys and the output +the aggregation pipeline is structured. For the flags and the output schema, see the [Developers reference](../analyzers/developers.md). --- @@ -2202,95 +2958,151 @@ schema, see the [Developers reference](../analyzers/developers.md). ## What it measures The developers analyzer computes **per-developer contribution statistics** -across Git history, including commit counts, line changes, language breakdown, -bus factor risk, and activity time series. +across Git history: commit counts, line changes, language breakdown, bus +factor risk and activity time series. ### Developer statistics -For each developer (identified by the identity detector): - -- **Commits**: Total number of commits -- **Lines added / removed / changed**: Aggregate line statistics -- **Net lines**: `added - removed`, showing net contribution -- **Languages**: Breakdown of line changes per programming language -- **Active period**: First and last ticks of activity, number of active ticks +For each developer (identified by the shared identity detector, so +`--people-dict`, `--exact-signatures` and `--anonymize` apply): + +- **Commits**: the number of commits attributed to the developer. +- **Lines added / removed / changed**: per commit, every file change is + line-diffed against the first parent. A deleted run immediately followed + by an inserted run counts as *changed* lines (the shorter of the two), the + rest as added or removed. A new file adds its line count, a deleted file + removes it, an empty (0-byte) blob is a 0-line text side, and a pure + rename is a 0 / 0 / 0 change that still registers the file's language. + Binary blobs carry no line statistics. +- **Net lines**: `added − removed`. +- **Languages**: the same statistics per language, detected from the path + and the blob content (`Other` when no language matches). +- **Active period**: first and last ticks of activity, number of distinct + active ticks. + +### Which commits count + +The devs walk visits every commit of the window in the user's walk mode +(`--first-parent` or the full DAG, unlike burndown which always follows +the first parent). A merge commit counts as a commit for its author but +contributes no line statistics — its lines belong to the commits it merges. +A commit whose filtered tree diff is empty (a trivial merge, an +`--allow-empty` commit, a commit touching only excluded paths) is skipped +unless `--empty-commits` is set, in which case it counts as a commit with +zero lines. Commits whose raw tree diff exceeds `--max-changes-per-commit` +are dropped before any analyzer sees them. ### Language statistics Aggregated across all developers: -- **Total lines per language**: Lines added (for backward compatibility) -- **Total contribution per language**: Lines added + removed, used for bus factor and ownership calculations -- **Contributors per language**: Which developers contribute to each language, measured by total contribution (added + removed) +- **Total lines per language**: lines added. +- **Total contribution per language**: lines added + removed, the measure + the bus factor and the ownership shares use. +- **Contributors per language**: each developer's contribution. ### Bus factor -Knowledge concentration risk per language, following the [CHAOSS Contributor Absence Factor](https://chaoss.community/kb/metric-bus-factor/) methodology. +Knowledge-concentration risk per language, following the [CHAOSS +Contributor Absence Factor](https://chaoss.community/kb/metric-bus-factor/) +methodology. For each language: -For each language, the analyzer computes: - -- **Bus factor number**: The smallest number of contributors responsible for 50% of total contributions (added + removed). This is the CHAOSS standard metric. -- **Total contributors**: Number of unique contributors to that language. -- **Primary/secondary owner**: The top two contributors and their ownership percentages. -- **Risk level**: Based on primary owner concentration. +- **Bus factor number**: the smallest number of contributors responsible + for 50 % of the total contribution. +- **Total contributors**: unique contributors to that language. +- **Primary / secondary owner**: the top two contributors with their + shares. The secondary owner is reported whenever the language has at + least two contributors — developer id 0 included. +- **Risk level**: from the primary owner's share. !!! danger "Risk levels" - - **CRITICAL** (>= 90%): A single developer owns nearly all the code - - **HIGH** (>= 80%): Very concentrated ownership - - **MEDIUM** (>= 60%): Moderate concentration - - **LOW** (< 60%): Healthy distribution + - **CRITICAL** (>= 90%): a single developer owns nearly all the code + - **HIGH** (>= 80%): very concentrated ownership + - **MEDIUM** (>= 60%): moderate concentration + - **LOW** (< 60%): healthy distribution -A **project-level bus factor** is also computed across all developers and all languages, reported in the aggregate section. +A **project-level bus factor** over all developers and languages is +reported in the aggregate section. ### Activity time series -Per-tick commit counts broken down by developer. Shows contribution velocity over time. +Per-tick commit counts broken down by developer, with the tick's first and +last committer time. ### Code churn (line velocity) -Per-tick lines added and removed. This measures raw line velocity — the volume of code changes over time. High values may indicate refactoring, feature development, or instability. +Per-tick lines added and removed. High values may indicate refactoring, +feature development or instability. !!! note "Terminology" - In academic literature, "code churn" specifically refers to recently-written code that is quickly rewritten. This analyzer measures the broader concept of line velocity (total additions and removals per time period). + In academic literature, "code churn" specifically refers to + recently-written code that is quickly rewritten. This analyzer measures + the broader concept of line velocity (total additions and removals per + time period). + +### Aggregate + +`total_developers` and `active_developers` are exact counts; +`estimated_total_developers` and `estimated_active_developers` are the +same cardinalities estimated by a HyperLogLog sketch (precision 14, about +0.8 % standard error) — they are the figures a streaming or merged run can +maintain without the exact sets, and they equal the exact counts on small +inputs. `analysis_period_ticks` is the newest tick of the window. --- ## Architecture -The developers analyzer is built on the **BaseHistoryAnalyzer** and **GenericAggregator** foundations: - -1. **Consume phase**: `Consume()` extracts author ID, line stats, and language breakdown, delegating state storage to the generic aggregator framework. The analyzer retains minimal internal state. -2. **Aggregation phase**: The `GenericAggregator` automatically handles per-commit memory spilling and per-tick grouping using pure function hooks (`extractTC`, `mergeState`, `buildTick`), eliminating custom state management boilerplate. -3. **Metrics & UI phase**: A pure function pipeline (`ComputeAllMetrics`) generates the output structures for JSON, YAML, and text, while declarative chart builders (`plotpage.BuildBarChart`, `plotpage.BuildLineChart`) render the HTML visualizations. +1. **Per-commit pre-pass** (parallel): the tree diff against the first + parent, the cap check, the change filter, the per-change line + statistics and the per-blob language. +2. **Chronological reduce**: the shared identity / tick stamps key every + commit's `CommitDevData`, grouped into per-tick / per-developer buckets. +3. **Metrics**: a pure function pipeline (`compute_all_metrics`) produces + the developers, languages, bus factor, activity, churn and aggregate + sections; the json / yaml / bin / text / plot formats are encodings of + that one value, and the timeseries formats list every kept commit in + tick order. -This unified approach significantly reduces boilerplate while maintaining full support for streaming output, decoupled aggregation, and budget-aware memory spilling. +The output is a pure function of the repository and the flags: two runs +produce identical bytes. --- ## Use cases -- **Team assessment**: Understand who contributes what, in which languages, and when. -- **Bus factor analysis**: Identify languages or components where a single developer departure would create critical knowledge gaps. The CHAOSS bus factor number tells you how many people need to leave before 50% of knowledge is lost. -- **Activity monitoring**: Track developer engagement over time. Declining activity may signal burnout or attrition risk. -- **Language migration tracking**: Monitor the adoption of a new language by watching language statistics over time. -- **Onboarding evaluation**: Measure how quickly new team members ramp up by comparing their activity curves. -- **Code churn analysis**: Detect periods of high line velocity that may correlate with instability or deadline pressure. +- **Team assessment**: who contributes what, in which languages, and when. +- **Bus factor analysis**: languages where a single departure would create + a critical knowledge gap. +- **Activity monitoring**: declining activity may signal attrition risk. +- **Language migration tracking**: watch language statistics over time. +- **Onboarding evaluation**: compare the activity curves of new team + members. +- **Code churn analysis**: periods of high line velocity. --- ## Limitations -- **Identity resolution**: Developer identity is determined by the identity detector (email-based by default). Multiple email addresses for the same person will appear as separate developers unless a mailmap is configured. -- **Merge commits**: By default, merge commits are processed only once (first encounter). Trivial merges are skipped unless `ConsiderEmptyCommits` is enabled. -- **Line attribution**: Lines are attributed to the commit author, not the committer. In workflows with heavy rebasing, this may differ from expectations. -- **Contribution measurement**: Contributions are measured as lines added + lines removed. This gives fair credit to refactoring work but does not distinguish between code complexity or quality. -- **Active developer threshold**: "Active developers" are those with commits in the most recent 90-day window (when tick size is known). Falls back to the recent 30% of the analysis period when tick size is unavailable. This threshold is not configurable. +- **Identity resolution**: identities are merged by the shared detector + (a name or an e-mail seen together links the two). Use `--people-dict` + for explicit aliases and `--exact-signatures` to keep every + `name <email>` pair apart. +- **Merge commits**: counted as commits, never as lines. +- **Line attribution**: lines are attributed to the commit author, not the + committer. +- **Contribution measurement**: `added + removed` credits refactoring work + but does not distinguish complexity or quality. +- **Active developer threshold**: "active developers" are those with + commits in the most recent 90 days (`90 · 24 / tick_size_hours` ticks, + clamped at the start of the window). The threshold is not configurable. --- ## See also -- [Developers reference](../analyzers/developers.md) — configuration keys and output schema. +- [Developers reference](../analyzers/developers.md) — flags and output + schema. - [Quick start](../getting-started/quickstart.md) — run developer analysis. @@ -2301,9 +3113,9 @@ This unified approach significantly reduces boilerplate while maintaining full s # Understanding file and developer coupling This page explains the mental model behind the couples analyzer: what file and -developer coupling mean, how coupling strength is computed, and how the -aggregation pipeline is structured. For configuration keys and the output -schema, see the [Couples reference](../analyzers/couples.md). +developer coupling mean, how coupling strength is computed, and how the walk +counts renames, merges and large commits. For the output schema, see the +[Couples reference](../analyzers/couples.md). --- @@ -2316,30 +3128,62 @@ collaborating (or competing). ### File coupling -A square matrix where each cell `[i][j]` counts the number of commits in which files `i` and `j` both appeared. High co-change counts indicate tight coupling between files. +A square matrix where each cell `[i][j]` is the number of commits in the +window in which files `i` and `j` both changed. The matrix is symmetric and +the diagonal `[i][i]` is file `i`'s own change count. High co-change counts +indicate tight coupling between files. **Coupling strength** is computed using the code-maat formula: -``` -strength = co_changes / average(revisions_file1, revisions_file2) +```text +strength = co_changes / average(changes_file1, changes_file2) ``` -Where `revisions_fileN` is the diagonal element of the file matrix (the file's self-change count). The result is capped at 1.0. This normalizes coupling by the average activity of both files, so a pair of files that change together 10 times out of 20 average revisions scores 50%. +Where `changes_fileN` is the diagonal element of the file matrix. The result +is capped at 1.0. This normalizes coupling by the average activity of both +files, so a pair of files that change together 10 times out of 20 average +revisions scores 50%. !!! info "Changeset size filter" - Commits touching more than **1000 files** are excluded from coupling analysis. Mass changes (e.g., formatting, license updates, dependency bumps) produce noise rather than meaningful coupling signal. + Commits touching more than **1000 files** contribute no coupling + context. Mass changes (formatting, license updates, dependency bumps) + produce noise rather than meaningful coupling signal. Such a commit still + counts for ownership: its author owns every file it touched, and its + ndjson record keeps the commit's hash with an empty `CouplingFiles` + list and the full `AuthorFiles` map. ### Developer coupling -A matrix where each cell `[i][j]` counts the number of times developers `i` and `j` committed to the same file. Developer coupling strength uses the same code-maat formula normalized by each developer's individual commit activity. +A matrix where each cell `[i][j]` sums, over every file both developers +changed, `min(changes_i, changes_j)` — the number of file changes the two +developers share. The diagonal is a developer's own file-change count, and +the coupling strength uses the same code-maat formula over those counts. ### File ownership -For each tracked file, the analyzer reports its line count and the number of distinct contributors. Single-owner files are flagged as bus-factor risks. +For each file present at the window head, the analyzer reports its line +count (newlines in the blob at the head) and the number of distinct +contributors who changed it in the window. Single-owner files are flagged as +bus-factor risks. ### Rename tracking -The analyzer tracks file renames to avoid counting a renamed file as both a deletion and an insertion, which would break coupling chains. +The tree diff detects renames (a pure `git mv` and a move with edits above +the similarity threshold). When a rename is walked, the old path's whole +accumulated history — its co-change row and column, its change count and its +owners — moves to the new path, so the report shows ONE row under the new +name. A rename that also edits the file counts that commit as a change of the +new path. Renames chain (`a → b → c` ends up under `c`). + +### Every change counts once + +The walk visits commits oldest-first and keeps one set of files already +changed in the window. A merge commit (when `--first-parent` is off) lists, +against its first parent, every file the merged branch changed; those changes +were already counted at the branch's own commits, so the merge contributes +only the files no earlier commit of the window changed, and it records no +deletions. Under `--first-parent` the branch is not walked and the merge is +an ordinary commit that changes those files. --- @@ -2356,15 +3200,26 @@ The analyzer tracks file renames to avoid counting a renamed file as both a dele ## Architecture -The couples analyzer follows the **TC/Aggregator pattern**: +The analyzer runs on the shared history core (one commit window, one +identity detector, one tick assigner, one change filter) in three steps: -1. **Consume phase**: Each commit produces a `TC{Data: *CommitData}` containing the coupling context (list of co-changed files), per-file author touch counts (always 1 per file per commit), rename pairs, and whether the author's commit count was incremented. Commits exceeding `CouplesMaximumMeaningfulContextSize` (1000 files) are skipped, producing an empty coupling context. +1. **Per-commit products** (parallel, pure): the filtered tree diff of every + commit against its first parent. -2. **Aggregation phase**: The `Aggregator` accumulates the file co-occurrence matrix using `SpillStore[map[string]int]`, along with per-person file touch counts (`people`), commit counts (`peopleCommits`), and rename tracking. When memory pressure exceeds `SpillBudget`, the file coupling matrix is spilled to disk via gob encoding. `Collect()` merges spilled data using additive merge semantics. +2. **Chronological reduce**: every commit, oldest-first, yields a payload — + the coupling context (co-changed files, empty above the cap), the + author's file touches and the rename pairs — which the in-memory + aggregator sums into the file co-occurrence matrix and the per-developer + file touches, applying each rename as it happens. -3. **Serialization phase**: `ticksToReport()` reconstructs the full report (PeopleMatrix, PeopleFiles, Files, FilesLines, FilesMatrix, ReversedPeopleDict) from aggregated TICKs, then `ComputeAllMetrics()` produces the final typed output for any format (text, plot, JSON, YAML, binary). +3. **Report**: the accumulated state is reduced to the files present in the + newest commit of the window (with their line counts), and the typed + metrics (`file_coupling`, `developer_coupling`, `file_ownership`, + `aggregate`) are rendered for every format (text, plot, JSON, YAML, + binary, ndjson, timeseries). -**Working state** (`merges`, `seenFiles`) stays in the analyzer for merge-mode dedup across commits. **Accumulated output** (file couplings, people maps, renames) is owned entirely by the aggregator. +The output is a pure function of the repository and the flags: it does not +depend on the host CPU count. --- @@ -2372,7 +3227,7 @@ The couples analyzer follows the **TC/Aggregator pattern**: The coupling strength formula follows the **code-maat** academic standard (Adam Tornhill, "Your Code as a Crime Scene"): -``` +```text degree = shared_revisions / average(revisions_A, revisions_B) ``` @@ -2386,16 +3241,16 @@ The aggregate `avg_coupling_strength` is the mean of all per-pair coupling stren ## Limitations -- **Large commits excluded**: Commits touching more than 1000 files are skipped to avoid noise from mass changes (formatting, license headers, dependency updates). -- **No temporal decay**: All commits are weighted equally. A coupling from long ago counts the same as a recent one. Consider filtering by date range for recent coupling analysis. -- **Merge commits**: Merge commits are processed only once (first encounter) to avoid double-counting changes that appear in multiple branches. -- **File deletions**: Deleted files are included in the coupling matrix during the analysis window but may not appear in the final output if they no longer exist at HEAD. +- **Large commits excluded from coupling**: Commits touching more than 1000 files feed no coupling context (they still count for ownership). +- **No temporal decay**: All commits are weighted equally. A coupling from long ago counts the same as a recent one. Consider `--since` or `--limit` for recent coupling analysis. +- **Merge commits**: A conflict resolution made inside a merge commit touches a file an earlier commit already changed, so it is not counted as a separate change. A merge whose branch commits fall outside the window is the first commit of the window that changes those files, so they count at the merge. +- **Head-only output**: Only files present in the newest commit of the window appear; files deleted inside the window are dropped from the matrices and the ownership list, and a file recreated after a deletion keeps the deleted file's counts under the same path. --- ## See also -- [Couples reference](../analyzers/couples.md) — configuration keys and output schema. +- [Couples reference](../analyzers/couples.md) — output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. @@ -2406,8 +3261,8 @@ The aggregate `avg_coupling_strength` is the mean of all per-pair coupling stren # Understanding file history analysis This page explains the mental model behind the file history analyzer: what -per-file lifecycle data it records, how hotspots and churn are derived, and how -rename detection maintains continuity. For configuration keys and the output +per-file lifecycle data it records, how hotspots, churn and the composition +are derived, and how rename detection maintains continuity. For the output schema, see the [File history reference](../analyzers/file-history.md). --- @@ -2416,16 +3271,22 @@ schema, see the [File history reference](../analyzers/file-history.md). The file history analyzer tracks the **lifecycle of every file** through Git history, recording which commits touched each file, which developers modified -it, and aggregating line statistics per contributor. It supports rename -detection to maintain continuity across file moves. +it, and aggregating line statistics per contributor. It follows renames to +maintain continuity across file moves. ### Per-file commit history -For each file present in the repository at HEAD, the analyzer records the ordered list of commits that created, modified, or deleted it. +For each file present at the window head, the analyzer records the list of +commits in the window that created, modified or deleted it (a deletion +followed by a re-creation starts a new list: git treats that as a new file). ### Per-file contributor breakdown -For each file, a map of developer IDs to line statistics (added, removed, changed). This shows who contributed what to each file. +For each file, a map of developer IDs to line statistics (added, removed, +changed), attributed commit by commit to the commit's author. A run of +removed lines immediately followed by added lines counts as *changed* +lines. Merge commits contribute no line stats (their diff against the first +parent repeats the merged branch's changes). ### Code hotspots @@ -2439,11 +3300,38 @@ Files with high commit counts are flagged as hotspots with risk levels: ### File churn -A composite score combining commit frequency and line change volume. High-churn files may indicate instability or areas of active development. +A composite score combining commit frequency and line change volume: +`commit_count + (added + removed + changed) / 100`. High-churn files may +indicate instability or areas of active development. + +### File composition + +Every changed file is classified by the shared classifier over its path +**and** its blob content (the first 8 KiB): `binary` (NUL bytes), `image`, +`vendor`, `generated` (generated paths or a generated-code marker in the +content), `documentation`, `configuration`, `dotfile`, else `source`. + +- `composition` counts the **distinct files changed in the window** per + category: a file edited in eight commits is one file. A single-file + repository therefore reports `source 1 (100%)`, not the number of edits. +- `composition_ts` counts the distinct files changed in **each tick** + (`--tick-size`, 24 h by default), with the tick's `start_time` / + `end_time` — the first and last committer time of the tick's commits. + +A file's latest classification wins (text that became binary counts as +binary), and a rename retires the old path, so the moved file is counted +once. The composition covers every file changed in the window, including +files deleted before the window head; the per-file sections cover only the +files at the head. ### Rename support -When Git detects a file rename (e.g., `old/path.go` to `new/path.go`), the analyzer transfers the full history from the old path to the new path, maintaining a continuous record. +When Git detects a file rename (a pure `git mv`, or a move with edits above +the similarity threshold), the analyzer transfers the full commit list from +the old path to the new path and appends the rename commit, maintaining a +continuous record; the rename's own line stats (the edit, if any) are +attributed to the new path. The ndjson record of the commit carries the +`FromPath` / `ToPath` pair. --- @@ -2454,22 +3342,23 @@ When Git detects a file rename (e.g., `old/path.go` to `new/path.go`), the analy - **Onboarding guides**: New team members can see which files each developer owns to know who to ask about specific code. - **Refactoring ROI**: Identify files with both high churn and many contributors -- refactoring these produces the largest payoff. - **Risk assessment**: Files with a single contributor and high commit counts are both hot and concentrated -- a bus factor risk. +- **Change mix over time**: The composition time series shows whether a period's changes went into source, documentation, configuration or generated files. --- ## Limitations -- **HEAD-only output**: Only files present at HEAD are included in the final output. Files that were deleted before the last commit are tracked during analysis but excluded from results. -- **Merge handling**: Merge commits are processed only once to avoid double-counting. Non-merge context changes in merge commits are skipped. -- **Rename detection**: Depends on Git's rename detection heuristics. If a file is both renamed and heavily modified in the same commit, Git may not detect the rename. +- **HEAD-only per-file output**: Only files present in the newest commit of the window are listed in `file_churn`, `file_contributors`, `hotspots` and the aggregate. Files deleted inside the window are tracked during analysis (and counted in the composition) but excluded from the per-file results. +- **Merge handling**: A merge commit appends to the commit lists of the files it lists against its first parent and counts them in its tick's composition, but contributes no line stats. Under `--first-parent` a merge is an ordinary commit. +- **Rename detection**: Depends on Git's rename detection heuristics (50 % similarity). A file that is renamed and rewritten beyond that threshold in one commit is a deletion plus an insertion. - **Churn score formula**: The churn score is `commit_count + (added + removed + changed) / 100`. This is a simple heuristic, not a rigorous statistical measure. -- **No time-series**: Unlike the burndown analyzer, file history does not produce time-series data. It provides aggregate per-file statistics only. +- **Binary blobs**: A change whose blob is binary contributes no line stats (it still counts as a commit and as a `binary` file in the composition). --- ## See also -- [File history reference](../analyzers/file-history.md) — configuration keys and output schema. +- [File history reference](../analyzers/file-history.md) — output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. @@ -2481,55 +3370,87 @@ When Git detects a file rename (e.g., `old/path.go` to `new/path.go`), the analy This page explains the mental model behind the code quality analyzer: which static metrics it tracks per commit and how it aggregates them per tick. For -configuration keys and the output schema, see the +the output schema and the exact per-field definitions, see the [Quality reference](../analyzers/quality.md). --- ## What it measures -The code quality analyzer tracks **complexity, Halstead metrics, comment -quality, and cohesion** across Git history. For each commit, it runs four -static analyzers on UAST-parsed changed files and produces per-file quality -metrics. +The code quality analyzer answers one question per tick: *what did the code +touched in this period look like?* For every commit in the window it takes +the files the commit changed, parses each one's new version into a UAST, and +runs the four static analyzers on it — exactly the analyzers behind +`static/complexity`, `static/halstead`, `static/comments` and +`static/cohesion`. The per-file numbers are therefore the numbers those +commands print for the same file. -### Per-file metrics +### Per-file sample -For each changed file in a commit, the analyzer computes: +For each parsed file in a commit, the analyzer records: | Category | Metrics | |---|---| -| **Complexity** | Cyclomatic complexity, cognitive complexity, max single-function complexity, function count | -| **Halstead** | Volume, effort, delivered bugs | -| **Comments** | Overall comment score, documentation coverage | -| **Cohesion** | Cohesion score | +| **Complexity** | summed cyclomatic complexity, summed cognitive complexity, max single-function complexity, function count | +| **Halstead** | volume, effort, delivered bugs (over the file's functions, with operator and operand tokens recovered from the file bytes) | +| **Comments** | good-comment ratio, documentation coverage | +| **Cohesion** | cohesion score — only for files with at least one function | -### Statistical aggregation +The sample is computed by `cf_quality::analyze_file(root, source)`, which +feeds both the UAST and the file bytes to the component analyzers. The one +deliberate difference from the static commands is a discovery depth cap: +functions and comments nested more than ten levels below the file root are +not discovered (each discovered function is analyzed in full). -Per-tick statistics are computed from per-file arrays: +### Which files count -- **Mean, median, P95, max** for complexity and Halstead volume -- **Sum** for delivered bugs and Halstead volume -- **Min** for comment score and cohesion (worst-case tracking) -- **Total** files analyzed and functions counted +- A file counts when its parse succeeded. Unsupported languages, binaries, + blobs above 256 KiB and files whose parse fails contribute nothing — they + are not "analyzed files" and they do not pad the statistics with zeros. +- A parsed file without functions is an analyzed file: its complexity and + Halstead scalars are zero (there is nothing to measure) and its comment + scalars are whatever the comments analyzer reports. It carries **no + cohesion sample**, mirroring the static cohesion analyzer's "no data". +- Deleted files contribute nothing. ---- +### Per-tick aggregation -## Architecture +Commits are bucketed by committer time into ticks (`--tick-size`, default +24 hours). A tick's statistics are computed over the concatenated samples of +its commits — mean, median, P95 and max for complexity and Halstead volume, +sums for delivered bugs and volume, mean and minimum for the comment score, +mean and minimum for cohesion. + +A tick in which nothing was analyzed does not exist in the report: it is not +a tick with zero complexity, it is a tick with no data. The aggregate is +computed over the ticks that do exist, and the cohesion aggregates over the +ticks that hold cohesion data, so an aggregate minimum can never exceed the +matching aggregate mean. -The quality analyzer follows the **TC/Aggregator pattern**: +### Large commits -1. **Consume phase**: For each commit, `Consume()` runs four static analyzers (complexity, Halstead, comments, cohesion) on each changed file's UAST, returning per-file metrics as a `TC{Data: *TickQuality}`. The analyzer retains no per-commit state. -2. **Aggregation phase**: A `quality.Aggregator` collects TCs, merges `TickQuality` data by time bucket (tick), and produces `TICK` results. -3. **Serialization phase**: `SerializeTICKs()` converts aggregated TICKs into JSON, YAML, binary, or HTML plot output via `ComputeAllMetrics()`. +Every surviving change of a commit is analyzed; there is no per-commit file +cap other than `--max-changes-per-commit`, which drops a commit entirely when +its raw tree diff exceeds the limit. -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +--- + +## Architecture + +1. **Walk**: the shared history core yields the commit window, identity and + tick stamps; a parallel pre-pass computes each commit's filtered tree diff + and parses the changed blobs once (one parser per worker thread). +2. **Sample**: `cf_quality::analyze_file` turns each parsed file into a + `FileSample`; a commit's samples form its `TickQuality`. +3. **Reduce**: samples are merged per tick in chronological order and + `cf_quality::compute_all_metrics` derives the time series and the + aggregate. The output is a pure function of the repository and the flags. --- ## See also -- [Quality reference](../analyzers/quality.md) — configuration keys and output schema. +- [Quality reference](../analyzers/quality.md) — output schema and field definitions. - [Quick start](../getting-started/quickstart.md) — run history analysis. @@ -2540,39 +3461,51 @@ This separation enables streaming output, budget-aware memory spilling, and deco # Understanding comment sentiment analysis This page explains the mental model behind the sentiment analyzer: how it -extracts and filters comments, how VADER and the multilingual lexicon score -them, and the software-engineering domain adjustments that keep technical -language from skewing the result. For configuration keys and the output schema, -see the [Sentiment reference](../analyzers/sentiment.md). +finds the comments a commit introduced or changed, how it filters them, how +VADER and the multilingual lexicon score them, and the software-engineering +domain adjustments that keep technical language from skewing the result. For +the flags and the output schema, see the +[Sentiment reference](../analyzers/sentiment.md). --- ## What it measures -The sentiment analyzer classifies **comment sentiment** across Git history. For -each commit, it extracts new or changed comments via UAST parsing, filters out -noise, and classifies the remaining comments as positive, negative, or neutral. -This reveals how developer sentiment evolves over time. +The sentiment analyzer tracks the **emotional tone of the comments developers +write** over Git history. For each commit it looks at the comments that +commit added or edited, filters out noise, scores the rest, and reports one +score per tick that received such comments. This reveals how developer +sentiment evolves over time — without re-counting the unchanged comments of +every touched file. ### Per-commit comment extraction -For each commit, the analyzer: - -1. Computes the UAST diff between the old and new file versions -2. Extracts comment nodes from the new UAST -3. Merges adjacent comment lines into single blocks -4. Filters out noise (short comments, license headers, function signatures) -5. Returns the filtered comments as a `TC` payload (`sentiment.CommitResult`) +For every Insert or Modify change of a commit that survives the shared change +filter, the analyzer: + +1. Parses the **new** version of the file into a UAST and collects its + `Comment` nodes; for a Modify it parses the **old** version too (both + parses go through the per-commit parse cache, so each blob is parsed once). +2. Merges adjacent comment lines into blocks on each side (a block comment, a + run of `//` lines, a Javadoc / Doxygen block whose ` * ` continuation + markers are stripped). +3. Filters each side's blocks (see below). +4. Keeps the **multiset difference new − old**: a block whose text also + occurs in the old version cancels one occurrence and is not counted. A + commit that only adds code to a commented file therefore contributes no + comment; an edited comment counts once, as new text; a brand-new file + contributes all of its comments. ### Comment filtering Comments are filtered using several heuristics: -- **Minimum length**: Comments shorter than the threshold are skipped -- **Letter ratio**: At least 60% of characters must be Unicode letters (filters out commented-out code) -- **First character**: Must start with a Unicode letter or digit (supports all scripts) -- **License detection**: Comments matching license/copyright patterns (including UK "licence" spelling) are excluded -- **Function name removal**: Inline function references like `doThing()` are stripped before analysis +- **Minimum length**: comments shorter than `--min-comment-len` characters (default + 20, after delimiters are stripped) are skipped; `0` disables the check +- **Letter ratio**: at least 60% of the bytes must be Unicode letters (filters out commented-out code) +- **First character**: must be a Unicode letter or digit (supports all scripts) +- **License detection**: comments matching license/copyright patterns (including UK "licence" spelling) are excluded +- **Function name removal**: inline function references like `doThing()` are stripped before analysis ### Multilingual support @@ -2584,18 +3517,29 @@ The analyzer supports comments in any language that uses Unicode letters: - **Latin**: English, French, German, Spanish, etc. - All other Unicode letter scripts -Comment filtering uses Unicode-aware regex patterns (`\p{L}` for letters, `\p{N}` for digits) rather than ASCII-only ranges, ensuring comments in non-English languages are not silently dropped. +Comment filtering uses Unicode-aware character classes (`\p{L}` for letters, +`\p{N}` for digits) rather than ASCII-only ranges, ensuring comments in +non-English languages are not silently dropped. !!! info "Multilingual sentiment scoring" Sentiment scoring uses VADER's English lexicon as the base, extended with **93,000+ multilingual word entries** from the Chen-Skiena lexicon dataset (ACL 2014) covering **32 languages**. Non-ASCII words from the dataset are injected into VADER's lexicon at startup, enabling basic sentiment scoring for comments in Russian, Chinese, Japanese, Korean, Arabic, and 27 other languages. VADER's grammatical rules (negation, intensifiers) still operate on English syntax, so scoring accuracy for non-English comments is lower than for English — but significantly better than no coverage. -### Sentiment classification +### Sentiment scoring -Filtered comments are classified as positive, negative, or neutral using **VADER** (Valence Aware Dictionary and sEntiment Reasoner) via the [GoVader](https://github.com/jonreiter/govader) library, enhanced with **software engineering domain adjustments**. +Filtered comments are scored with **VADER** (Valence Aware Dictionary and +sEntiment Reasoner), implemented in `cf-sentiment` over the vendored VADER +lexicon and emoji table, enhanced with **software engineering domain +adjustments**. #### VADER base scoring -VADER is a lexicon and rule-based sentiment analyzer designed for social media and short text. It handles negations, intensifiers, and punctuation. The compound score (-1 to 1) is mapped to our [0, 1] range. +VADER is a lexicon and rule-based sentiment analyzer designed for social +media and short text. It handles negations, intensifiers, contrastive `but`, +idioms, ALLCAPS emphasis, `!` / `?` emphasis and emoji. The compound score in +`[-1, 1]` is mapped to the reported `[0, 1]` range (`score = (compound + 1) / +2`, so `0.5` is neutral). The implementation reproduces the published VADER +example scores (for instance `VADER is smart, handsome, and funny!` → +compound `0.8439`). #### Multilingual lexicon extension @@ -2611,65 +3555,81 @@ At startup, the analyzer injects ~93,000 multilingual word entries from the [Che Only non-ASCII words are injected to avoid overriding VADER's curated English entries. Words receive binary valence: +1.5 (positive) or -1.5 (negative), which is the mid-range of VADER's scale. -To regenerate lexicons from updated source data: - -```bash -go run tools/lexgen/lexgen.go -pos pos_words.txt -neg neg_words.txt \ - -o internal/analyzers/sentiment/lexicons/lexicon_data.gen.go -``` +The lexicon data is vendored as one tab-separated file per language under +`crates/cf-sentiment-lexicons/data/` (e.g. `de.tsv`, `ja.tsv`), with a +`MANIFEST.tsv` listing them. The `cf-sentiment-lexicons` build script compiles +these tables into the crate at build time, so updating a lexicon is a matter of +editing the corresponding `data/*.tsv` file and rebuilding — no code generation +step is required. #### SE-domain adjustments -VADER frequently misclassifies technical terms that sound negative in natural language but are neutral in software engineering. The analyzer applies domain-specific adjustments: +VADER scores some words negative that are neutral in software engineering, +and it does not know some words that are negative there. The scorer matches +the lists below as **whole words** (or whole phrases) — `deadline` is not +`dead`, `skill` is not `kill`, `hacker` is not `hack` — and adjusts the +comment's compound score: -**Neutralized terms** (pushed toward neutral): -`kill`, `abort`, `fatal`, `terminate`, `dead`, `destroy`, `panic`, `deprecated`, `obsolete`, `execute`, `exploit`, `conflict`, `revert`, `reject`, `critical` - -**Genuinely negative in SE** (pushed toward negative): -`hack`, `hacky`, `kludge`, `workaround`, `spaghetti`, `nightmare`, `technical debt` - -#### Comment length weighting - -Longer comments carry more weight in the sentiment score, as they tend to contain more meaningful sentiment signal. Weight is capped at 3x the average to prevent a single long comment from dominating. - -#### Trend analysis - -Sentiment trend is computed using **linear regression** (least squares) rather than a simple first-to-last comparison. This makes the trend robust to outliers and intermediate noise. - ---- +- every **neutralizer** hit contributes `(0 − compound) × 0.8`; +- every **negative term** hit contributes its shift; +- the mean contribution is added to the compound, which is then clamped to + `[-1, 1]`. -## SE-domain lexicon - -The following terms have special handling in the sentiment scorer: - -### Technical terms neutralized (not negative) - -These terms are common in SE but trigger VADER's negative scoring: +**Neutralized terms** (whole-word hits pull the comment toward neutral). The +list holds exactly the terms the VADER lexicon scores negative that are +neutral in code comments; a word VADER does not score cannot be misclassified +by it and is deliberately not listed: | Term | Context | |---|---| -| `kill` | Process management | -| `abort` | Transaction/operation cancellation | -| `fatal` | Error severity levels | -| `terminate` | Process/thread lifecycle | -| `panic` | Error handling (Go, Rust) | -| `deprecated` | API lifecycle | -| `execute` | Command/query execution | -| `conflict` | Merge conflicts | -| `critical` | Severity levels | +| `kill`, `killed`, `killing`, `kills` | process management | +| `fatal` | error severity levels | +| `dead` | dead code, dead letters, dead-lock | +| `destroy`, `destroyed` | object lifecycle | +| `panic` | error handling (Go, Rust) | +| `obsolete` | API lifecycle | +| `exploit` | security | +| `conflict`, `conflicts` | merge conflicts | +| `reject`, `rejected` | request handling, promises | +| `critical` | severity levels, critical sections | +| `argument`, `arguments` | function arguments | +| `block`, `blocked` | blocks, blocking calls | +| `interrupt`, `interrupted` | signals, threads | +| `trap` | signal / instruction traps | +| `dump` | core dumps, data dumps | + +**Genuinely negative in SE** (whole-word hits push the comment toward +negative; none of these is scored by the VADER lexicon): + +| Term | Shift | Indication | +|---|---|---| +| `hack` | −0.3 | quick-and-dirty solutions | +| `hacky` | −0.4 | quick-and-dirty solutions | +| `kludge` | −0.5 | inelegant fixes | +| `workaround` | −0.2 | avoiding the root cause | +| `technical debt` | −0.3 | accumulated shortcuts | +| `spaghetti` | −0.4 | poor code structure | +| `nightmare` | −0.4 | maintenance difficulty | -### Terms with genuine negative sentiment +Words VADER already scores negative (`awful`, `terrible`, `horrible`, `debt`, +`bug`, …) are not listed: they keep their lexicon valence. -These terms indicate real frustration or code quality issues: +#### Comment length weighting -| Term | Indication | -|---|---| -| `hack` / `hacky` | Quick-and-dirty solutions | -| `kludge` | Inelegant fixes | -| `workaround` | Avoiding root cause | -| `spaghetti` | Poor code structure | -| `nightmare` | Maintenance difficulty | -| `technical debt` | Accumulated shortcuts | +Longer comments carry more weight in a tick's score, as they tend to contain +more meaningful sentiment signal. A comment's weight is its length in characters +over the tick's average comment length, capped at 3× to prevent a single long +comment from dominating. + +#### Tick scores, trend and periods + +A tick's score is the length-weighted mean of its comments' scores. Only +ticks with at least one scored comment receive a score; a tick of code-only +commits is not "neutral" and not "negative" — it has no data and is absent +from the time series. The trend is the least-squares line through the scored +ticks, evaluated at the first and last scored tick and clamped to `[0, 1]`; +the low-sentiment periods are the scored ticks at or below the negative +threshold, worst first. --- @@ -2686,17 +3646,18 @@ These terms indicate real frustration or code quality issues: ## Limitations - **Non-English scoring accuracy**: While 32 languages have lexicon coverage via the Chen-Skiena dataset, VADER's grammatical rules (negation handling, intensifiers, punctuation effects) are English-specific. Non-English comments get word-level sentiment from the lexicon but miss syntactic nuances. -- **UAST dependency**: Requires UAST parsing support for the target language. Files in unsupported languages are skipped. +- **UAST dependency**: Requires UAST parsing support for the target language. Files in unsupported languages are skipped (shell scripts fall back to line-wise `#` comments). - **Sarcasm**: Sarcasm, irony, and context-dependent meaning can mislead the classifier. Comments like "great, another production outage" may be scored as positive. - **Comment extraction**: Only comments that appear in the UAST are analyzed. Preprocessor directives, build file comments, and non-code files are excluded. -- **CPU intensive**: The sentiment analyzer performs UAST parsing for every modified file in every commit. For large repositories, this is significantly slower than non-UAST analyzers. It benefits from parallel execution via the framework's worker pool. -- **Minimum length filter**: The default minimum of 20 characters filters out many short but potentially meaningful comments (e.g., `// FIXME`, `// HACK`). Lower `MinLength` to capture these, at the cost of more noise. +- **Whole-comment adjustment**: an SE-domain hit adjusts the whole comment's compound score, not just the matched word, so a positive remark that also mentions `kill` is pulled toward neutral. +- **CPU intensive**: The sentiment analyzer parses both versions of every modified file in every commit. For large repositories, this is significantly slower than non-UAST analyzers; the per-commit pre-pass runs in parallel. +- **Minimum length filter**: The default minimum of 20 characters filters out many short but potentially meaningful comments (e.g., `// FIXME`, `// HACK`). Lower `--min-comment-len` to capture these, at the cost of more noise. --- ## See also -- [Sentiment reference](../analyzers/sentiment.md) — configuration keys and output schema. +- [Sentiment reference](../analyzers/sentiment.md) — flags, tick semantics and output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. @@ -2708,8 +3669,8 @@ These terms indicate real frustration or code quality issues: This page explains the mental model behind the shotness analyzer: what node-level change frequency and coupling mean, how coupling strength is -computed, and how the aggregation pipeline is structured. For configuration -keys and the output schema, see the +computed, and how the walk is structured. For configuration keys, the entity +lifecycle table and the output schema, see the [Shotness reference](../analyzers/shotness.md). --- @@ -2721,19 +3682,28 @@ individual code entities (functions, methods, classes) across Git history. Unlike the couples analyzer which operates at file granularity, shotness operates at the UAST node level, providing fine-grained co-change analysis. -### Node change frequency +### Entity change frequency -For each code entity matched by the DSL query (functions by default), the analyzer counts how many commits modified lines within that entity's span. Entities that change frequently are "hot" -- they are likely volatile, complex, or central to the system. +For each declaration the structural DSL selects (every function-role node by +default — top-level functions, class methods, nested functions), the analyzer +counts how many commits modified lines within that entity's span. Entities +that change frequently are "hot" -- they are likely volatile, complex, or +central to the system. An entity is identified by its type, its **qualified +name** (`Class.method`, `Receiver.method`, `outer.inner`) and its file, so +same-named methods on different types never merge. -### Node co-change coupling +### Entity co-change coupling -When two code entities are modified in the same commit, their coupling counter is incremented. This produces a fine-grained coupling matrix at the function level, which is more precise than file-level coupling from the couples analyzer. +When two entities are modified in the same commit, their coupling counter is +incremented. This produces a fine-grained coupling matrix at the entity +level, which is more precise than file-level coupling from the couples +analyzer. ### Coupling strength Coupling strength is normalized to a 0-1 scale using the formula: -``` +```text strength(A, B) = co_changes(A, B) / max(co_changes(A, B), changes(A), changes(B)) ``` @@ -2741,7 +3711,7 @@ This ensures the result is always in [0, 1] and provides a meaningful confidence ### Risk classification -Nodes are classified into risk levels based on absolute change counts: +Entities are classified into risk levels based on absolute change counts: | Risk Level | Threshold | Meaning | |---|---|---| @@ -2751,25 +3721,48 @@ Nodes are classified into risk levels based on absolute change counts: ### How it works -For each commit: - -1. Parse the before and after versions of each changed file into UAST -2. Apply the `dsl_struct` query to select target nodes (e.g., functions) -3. Apply the `dsl_name` query to extract the name of each node -4. Map diff hunks to nodes using line-range overlaps -5. Emit a per-commit TC (Transient Commit result) with touched node deltas and coupling pairs - -After all commits are processed, the Aggregator accumulates TCs into a final report with sorted nodes and a sparse co-change matrix. +For each commit of the window, oldest first: + +1. Compute the tree diff against the first parent (the whole tree for a + root commit); drop the commit if the raw diff exceeds + `--max-changes-per-commit`; apply the language / vendor / generated + filter. +2. Parse the Before and After versions of each changed file into a UAST + (blobs above 256 KiB are skipped). +3. Run the structural query (`dsl_struct`) over each tree to select the + declarations, name each one with the name query (`dsl_name`) and qualify + it with its enclosing declarations and receiver type. +4. Line-diff the two versions and map every hunk to the entities whose + line span it overlaps. +5. Record the commit's **effects**: the entities it touched (one change + each, coupled pairwise), the entities whose declaration or file + disappeared (ended), and the entities a file rename moved. + +Steps 1–4 are pure per-commit work and run in parallel across cores; step 5 +runs sequentially in commit order over the live entity state, so the output +is a pure function of the repository and the flags, never of the host. +Merge commits are skipped (their diff re-shows the side line already +walked) unless the walk is `--first-parent`. ### Architecture -The shotness analyzer follows the **TC/Aggregator** pattern: - -- **Consume phase**: Per-commit processing builds working state (`nodes`, `files` maps for deletion/rename tracking) and emits a `TC{Data: *CommitData}` with node touch deltas and coupling pairs. -- **Aggregation phase**: The `Aggregator` accumulates node counts and coupling matrices from the TC stream. It supports disk-backed spilling via `SpillStore` for memory-bounded operation. -- **Serialization phase**: `SerializeTICKs()` converts aggregated tick data into the `Nodes`/`Counters` report consumed by `ComputeAllMetrics()` and plot generation. +- **Pre-pass** (`cf-commands`, shared with the other UAST history + analyzers): the tree diff, the per-commit parse cache and the entity + extraction produce one state-independent *product* per change. +- **Reduce** (`cf-commands` → `cf-shotness`): the reducer turns each + commit's products into `EntityEffect` records (touched / ended / moved + from) over the live `ShotnessState`, resolving file-level events into the + entities currently declared in the file. +- **Report** (`cf-shotness`): replaying the effect records in walk order + over an empty state yields the live entity set; its key-sorted + `Nodes`/`Counters` matrix feeds `compute_all_metrics` (hotness, coupling, + hotspots, aggregate), the plot sections and the text report. The ndjson + and timeseries formats list the same per-commit records. -The `nodes` map remains in the analyzer as working state because `handleDeletion`, `handleInsertion`, `handleModification`, and `applyRename` read and mutate it during `Consume()`. The aggregator maintains its own separate accumulation of counts and couplings. +There is one source of truth — the effect records and the state transition +that replays them — so a multi-analyzer run (which shares the walk with +imports, quality, sentiment and typos) and a standalone run produce the +same report. --- @@ -2804,18 +3797,19 @@ The `nodes` map remains in the analyzer as working state because `handleDeletion ## Limitations -- **UAST required**: Only languages with UAST parser support are analyzed. Files in unsupported languages are skipped entirely. -- **CPU intensive**: The analyzer performs UAST parsing on both the before and after versions of every changed file in every commit. This makes it one of the most expensive analyzers. It benefits from parallel execution. -- **Name collisions**: If two functions in different files have the same name, they are tracked as distinct nodes (the file path is part of the key). However, if a file is renamed, the analyzer updates all associated nodes. -- **Shallow extraction within a file**: When multiple structural nodes in the same file share the same extracted name (e.g., nested functions with identical names), only one is tracked. The last one encountered wins. Qualified paths (e.g., `OuterClass.innerMethod`) are not built. -- **DSL limitations**: The DSL query must match nodes that have position information (`Pos` field) in the UAST. Nodes without position data cannot be mapped to diff hunks. -- **Large functions**: A change anywhere within a function's line range counts as a change to that function. Very large functions (hundreds of lines) will have inflated change counts. +- **UAST required**: Only languages with UAST parser support are analyzed. Files in unsupported languages, binary blobs, blobs above 256 KiB and files that fail to parse are skipped for that commit. +- **CPU intensive**: The analyzer parses both the before and after versions of every changed file in every commit. The per-commit work is spread across cores; the reduce is sequential. +- **The report is the present**: an entity that no longer exists at the newest commit of the window (its declaration was removed or its file deleted) is dropped, together with its couplings. An in-place rename of a declaration (`f` → `f2`) is an end plus a birth: the history of `f` does not carry over. A file rename does carry everything over. +- **Qualification follows the mapping**: Rust `impl` methods are not qualified by the `impl` target, and a language whose mapping sets no declaration names yields no entities. Two selected nodes in one file that resolve to the same type and qualified name are one entity. +- **Position required**: a selected node without position information cannot be mapped to diff hunks and is never touched. +- **Large entities**: A change anywhere within an entity's line range counts as a change to that entity. Very large functions (hundreds of lines) will have inflated change counts, and a change inside a method also touches the enclosing class when classes are selected. +- **Merge commits** are not counted (see the lifecycle table in the reference), so a side line's work is attributed to its own commits. --- ## See also -- [Shotness reference](../analyzers/shotness.md) — configuration keys and output schema. +- [Shotness reference](../analyzers/shotness.md) — configuration keys, the entity lifecycle and the output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. @@ -2842,15 +3836,17 @@ and their corrections by analyzing UAST identifier changes across Git history. For each commit, the analyzer: -1. Computes the diff between the old and new versions of each changed file -2. Identifies delete/insert hunk pairs of equal size (same number of lines) -3. Compares corresponding lines using **Levenshtein distance** -4. For line pairs within the distance threshold, extracts UAST identifiers from the old and new versions -5. If exactly one identifier changed between the two versions, it records a typo-fix pair +1. Computes the line diff between the old and new versions of each changed file (both versions must be text and parse with the UAST) +2. Aligns delete/insert hunk pairs of equal size (line *i* of each) +3. Keeps the aligned line pairs whose whole lines are within the distance threshold — a cheap prefilter that bounds the UAST work +4. Extracts the UAST identifiers of each kept line pair and diffs them as multisets: identifiers present on both lines cancel, whatever their position +5. Records a typo-fix pair when exactly one identifier remains on each side, both are non-empty and different, and the two identifiers are within the distance threshold of each other + +The distance is therefore applied twice: to the whole line (prefilter) and to the identifier pair (the rule). A literal change next to an unchanged identifier leaves nothing after cancellation and is never a typo; a rename to an unrelated name fails the pair bound. ### Levenshtein distance -The Levenshtein distance is the minimum number of single-character edits (insertions, deletions, substitutions) needed to transform one string into another. A small distance between the old and new line suggests a typo fix rather than a semantic change. +The Levenshtein distance is the minimum number of single-character edits (insertions, deletions, substitutions) needed to transform one string into another. A small distance between the old and new identifier suggests a typo fix rather than a semantic change. !!! info "Example" - `recieve` to `receive` -- distance 2 (transposed characters) @@ -2861,13 +3857,11 @@ The Levenshtein distance is the minimum number of single-character edits (insert ## Architecture -The typos analyzer follows the **TC/Aggregator pattern**: - -1. **Consume phase**: For each commit, `Consume()` computes diffs, identifies line pairs within Levenshtein distance, and extracts UAST identifier changes. Per-commit typos are returned as `TC{Data: []Typo}`. The analyzer retains no per-commit state; only the `lcontext` (Levenshtein context) is kept as working state. -2. **Aggregation phase**: A `typos.Aggregator` collects TCs into a `SliceSpillStore[Typo]`. `FlushTick()` deduplicates typos by `wrong|correct` key (keeping the first occurrence), returning a `TickData` with the unique set. -3. **Serialization phase**: `SerializeTICKs()` assembles all tick data into an `analyze.Report{"typos": allTypos}`, then delegates to `ComputeAllMetrics()` for JSON, YAML, binary, or HTML plot output. +The detection rules and the metrics live in the `cf-typos` crate; the history walk in `cf-commands` supplies the git and UAST inputs: -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +1. **Per-commit product**: for every Modify change the walk reads both blobs, runs the libgit2 line diff and both UAST parses, and asks `cf_typos::candidate_line_pairs` for the aligned line pairs within the prefilter and `cf_typos::identifier_pair` for the one identifier each pair changed. The product is the commit's list of occurrences (file, 1-based line, commit); it is a pure function of the commit, so it is computed in parallel across cores. +2. **Chronological reduce**: the shared history core stamps every commit with its identity and tick in oldest-first order, and the occurrences are concatenated in that order — the report never depends on how the work was distributed. +3. **Metrics**: `cf_typos::metrics_report_value` derives `typo_list` (each distinct pair once, first fix wins), `patterns` (occurrence counts above one), `file_typos` and `aggregate` from the occurrence list, as one value tree that json, yaml and bin encode. --- @@ -2884,10 +3878,10 @@ This separation enables streaming output, budget-aware memory spilling, and deco ## Limitations - **UAST required**: Only languages with UAST parser support are analyzed. Identifiers in unsupported languages are not extracted. -- **Single-identifier changes only**: The analyzer only records a typo when exactly one identifier changes between the old and new lines. Multi-identifier changes are skipped to avoid false positives. +- **Single-identifier changes only**: The analyzer only records a typo when exactly one identifier differs between the old and new lines after the shared identifiers cancel. Multi-identifier changes are skipped to avoid false positives. - **Equal-length hunks only**: Only delete/insert hunk pairs with the same number of lines are considered. A typo fix that also adds or removes lines will be missed. - **False positives**: Intentional identifier renames with small Levenshtein distance (e.g., `idx` to `jdx`) will be reported as typos. -- **Deduplication**: Typo pairs are deduplicated by the `wrong|correct` key. The same typo fixed in multiple commits is reported only once. +- **One list entry per pair**: `typo_list` reports each `wrong → correct` pair once, at its first fix; the number of times the pair was fixed is in `patterns` (when above one) and in the per-file and aggregate counts. - **CPU intensive**: Like all UAST-based analyzers, the typos analyzer parses both file versions for every changed file in every commit. --- @@ -2920,27 +3914,29 @@ that deviate significantly from the historical baseline. ### Z-score analysis -For each time tick, the analyzer computes Z-scores for six metrics using a trailing sliding window: +For each time tick, the analyzer computes Z-scores for six metrics using a trailing window: | Metric | Description | |---|---| | **Net churn** | `lines_added - lines_removed` | -| **Files changed** | Number of files modified in the tick | +| **Files changed** | Number of **distinct** files touched in the tick (a file modified by several commits counts once; a deleted file counts) | | **Lines added** | Total lines added across all commits in the tick | | **Lines removed** | Total lines removed across all commits in the tick | | **Language diversity** | Number of distinct programming languages modified | | **Author count** | Number of distinct developers active in the tick | -A Z-score measures how many standard deviations a value is from the rolling mean. When any metric's absolute Z-score exceeds the threshold, the tick is flagged as anomalous. +A Z-score measures how many standard deviations a value is from the rolling mean. When any metric's absolute Z-score strictly exceeds the threshold, the tick is flagged as anomalous. !!! info "Z-score interpretation" - **|Z| < 2.0**: Within normal variation - **|Z| 2.0 - 3.0**: Unusual, worth investigating - **|Z| > 3.0**: Highly anomalous, likely a significant event -### Sliding window +### Trailing window -The Z-score is computed against a trailing window of the previous N ticks (default 20). This means the "normal" baseline adapts to gradual trends, and only sudden deviations are flagged. +The Z-score of a tick is computed against the previous `window_size` ticks (default 20) — the ticks that exist when the history is shorter — using the mean and the population standard deviation of that window. The "normal" baseline therefore adapts to gradual trends, and only sudden deviations are flagged. + +A window needs at least two observations: the first tick has no window and the second has a single observation, so both score `0`. When a window of two or more identical values is followed by a different value, there is no variance to scale by and the signed sentinel `±100` is assigned. ### Anomaly records @@ -2950,22 +3946,20 @@ Each detected anomaly includes: - **Z-scores**: Per-metric Z-scores showing which dimension is anomalous - **Max absolute Z-score**: The highest Z-score across all metrics (used for severity ranking) - **Raw metrics**: The actual values for that tick -- **Files**: List of files changed in the anomalous tick +- **Files**: The distinct files changed in the anomalous tick -Anomalies are sorted by severity (highest absolute Z-score first). +Anomalies are sorted by severity (highest absolute Z-score first, ties by tick). When nothing is flagged the list is empty, never absent. --- ## Architecture -The anomaly analyzer follows the **TC/Aggregator pattern**: - -1. **Consume phase**: For each commit, `Consume()` extracts file changes, line stats, language diversity, and author ID, returning per-commit metrics as a `TC{Data: *CommitAnomalyData}`. The analyzer retains no per-commit state. -2. **Aggregation phase**: An `anomaly.Aggregator` collects TCs, merges `CommitAnomalyData` into per-tick `TickMetrics` (aggregating files, lines, languages, and unique authors), and produces `TICK` results. -3. **Z-score detection**: `ticksToReport()` runs sliding-window Z-score analysis on six dimensions over the aggregated tick metrics to detect anomalies. -4. **Serialization phase**: `SerializeTICKs()` converts aggregated TICKs into JSON, YAML, binary, or HTML plot output via `ComputeAllMetrics()`. +The math lives in the `cf-anomaly` crate; the history walk in `cf-commands` supplies the per-commit inputs: -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +1. **Per-commit product**: for every commit the walk takes the filtered tree diff against the first parent, the files it names (a delete names the deleted file), the line stats (skipped for merges) and the language of every touched blob — a pure function of the commit, computed in parallel across cores. +2. **Chronological reduce**: the shared history core stamps every commit with its identity and tick in oldest-first order; `cf_anomaly::aggregate_commits_to_ticks` merges the commits of a tick (lines add up; files, languages and authors merge as sets). +3. **Z-score detection**: `cf_anomaly::detect_anomalies_from_ticks` runs the trailing-window Z-scores on the six dimensions with the validated `Config` (`--anomaly-window`; threshold 2.0). +4. **Serialization**: `compute_all_metrics` produces one value tree (anomalies, time series, aggregate) that json, yaml and bin encode; the plot path adds the cross-analyzer enrichment over the co-selected analyzers' time series. --- @@ -2982,10 +3976,10 @@ This separation enables streaming output, budget-aware memory spilling, and deco ## Limitations -- **Cold start**: The first `window_size` ticks have no or limited history for Z-score computation. Anomalies during this period are less reliable. +- **Cold start**: The first ticks have little history: the first two score `0` on every metric, and the following ones are scored against a window shorter than `window_size`. Anomalies early in a history are less reliable. - **Gradual drift**: Z-scores detect sudden deviations, not gradual trends. A slow but steady increase in churn will update the rolling mean and never trigger an anomaly. -- **Zero-variance sentinel**: When the window has zero standard deviation (all values identical) and the current value differs, a sentinel Z-score of 100 is assigned. This ensures detection but may inflate severity rankings. -- **Tick granularity**: Metrics are aggregated per tick (default 24-hour window). Sub-tick anomalies are not detected. +- **Zero-variance sentinel**: When the window (two or more observations) has zero standard deviation and the current value differs, a sentinel Z-score of `±100` is assigned. This ensures detection but may inflate severity rankings. +- **Tick granularity**: Metrics are aggregated per tick (default 24 hours, `--tick-size`). Sub-tick anomalies are not detected, and days without commits are not ticks. - **Not causal**: The analyzer detects statistical anomalies, not root causes. Investigation is required to determine whether an anomaly represents a real problem. - **Multi-dimensional**: An anomaly is flagged when *any* single metric exceeds the threshold. This means false positive rates increase with the number of metrics (currently six). diff --git a/llms.txt b/llms.txt index b1c34527..9d1ed1ce 100644 --- a/llms.txt +++ b/llms.txt @@ -2,7 +2,7 @@ > Codefang is a comprehensive code analysis platform that understands your code deeply — not just as text, but as structure (AST) and history (Git) — for tracking technical debt, analyzing developer churn, and feeding context to AI agents. -Codefang is a spiritual successor to source{d}'s `hercules`, rebuilt on a modern engine. It splits into two binaries: `uast` (the parser, turning source into a Universal Abstract Syntax Tree across 60+ languages via Tree-sitter) and `codefang` (the analyzer, consuming UASTs for static analysis or Git repositories for history mining). Analyzers are pluggable, and the tool is designed to be AI-agent ready through an MCP server and a UNIX-pipe-friendly CLI. +Codefang is a spiritual successor to source{d}'s `hercules`, rebuilt on a modern engine. It splits into two binaries: `uast` (the parser, turning source into a Universal Abstract Syntax Tree across 60+ languages via Tree-sitter) and `codefang` (the analyzer, consuming UASTs for static analysis or Git repositories for history mining). Analyzers are pluggable, every report has deterministic machine formats (JSON, YAML, NDJSON, time series, binary), and the CLI is UNIX-pipe-friendly; an MCP server library exists but no command launches it yet. ## README @@ -18,7 +18,7 @@ Codefang is a spiritual successor to source{d}'s `hercules`, rebuilt on a modern - [How to run Codefang in CI](https://sumatoshi-tech.github.io/codefang/how-to/run-in-ci/): wire Codefang into a continuous-integration pipeline. - [How to read a burndown chart](https://sumatoshi-tech.github.io/codefang/how-to/read-a-burndown-chart/): interpret the survival-over-time output of the burndown analyzer. - [How to analyze a bare repository](https://sumatoshi-tech.github.io/codefang/how-to/analyze-a-bare-repo/): run history analysis against a bare Git clone with no working tree. -- [How to attach an MCP client](https://sumatoshi-tech.github.io/codefang/how-to/attach-an-mcp-client/): connect an AI agent to Codefang through the MCP server. +- [How to attach an MCP client](https://sumatoshi-tech.github.io/codefang/how-to/attach-an-mcp-client/): how an AI agent will connect through the MCP server once a launcher ships (not available in this release). ## Reference diff --git a/mkdocs.yml b/mkdocs.yml index 540619f3..30b4c48f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -126,6 +126,7 @@ nav: - Comments: analyzers/comments.md - Imports (static): analyzers/imports.md - Clones: analyzers/clones.md + - Composition: analyzers/composition.md - History analyzers: - Burndown: analyzers/burndown.md - Developers: analyzers/developers.md @@ -143,7 +144,7 @@ nav: - Architecture: - Overview: architecture/overview.md - UAST system: architecture/uast.md - - Streaming pipeline: architecture/streaming-pipeline.md + - History pipeline: architecture/streaming-pipeline.md - Explanation: - Static analyzers: - Complexity metrics: explanation/complexity.md diff --git a/site/analyzers/anomaly.md b/site/analyzers/anomaly.md index f6d14d9c..0135b5c1 100644 --- a/site/analyzers/anomaly.md +++ b/site/analyzers/anomaly.md @@ -14,8 +14,8 @@ it, see the [Quick start](../getting-started/quickstart.md). | Option | Type | Default | Description | |---|---|---|---| -| `TemporalAnomaly.Threshold` | `float` | `2.0` | Z-score threshold for anomaly detection (in standard deviations). Ticks where any metric exceeds this threshold are flagged. | -| `TemporalAnomaly.WindowSize` | `int` | `20` | Sliding window size in ticks for computing rolling mean and standard deviation. Minimum value: 2. | +| `TemporalAnomaly.Threshold` | `float` | `2.0` | Z-score threshold for anomaly detection (in standard deviations). A tick is flagged when any metric's absolute Z-score **strictly exceeds** it. Must be `> 0`. | +| `TemporalAnomaly.WindowSize` | `int` | `20` | Trailing window size in ticks for computing the rolling mean and standard deviation. Must be `>= 2`. | ```yaml # .codefang.yml @@ -25,7 +25,12 @@ history: window_size: 20 ``` -The `--anomaly-threshold` and `--anomaly-window` CLI flags set these options for a single run. +The `--anomaly-window` CLI flag sets the window for a single run; an +out-of-range value is a named error before any walk +(`Error: invalid --anomaly-window: must be >= 2, got 1`). +`--anomaly-threshold` sets the Z-score threshold (default `2.0`, must be +`> 0`: `Error: invalid --anomaly-threshold: must be > 0, got 0`). +`--tick-size` sets the width of one tick (default 24 hours). !!! tip "Tuning parameters" - **Lower threshold** (e.g., 1.5): More sensitive, flags more events. Good for quiet repositories. @@ -35,120 +40,169 @@ The `--anomaly-threshold` and `--anomaly-window` CLI flags set these options for --- +## Window semantics + +The Z-score of tick *i* is computed against the ticks **before** it: +the previous `window_size` ticks, or every earlier tick while the history +is shorter than the window. The mean and the **population** standard +deviation of that window give `z = (value - mean) / stddev`. + +- The first tick has no window and the second tick has one observation: + both score `0` on every metric — a single observation has no spread, so + it never flags anything. +- A window of two or more identical values (zero variance) whose next + value differs scores the signed sentinel `±100`; an identical next value + scores `0`. + +Ticks only exist where commits are: a day without commits is not a tick. + +--- + +## Per-tick metrics + +| Metric | Definition | +|---|---| +| `files_changed` | Number of **distinct** files touched by the tick's commits (a file modified by three commits counts once; a deleted file counts and is named) | +| `lines_added` / `lines_removed` | Summed over the tick's commits (merge commits contribute no line stats) | +| `net_churn` | `lines_added - lines_removed` | +| `language_diversity` | Number of distinct languages among the touched files | +| `author_count` | Number of distinct authors | + +--- + ## Example output +Ten quiet days followed by a busy day (`codefang run -a history/anomaly --format json .`): + === "JSON" ```json { "anomalies": [ { - "tick": 45, + "tick": 10, "z_scores": { - "net_churn": 4.2, - "files_changed": 3.8, - "lines_added": 3.5, - "lines_removed": 1.2, - "language_diversity": 0.5, - "author_count": 0.3 + "net_churn": 179, + "files_changed": 100, + "lines_added": 189, + "lines_removed": 100, + "language_diversity": 0, + "author_count": 0 }, - "max_abs_z_score": 4.2, + "max_abs_z_score": 189, "metrics": { - "files_changed": 142, - "lines_added": 8500, - "lines_removed": 1200, - "net_churn": 7300, - "language_diversity": 3, - "author_count": 2 + "files_changed": 11, + "lines_added": 200, + "lines_removed": 10, + "net_churn": 190, + "language_diversity": 1, + "author_count": 1 }, - "files": ["pkg/core/engine.go", "pkg/core/parser.go", "..."] + "files": ["burst1.txt", "burst2.txt", "...", "q1.txt"] } ], "time_series": [ { "tick": 0, + "start_time": "2021-01-01T12:00:00Z", + "end_time": "2021-01-01T12:00:00Z", "metrics": { - "files_changed": 12, - "lines_added": 450, - "lines_removed": 120, - "net_churn": 330, - "language_diversity": 2, - "author_count": 3 + "files_changed": 1, + "lines_added": 10, + "lines_removed": 0, + "net_churn": 10, + "language_diversity": 1, + "author_count": 1 }, "is_anomaly": false, - "churn_z_score": 0.0 + "churn_z_score": 0 }, { - "tick": 45, + "tick": 10, + "start_time": "2021-01-11T12:00:00Z", + "end_time": "2021-01-11T12:20:00Z", "metrics": { - "files_changed": 142, - "lines_added": 8500, - "lines_removed": 1200, - "net_churn": 7300, - "language_diversity": 3, - "author_count": 2 + "files_changed": 11, + "lines_added": 200, + "lines_removed": 10, + "net_churn": 190, + "language_diversity": 1, + "author_count": 1 }, "is_anomaly": true, - "churn_z_score": 4.2 + "churn_z_score": 179 } ], "aggregate": { - "total_ticks": 120, - "total_anomalies": 5, - "anomaly_rate": 4.17, - "threshold": 2.0, + "total_ticks": 11, + "total_anomalies": 1, + "anomaly_rate": 9.090909090909092, + "threshold": 2, "window_size": 20, - "churn_mean": 340.5, - "churn_stddev": 180.2, - "files_mean": 15.3, - "files_stddev": 8.7, - "lang_diversity_mean": 2.1, - "lang_diversity_stddev": 0.8, - "author_count_mean": 2.8, - "author_count_stddev": 1.2 + "churn_mean": 27.272727272727273, + "churn_stddev": 51.46771434438011, + "files_mean": 1.9090909090909092, + "files_stddev": 2.8747978728797806, + "lang_diversity_mean": 1, + "lang_diversity_stddev": 0, + "author_count_mean": 1, + "author_count_stddev": 0 } } ``` + The quiet days alternate 10 and 12 added lines (mean 11, population + standard deviation 1), so the busy day's net churn of 190 scores + `(190 - 11) / 1 = 179`; every quiet day touched exactly one file, so + the 11 distinct files score the zero-variance sentinel `100`. + === "YAML" ```yaml - anomalies: - - tick: 45 - max_abs_z_score: 4.2 - z_scores: - net_churn: 4.2 - files_changed: 3.8 - metrics: - files_changed: 142 - lines_added: 8500 - net_churn: 7300 - aggregate: - total_ticks: 120 - total_anomalies: 5 - anomaly_rate: 4.17 - threshold: 2.0 - window_size: 20 - time_series: - - tick: 0 - is_anomaly: false - churn_z_score: 0.0 - - tick: 45 - is_anomaly: true - churn_z_score: 4.2 + codefang (v2): + version: ... + hash: ... + TemporalAnomaly: + anomalies: + - tick: 10 + z_scores: + net_churn: 179 + files_changed: 100 + max_abs_z_score: 189 + metrics: + files_changed: 11 + lines_added: 200 + net_churn: 190 + files: + - burst1.txt + time_series: + - tick: 0 + is_anomaly: false + churn_z_score: 0 + - tick: 10 + is_anomaly: true + churn_z_score: 179 + aggregate: + total_ticks: 11 + total_anomalies: 1 + threshold: 2 + window_size: 20 ``` --- ## Output structure -The anomaly analyzer produces three main output sections: +| Section | Description | +|---|---| +| `anomalies` | The flagged ticks, sorted by severity (highest `max_abs_z_score` first, ties by tick): `tick`, the six `z_scores`, `max_abs_z_score`, the raw `metrics`, and `files` (the distinct files touched in the tick, sorted). Always a list — `[]` when nothing is flagged. | +| `time_series` | One entry per tick: `tick`, `start_time` / `end_time` (committer-time bounds, RFC3339 UTC), the raw `metrics`, `is_anomaly`, `churn_z_score`. The raw values appear once, under `metrics`. | +| `aggregate` | `total_ticks`, `total_anomalies`, `anomaly_rate` (percent), the `threshold` and `window_size` used, and the global mean / population standard deviation of net churn, files changed, language diversity and author count over every tick. | -| Section | Type | Description | -|---|---|---| -| `anomalies` | `risk` | List of detected anomalies, sorted by severity (highest Z-score first) | -| `time_series` | `time_series` | Per-tick metrics with anomaly annotations and Z-scores | -| `aggregate` | `aggregate` | Summary statistics: total ticks, anomaly rate, global mean/stddev for each metric | +`bin` carries the json value in the CFB1 envelope; `yaml` renders it under +`TemporalAnomaly:`. The per-commit `ndjson` records carry each commit's +`files_changed`, `lines_added`, `lines_removed`, `net_churn`, `files` +(a delete names the deleted file), `languages` and `author_id`. --- diff --git a/site/analyzers/burndown.md b/site/analyzers/burndown.md index 301b80fd..d46e4812 100644 --- a/site/analyzers/burndown.md +++ b/site/analyzers/burndown.md @@ -1,140 +1,167 @@ # Burndown analyzer reference The burndown analyzer tracks **code survival over time** by following every -line of code through Git history. Optionally, it tracks per-file and -per-developer breakdowns. +line of code through Git history: per time band, the lines written in that +band that are still alive at each later sample. Optionally, it tracks +per-file and per-developer breakdowns and the developer interaction matrix. -For the conceptual model — what code survival means and how the line-tracking -algorithm works — see [Understanding code burndown](../explanation/burndown.md). -To run it, see the [Quick start](../getting-started/quickstart.md). +For the conceptual model — what code survival means and how the +line-tracking algorithm works — see +[Understanding code burndown](../explanation/burndown.md). To run it, see +the [Quick start](../getting-started/quickstart.md). --- -## Configuration options +## Options -| Option | Type | Default | Description | +The analyzer reads its options from the `codefang run` flags. The shared +history flags (`--head`, `--limit`, `--since`, `--tick-size`, +`--max-changes-per-commit`, `--languages`, the identity flags) apply as +documented in the [CLI reference](../guide/cli-reference.md); burndown +always walks the first parent, whatever `--first-parent` says. + +| Flag | Type | Default | Description | |---|---|---|---| -| `Burndown.Granularity` | `int` | `30` | Number of time ticks per age band. Controls the width of each band in the burndown chart. | -| `Burndown.Sampling` | `int` | `30` | How frequently to record a snapshot (in ticks). Lower values give more data points but increase memory. | -| `Burndown.TrackFiles` | `bool` | `false` | Record per-file burndown statistics. | -| `Burndown.TrackPeople` | `bool` | `false` | Record per-developer burndown and interaction matrix. | -| `Burndown.HibernationThreshold` | `int` | `1000` | Minimum node count in a branch before memory compression triggers. | -| `Burndown.HibernationOnDisk` | `bool` | `true` | Save hibernated state to disk to reduce memory pressure. | -| `Burndown.HibernationDirectory` | `string` | `""` | Temporary directory for hibernated state files. Uses system temp if empty. | -| `Burndown.Debug` | `bool` | `false` | Validate internal tree structures at each step (slow; for development only). | -| `Burndown.Goroutines` | `int` | `NumCPU` | Number of goroutines for parallel per-file processing within a commit. | - -Set options via the configuration file or CLI flags: - -```yaml -# .codefang.yml -history: - burndown: - granularity: 30 - sampling: 30 - track_files: true - track_people: true - hibernation_threshold: 1000 - hibernation_to_disk: true - goroutines: 8 -``` +| `--granularity` | `int` | `30` | Ticks per age band of the survival matrix. Must be `> 0`; a non-positive value is rejected (`Error: invalid --granularity: must be > 0, got 0`). | +| `--sampling` | `int` | `30` | Ticks per sample row. Must be `> 0` (a non-positive value is rejected); independent of `--granularity`. | +| `--burndown-files` | `bool` | `false` | Per-file survival: fills `file_survival`, `tracked_files` and the ndjson `FileDeltas`. | +| `--burndown-people` | `bool` | `false` | Per-developer survival and the interaction matrix: fills `developer_survival`, `interactions`, `tracked_developers`, the ndjson `PeopleDeltas` / `MatrixDeltas` and the text "Top Developers" section. With `--burndown-files` it also fills each file's `ownership`. | + +!!! note "Configuration file" + The `history.burndown.*` keys of `.codefang.yaml` describe the same + options in the configuration schema, but `codefang run` applies the + flags above only. + +### Window semantics + +In a `--limit`, `--since` or `--head` window the tracked state is seeded +from the window's base tree (the first in-window commit's parent) at tick +0, so each commit's `LinesAdded` / `LinesRemoved` are the lines it really +changed and `--head` reports the same as `--limit 1`. The base lines form +band 0 of the global curve and carry no author. Commits over +`--max-changes-per-commit` are dropped before the ticks are assigned, as +devs does. --- ## Output formats -### Text (terminal) +The examples below come from a ten-commit fixture +(`bins/codefang/tests/burndown_e2e.rs`) run with `--burndown-files +--burndown-people`. -The `--format text` output produces a concise terminal summary: +### Text (terminal) ```text -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ Burndown: project-name 730d ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Burndown: repoA 310d ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Summary - ─────────────────────────────────────────────── - Current Lines 52,340 - Peak Lines 78,200 - Survival Rate [████████████████░░░░] 66.9% + ──────────────────────────────────────────────────────────────────────────── + Current Lines 15 + Peak Lines 31 + Survival Rate [█████████░░░░░░░░░░░] 48.4% Code Age Distribution - ─────────────────────────────────────────────── - < 1 mo ████████████████████ 34% (17,796) - 1–3 mo ████████████░░░░░░░░ 24% (12,562) - 3–6 mo ████████░░░░░░░░░░░░ 18% ( 9,421) - 6–12 mo ██████░░░░░░░░░░░░░░ 14% ( 7,328) - > 12 mo ████░░░░░░░░░░░░░░░░ 10% ( 5,233) + ──────────────────────────────────────────────────────────────────────────── + < 1 month █░░░░░░░░░░░░░░░░░░░ 6% (1) + 1-3 months █░░░░░░░░░░░░░░░░░░░ 6% (1) + 3-6 months █████████░░░░░░░░░░░ 46% (7) + 6-12 months ████████░░░░░░░░░░░░ 40% (6) Top Developers (by surviving lines) - ─────────────────────────────────────────────── - alice 18,200 [█████████████████░░░] 72.8% - bob 8,400 [█████████████░░░░░░░] 65.1% - charlie 5,100 [████████░░░░░░░░░░░░] 48.2% + ──────────────────────────────────────────────────────────────────────────── + alice 11 [██████████░░░░░░░░░░] 50.0% + bob 4 [████████░░░░░░░░░░░░] 44.4% ``` -The text output shows at most 5 age bands and 5 developers (`mo` = months). Survival rates are color-coded: green (>70%), yellow (50-70%), red (<50%). +The header carries the repository name and `analysis_period_days`. The age +distribution groups the last sample's bands by their age at the end of the +window (band `i` of `n` is `(n − i) · granularity · tick_hours / 24 / 30` +months old; the newest band is the youngest) into at most five buckets. +"Top Developers" lists up to five entries of `developer_survival` and is +printed only with `--burndown-people`. Survival rates are color-coded: +green (> 70 %), yellow (50–70 %), red (< 50 %). ### Plot (HTML) -The `--format plot` output generates an interactive HTML page with: - -- **Summary section**: Key statistics (current lines, peak lines, survival rate, analysis period, developer/file counts) -- **Burndown chart**: Stacked area chart showing code survival by age band or by year (for projects spanning 2+ years) -- **Interpretation hints**: Guidance on reading the chart +`--format plot` writes an interactive page with the summary statistics +(current lines, peak lines, survival rate, analysis period, developer and +file counts when tracked) and the stacked burndown chart — one series per +age band labelled by age (`3mo`, `2y`, …), or per calendar year when the +history spans two or more years. ### JSON / YAML +Both formats encode the same value; an empty list is `[]` in both. + === "JSON" ```json { "aggregate": { - "total_current_lines": 52340, - "total_peak_lines": 78200, - "overall_survival_rate": 0.669, - "analysis_period_days": 730, - "num_bands": 25, - "num_samples": 25, - "tracked_files": 342, - "tracked_developers": 12 + "total_current_lines": 15, + "total_peak_lines": 31, + "overall_survival_rate": 0.4838709677419355, + "analysis_period_days": 310, + "num_bands": 11, + "num_samples": 11, + "tracked_files": 3, + "tracked_developers": 2 }, "global_survival": [ { "sample_index": 0, - "total_lines": 1200, - "survival_rate": 0.015, - "band_breakdown": [1200, 0, 0] + "total_lines": 10, + "survival_rate": 0.3225806451612903, + "band_breakdown": [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, { - "sample_index": 24, - "total_lines": 52340, - "survival_rate": 0.669, - "band_breakdown": [320, 890, 1450, 2100, 4800, "..."] + "sample_index": 10, + "total_lines": 15, + "survival_rate": 0.4838709677419355, + "band_breakdown": [0, 0, 0, 3, 3, 1, 6, 0, 1, 0, 1] } ], "file_survival": [ { - "path": "pkg/core/engine.go", - "current_lines": 450, - "ownership": {"0": 325, "1": 125}, + "path": "script.py", + "current_lines": 8, + "peak_lines": 9, + "survival_rate": 0.8888888888888888, + "ownership": {"0": 4, "1": 4}, + "top_owner_id": 0, "top_owner_name": "alice", - "top_owner_percentage": 72.3 + "top_owner_percentage": 50 + }, + { + "path": "b.go", + "current_lines": 0, + "peak_lines": 15, + "survival_rate": 0, + "ownership": {} } ], "developer_survival": [ - { - "name": "alice", - "current_lines": 18200, - "peak_lines": 25000, - "survival_rate": 0.728 - } + {"id": 0, "name": "alice", "current_lines": 11, "peak_lines": 22, "survival_rate": 0.5}, + {"id": 1, "name": "bob", "current_lines": 4, "peak_lines": 9, "survival_rate": 0.4444444444444444} ], "interactions": [ { + "author_id": 0, "author_name": "alice", - "modifier_name": "bob", - "lines_modified": 342, + "modifier_id": 0, + "modifier_name": "alice", + "lines_modified": 7, + "is_self_modify": true + }, + { + "author_id": 1, + "author_name": "bob", + "modifier_id": 0, + "modifier_name": "alice", + "lines_modified": 5, "is_self_modify": false } ] @@ -145,22 +172,87 @@ The `--format plot` output generates an interactive HTML page with: ```yaml aggregate: - total_current_lines: 52340 - total_peak_lines: 78200 - overall_survival_rate: 0.669 - analysis_period_days: 730 + total_current_lines: 15 + total_peak_lines: 31 + overall_survival_rate: 0.4838709677419355 + analysis_period_days: 310 + num_bands: 11 + num_samples: 11 + tracked_files: 3 + tracked_developers: 2 global_survival: - - sample_index: 0 - total_lines: 1200 - survival_rate: 0.015 - - sample_index: 24 - total_lines: 52340 - survival_rate: 0.669 + - sample_index: 0 + total_lines: 10 + survival_rate: 0.3225806451612903 + band_breakdown: + - 10 + - 0 + file_survival: + - path: script.py + current_lines: 8 + peak_lines: 9 + survival_rate: 0.8888888888888888 + ownership: + 0: 4 + 1: 4 + top_owner_id: 0 + top_owner_name: alice + top_owner_percentage: 50 + developer_survival: + - id: 0 + name: alice + current_lines: 11 + peak_lines: 22 + survival_rate: 0.5 + interactions: + - author_id: 0 + author_name: alice + modifier_id: 0 + modifier_name: alice + lines_modified: 7 + is_self_modify: true ``` +Fields: + +| Field | Meaning | +|---|---| +| `aggregate.total_peak_lines` | Sum over bands of each band's maximum: the lines ever written that lived until a sample boundary. | +| `aggregate.total_current_lines` | Live lines at the last sample. | +| `aggregate.overall_survival_rate` | `current / peak`, never above 1. | +| `aggregate.analysis_period_days` | `last_tick · tick_size_hours / 24` — the window's tick span. | +| `aggregate.num_bands`, `num_samples` | `last_tick / granularity + 1`, `last_tick / sampling + 1`. | +| `aggregate.tracked_files`, `tracked_developers` | Entries of `file_survival` / `developer_survival` (0 when the flag is off). | +| `global_survival[]` | One row per sample: `total_lines`, `survival_rate` (`total / peak`) and `band_breakdown` (live lines per age band, band 0 the oldest). | +| `file_survival[]` | Per file (final path, renames followed), most surviving lines first: `current_lines`, `peak_lines`, `survival_rate`; with `--burndown-people` also `ownership` (live lines per developer id) and, for a live file, `top_owner_id` / `top_owner_name` / `top_owner_percentage`. | +| `developer_survival[]` | Per developer, most surviving lines first: `current_lines`, `peak_lines`, `survival_rate`. | +| `interactions[]` | Per `(author, modifier)` pair, most lines first: `lines_modified` — lines written by the author that the modifier deleted or replaced inside the window; `is_self_modify` when both are the same developer. | + +### NDJSON and timeseries + +`--format ndjson` writes one record per commit of the window (the base +tree is not a commit and has no record): + +```json +{"hash":"f08da9bc…","tick":186,"author_id":0,"timestamp":"2021-07-06T12:00:00Z","analyzer":"burndown","data":{"GlobalDeltas":{"186":{"0":-7,"186":6,"31":-5}},"FileDeltas":null,"PeopleDeltas":{"0":{"186":{"0":-7,"186":6}},"1":{"186":{"31":-5}}},"MatrixDeltas":{"0":{"0":7},"1":{"0":5}},"LinesAdded":6,"LinesRemoved":12}} +``` + +- `GlobalDeltas` — `current tick → written tick → delta` of the commit; +- `FileDeltas` — the same per final path (`null` without + `--burndown-files`); +- `PeopleDeltas` — the same per author of the affected lines, and + `MatrixDeltas` — `author → modifier → lines deleted or replaced` (both + `null` without `--burndown-people`); +- `LinesAdded` / `LinesRemoved` — gross counts: a replaced line is one + removed and one added (devs reports it as *changed* instead). + +`--format timeseries` lists every commit with its own tick and the +`{"lines_added", "lines_removed"}` pair. + --- ## See also -- [Understanding code burndown](../explanation/burndown.md) — the mental model, algorithm, use cases, and limitations. +- [Understanding code burndown](../explanation/burndown.md) — the mental + model, algorithm, use cases, and limitations. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/analyzers/clones.md b/site/analyzers/clones.md index 80c0dfc5..f8d50445 100644 --- a/site/analyzers/clones.md +++ b/site/analyzers/clones.md @@ -1,9 +1,10 @@ # Clone detection analyzer reference -The clone detection analyzer finds **duplicate and near-duplicate functions** -across your entire codebase using MinHash signatures and Locality-Sensitive -Hashing (LSH). It operates on the UAST representation and detects clones -**cross-file** — not just within a single file. +The clone detection analyzer (`static/clones`) finds **duplicate and +near-duplicate functions** across your entire codebase using MinHash +signatures and Locality-Sensitive Hashing (LSH). It operates on the UAST +representation and detects clones **cross-file** — not just within a single +file. For the conceptual model — the clone-type taxonomy, the methodology, and why MinHash + LSH — see @@ -20,6 +21,59 @@ The clone detection analyzer uses the UAST directly and has no analyzer-specific |---|---|---|---| | *(none)* | -- | -- | Uses UAST; no analyzer-specific config | +The walk honors the run-wide flags: `--languages` selects the files to parse, +and `--include-vendored` / `--include-generated` add vendored / generated +files to the analysis. + +--- + +## Clone types + +| Type | `similarity` | Rule | +|---|---|---| +| **Type-1** (exact) | `1.00` | identical AST structure **and** identical tokens | +| **Type-2** (renamed) | `1.00` | identical AST structure; identifiers and/or literals differ | +| **Type-3** (near-miss) | `0.50` … `< 1.00` (or `1.00` with a reordered body) | modified structure whose exact shingle Jaccard is ≥ 0.5 | + +`similarity` is the **exact Jaccard index of the two functions' shingle +sets** (not a MinHash estimate); it is `1.0` whenever the structure is +identical. Comments never affect the structure or the tokens, so a copy that +only gained comments is still Type-1. + +--- + +## Metrics + +| Metric | Definition | +|---|---| +| `total_functions` | every function, method and closure (UAST `Lambda`) found, including ones too small to pair | +| `total_clone_pairs` | the exact number of pairs found (not capped) | +| `clone_ratio` | **functions that take part in at least one pair / `total_functions`** — bounded to `[0, 1]`; e.g. 3 functions where two are copies of each other → `2 / 3 = 0.67` | +| `clone_type_distribution` | exact per-type pair counts | +| `clone_pairs` | the stored detail: the best 1000 pairs ordered by similarity descending, then by `func_a`, `func_b` | + +The status message and the `N/10` score are **both keyed off `clone_ratio`**: + +| `clone_ratio` | Status | Score | +|---|---|---| +| `0` | `No code clones detected` | `10/10` | +| `≤ 0.1` | `Low duplication - few clone pairs detected` | `round((1 − ratio) × 10)/10` | +| `≤ 0.3` | `Moderate duplication - consider refactoring clone pairs` | ″ | +| `> 0.3` | `High duplication - significant refactoring recommended` | ″ | + +Two files that are copies of each other have a ratio of `1.0`: `High +duplication`, `0/10`. + +### Candidates and minimum size + +- Every function, method and closure is counted. A closure is named by its + enclosing function and ordinal (`Handle.func#2`), or `func#N` at file level. +- A function must have at least **20 UAST nodes** (comments excluded) to + become a clone candidate; smaller ones (one-line getters, `return a + b`) + are counted in `total_functions` but never paired, because they hash + identically regardless of purpose. +- A closure is never paired with the function it is written in. + --- ## Example output @@ -27,59 +81,127 @@ The clone detection analyzer uses the UAST directly and has no analyzer-specific === "Text" ``` - +----------------------------------------------+ - | CLONE DETECTION | - | Score: 7/10 | - | Low duplication - few clone pairs detected | - +----------------------------------------------+ - - Key Metrics - Total Functions ....... 346 - Clone Pairs ........... 12 - Clone Ratio ........... 0.03 - - Distribution - Type-1 (Exact) ....... 17% (2) - Type-2 (Renamed) ..... 50% (6) - Type-3 (Near-miss) ... 33% (4) - - Issues (sorted worst-first) - pkg/a.go::Handler <-> pkg/b.go::Handler Type-1 1.00 - cmd/run.go::Execute <-> cmd/serve.go::Execute Type-2 0.92 + ┃ CLONE DETECTION Score: 3/10 ┃ + + Summary: High duplication - significant refactoring recommended + + Key Metrics + Total Functions 4 Clone Pairs 2 + Clone Ratio 0.8 + + Distribution + Type-1 (Exact) ████████████████████░░░░░░░░░░░░░░░░░░░░ 50% (1) + Type-2 (Renamed) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% (0) + Type-3 (Near-miss) ████████████████████░░░░░░░░░░░░░░░░░░░░ 50% (1) + + Top Issues + a.go::ProcessItems <-> b.go::ProcessItems Type-1 1.00 + a.go::ProcessItems <-> d.go::ProcessItemsPlus Type-3 0.63 ``` === "JSON" + `--format json` emits the sections envelope (the shape every static + analyzer shares): + ```json { - "total_functions": 346, - "total_clone_pairs": 12, - "clone_ratio": 0.035, - "clone_pairs": [ + "overall_score_label": "3/10", + "sections": [ { - "func_a": "pkg/a.go::Handler", - "func_b": "pkg/b.go::Handler", - "similarity": 1.0, - "clone_type": "Type-1" - }, - { - "func_a": "cmd/run.go::Execute", - "func_b": "cmd/serve.go::Execute", - "similarity": 0.92, - "clone_type": "Type-2" + "title": "CLONE DETECTION", + "score_label": "3/10", + "status": "High duplication - significant refactoring recommended", + "metrics": [ + { "label": "Total Functions", "value": "4" }, + { "label": "Clone Pairs", "value": "2" }, + { "label": "Clone Ratio", "value": "0.8" } + ], + "distribution": [ + { "label": "Type-1 (Exact)", "percent": 0.5, "count": 1 }, + { "label": "Type-2 (Renamed)", "percent": 0, "count": 0 }, + { "label": "Type-3 (Near-miss)", "percent": 0.5, "count": 1 } + ], + "issues": [ + { "name": "a.go::ProcessItems <-> b.go::ProcessItems", "location": "Type-1", "value": "1.00", "severity": "poor" }, + { "name": "a.go::ProcessItems <-> d.go::ProcessItemsPlus", "location": "Type-3", "value": "0.63", "severity": "fair" } + ], + "score": 0.25 } ], - "message": "Low duplication - few clone pairs detected" + "overall_score": 0.25 } ``` + `distribution[].percent` is a fraction in `0..1` (the renderer contract + shared by every analyzer); `issues[].location` carries the clone type, + `issues[].value` the similarity with two decimals, and `severity` is + `poor` at similarity ≥ 0.8, `fair` below. + +=== "YAML / bin" + + `--format yaml` and `--format bin` carry the computed metrics with the + full stored pair list: + + ```yaml + total_functions: 4 + total_clone_pairs: 2 + clone_ratio: 0.75 + clone_type_distribution: + Type-1: 1 + Type-2: 0 + Type-3: 1 + clone_pairs: + - func_a: a.go::ProcessItems + func_b: b.go::ProcessItems + similarity: 1 + clone_type: Type-1 + - func_a: a.go::ProcessItems + func_b: d.go::ProcessItemsPlus + similarity: 0.6349206349206349 + clone_type: Type-3 + message: High duplication - significant refactoring recommended + ``` + === "HTML" The HTML plot output includes a **Clone Type Distribution** pie chart showing the breakdown of detected clones by type (Type-1, Type-2, Type-3). --- +## `--per-file` + +Clone detection is cross-file, so a per-file view answers "which of this +file's functions are duplicated elsewhere?". With `--per-file` the section +gains a `files` array with one entry per parsed file, in walk order: + +```json +{ + "file_path": "a.go", + "score_label": "0/10", + "status": "1 of 1 functions in clone pairs", + "metrics": [ + { "label": "Total Functions", "value": "1" }, + { "label": "Cloned Functions", "value": "1" }, + { "label": "Clone Ratio", "value": "1.0" } + ], + "issues": [ + { "name": "a.go::ProcessItems <-> b.go::ProcessItems", "location": "Type-1", "value": "1.00", "severity": "poor" } + ], + "score": 0 +} +``` + +`Cloned Functions` counts the file's functions that take part in **any** +pair; `issues` lists the stored pairs (the best 1000 overall) that involve the +file. A file whose functions are not duplicated reads `No functions in clone +pairs`; a file without functions reads `No functions found for clone +analysis`. + +--- + ## See also - [Understanding clone detection](../explanation/clones.md) — the mental model, methodology, references, and limitations. +- [Composition](composition.md) — the other cross-file static analyzer. - [Quick start](../getting-started/quickstart.md) — run static analysis. diff --git a/site/analyzers/cohesion.md b/site/analyzers/cohesion.md index 76c00cb5..bf0d36bd 100644 --- a/site/analyzers/cohesion.md +++ b/site/analyzers/cohesion.md @@ -1,12 +1,18 @@ # Cohesion analyzer reference -The cohesion analyzer computes **LCOM-HS (Henderson-Sellers)** and **variable -sharing ratio** metrics to identify files and modules with low internal -cohesion. +The cohesion analyzer computes **LCOM-HS (Henderson-Sellers)** per file and a +**variable sharing ratio** per function to identify files and modules with +low internal cohesion. -For the conceptual model — what LCOM-HS measures and how to read the scores — -see [Understanding cohesion metrics](../explanation/cohesion.md). To run it, -see the [Quick start](../getting-started/quickstart.md). +For the conceptual model — what counts as a variable, what LCOM-HS measures +and how to read the scores — see +[Understanding cohesion metrics](../explanation/cohesion.md). To run it, see +the [Quick start](../getting-started/quickstart.md). + +```bash +codefang run -a static/cohesion --format text . +codefang run -a static/cohesion --format json --per-file . +``` --- @@ -18,101 +24,179 @@ The cohesion analyzer uses the UAST directly and has no analyzer-specific config |---|---|---|---| | *(none)* | -- | -- | Uses UAST; no analyzer-specific config | +The run-level walk flags apply (`--languages`, `--include-generated`, +`--include-vendored`, `--per-file`). + +--- + +## Metrics and thresholds + +| Metric | Meaning | +|---|---| +| Total Functions | Every function found, same-named ones included; methods are reported as `Type.name` | +| LCOM Score | Function-weighted mean of the per-file LCOM-HS values (0 = cohesive, 1 = not) | +| Cohesion Score | `1 - LCOM Score`; the section score (`6/10` = `0.60`) | +| Avg Cohesion | Mean per-function cohesion (shared variables / distinct variables) | + +Metrics render with two decimals. Per-function cohesion is bucketed as: + +| Bucket | Range | Issue severity | +|---|---|---| +| Excellent | `>= 0.6` | not listed | +| Good | `[0.4, 0.6)` | `good` | +| Fair | `[0.3, 0.4)` | `fair` | +| Poor | `< 0.3` | `poor` | + +The distribution always sums to Total Functions. The issues list holds every +function below Excellent, ordered by cohesion ascending, then name; `text` +shows the first five (`-v` for all). + +The summary message follows the Cohesion Score: Excellent `>= 0.7`, Good +`>= 0.4`, Fair `>= 0.3`, otherwise Poor. + +### No data + +Files without functions (struct-only files, empty files, Markdown) are +excluded from every number. When no analyzed file has a function — including +an existing empty directory — the section reports `No cohesion data +available`, `Total Functions 0`, score `0` (`0/10`), no distribution and an +empty issues list, and the command exits 0. Unreadable or unparseable files +are skipped and counted in the `progress: skipped N …` line on stderr. + --- ## Example output +The fixture is two Go files — `share.go` with `f` using `{x, y}`, `g` using +`{x}` and `h` using `{x, y, z}` (LCOM 1/3), and `disjoint.go` with `f2` using +`{p}` and `g2` using `{q}` (LCOM 0.5) — plus a struct-only `types.go`. + === "Text" ``` - ╭──────────────────────────────────────────────╮ - │ COHESION │ - │ Score: 7/10 │ - │ Good cohesion - functions have reasonable │ - │ focus │ - ╰──────────────────────────────────────────────╯ - - Key Metrics - Total Functions ....... 5 - LCOM Score ............ 0.30 - Cohesion Score ........ 0.70 - Avg Cohesion .......... 0.65 - - Distribution - Excellent (>0.6) ..... 40% (2) - Good (0.4-0.6) ....... 20% (1) - Fair (0.3-0.4) ....... 20% (1) - Poor (<0.3) .......... 20% (1) - - Issues (sorted worst-first) - isolatedHelper 0.10 Poor - utilFunction 0.35 Fair - parseInput 0.45 Good + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ COHESION Score: 6/10 ┃ + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + Summary: Good overall cohesion with room for improvement + + Key Metrics + ──────────────────────────────────────────────────────────────────────────── + Total Functions 5 LCOM Score 0.40 + Cohesion Score 0.60 Avg Cohesion 0.53 + + Distribution + ──────────────────────────────────────────────────────────────────────────── + Excellent (>=0.6) ████████████████████████░░░░░░░░░░░░░░░░ 60% (3) + Good (0.4-0.6) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% (0) + Fair (0.3-0.4) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% (0) + Poor (<0.3) ████████████████░░░░░░░░░░░░░░░░░░░░░░░░ 40% (2) + + Top Issues + ──────────────────────────────────────────────────────────────────────────── + f2 disjoint.go 0.00 + g2 disjoint.go 0.00 + ``` + + `--format compact` renders the one-line form: + + ``` + COHESION [██████░░░░] 6/10 Good overall cohesion with room for improvement ``` === "JSON" + `--format json` emits the section report (`text` and `compact` render the + same section). With `--per-file`, a `files` array sits between `issues` + and `score`: one entry per analyzed file with that file's own metrics, + distribution and issues; a function-free file carries + `"status": "No functions found"`. + ```json { - "function_cohesion": [ - { - "name": "isolatedHelper", - "cohesion": 0.1, - "quality_level": "Poor" - }, - { - "name": "utilFunction", - "cohesion": 0.35, - "quality_level": "Fair" - }, - { - "name": "parseInput", - "cohesion": 0.45, - "quality_level": "Good" - }, - { - "name": "processData", - "cohesion": 0.8, - "quality_level": "Excellent" - }, + "overall_score_label": "6/10", + "sections": [ { - "name": "handleRequest", - "cohesion": 0.9, - "quality_level": "Excellent" + "title": "COHESION", + "score_label": "6/10", + "status": "Good overall cohesion with room for improvement", + "metrics": [ + { "label": "Total Functions", "value": "5" }, + { "label": "LCOM Score", "value": "0.40" }, + { "label": "Cohesion Score", "value": "0.60" }, + { "label": "Avg Cohesion", "value": "0.53" } + ], + "distribution": [ + { "label": "Excellent (>=0.6)", "percent": 0.6, "count": 3 }, + { "label": "Good (0.4-0.6)", "percent": 0, "count": 0 }, + { "label": "Fair (0.3-0.4)", "percent": 0, "count": 0 }, + { "label": "Poor (<0.3)", "percent": 0.4, "count": 2 } + ], + "issues": [ + { "name": "f2", "location": "disjoint.go", "value": "0.00", "severity": "poor" }, + { "name": "g2", "location": "disjoint.go", "value": "0.00", "severity": "poor" } + ], + "score": 0.6 } ], - "distribution": { - "excellent": 2, - "good": 1, - "fair": 1, - "poor": 1 - }, - "low_cohesion_functions": [ - { - "name": "isolatedHelper", - "cohesion": 0.1, - "risk_level": "HIGH", - "recommendation": "Consider splitting into multiple focused functions" - }, - { - "name": "utilFunction", - "cohesion": 0.35, - "risk_level": "MEDIUM", - "recommendation": "Review function responsibilities for possible separation" - } - ], - "aggregate": { - "total_functions": 5, - "lcom": 0.3, - "lcom_variant": "LCOM-HS (Henderson-Sellers)", - "cohesion_score": 0.7, - "function_cohesion": 0.52, - "health_score": 70, - "message": "Good cohesion - functions have reasonable focus" - } + "overall_score": 0.6 } ``` +=== "YAML / binary" + + `--format yaml` and `--format bin` (a `CFB1` envelope over the compact + JSON of the same value) emit the computed metrics: every function with + its quality level (cohesion ascending), the bucket counts (only buckets + that occur), the functions below Good with a risk level (`HIGH` below + Fair, `MEDIUM` otherwise), and the aggregate. + + ```yaml + function_cohesion: + - name: f2 + source_file: disjoint.go + cohesion: 0 + quality_level: Poor + - name: g2 + source_file: disjoint.go + cohesion: 0 + quality_level: Poor + - name: h + source_file: share.go + cohesion: 0.6666666666666666 + quality_level: Excellent + - name: f + source_file: share.go + cohesion: 1 + quality_level: Excellent + - name: g + source_file: share.go + cohesion: 1 + quality_level: Excellent + distribution: + excellent: 3 + poor: 2 + low_cohesion_functions: + - name: f2 + source_file: disjoint.go + cohesion: 0 + risk_level: HIGH + recommendation: Consider splitting into multiple focused functions + - name: g2 + source_file: disjoint.go + cohesion: 0 + risk_level: HIGH + recommendation: Consider splitting into multiple focused functions + aggregate: + total_functions: 5 + lcom: 0.4 + lcom_variant: LCOM-HS (Henderson-Sellers) + cohesion_score: 0.6 + function_cohesion: 0.5333333333333333 + health_score: 60 + message: Good overall cohesion with room for improvement + ``` + === "HTML" The HTML plot output includes two charts: @@ -121,9 +205,14 @@ The cohesion analyzer uses the UAST directly and has no analyzer-specific config 2. **Cohesion Distribution** — A pie chart showing the distribution of functions across quality categories (Excellent, Good, Fair, Poor). + The raw report behind the plots lists every function with its + `start_line`, `line_count` (the function's line span), `variable_count` + (distinct variables), `cohesion`, the three assessments and + `_source_file`. + --- ## See also -- [Understanding cohesion metrics](../explanation/cohesion.md) — the mental model, formulas, references, and limitations. +- [Understanding cohesion metrics](../explanation/cohesion.md) — the mental model, variable rules, formulas, per-language notes, and limitations. - [Quick start](../getting-started/quickstart.md) — run static analysis. diff --git a/site/analyzers/comments.md b/site/analyzers/comments.md index 1ab238fb..433f998b 100644 --- a/site/analyzers/comments.md +++ b/site/analyzers/comments.md @@ -1,73 +1,214 @@ # Comments analyzer reference -The comments analyzer evaluates **documentation coverage** by examining comment -presence, placement, and density across your source code. +The comments analyzer (`static/comments`) measures **documentation coverage** +and **comment placement** across your source code: which declarations carry a +doc comment, where every comment sits, and what that adds up to per file and +per repository. -For the conceptual model — what documentation coverage means and what a healthy -comment density looks like — see -[Understanding documentation coverage](../explanation/comments.md). To run it, -see the [Quick start](../getting-started/quickstart.md). +For the mental model — what "documented" means, the placement categories and +the gap rule — see [Understanding documentation coverage](../explanation/comments.md). +To run it, see the [Quick start](../getting-started/quickstart.md). --- ## Configuration options -The comments analyzer uses the UAST directly and has no analyzer-specific configuration options. +The comments analyzer uses the UAST directly and has no analyzer-specific +configuration options. | Option | Type | Default | Description | |---|---|---|---| | *(none)* | -- | -- | Uses UAST; no analyzer-specific config | +The shared static flags apply: `--per-file` adds one entry per analyzed file +to the JSON report, `--languages` and the path policy flags select the files. + +--- + +## Definitions + +| Term | Definition | +|---|---| +| **Declaration** | A UAST `Function`, `Method`, `Class`, `Interface` or `Struct` node. A nameless `Class` / `Struct` / `Interface` node (a type literal, a class body) is not a declaration. A declaration without a resolvable name is reported as `unknown`. | +| **Comment** | A UAST `Comment` node. Nested comment pieces under one node count as one comment. Comments on adjacent lines form one **block**. | +| **above** | The block ends on the line directly above a declaration (attribute / decorator lines belong to the declaration). The block **documents** that declaration; every comment in it is **good**. | +| **docstring** | The block is a documentation comment (UAST `Doc` role) that is the first statement of a declaration body — a Python docstring. It **documents** that declaration; **good**. A string literal later in the body is **inline**. | +| **inline** | The block lies inside a declaration's span (a body comment). Attributed to the innermost declaration; **bad**. | +| **unassociated** | Any other block (file headers, orphans, a comment separated from the declaration by a blank line); **bad**. | +| **Doc Coverage** | documented declarations / all declarations — summed over every analyzed file (Σ/Σ). | +| **Good Ratio** | good comments / all comments — summed over every analyzed file (Σ/Σ). | +| **Score** | The Good Ratio, shown as `N/10`. | +| **Status** | Threshold label of the score: `>= 0.8` Excellent, `>= 0.6` Good, `>= 0.4` Fair, else Poor. With neither comments nor declarations: `No comments found`. | +| **Risk level** (yaml / bin) | Length of an undocumented declaration: `HIGH` at 50 lines or more, `MEDIUM` at 10 or more, else `LOW`. | + +A file the parser supports but cannot parse (for example Markdown) is listed +as an empty file: it has no declarations and no comments, so it never changes +a ratio. + --- ## Example output +Fixture: `a.go` has `Add` (documented) and `Sub`, plus a package comment and a +body comment; `b.go` has `Mul` (its comment is followed by a blank line), `Div` +(documented by a block comment) and `Mod`; `README.md` is prose. + === "JSON" ```json { - "comments": { - "files": [ - { - "file": "main.go", - "total_lines": 250, - "comment_lines": 45, - "comment_density": 0.18, - "functions_total": 12, - "functions_documented": 8, - "documentation_coverage": 0.67, - "undocumented": [ - {"name": "helperFunc", "line": 89}, - {"name": "processItem", "line": 134}, - {"name": "cleanup", "line": 201}, - {"name": "retryLoop", "line": 220} - ] - } - ], - "summary": { - "total_files": 1, - "avg_comment_density": 0.18, - "avg_documentation_coverage": 0.67, - "total_undocumented_functions": 4 + "overall_score_label": "4/10", + "sections": [ + { + "title": "COMMENTS", + "score_label": "4/10", + "status": "Fair comment quality - consider improving placement", + "metrics": [ + {"label": "Total Comments", "value": "5"}, + {"label": "Good Comments", "value": "2"}, + {"label": "Bad Comments", "value": "3"}, + {"label": "Doc Coverage", "value": "40.0%"}, + {"label": "Good Ratio", "value": "40.0%"}, + {"label": "Total Functions", "value": "5"} + ], + "distribution": [ + {"label": "Documented", "percent": 0.4, "count": 2}, + {"label": "Undocumented", "percent": 0.6, "count": 3} + ], + "issues": [ + {"name": "Mod", "location": "b.go", "value": "undocumented", "severity": "poor"}, + {"name": "Mul", "location": "b.go", "value": "undocumented", "severity": "poor"}, + {"name": "Sub", "location": "a.go", "value": "undocumented", "severity": "poor"} + ], + "score": 0.4 } - } + ], + "overall_score": 0.4 } ``` + Issues are sorted by name, then file, then line. `distribution` is + omitted when there are no declarations. + + With `--per-file`, a `files` array sits between `issues` and `score`, + one entry per analyzed file (empty files included), in walk order: + + ```json + { + "file_path": "b.go", + "score_label": "5/10", + "status": "Fair comment quality - consider improving placement", + "metrics": [ ... the same six metrics for this file ... ], + "distribution": [ + {"label": "Documented", "percent": 0.3333333333333333, "count": 1}, + {"label": "Undocumented", "percent": 0.6666666666666666, "count": 2} + ], + "issues": [ + {"name": "Mod", "location": "b.go", "value": "undocumented", "severity": "poor"}, + {"name": "Mul", "location": "b.go", "value": "undocumented", "severity": "poor"} + ], + "score": 0.5 + } + ``` + + `file_path` and every `location` are relative to the analyzed root + (`sub/c.go`); a single-file root stamps the file name. + +=== "YAML" + + ```yaml + comment_quality: + - line_number: 4 + source_file: a.go + language: go + directory: . + quality: good # good | bad + placement: above # above | docstring | inline | unassociated + target_type: Function + target_name: Add + score: 1 + - line_number: 6 + source_file: a.go + language: go + directory: . + quality: bad + placement: inline + target_type: Function + target_name: Add + score: -0.5 + function_documentation: + - name: Add + type: Function + line: 5 + lines: 4 + source_file: a.go + language: go + directory: . + is_documented: true + doc_line: 4 # 0 when undocumented + status: Documented + - name: Sub + type: Function + line: 10 + lines: 3 + source_file: a.go + language: go + directory: . + is_documented: false + doc_line: 0 + status: Undocumented + undocumented_functions: + - name: Sub + type: Function + line: 10 + lines: 3 + source_file: a.go + language: go + directory: . + risk_level: LOW # LOW | MEDIUM | HIGH + aggregate: + total_comments: 5 + good_comments: 2 + bad_comments: 3 + overall_score: 0.4 + total_functions: 5 + documented_functions: 2 + good_comments_ratio: 0.4 + documentation_coverage: 0.4 + health_score: 40 + message: Fair comment quality - consider improving placement + ``` + + Rows are sorted by file, then line. `--format bin` carries exactly this + value in the CFB1 envelope. + === "Text" ``` - Documentation Coverage - main.go - density=18% coverage=67% (8/12 functions documented) - undocumented: helperFunc:89 processItem:134 cleanup:201 retryLoop:220 + COMMENTS Score: 4/10 - Summary: 1 file, avg density=18%, avg coverage=67% + Summary: Fair comment quality - consider improving placement + + Key Metrics + Total Comments 5 Good Comments 2 + Bad Comments 3 Doc Coverage 40.0% + Good Ratio 40.0% Total Functions 5 + + Distribution + Documented ████████████████ 40% (2) + Undocumented ████████████████████████ 60% (3) ``` + The text and compact formats show the summary without the issue list. + +Per-comment scores (`score` in `comment_quality`): `1.0` when above, the +enclosing declaration's penalty when inline (`Function` / `Method` `-0.5`, +`Class` / `Interface` / `Struct` `-0.3`, others `-0.1`), `-0.2` when +unassociated. Only the good / bad classification feeds the ratios. + --- ## See also -- [Understanding documentation coverage](../explanation/comments.md) — the mental model, comment types, and limitations. +- [Understanding documentation coverage](../explanation/comments.md) — the mental model, placement categories, the gap rule and limitations. - [Quick start](../getting-started/quickstart.md) — run static analysis. diff --git a/site/analyzers/complexity.md b/site/analyzers/complexity.md index 0f47b206..3e9f76d2 100644 --- a/site/analyzers/complexity.md +++ b/site/analyzers/complexity.md @@ -2,11 +2,13 @@ The complexity analyzer measures three dimensions of code complexity from source code: **cyclomatic complexity**, **cognitive complexity**, and -**nesting depth**. It operates on the UAST representation of your source -files. +**nesting depth**, plus the **lines of code** of every function. It operates +on the UAST representation of your source files and reads the file source +alongside it (operators, identifiers and line counts are recovered from the +source where a language mapping does not carry them). -For the conceptual model — what each metric means and how cyclomatic and -cognitive complexity differ — see +For the conceptual model — what each metric means, the exact counting rules, +and how the rules are validated — see [Understanding complexity metrics](../explanation/complexity.md). To run it, see the [Quick start](../getting-started/quickstart.md). @@ -20,82 +22,251 @@ The complexity analyzer uses the UAST directly and has no analyzer-specific conf |---|---|---|---| | *(none)* | -- | -- | Uses UAST; no analyzer-specific config | +Run-level flags that affect it: `--languages`, `--include-vendored`, +`--include-generated` (which files are walked) and `--per-file` (JSON only, +see below). + +--- + +## Output formats + +| Format | Shape | Notes | +|---|---|---| +| `json` | Sections report: one scored `COMPLEXITY` section with `metrics`, `distribution`, `issues` (every function, ranked) | `--per-file` adds a `files` array with one entry per analyzed file (function-free files included) | +| `yaml`, `bin` | Computed metrics: `function_complexity`, `distribution`, `high_risk_functions`, `aggregate` | `bin` wraps the compact JSON of the same value in the `CFB1` envelope; `--per-file` is not applied to these formats | +| `text`, `compact` | Terminal rendering of the `json` section | `text` prints the key metrics, the distribution and the top five functions ("Top Issues"; `--verbose` prints all) | +| `plot` | HTML pages built from the raw aggregated report | `report.json` carries every function with its assessments | + +Functions are ordered the same way everywhere: cyclomatic descending, then +cognitive descending, then nesting descending, then name, file and start +line ascending. Ties are therefore deterministic across runs and formats. + --- ## Example output +The examples below are the real output for a Go file with ten functions +(`crates/cf-complexity/tests/fixtures/cc.go`, the file the test suite +measures). + === "JSON" ```json { - "function_complexity": [ - { - "name": "processFile", - "source_file": "cmd/server/main.go", - "language": "go", - "directory": "cmd/server", - "cyclomatic_complexity": 8, - "cognitive_complexity": 12, - "nesting_depth": 3, - "lines_of_code": 45, - "complexity_density": 0.178, - "risk_level": "LOW" - }, - { - "name": "validate", - "source_file": "cmd/server/main.go", - "language": "go", - "directory": "cmd/server", - "cyclomatic_complexity": 15, - "cognitive_complexity": 22, - "nesting_depth": 5, - "lines_of_code": 80, - "complexity_density": 0.188, - "risk_level": "MEDIUM" - } - ], - "high_risk_functions": [ + "overall_score_label": "6/10", + "sections": [ { - "name": "validate", - "source_file": "cmd/server/main.go", - "language": "go", - "directory": "cmd/server", - "cyclomatic_complexity": 15, - "cognitive_complexity": 22, - "risk_level": "MEDIUM", - "issues": ["High cyclomatic complexity", "Deep nesting"] + "title": "COMPLEXITY", + "score_label": "6/10", + "status": "Fair complexity - some functions could be simplified", + "metrics": [ + { "label": "Total Functions", "value": "10" }, + { "label": "Avg Complexity", "value": "3.6" }, + { "label": "Max Complexity", "value": "8" }, + { "label": "Total Complexity", "value": "36" }, + { "label": "Cognitive Total", "value": "29" }, + { "label": "Max Nesting", "value": "3" }, + { "label": "Decision Points", "value": "26" } + ], + "distribution": [ + { "label": "Simple (1-5)", "percent": 0.9, "count": 9 }, + { "label": "Moderate (6-10)", "percent": 0.1, "count": 1 }, + { "label": "Complex (11-20)", "percent": 0, "count": 0 }, + { "label": "Very Complex (>20)", "percent": 0, "count": 0 } + ], + "issues": [ + { + "name": "decisions", + "location": "cc.go", + "value": "CC=8 | Cog=7 | Nest=1", + "severity": "fair" + }, + { + "name": "manyCases", + "location": "cc.go", + "value": "CC=5 | Cog=1 | Nest=1", + "severity": "good" + } + ], + "score": 0.6 } ], - "distribution": { - "simple": 180, - "moderate": 25, - "complex": 7 - }, - "aggregate": { - "total_functions": 212, - "average_complexity": 3.2, - "max_complexity": 15, - "health_score": 78.5, - "message": "Fair complexity - some functions could be simplified" - } + "overall_score": 0.6 } ``` + (`issues` continues with the remaining eight functions.) `Cognitive + Total` is the sum over all functions and `Max Nesting` the maximum, never + a per-file average. With `--per-file`, a `files` array sits between + `issues` and `score`; each entry carries the file's own `metrics`, + `distribution` (omitted for function-free files), `issues`, `status` + and `score`, and the per-file metrics sum (or max) to the section's. + +=== "YAML" + + ```yaml + function_complexity: + - name: decisions + source_file: cc.go + language: go + directory: . + cyclomatic_complexity: 8 + cognitive_complexity: 7 + nesting_depth: 1 + lines_of_code: 22 + complexity_density: 0.36363636363636365 + risk_level: MEDIUM + - name: manyCases + source_file: cc.go + language: go + directory: . + cyclomatic_complexity: 5 + cognitive_complexity: 1 + nesting_depth: 1 + lines_of_code: 14 + complexity_density: 0.35714285714285715 + risk_level: MEDIUM + distribution: + moderate: 1 + simple: 9 + high_risk_functions: [] + aggregate: + total_functions: 10 + average_complexity: 3.6 + max_complexity: 8 + total_complexity: 36 + cognitive_complexity: 29 + nesting_depth: 3 + decision_points: 26 + health_score: 75.5 + message: Fair complexity - some functions could be simplified + ``` + Each function record includes `source_file`, `language`, and `directory` - for file-level joins and DWH aggregation. + for file-level joins and DWH aggregation. `--format bin` carries exactly + this value as compact JSON inside the `CFB1` envelope. === "Text" ``` - Complexity Analysis - processFile (main.go:42) cyclomatic=8 cognitive=12 nesting=3 - validate (main.go:105) cyclomatic=15 cognitive=22 nesting=5 + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ COMPLEXITY Score: 6/10 ┃ + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + Summary: Fair complexity - some functions could be simplified + + Key Metrics + ──────────────────────────────────────────────────────────────────────────── + Total Functions 10 Avg Complexity 3.6 + Max Complexity 8 Total Complexity 36 + Cognitive Total 29 Max Nesting 3 + Decision Points 26 - Summary: 2 functions, avg cyclomatic=11.5, max nesting=5 + Distribution + ──────────────────────────────────────────────────────────────────────────── + Simple (1-5) ████████████████████████████████████░░░░ 90% (9) + Moderate (6-10) ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 10% (1) + Complex (11-20) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% (0) + Very Complex (>20) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% (0) + + Top Issues + ──────────────────────────────────────────────────────────────────────────── + decisions cc.go CC=8 | Cog=7 | Nest=1 + manyCases cc.go CC=5 | Cog=1 | Nest=1 + nested cc.go CC=4 | Cog=6 | Nest=3 + loopCond cc.go CC=4 | Cog=3 | Nest=1 + boolSeq cc.go CC=4 | Cog=2 | Nest=0 ``` --- +## Field reference + +### Per function (`function_complexity`, `issues`, `report.json: functions`) + +| Field | Meaning | +|---|---| +| `name` | The function's identifier (`anonymous` when none can be recovered); never source text | +| `source_file`, `language`, `directory` | File relative to the analyzed root, detected language, directory of the file (`.` at the root) | +| `cyclomatic_complexity` | `1` + decision points (see the [counting rules](../explanation/complexity.md#counting-rules)) | +| `cognitive_complexity` | SonarSource cognitive complexity | +| `nesting_depth` | Maximum depth of nested control-flow blocks | +| `lines_of_code` | Non-blank, non-comment-only lines of the function's span; lines of a nested named function belong to that function | +| `complexity_density` | `cyclomatic_complexity / lines_of_code` (`0` without lines) | +| `risk_level` | `LOW` / `MEDIUM` / `HIGH` / `CRITICAL`, see [Thresholds](#thresholds) | +| `complexity_assessment`, `cognitive_assessment`, `nesting_assessment` | Per-function labels in the raw report (`report.json`), see [Thresholds](#thresholds) | + +### Aggregate (`aggregate`, the JSON `metrics`) + +| Field | JSON metric | Meaning | +|---|---|---| +| `total_functions` | Total Functions | Number of functions | +| `average_complexity` | Avg Complexity | `total_complexity / total_functions` (function-weighted; `0` without functions) | +| `max_complexity` | Max Complexity | Highest per-function cyclomatic complexity | +| `total_complexity` | Total Complexity | Sum of cyclomatic complexity | +| `cognitive_complexity` | Cognitive Total | Sum of cognitive complexity | +| `nesting_depth` | Max Nesting | Highest per-function nesting depth | +| `decision_points` | Decision Points | Sum of `cyclomatic_complexity - 1` | +| `health_score` | -- | 0..100 from the average, see [Thresholds](#thresholds) | +| `message` | `status` | Band message from the average | + +`distribution` (yaml/bin) counts functions per cyclomatic bucket and omits +empty buckets; `high_risk_functions` lists every function crossing at least +one high threshold, ordered by risk level then the function order, with its +`issues` (`High cyclomatic complexity`, `High cognitive complexity`, `Deep +nesting`). + +--- + +## Thresholds + +Every threshold is defined once (`cf_complexity::thresholds`) and used by +every format. + +**Risk level** (`risk_level`): each dimension scores 2 at or above its high +threshold, 1 at or above its moderate threshold, else 0; the sum maps to a +level. + +| Dimension | Moderate (+1) | High (+2) | +|---|---|---| +| Cyclomatic | ≥ 5 | ≥ 10 | +| Cognitive | ≥ 7 | ≥ 15 | +| Nesting | ≥ 3 | ≥ 5 | + +| Score | Level | +|---|---| +| 0 | `LOW` | +| 1–2 | `MEDIUM` | +| 3–4 | `HIGH` | +| 5–6 | `CRITICAL` | + +Worked examples: cyclomatic 8, cognitive 12, nesting 3 scores 1 + 1 + 1 = +3 → `HIGH`; cyclomatic 15, cognitive 22, nesting 5 scores 2 + 2 + 2 = 6 → +`CRITICAL`. The high thresholds are also what `high_risk_functions` +reports as issues. + +**Cyclomatic buckets**: yaml/bin `distribution` uses `simple` (≤ 5), +`moderate` (6–10), `complex` (> 10); the terminal/JSON distribution uses +`Simple (1-5)`, `Moderate (6-10)`, `Complex (11-20)`, `Very Complex (>20)`. +Issue `severity` is `good` (≤ 5), `fair` (6–10), `poor` (≥ 11). + +**Per-function assessments** (raw report): cyclomatic ≤ 1 `🟢 Simple`, ≤ 5 +`🟡 Moderate`, else `🔴 Complex`; cognitive ≤ 5 `🟢 Low`, ≤ 10 `🟡 Medium`, +else `🔴 High`; nesting ≤ 3 `🟢 Shallow`, ≤ 5 `🟡 Moderate`, else `🔴 Deep`. + +**Average complexity bands** (message, section score, health score): + +| Average cyclomatic | Message | Score | Health | +|---|---|---|---| +| ≤ 1 | Excellent complexity - functions are simple and maintainable | 10/10 | 100 | +| ≤ 3 | Good complexity - functions have reasonable complexity | 8/10 | 80 + (3 − avg) × 10 | +| ≤ 5 | Fair complexity - some functions could be simplified | 6/10 | 50 + (7 − avg) × 7.5 | +| ≤ 7 | Fair complexity - some functions could be simplified | 4/10 | 50 + (7 − avg) × 7.5 | +| ≤ 10 | High complexity - functions are complex and should be refactored | 2/10 | max(0, 50 − (avg − 7) × 5) | +| > 10 | High complexity - functions are complex and should be refactored | 1/10 | max(0, 50 − (avg − 7) × 5) | + +--- + ## Terminal output (readable risk triage) Top issue rows include all core complexity dimensions in one compact value: @@ -105,11 +276,13 @@ Top issue rows include all core complexity dimensions in one compact value: Example: ```text -BoolChain CC=5 | Cog=3 | Nest=1 -NestedIf CC=4 | Cog=6 | Nest=3 +decisions cc.go CC=8 | Cog=7 | Nest=1 +nested cc.go CC=4 | Cog=6 | Nest=3 ``` -Issues are sorted numerically by cyclomatic complexity (then cognitive, then nesting) so the highest-risk functions consistently appear first. +Issues are sorted by cyclomatic complexity (then cognitive, then nesting, +then name and file) so the highest-risk functions consistently appear first. +`text` prints the top five; `--verbose` prints every function. --- @@ -123,7 +296,23 @@ The plot output emphasizes fast interpretation for code-review and planning: --- +## Troubleshooting + +- **A function is missing**: declarations without a body (C prototypes, + Go interface methods and function types, Rust trait signatures, Java + abstract/interface methods) are never reported. Anonymous function + literals outside any function (Go `var f = func() {}`) are not reported + either; inside a function they count toward that function. +- **Two functions with the same name**: the rows are distinct by + `source_file` (and start line in the ordering); nothing is merged. +- **Deeply nested sources**: every format handles tens of thousands of + nesting levels (the walk grows its stack on demand). +- **`--per-file` with `yaml`/`bin`**: the flag only enriches the `json` + sections report; the other formats are unchanged. + +--- + ## See also -- [Understanding complexity metrics](../explanation/complexity.md) — the mental model, McCabe vs cognitive, and limitations. +- [Understanding complexity metrics](../explanation/complexity.md) — the mental model, the counting rules, and how they are validated. - [Quick start](../getting-started/quickstart.md) — run static analysis. diff --git a/site/analyzers/composition.md b/site/analyzers/composition.md new file mode 100644 index 00000000..213b5743 --- /dev/null +++ b/site/analyzers/composition.md @@ -0,0 +1,196 @@ +# Composition analyzer reference + +The composition analyzer (`static/composition`) classifies **every file** in +a tree into one of nine categories — source, documentation, configuration, +vendor, generated, dotfile, image, binary, other — and reports the counts +and shares. It is a raw-file analyzer: it reads each file's path and first +8 KiB, never parses, and therefore sees files the UAST analyzers skip. + +It is informational: its score is always `Info`. + +--- + +## What it counts + +The analyzer classifies every regular file the walk visits (directories are +recursed, `.git` is skipped). **Vendored and generated files are always +counted** — reporting their share is what the analyzer is for — so the +`--include-vendored` / `--include-generated` flags do not change its +output. The only filter that applies is `--languages`, which narrows the +walk to the selected languages' files: + +```bash +codefang run -a static/composition . # every file +codefang run -a static/composition --languages go . # only Go files, still classified into every category +``` + +--- + +## Categories and classification rules + +Each file is classified against its **root-relative path** and its content +header by a fixed-priority cascade; the first matching rule wins: + +| Order | Category | Rule | +|---|---|---| +| 1 | `binary` | non-empty content with a NUL byte in the first 8000 bytes | +| 2 | `image` | extension is exactly `.png`, `.jpg`, `.jpeg` or `.gif` (case-sensitive) | +| 3 | `vendor` | the path matches enry's vendor table (`vendor/`, `node_modules/`, `third_party/`, `testdata/`, minified bundles, …) | +| 4 | `generated` | generated name (`.pb.go`, `.deepcopy.go`, `_string.go`, `zz_generated*`, `mock_*`, `.min.js`, …) or a generated marker (`Code generated`, `DO NOT EDIT`, `@generated`, …) in the first 512 bytes | +| 5 | `documentation` | enry's documentation paths (`docs/`, `Documentation/`, `examples/`, `man/`, `README*`, `LICENSE*`, `CHANGELOG*`, …) or a prose extension (`.md`, `.rst`, `.txt`, `.adoc`, `.org`, …) anywhere | +| 6 | `configuration` | the extension's language is XML, JSON, TOML, YAML or INI | +| 7 | `dotfile` | the base name starts with `.` | +| 8 | `source` | enry recognizes a language from the file name, extension, shebang or modeline (`.go`, `Makefile`, `Dockerfile`, `#!/usr/bin/env python3`, …) | +| 9 | `other` | nothing above matched (unknown extension, no recognizable language) | + +Because the order is fixed, `vendor/README.md` is `vendor`, `.golangci.yml` +is `configuration`, `docs/x.pb.go` is `generated`, and a `.gitignore` is a +`dotfile`. + +!!! note "`.sql` files" + enry lists `.sql` under PL/SQL first, so SQL files are `source`, not + `configuration`. Data formats enry treats as languages (CSV, …) are + `source` as well; only the five configuration languages above are + `configuration`. + +--- + +## Output + +### Section + +| Field | Meaning | +|---|---| +| `status` | `File composition analysis completed`, or `No files analyzed` for an empty tree | +| `metrics` | `Total Files`, `Source Files`, `Source %` | +| `distribution` | one row per non-empty category in the order above: `label`, `percent` (a **fraction in `0..1`**, the shared renderer contract), `count`; omitted when no file was analyzed | +| `issues` | one row per non-empty non-source category: `value` = `"<count> files (<pct>%)"`, `severity` = `poor` for `binary`, `info` otherwise | +| `score` / `score_label` | `-1` / `Info` | + +### `--per-file` + +With `--per-file` the section gains a `files` array with one entry per +classified file, in walk order: + +```json +{ + "file_path": "vendor/x/y.go", + "score_label": "Info", + "status": "Classified as vendor", + "metrics": [ + { "label": "Total Files", "value": "1" }, + { "label": "Vendor Files", "value": "1" }, + { "label": "Vendor %", "value": "100.0%" }, + { "label": "Language", "value": "Go" } + ], + "distribution": [ { "label": "vendor", "percent": 1, "count": 1 } ], + "issues": [], + "score": -1 +} +``` + +`Language` is the enry language of the file, or `unknown`. + +--- + +## Example output + +=== "Text" + + ``` + ┃ COMPOSITION Score: Info ┃ + + Summary: File composition analysis completed + + Key Metrics + Total Files 13 Source Files 3 + Source % 23.1% + + Distribution + source █████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 23% (3) + documentation ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 15% (2) + configuration ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 15% (2) + vendor ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 7% (1) + generated ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 7% (1) + dotfile ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 7% (1) + image ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 7% (1) + binary ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 7% (1) + other ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 7% (1) + ``` + +=== "JSON" + + `--format json` emits the sections envelope: + + ```json + { + "overall_score_label": "Info", + "sections": [ + { + "title": "COMPOSITION", + "score_label": "Info", + "status": "File composition analysis completed", + "metrics": [ + { "label": "Total Files", "value": "13" }, + { "label": "Source Files", "value": "3" }, + { "label": "Source %", "value": "23.1%" } + ], + "distribution": [ + { "label": "source", "percent": 0.23076923076923078, "count": 3 }, + { "label": "vendor", "percent": 0.07692307692307693, "count": 1 }, + { "label": "other", "percent": 0.07692307692307693, "count": 1 } + ], + "issues": [ + { "name": "vendor", "location": "", "value": "1 files (7.7%)", "severity": "info" }, + { "name": "binary", "location": "", "value": "1 files (7.7%)", "severity": "poor" } + ], + "score": -1 + } + ], + "overall_score": -1 + } + ``` + +=== "YAML / bin" + + `--format yaml` and `--format bin` carry the raw aggregate — every + category, zero counts included, percentages in `0..100`: + + ```yaml + breakdown: + binary: 1 + configuration: 2 + documentation: 2 + dotfile: 1 + generated: 1 + image: 1 + other: 1 + source: 3 + vendor: 1 + percentages: + binary: 7.6923076923076925 + configuration: 15.384615384615385 + documentation: 15.384615384615385 + dotfile: 7.6923076923076925 + generated: 7.6923076923076925 + image: 7.6923076923076925 + other: 7.6923076923076925 + source: 23.076923076923077 + vendor: 7.6923076923076925 + total_files: 13 + ``` + +--- + +## Configuration options + +| Option | Type | Default | Description | +|---|---|---|---| +| *(none)* | -- | -- | No analyzer-specific config; `--languages` narrows the walk | + +--- + +## See also + +- [Clones](clones.md) — the other cross-file static analyzer. +- [Quick start](../getting-started/quickstart.md) — run static analysis. diff --git a/site/analyzers/couples.md b/site/analyzers/couples.md index 8c8a0e03..ce0653bf 100644 --- a/site/analyzers/couples.md +++ b/site/analyzers/couples.md @@ -4,7 +4,7 @@ The couples analyzer identifies **file coupling** and **developer coupling** by analyzing co-change patterns across Git history. For the conceptual model — what coupling means, how coupling strength is -computed, and the methodology — see +computed, and how renames, merges and large commits are handled — see [Understanding file and developer coupling](../explanation/couples.md). To run it, see the [Quick start](../getting-started/quickstart.md). @@ -12,12 +12,20 @@ it, see the [Quick start](../getting-started/quickstart.md). ## Configuration options -The couples analyzer has no additional configuration options. It uses the identity detector for developer mapping and the tree diff analyzer for change detection. +The couples analyzer has no additional configuration options. It uses the +shared history core: the commit window (`--limit`, `--since`, `--head`, +`--first-parent`), the identity detector (`--exact-signatures`, +`--people-dict`, `--anonymize`) and the change filter (`--languages`, +vendor / generated policy) documented in the +[CLI reference](../guide/cli-reference.md). | Option | Type | Default | Description | |---|---|---|---| | *(none)* | -- | -- | No analyzer-specific configuration | +The changeset cap is fixed: a commit touching more than **1000 files** feeds +no coupling context (see the explanation page). + --- ## Output formats @@ -33,24 +41,37 @@ The text format provides a terminal-friendly summary with: ### Plot (HTML) -The HTML plot includes three interactive chart sections: +The HTML plot includes two interactive chart sections: 1. **Top File Couples** — Horizontal bar chart of the 20 most co-changed file pairs -2. **Developer Coupling Heatmap** — Matrix showing developer collaboration intensity -3. **File Ownership Distribution** — Pie chart categorizing files by contributor count (single owner, 2-3, 4-5, 6+) +2. **File Ownership Distribution** — Pie chart categorizing files by contributor count (single owner, 2-3, 4-5, 6+) ### JSON / YAML -Structured output with four top-level sections: `file_coupling`, `developer_coupling`, `file_ownership`, and `aggregate`. - -### Executive summary (ReportSection) - -The couples analyzer provides a `ReportSection` for use in combined reports: - -- **Score**: `1.0 - avg_coupling_strength` (lower coupling = better score, 0-1 scale) -- **Key Metrics**: Total files, developers, co-changes, highly coupled pairs, average coupling -- **Distribution**: Coupling strength buckets — Strong (>70%), Moderate (40-70%), Weak (10-40%), Minimal (<10%) -- **Issues**: File pairs sorted by coupling strength descending, with severity labels (poor/fair/good) +Structured output with four top-level sections: `file_coupling`, +`developer_coupling`, `file_ownership`, and `aggregate`. + +| Section | Field | Meaning | +|---|---|---| +| `file_coupling[]` | `co_changes` | Number of commits in the window in which both files changed | +| `file_coupling[]` | `coupling_strength` | `co_changes / avg(changes_file1, changes_file2)`, capped at 1.0 | +| `developer_coupling[]` | `shared_file_changes` | Sum over shared files of `min(changes_dev1, changes_dev2)` | +| `file_ownership[]` | `lines` | Newline count of the file at the window head | +| `file_ownership[]` | `contributors` | Distinct developers who changed the file in the window | +| `aggregate` | `total_files` | Files present at the window head that were changed in the window | +| `aggregate` | `avg_coupling_strength` | Mean of the per-pair coupling strengths | +| `aggregate` | `highly_coupled_pairs` | Pairs with 10 or more co-changes | + +`file_coupling` is `null` (YAML `[]`) when no pair co-changed in the window. + +### NDJSON / timeseries + +`--format ndjson` emits one record per walked commit with +`CouplingFiles` (the coupling context — empty above the cap), `AuthorFiles` +(every file the author touched, deletions included), `Renames` +(`FromName` / `ToName` pairs) and `CommitCounted`. `--format timeseries` +reports, per commit and under its tick, `files_touched` (the size of +`AuthorFiles`) and `author_id`. --- diff --git a/site/analyzers/developers.md b/site/analyzers/developers.md index 6c3577ae..e2ef8120 100644 --- a/site/analyzers/developers.md +++ b/site/analyzers/developers.md @@ -1,156 +1,199 @@ # Developers analyzer reference The developers analyzer computes **per-developer contribution statistics** -across Git history, including commit counts, line changes, language breakdown, -bus factor risk, and activity time series. +across Git history: commit counts, lines added / removed / changed with a +per-language breakdown, bus factor risk, and activity time series. -For the conceptual model — what each statistic means and how the bus factor is -derived — see [Understanding developer analysis](../explanation/developers.md). -To run it, see the [Quick start](../getting-started/quickstart.md). +For the conceptual model — what each statistic means and how the bus factor +is derived — see +[Understanding developer analysis](../explanation/developers.md). To run +it, see the [Quick start](../getting-started/quickstart.md). --- -## Configuration options +## Options -| Option | Type | Default | Description | +The analyzer reads its options from the `codefang run` flags. The shared +history flags (`--head`, `--limit`, `--since`, `--first-parent`, +`--tick-size`, `--max-changes-per-commit`, `--languages`, `--people-dict`, +`--exact-signatures`, `--anonymize`) apply as documented in the +[CLI reference](../guide/cli-reference.md). + +| Flag | Type | Default | Description | |---|---|---|---| -| `Devs.ConsiderEmptyCommits` | `bool` | `false` | Include empty commits (e.g., trivial merges) in commit counts | -| `Devs.Anonymize` | `bool` | `false` | Replace developer names with pseudonyms (Developer-A, Developer-B, etc.) | +| `--empty-commits` | `bool` | `false` | Count commits whose filtered diff is empty (trivial merges, `--allow-empty` commits, commits touching only excluded paths) as commits with zero lines. | +| `--anonymize` | `bool` | `false` | Replace developer names with pseudonyms (`Developer-A`, `Developer-B`, …). | -```yaml -# .codefang.yml -history: - devs: - consider_empty_commits: false - anonymize: false -``` +!!! note "Configuration file" + The `history.devs.*` keys of `.codefang.yaml` describe the same + options in the configuration schema, but `codefang run` applies the + flags above only. --- ## Example output +From a twelve-commit fixture with three developers +(`bins/codefang/tests/devs_e2e.rs`); the `activity` and `churn` lists are +cut to their first tick. + === "JSON" ```json { + "aggregate": { + "total_commits": 10, + "total_lines_added": 30, + "total_lines_removed": 15, + "total_developers": 3, + "active_developers": 1, + "estimated_total_developers": 3, + "estimated_active_developers": 1, + "analysis_period_ticks": 310, + "project_bus_factor": 2, + "total_languages": 2 + }, "developers": [ { "id": 0, "name": "alice", "email": "alice@example.com", - "commits": 342, - "lines_added": 28500, - "lines_removed": 12300, - "lines_changed": 8400, - "net_lines": 16200, + "commits": 6, + "lines_added": 16, + "lines_removed": 0, + "lines_changed": 0, + "net_lines": 16, "languages": [ - {"language": "Go", "added": 22000, "removed": 9800, "changed": 6200}, - {"language": "Python", "added": 6500, "removed": 2500, "changed": 2200} + {"language": "Go", "added": 11, "removed": 0, "changed": 0}, + {"language": "Python", "added": 5, "removed": 0, "changed": 0} ], "first_tick": 0, - "last_tick": 120, - "active_ticks": 85 + "last_tick": 310, + "active_ticks": 6 + }, + { + "id": 2, + "name": "carol", + "email": "carol@example.com", + "commits": 1, + "lines_added": 6, + "lines_removed": 12, + "lines_changed": 0, + "net_lines": -6, + "languages": [ + {"language": "Go", "added": 6, "removed": 12, "changed": 0} + ], + "first_tick": 155, + "last_tick": 155, + "active_ticks": 1 } ], "languages": [ { "name": "Go", - "total_lines": 45000, - "total_contribution": 67800, - "contributors": {"0": 54600, "1": 13200} + "total_lines": 22, + "total_contribution": 37, + "contributors": {"0": 11, "1": 8, "2": 18} } ], "busfactor": [ { - "language": "Python", - "bus_factor": 1, - "total_contributors": 2, - "primary_dev_id": 0, - "primary_dev_name": "alice", - "primary_dev_email": "alice@example.com", - "primary_percentage": 67.1, - "secondary_dev_id": 1, - "secondary_dev_name": "bob", - "secondary_dev_email": "bob@example.com", - "secondary_percentage": 32.9, - "risk_level": "MEDIUM" + "language": "Go", + "bus_factor": 2, + "total_contributors": 3, + "primary_dev_id": 2, + "primary_dev_name": "carol", + "primary_dev_email": "carol@example.com", + "primary_percentage": 48.64864864864865, + "secondary_dev_id": 0, + "secondary_dev_name": "alice", + "secondary_dev_email": "alice@example.com", + "secondary_percentage": 29.72972972972973, + "risk_level": "LOW" } ], "activity": [ { "tick": 0, - "start_time": "2024-01-15T10:30:00Z", - "end_time": "2024-01-16T08:45:00Z", - "total_commits": 5, - "by_developer": [ - {"dev_id": 0, "commits": 3}, - {"dev_id": 1, "commits": 2} - ] + "start_time": "2021-01-01T12:00:00Z", + "end_time": "2021-01-01T12:00:00Z", + "by_developer": [{"dev_id": 0, "commits": 1}], + "total_commits": 1 } ], "churn": [ { "tick": 0, - "start_time": "2024-01-15T10:30:00Z", - "end_time": "2024-01-16T08:45:00Z", - "lines_added": 450, - "lines_removed": 120, - "net_change": 330 + "start_time": "2021-01-01T12:00:00Z", + "end_time": "2021-01-01T12:00:00Z", + "lines_added": 10, + "lines_removed": 0, + "net_change": 10 } - ], - "aggregate": { - "total_commits": 850, - "total_developers": 5, - "active_developers": 3, - "analysis_period_ticks": 120, - "project_bus_factor": 2, - "total_languages": 4 - } + ] } ``` - **Key fields for analytics:** - - - `developers[].email` — split from previously pipe-delimited name - - `developers[].languages` — flattened from map to sorted array - - `activity[].by_developer` — flattened from `map[int]int` to `[{dev_id, commits}]` array - - `activity[].start_time` / `end_time` — RFC 3339 tick boundaries - - `churn[].start_time` / `end_time` — RFC 3339 tick boundaries - - `busfactor[].primary_dev_email` / `secondary_dev_email` — split identity fields - === "YAML" ```yaml aggregate: - total_commits: 850 - total_lines_added: 95000 - total_lines_removed: 42000 - total_developers: 5 - active_developers: 3 - project_bus_factor: 2 - total_languages: 4 + total_commits: 10 + total_lines_added: 30 + total_lines_removed: 15 + total_developers: 3 + active_developers: 1 + estimated_total_developers: 3 + estimated_active_developers: 1 + analysis_period_ticks: 310 + project_bus_factor: 2 + total_languages: 2 developers: - - name: alice - commits: 342 - lines_added: 28500 - lines_removed: 12300 - net_lines: 16200 - languages: - - name: Go - total_lines: 45000 - total_contribution: 67800 + - id: 0 + name: alice + email: alice@example.com + commits: 6 + lines_added: 16 + lines_removed: 0 + lines_changed: 0 + net_lines: 16 busfactor: - - language: Python - bus_factor: 1 - total_contributors: 2 - primary_dev_name: alice - primary_percentage: 67.1 - risk_level: MEDIUM + - language: Go + bus_factor: 2 + total_contributors: 3 + primary_dev_id: 2 + primary_dev_name: carol + primary_percentage: 48.64864864864865 + secondary_dev_id: 0 + secondary_dev_name: alice + risk_level: LOW ``` +Fields: + +| Field | Meaning | +|---|---| +| `aggregate.total_commits` | Commits counted (merges included, empty commits only with `--empty-commits`). | +| `aggregate.total_developers`, `active_developers` | Exact counts; active = with commits in the last 90 days of the window. | +| `aggregate.estimated_total_developers`, `estimated_active_developers` | The same cardinalities estimated by a HyperLogLog sketch (precision 14, about 0.8 % error); equal to the exact counts on small inputs. | +| `aggregate.analysis_period_ticks` | The newest tick of the window. | +| `aggregate.project_bus_factor` | Smallest number of developers covering 50 % of all contributions (`added + removed`). | +| `developers[]` | Sorted by commits (id on a tie): `email` is omitted when unknown; `languages` is `null` in JSON (`[]` in YAML) for a developer with no line statistics; `first_tick` / `last_tick` / `active_ticks` describe the active period. | +| `languages[]` | Sorted by `total_lines` (lines added): `total_contribution` is `added + removed`; `contributors` maps developer id → contribution. | +| `busfactor[]` | Per language, highest risk first: `bus_factor`, `total_contributors`, the primary and secondary owners (the secondary is present whenever the language has two or more contributors — developer id 0 included), `primary_percentage` / `secondary_percentage`, `risk_level`. | +| `activity[]`, `churn[]` | Per tick, with the tick's first and last committer time: commits per developer / lines added, removed and the net change. | + +### NDJSON and timeseries + +`--format ndjson` writes one record per commit with line statistics +(`{"commits", "lines_added", "lines_removed", "lines_changed", "author_id", +"languages"}`); `--format timeseries` lists every kept commit in tick order +with the same entry under `"devs"`. + --- ## See also -- [Understanding developer analysis](../explanation/developers.md) — the mental model, bus factor methodology, architecture, and limitations. -- [Quick start](../getting-started/quickstart.md) — run history analysis. +- [Understanding developer analysis](../explanation/developers.md) — the + mental model, bus factor methodology, architecture, and limitations. +- [Quick start](../getting-started/quickstart.md) — run developer analysis. diff --git a/site/analyzers/file-history.md b/site/analyzers/file-history.md index b0f54297..b58248a3 100644 --- a/site/analyzers/file-history.md +++ b/site/analyzers/file-history.md @@ -2,10 +2,11 @@ The file history analyzer tracks the **lifecycle of every file** through Git history, recording which commits touched each file, which developers modified -it, and aggregating line statistics per contributor. +it, aggregating line statistics per contributor, and classifying the changed +files by category. -For the conceptual model — what the lifecycle data means and how hotspots and -churn are derived — see +For the conceptual model — what the lifecycle data means, how hotspots, churn +and the composition are derived — see [Understanding file history analysis](../explanation/file-history.md). To run it, see the [Quick start](../getting-started/quickstart.md). @@ -13,7 +14,12 @@ it, see the [Quick start](../getting-started/quickstart.md). ## Configuration options -The file history analyzer has no additional configuration options. +The file history analyzer has no additional configuration options. It uses +the shared history core: the commit window (`--limit`, `--since`, `--head`, +`--first-parent`), the identity detector (`--exact-signatures`, +`--people-dict`, `--anonymize`), the tick size (`--tick-size`) and the change +filter (`--languages`, vendor / generated policy) documented in the +[CLI reference](../guide/cli-reference.md). | Option | Type | Default | Description | |---|---|---|---| @@ -21,6 +27,58 @@ The file history analyzer has no additional configuration options. --- +## Output + +### JSON / YAML + +Six top-level sections. Every per-file section lists only the files present +in the **newest commit of the window** (the window head); a file deleted +inside the window is absent. + +| Section | Field | Meaning | +|---|---|---| +| `file_churn[]` | `commit_count` | Commits in the window that touched the file, following renames | +| `file_churn[]` | `contributor_count` | Distinct developers with line stats on the file | +| `file_churn[]` | `total_lines_added` / `_removed` / `_changed` | Sum of the per-commit line stats | +| `file_churn[]` | `churn_score` | `commit_count + (added + removed + changed) / 100` | +| `file_contributors[]` | `contributors[]` | Per developer id: `added`, `removed`, `changed` | +| `file_contributors[]` | `top_contributor_id` / `top_contributor_lines` | Developer with the most `added + changed` lines | +| `hotspots[]` | `risk_level` | `CRITICAL` (>= 50 commits), `HIGH` (>= 30), `MEDIUM` (>= 15) | +| `aggregate` | `total_files` / `total_commits` / `total_contributors` | Over the files at the window head (`total_commits` sums their commit counts) | +| `composition` | `breakdown` | Distinct files changed in the window, per category (non-zero categories only) | +| `composition` | `percentages` | The same as a percentage of the distinct files changed (every category) | +| `composition_ts[]` | `tick`, `start_time`, `end_time`, `breakdown` | Distinct files changed in that tick, per category, with the tick's first and last committer time (RFC3339 UTC) | + +Categories are `source`, `documentation`, `configuration`, `vendor`, +`generated`, `dotfile`, `image`, `binary`, decided by the shared classifier +over the path **and** the blob content (a NUL-containing blob is `binary`, a +generated-code marker makes `generated`, whatever the path says). A file +counts once per scope under its latest classification; a renamed file keeps +one identity. + +### Text + +A summary (files, commits, contributors, average commits per file, high churn +files), the **File Composition** table (one row per non-zero category with +its share of the distinct files changed) and the **Most Modified Files** list +(top 10 by commit count). + +### Plot (HTML) + +1. **Most Modified Files** — bar chart of the 20 files with the most commits +2. **File Composition Over Time** — stacked area of the distinct files changed per tick, by category + +### NDJSON / timeseries + +`--format ndjson` emits one record per walked commit with its +`PathActions` (`Path`, `Action` 0 insert / 1 delete / 2 modify, `FromPath` / +`ToPath` for a rename), its `LineStatUpdates` (`null` for a merge) and the +per-commit `Composition` counts. `--format timeseries` reports, per commit and +under its tick, `files_touched`, `lines_added` / `lines_removed` / +`lines_changed` and the `inserts` / `deletes` / `modifies` counts. + +--- + ## Example output === "JSON" @@ -47,14 +105,6 @@ The file history analyzer has no additional configuration options. "churn_score": 15.55 } ], - "hotspots": [ - { - "path": "pkg/core/engine.go", - "commit_count": 87, - "churn_score": 146.5, - "risk_level": "CRITICAL" - } - ], "file_contributors": [ { "path": "pkg/core/engine.go", @@ -68,6 +118,14 @@ The file history analyzer has no additional configuration options. "top_contributor_lines": 2800 } ], + "hotspots": [ + { + "path": "pkg/core/engine.go", + "commit_count": 87, + "churn_score": 146.5, + "risk_level": "CRITICAL" + } + ], "aggregate": { "total_files": 342, "total_commits": 1250, @@ -75,7 +133,22 @@ The file history analyzer has no additional configuration options. "avg_commits_per_file": 3.65, "avg_contributors_per_file": 1.8, "high_churn_files": 15 - } + }, + "composition": { + "breakdown": {"configuration": 12, "documentation": 30, "source": 300}, + "percentages": { + "binary": 0, "configuration": 3.5, "documentation": 8.8, "dotfile": 0, + "generated": 0, "image": 0, "source": 87.7, "vendor": 0 + } + }, + "composition_ts": [ + { + "tick": 0, + "start_time": "2021-01-01T09:12:00Z", + "end_time": "2021-01-01T18:40:00Z", + "breakdown": {"documentation": 1, "source": 4} + } + ] } ``` @@ -96,11 +169,20 @@ The file history analyzer has no additional configuration options. total_commits: 1250 avg_commits_per_file: 3.65 high_churn_files: 15 + composition: + breakdown: + source: 300 + composition_ts: + - tick: 0 + start_time: "2021-01-01T09:12:00Z" + end_time: "2021-01-01T18:40:00Z" + breakdown: + source: 4 ``` --- ## See also -- [Understanding file history analysis](../explanation/file-history.md) — the mental model, hotspots, churn, and limitations. +- [Understanding file history analysis](../explanation/file-history.md) — the mental model, hotspots, churn, composition and limitations. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/analyzers/halstead.md b/site/analyzers/halstead.md index 897e20dd..0431a9b8 100644 --- a/site/analyzers/halstead.md +++ b/site/analyzers/halstead.md @@ -1,58 +1,296 @@ # Halstead analyzer reference -The Halstead analyzer computes **Halstead complexity metrics** (1977) based on -operator and operand counts extracted from the UAST. +The Halstead analyzer computes **Halstead complexity metrics** (1977) from +the operators and operands of every function in the UAST. -For the conceptual model — what operators and operands are and which derived -metrics matter most — see +For the conceptual model — what counts as an operator or operand, how the +codebase aggregate is defined and what the thresholds mean — see [Understanding Halstead metrics](../explanation/halstead.md). To run it, see the [Quick start](../getting-started/quickstart.md). +```sh +codefang run -a static/halstead --format json ./src +codefang run -a static/halstead --format json --per-file ./src +codefang run -a static/halstead --format yaml ./src +codefang run -a static/halstead --format plot --output ./out ./src +``` + --- ## Configuration options -The Halstead analyzer uses the UAST directly and has no analyzer-specific configuration options. +The Halstead analyzer uses the UAST directly and has no analyzer-specific +configuration options. | Option | Type | Default | Description | |---|---|---|---| | *(none)* | -- | -- | Uses UAST; no analyzer-specific config | +The run-level flags apply: `--per-file` (json only — yaml and bin always +report the codebase aggregate), `--include-vendored` / `--include-generated`, +and `--languages`. + +--- + +## Definitions + +| Symbol | Meaning | Formula | +|---|---|---| +| `n1` | distinct operators | — | +| `n2` | distinct operands | — | +| `N1` | total operators | — | +| `N2` | total operands | — | +| Vocabulary `n` | | `n1 + n2` | +| Length `N` | | `N1 + N2` | +| Estimated length | | `n1·log2(n1) + n2·log2(n2)` | +| Volume `V` | | `N · log2(n)` | +| Difficulty `D` | | `(n1 / 2) · (N2 / n2)` | +| Effort `E` | | `D · V` | +| Time `T` (seconds) | | `E / 18` | +| Bugs `B` | | `V / 3000` | + +### Classification table + +| UAST node (inside a function body) | Counted as | Name | +|---|---|---| +| leaf `Identifier`, leaf `Literal` | operand | its text (`token`, else the source span, else `props.name` / `props.value`) | +| leaf `Identifier` with the `Type` role | operator (type specifier) | its text | +| any node with `props["operator"]` | operator | that property, and nothing else from the node | +| `Call` / `Index` / `Slice` | operator | fixed `()` / `[]` / `[:]` | +| every other node | operator(s) | the node's *residue*: its source span with its children blanked (` := `, ` + `, `return `, `.`, `++`), split into symbol runs and whitelisted keywords | +| `Parameter` (type or role), `Comment`, `DocString` | skipped | — | +| the function's signature (every child before its body) | skipped | — | + +A quoted literal (`"x"`) is one operand including its quotes; a composite +literal or qualified name is classified by its parts, so `fmt.Println` +yields the operands `fmt`, `Println` and the operator `.`. Closures and +lambdas belong to the function that contains them; nested named functions +and methods are their own records. A node type name is never a token and a +statement's text is never an operand. The keyword whitelist and the +canonical fallbacks are in `crates/cf-halstead/src/classify.rs`. + +### Aggregate + +The codebase aggregate — and each `--per-file` entry — is the **union** of +the operator and operand multisets of its functions: `n1` / `n2` are +distinct across every function, `N1` / `N2` are the summed occurrences, and +V / D / E / T / B derive from those four numbers. Per-function statistics +(mean and max volume, difficulty and effort; the high-effort count and +share) sit alongside. + +### Thresholds + +All bands are lower-inclusive (a value at a bound is in the higher band). + +| Per function | Low / Simple | Medium / Moderate | High / Complex | Very High | +|---|---|---|---|---| +| Volume `V` (distribution, `complexity_level`) | `V < 100` | `100 ≤ V < 1000` | `1000 ≤ V < 5000` | `V ≥ 5000` | +| Difficulty `D` (`difficulty_assessment`) | `D < 5` | `5 ≤ D < 15` | `D ≥ 15` | — | +| Effort `E` (`effort_assessment`) | `E < 1000` | `1000 ≤ E < 10000` | `E ≥ 10000` | — | + +* **High-effort function**: `E ≥ 10000` (listed under + `high_effort_functions`; `risk_level` is `HIGH` from `E ≥ 50000`, else + `MEDIUM`). +* **Issue severity**: `poor` when `E ≥ 50000` or `B ≥ 1.0`; `fair` when + `E ≥ 10000` or `B ≥ 0.3`; else `good`. +* **Status message and health score** come from the share `h` of + high-effort functions: + +| `h` | `status` / `message` | `health_score` | +|---|---|---| +| no functions | `No functions found` | 100 | +| `h < 5%` | `Low Halstead complexity - well-structured code` | 100 → 90 | +| `5% ≤ h < 15%` | `Moderate Halstead complexity - acceptable` | 90 → 70 | +| `15% ≤ h < 30%` | `High Halstead complexity - consider refactoring` | 70 → 40 | +| `h ≥ 30%` | `Very high Halstead complexity - significant refactoring recommended` | 40 → 0 | + +The health score interpolates linearly between the anchors +`(0, 100)`, `(5%, 90)`, `(15%, 70)`, `(30%, 40)`, `(100%, 0)`. The section +`score` is `health_score / 100` and `score_label` is `round(score · 10)/10`. + +Function lists (`issues`, `function_halstead`, `high_effort_functions`) are +ordered by effort descending, then volume descending, then name, then file. + --- ## Output formats -### Terminal (text/summary) +The examples below are the real output for a two-file fixture: `hal.go` +(`c := a + b; return c * 2`) and `calls.go` (three `fmt.Println(...)` +calls). + +### JSON (`--format json`) + +```json +{ + "overall_score_label": "10/10", + "sections": [ + { + "title": "HALSTEAD", + "score_label": "10/10", + "status": "Low Halstead complexity - well-structured code", + "metrics": [ + { "label": "Total Functions", "value": "2" }, + { "label": "Distinct Operators (n1)", "value": "6" }, + { "label": "Distinct Operands (n2)", "value": "8" }, + { "label": "Total Operators (N1)", "value": "10" }, + { "label": "Total Operands (N2)", "value": "14" }, + { "label": "Vocabulary", "value": "14" }, + { "label": "Length", "value": "24" }, + { "label": "Volume", "value": "91.4" }, + { "label": "Difficulty", "value": "5.2" }, + { "label": "Effort", "value": "479.7" }, + { "label": "Est. Bugs", "value": "0.0" }, + { "label": "Mean Effort", "value": "77.4" }, + { "label": "Max Effort", "value": "87.2" }, + { "label": "High-Effort Functions", "value": "0" } + ], + "distribution": [ + { "label": "Low (<100)", "percent": 1, "count": 2 }, + { "label": "Medium (100-1000)", "percent": 0, "count": 0 }, + { "label": "High (1000-5000)", "percent": 0, "count": 0 }, + { "label": "Very High (>=5000)", "percent": 0, "count": 0 } + ], + "issues": [ + { "name": "calls", "location": "calls.go", "value": "effort=87.2 | vol=38.8 | bugs=0.0", "severity": "good" }, + { "name": "hal", "location": "hal.go", "value": "effort=67.5 | vol=27.0 | bugs=0.0", "severity": "good" } + ], + "score": 1 + } + ], + "overall_score": 1 +} +``` + +With `--per-file` the section gains a `files` array with one entry per +analyzed file (function-free files included), each carrying the same +`metrics` list computed from that file's own functions, its own +`distribution` (absent when the file has no functions), its `issues`, and +a `score` from its own high-effort share: + +```json +"files": [ + { + "file_path": "calls.go", + "score_label": "10/10", + "status": "Low Halstead complexity - well-structured code", + "metrics": [ + { "label": "Total Functions", "value": "1" }, + { "label": "Distinct Operators (n1)", "value": "2" }, + { "label": "Distinct Operands (n2)", "value": "4" }, + { "label": "Total Operators (N1)", "value": "6" }, + { "label": "Total Operands (N2)", "value": "9" }, + ... + ], + "distribution": [ ... ], + "issues": [ ... ], + "score": 1 + }, + ... +] +``` + +A folder with no functions (or no analyzable files) produces the same shape +with every count at 0, `"status": "No functions found"`, no `distribution`, +and empty `issues` / `files`; the exit code is 0. + +### YAML / bin (`--format yaml`, `--format bin`) + +Both formats serialize the same value (bin wraps it in the CFB1 envelope): -The section surfaces foundational counts before derived metrics: +```yaml +function_halstead: + - name: calls + source_file: calls.go + language: go + directory: . + volume: 38.77443751081734 + difficulty: 2.25 + effort: 87.24248439933902 + time_to_program: 4.846804688852168 + delivered_bugs: 0.012924812503605781 + complexity_level: Low + - name: hal + source_file: hal.go + language: go + directory: . + volume: 27 + difficulty: 2.5 + effort: 67.5 + time_to_program: 3.75 + delivered_bugs: 0.009 + complexity_level: Low +distribution: + low: 2 + medium: 0 + high: 0 + very_high: 0 +high_effort_functions: [] +aggregate: + total_functions: 2 + volume: 91.3765181293825 + difficulty: 5.25 + effort: 479.72672017925817 + time_to_program: 26.65148445440323 + delivered_bugs: 0.030458839376460836 + distinct_operators: 6 + distinct_operands: 8 + total_operators: 10 + total_operands: 14 + vocabulary: 14 + length: 24 + estimated_length: 39.50977500432694 + mean_volume: 32.88721875540867 + max_volume: 38.77443751081734 + mean_difficulty: 2.375 + max_difficulty: 2.5 + mean_effort: 77.37124219966951 + max_effort: 87.24248439933902 + high_effort_functions: 0 + high_effort_share: 0 + health_score: 100 + message: Low Halstead complexity - well-structured code +``` -- `Distinct Operators (n1)` -- `Distinct Operands (n2)` -- `Total Operators (N1)` -- `Total Operands (N2)` -- `Vocabulary`, `Volume`, `Difficulty`, `Effort`, `Est. Bugs` +`high_effort_functions` entries carry `name`, `source_file`, `volume`, +`effort`, `time_to_program`, `delivered_bugs` and `risk_level`. -Top issues include compact multi-signal context: +### Terminal (`--format text`, `--format compact`) -- `effort=<...> | vol=<...> | bugs=<...>` +The section shows the status line and the same key-metrics list as JSON +(union counts, derived measures, mean / max effort, high-effort count). -Severity is determined from both effort and bug estimate. +### Plot (`--format plot --output <dir>`) -### Plot +`<dir>/report.json` carries the raw report under `halstead`: every function +in walk order with its counts, derived measures, assessments and the full +`operators` / `operands` maps, plus the union aggregate, per-function +statistics and `health_score`: -The plot report includes: +```json +{ + "name": "hal", + "_source_file": "hal.go", "_language": "go", "_directory": ".", + "distinct_operators": 4, "distinct_operands": 4, + "total_operators": 4, "total_operands": 5, + "vocabulary": 8, "length": 9, "estimated_length": 16, + "volume": 27, "difficulty": 2.5, "effort": 67.5, + "time_to_program": 3.75, "delivered_bugs": 0.009, + "volume_assessment": "🟢 Low", + "difficulty_assessment": "🟢 Simple", + "effort_assessment": "🟢 Low", + "operators": { "*": 1, "+": 1, ":=": 1, "return": 1 }, + "operands": { "2": 1, "a": 1, "b": 1, "c": 2 } +} +``` -1. **Top Functions by Effort** (Top 12, highest first) -2. **Volume vs Difficulty** risk map: - - X: volume - - Y: difficulty - - bubble size: estimated bugs - - color: low/medium/high risk bucket -3. **Volume Distribution** by bucket (`Low`, `Medium`, `High`, `Very High`) +The HTML page includes **Top Functions by Effort**, the **Volume vs +Difficulty** risk map (bubble size: estimated bugs) and the **Volume +Distribution** by bucket. --- ## See also -- [Understanding Halstead metrics](../explanation/halstead.md) — the mental model, formulas, counting policy, and limitations. +- [Understanding Halstead metrics](../explanation/halstead.md) — the mental model, counting policy, aggregation and limitations. - [Quick start](../getting-started/quickstart.md) — run static analysis. diff --git a/site/analyzers/imports.md b/site/analyzers/imports.md index 23f59e27..50e563a5 100644 --- a/site/analyzers/imports.md +++ b/site/analyzers/imports.md @@ -1,8 +1,9 @@ # Imports analyzer reference The imports analyzer extracts **import and dependency information** from source -code using UAST parsing. It operates in both static mode (single-file analysis) -and history mode (tracking import usage per developer over time). +code using UAST parsing. It operates in both static mode (per-file analysis +aggregated over a tree) and history mode (tracking import usage per developer +over time). For the conceptual model — what it measures and how the history-mode pipeline works — see [Understanding import and dependency @@ -11,6 +12,76 @@ analysis](../explanation/imports.md). To run it, see the --- +## Running static analysis + +```bash +codefang run -a static/imports --format json ./path/to/repo +codefang run -a static/imports --format json --per-file ./path/to/repo +codefang run -a static/imports --format yaml ./path/to/repo +codefang run -a static/imports --format json ./path/to/single/file.go +``` + +- The root may be a **directory** (walked recursively, `.git` and + cache-tagged directories skipped, vendor/generated paths excluded by the + path policy, `--languages` honoured) or a **single file**. +- Every supported file counts towards `Total Files`, including files with + no imports. +- A root with no analyzable files produces the empty report (status + `No import data available`, `Unique Imports` 0, `Total Files` 0) with + exit code 0. +- Output is deterministic: two runs over the same tree emit identical bytes. + +--- + +## What counts as one import + +Each file contributes the set of modules, packages or files its import +statements name — once per file, in first-seen order. Aliases, specifier +lists and keywords never appear as imports. The aggregate value of an import +is the **number of files importing it**. + +| Language | Statement | Reported import(s) | +|---|---|---| +| Go | `import "fmt"`, `str "strings"`, `. "math"`, `_ "embed"` | `fmt`, `strings`, `math`, `embed` | +| Python | `import os, sys`, `import numpy as np` | `os`, `sys`, `numpy` | +| Python | `from typing import List, Dict`, `from x import y as z` | `typing`, `x` | +| Python | `from . import sibling`, `from ..pkg.sub import thing` | `.sibling`, `..pkg.sub` | +| JavaScript / TypeScript | `import React from 'react'`, `import { a, b } from 'react'`, `import * as ns from 'ns-lib'`, `import './side.css'` | `react`, `ns-lib`, `./side.css` | +| JavaScript / TypeScript | `require('fs')`, `import('dyn-mod')`, `import fs = require("fs")`, `import type { T } from "./types"` | `fs`, `dyn-mod`, `./types` | +| Rust | `use std::collections::{HashMap, HashSet};`, `pub(crate) use std::io::{self, Read};` | `std::collections::HashMap`, `std::collections::HashSet`, `std::io`, `std::io::Read` | +| Rust | `use super::baz as qux;`, `use std::io::*;`, `extern crate alloc;` | `super::baz`, `std::io::*`, `alloc` | +| Java | `import java.util.List;`, `import java.util.*;`, `import static java.lang.Math.max;` | `java.util.List`, `java.util.*`, `java.lang.Math.max` | +| C / C++ | `#include <stdio.h>`, `#include "local.h"` | `stdio.h`, `local.h` (local file) | +| C# | `using System.IO;`, `using Alias = System.IO;`, `using static System.Math;` | `System.IO`, `System.IO`, `System.Math` | +| PHP | `use App\Models\User;`, `use App\Http\{A, B};`, `use Vendor\Thing as T;` | `App\Models\User`, `App\Http\A`, `App\Http\B`, `Vendor\Thing` | +| PHP | `require_once "x.php";`, `include "y.php";` | `x.php`, `y.php` (local files) | +| Ruby | `require "json"`, `require_relative "lib/x"` | `json`, `lib/x` (local file) | +| Kotlin | `import a.b.Thing as T`, `import kotlinx.coroutines.*` | `a.b.Thing`, `kotlinx.coroutines.*` | +| other languages | an import statement node | the generic statement rule: `from M import …` → `M`; `import … from 'm'` → `m`; `import a, b` → `a`, `b`; `use a::{b, c};` → `a::b`, `a::c`; any quoted target → the quoted string | + +Not reported: re-exports (`export { x } from './m'`) and dynamic calls whose +argument is not a string literal (`require(name)`). Kotlin imports are read +back from the source span (the Kotlin grammar lowers imports without +tokens); both the static and the history path hand the extractor the file +bytes. + +--- + +## Classification + +The yaml and bin formats classify every unique import by the language of the +first file that imported it: + +| Category | Rule | +|---|---| +| `relative` | `./x`, `../x`, `/x`, Python `.x`, Rust `crate::`/`super::`/`self::`, and local-file includes (C `"x.h"`, PHP `require`, Ruby `require_relative`) | +| `stdlib` | Go: first path element has no dot (`errors`, `net/http`) · Python: top-level module in `sys.stdlib_module_names` · JavaScript/TypeScript: `node:` prefix or a Node builtin (`fs`, `fs/promises`) · Rust: `std`, `core`, `alloc`, `proc_macro`, `test` · Java/Kotlin: `java.`, `javax.`, `jdk.`, `kotlin.` · C#: `System` · C/C++: standard and POSIX headers · Ruby: default libraries | +| `external` | everything else | + +`external_ratio` is `external_imports / total_imports`. + +--- + ## Configuration options ### Static mode @@ -19,42 +90,119 @@ No configuration options. Uses UAST directly. ### History mode -| Option | Type | Default | Description | -|---|---|---|---| -| `Imports.Goroutines` | `int` | `4` | Number of parallel goroutines for import extraction | -| `Imports.MaxFileSize` | `int` | `1048576` | Maximum file size in bytes; larger files are skipped | - -Set options via the configuration file: - -```yaml -# .codefang.yml -history: - imports: - goroutines: 8 - max_file_size: 2097152 # 2 MB -``` +No analyzer-specific options. The shared history flags apply: +`--tick-size` (the tick width in hours, default 24), the commit window +(`--head`, `--limit`, `--since`, `--first-parent`), the identity flags +(`--people-dict`, `--exact-signatures`, `--anonymize`) and the change +filter (`--languages`, vendor / generated policy). Blobs above 256 KiB are +not parsed. --- ## Example output +For a tree with `a.go` (`fmt`, `os`) and `b.py` (`import os, sys`): + === "Static (JSON)" ```json { + "overall_score_label": "Info", + "sections": [ + { + "title": "IMPORTS", + "score_label": "Info", + "status": "Found 3 unique imports", + "metrics": [ + { "label": "Unique Imports", "value": "3" }, + { "label": "Total Files", "value": "2" } + ], + "issues": [ + { "name": "os", "location": "", "value": "2", "severity": "info" }, + { "name": "fmt", "location": "", "value": "1", "severity": "info" }, + { "name": "sys", "location": "", "value": "1", "severity": "info" } + ], + "score": -1 + } + ], + "overall_score": -1 + } + ``` + + The section is info-only (`score` -1). `issues` lists every unique + import ordered by the number of importing files (descending), then by + name. With `--per-file`, a `files` array follows `issues` with one entry + per analyzed file: + + ```json + { + "file_path": "b.py", + "score_label": "Info", + "status": "Found 2 unique imports", + "metrics": [ + { "label": "Unique Imports", "value": "2" }, + { "label": "Total Files", "value": "1" } + ], + "issues": [ + { "name": "os", "location": "b.py", "value": "1", "severity": "info" }, + { "name": "sys", "location": "b.py", "value": "1", "severity": "info" } + ], + "score": -1 + } + ``` + +=== "Static (YAML)" + + ```yaml + import_list: + - path: fmt + category: stdlib + is_external: false + - path: os + category: stdlib + is_external: false + - path: sys + category: stdlib + is_external: false + categories: + - category: stdlib + count: 3 + dependencies: [] + aggregate: + total_imports: 3 + external_imports: 0 + internal_imports: 3 + unique_packages: 3 + external_ratio: 0 + ``` + + `import_list` is sorted by category then path; `categories` by count + (descending) then name. `dependencies` flags deeply nested relative + imports (three or more `..`, `MEDIUM`) and long paths (five or more `/`, + `LOW`). The `bin` format carries the same structure in a CFB1 envelope. + +=== "History (JSON)" + + ```bash + codefang run -a history/imports --format json ./path/to/repo + ``` + + ```json + { + "tick_size": 86400, + "author_index": ["alice|alice@x.io", "bob|bob@x.io"], "imports": { - "files": [ - { - "file": "main.go", - "language": "Go", - "imports": [ - "fmt", - "os", - "encoding/json", - "github.com/example/lib" - ] + "alice|alice@x.io": { + "go": { + "fmt": { "0": 1, "2": 1 }, + "net/http": { "2": 1 }, + "os": { "0": 1 } } - ] + }, + "bob|bob@x.io": { + "go": { "encoding/json": { "1": 1 }, "fmt": { "1": 1 } }, + "python": { "os": { "1": 1 }, "sys": { "1": 1 } } + } } } ``` @@ -62,13 +210,55 @@ history: === "History (YAML)" ```yaml - tick_size: 86400 - imports: - alice: {"Go":{"fmt":{"0":5,"1":3},"net/http":{"2":1}}} - bob: {"Go":{"encoding/json":{"0":2,"3":4}}} + codefang (v2): + version: ... + hash: ... + ImportsPerDeveloper: + tick_size: 86400 + author_index: + - alice|alice@x.io + - bob|bob@x.io + imports: + alice|alice@x.io: + go: + fmt: + 0: 1 + 2: 1 + net/http: + 2: 1 + os: + 0: 1 + bob|bob@x.io: + go: + encoding/json: + 1: 1 + fmt: + 1: 1 + python: + os: + 1: 1 + sys: + 1: 1 ``` - Each entry maps `developer -> language -> import_path -> tick -> count`. + The history report is one value for json, yaml and bin (the CFB1 + envelope over the json bytes): + + | Key | Description | + |---|---| + | `tick_size` | The tick width in seconds (`--tick-size` hours × 3600; `86400` by default) | + | `author_index` | Developer labels by author id, as the identity detector merged them (`--people-dict` names, `--anonymize` pseudonyms) | + | `imports` | `developer -> language -> import -> tick -> count`: how many times each commit of that developer, in a file of that language, in that tick, imported the path. Every Insert/Modify change's After version counts, so a file rewritten in two commits counts its imports twice. Languages are the parser's names (`go`, `python`, `javascript`, …). Tick keys are integers (decimal strings in json). | + + A history without any supported file is `{"tick_size": 86400, + "author_index": [...], "imports": {}}`; a developer whose commits + contain no imports does not appear under `imports`. Two runs over the + same repository emit identical bytes. + + The per-commit `ndjson` records carry `{"authorID", "entries": + [{"Lang", "Import"}]}` for every commit with at least one import; the + `timeseries` document carries `import_count` and the per-language + counts per commit under `imports-per-dev`. --- diff --git a/site/analyzers/index.md b/site/analyzers/index.md index 3f6741d7..325d4dec 100644 --- a/site/analyzers/index.md +++ b/site/analyzers/index.md @@ -12,28 +12,45 @@ Static analyzers parse source files into a UAST and compute metrics in a single | Analyzer | ID | Description | |---|---|---| -| [Complexity](complexity.md) | `complexity` | Cyclomatic complexity, cognitive complexity, nesting depth | -| [Cohesion](cohesion.md) | `cohesion` | LCOM4 class cohesion, method-field usage graphs | -| [Halstead](halstead.md) | `halstead` | Program length, vocabulary, volume, difficulty, effort | -| [Comments](comments.md) | `comments` | Documentation coverage, comment placement quality | -| [Imports](imports.md) | `imports` | Import and dependency analysis | -| [Clones](clones.md) | `clones` | Cross-file duplicate function detection via MinHash + LSH | +| [Complexity](complexity.md) | `static/complexity` | Cyclomatic complexity, cognitive complexity, nesting depth | +| [Cohesion](cohesion.md) | `static/cohesion` | LCOM over function–variable usage, function cohesion | +| [Halstead](halstead.md) | `static/halstead` | Program length, vocabulary, volume, difficulty, effort | +| [Comments](comments.md) | `static/comments` | Documentation coverage, comment placement quality | +| [Imports](imports.md) | `static/imports` | Import and dependency analysis | +| [Clones](clones.md) | `static/clones` | Cross-file duplicate function detection via MinHash + LSH | +| [Composition](composition.md) | `static/composition` | File-type breakdown: source, docs, config, vendor, generated, dotfile, image, binary, other | ### Running static analyzers -Pipe a UAST through `codefang analyze`: +Point `codefang run` at a file or a directory tree; the UAST is built +internally per file: ```bash # Single file, single analyzer -uast parse main.go | codefang analyze -a complexity +codefang run -a static/complexity main.go -# Single file, all static analyzers -uast parse main.go | codefang analyze +# Entire directory tree, single analyzer +codefang run -a static/complexity ./src/ -# Entire directory tree -codefang analyze -a complexity ./src/ +# Entire directory tree, all static analyzers +codefang run -a 'static/*' ./src/ ``` +Every static analyzer reads the **same** walk of the tree, so +`-a 'static/*'` reports exactly the numbers the single runs report: the +walk visits directories in lexical order (skipping `.git`, cache-tagged +directories and symlinked directories), admits the files the UAST parser +supports and `--languages` keeps, applies the vendor / generated / +`--extra-excluded-prefixes` policy to the root-relative path, reads each +file once (files over 64 MiB and unreadable files are skipped with one +stderr warning each), and applies the generated-content marker +(`DO NOT EDIT`, `Code generated`, … in the first 512 bytes) unless +`--include-generated`. `static/composition` classifies every file instead, +vendored and generated included. `--per-file` adds per-file entries to +`--format json` only. See the +[CLI reference](../guide/cli-reference.md#the-static-walk) for the full +contract. + --- ## History analyzers @@ -75,7 +92,7 @@ You can select analyzers using glob patterns with the `-a` flag: ```bash # All static analyzers -codefang analyze -a 'static/*' ./src/ +codefang run -a 'static/*' ./src/ # All history analyzers codefang run -a 'history/*' . @@ -88,7 +105,7 @@ codefang run -a '*' . ## Output formats -All analyzers support multiple output formats via the `-f` flag: +All analyzers support multiple output formats via the `--format` flag: === "JSON" diff --git a/site/analyzers/quality.md b/site/analyzers/quality.md index 72377267..49de9afd 100644 --- a/site/analyzers/quality.md +++ b/site/analyzers/quality.md @@ -1,9 +1,9 @@ # Code quality analyzer reference The code quality analyzer tracks **complexity, Halstead metrics, comment -quality, and cohesion** across Git history. For each commit, it runs four -static analyzers on UAST-parsed changed files and produces per-file quality -metrics. +quality, and cohesion** across Git history. For every commit it runs the four +static analyzers on the UAST of each changed file and records one sample per +file; the samples are bucketed per tick. For the conceptual model — which metrics it tracks and how it aggregates them per tick — see [Understanding code quality over @@ -14,11 +14,76 @@ time](../explanation/quality.md). To run it, see the ## Configuration options -The quality analyzer has no configurable options. It uses the UAST pipeline's language detection and parsing configuration. +The quality analyzer has no analyzer-specific options. It honors the shared +history flags (`--limit`, `--since`, `--head`, `--tick-size`, `--languages`, +`--max-changes-per-commit`, the vendor / generated policy) documented in the +[CLI reference](../guide/cli-reference.md). | Option | Type | Default | Description | |---|---|---|---| -| *(none)* | -- | -- | Uses UAST; no analyzer-specific config | +| *(none)* | -- | -- | Uses the shared history flags and the UAST pipeline's language support | + +--- + +## Which files count + +For each commit, the analyzer looks at the Insert and Modify changes that +survive the shared change filter and parses the **after** version of each +file: + +- A file whose UAST parse **succeeds** contributes exactly one sample. The + numbers are the static analyzers' numbers for that file + (`static/complexity`, `static/halstead`, `static/comments`, + `static/cohesion`, each fed the file bytes), with one difference: function + and comment discovery is capped at depth 10 below the file root. +- A file whose parse **fails**, an unsupported language, a binary blob, or a + blob above 256 KiB contributes nothing — it is not counted in + `files_analyzed`. +- A parsed file **without functions** contributes its complexity, Halstead + and comment scalars (the function-based ones are `0`) but **no cohesion + sample**: the static cohesion analyzer reports "no data" for such a file, + and so does quality. +- Deleted files contribute nothing. + +A tick (default: one day of committer time) in which no file was analyzed — +for example, a commit touching only text files — has **no entry** in the time +series and does not take part in the aggregate. + +--- + +## Per-tick statistics + +Every `time_series` entry carries the statistics of the files analyzed in +that tick: + +| Field | Definition | +|---|---| +| `complexity_mean` / `complexity_median` / `complexity_p95` / `complexity_max` | over each file's summed cyclomatic complexity (`total_complexity`) | +| `halstead_vol_mean` / `halstead_vol_median` / `halstead_vol_p95` / `halstead_vol_sum` | over each file's Halstead volume | +| `delivered_bugs_sum` | sum of each file's Halstead delivered bugs | +| `comment_score_mean` / `comment_score_min` | over each file's good-comment ratio (`overall_score`) | +| `doc_coverage_mean` | mean of each file's documented-declaration ratio | +| `cohesion_mean` / `cohesion_min` | over the cohesion scores of the files **with functions**; `0` when no file in the tick has one (no data) | +| `files_analyzed` | number of parsed files | +| `total_functions` | sum of each file's function count | +| `max_complexity` | highest single-function cyclomatic complexity | + +The P95 is the linearly interpolated 95th percentile. + +### Aggregate + +| Field | Definition | +|---|---| +| `total_ticks` | ticks with at least one analyzed file | +| `total_files_analyzed` | sum of `files_analyzed` over those ticks | +| `complexity_median_mean` / `complexity_p95_mean` / `halstead_vol_median_mean` / `comment_score_mean_mean` | mean of the per-tick value | +| `total_delivered_bugs` | sum of the per-tick `delivered_bugs_sum` | +| `min_comment_score` | minimum of the per-tick `comment_score_min` | +| `cohesion_mean_mean` / `min_cohesion` | mean of the per-tick `cohesion_mean` / minimum of the per-tick `cohesion_min`, over the ticks that hold a cohesion sample (`0` when none does) | + +Because every mean and minimum is taken over the same set of ticks, +`min_cohesion <= cohesion_mean_mean` and +`min_comment_score <= comment_score_mean_mean` always hold. --- @@ -26,14 +91,50 @@ The quality analyzer has no configurable options. It uses the UAST pipeline's la | Format | Flag | Description | |---|---|---| -| JSON | `--format json` | `ComputedMetrics` with `time_series` and `aggregate` fields | +| JSON | `--format json` | `{"time_series": [...], "aggregate": {...}}` | | YAML | `--format yaml` | Same structure as JSON | +| NDJSON | `--format ndjson` | One record per commit with that commit's per-file sample vectors | +| Time series | `--format timeseries` | Per-commit summary (`complexity_median`, `cognitive_median`, `max_complexity`, `functions`, `halstead_vol_median`, `halstead_effort_median`, `delivered_bugs_sum`, `comment_score_min`, `doc_coverage_mean`, `cohesion_min`, `files_analyzed`) | | Plot | `--format plot` | HTML page with complexity and Halstead charts plus summary stats | -| Binary | `--format binary` | Compact binary envelope for programmatic consumption | +| Binary | `--format bin` | Compact binary envelope for programmatic consumption | + +### JSON output + +```json +{ + "time_series": [ + { + "tick": 0, + "start_time": "2021-01-01T12:00:00Z", + "end_time": "2021-01-01T12:00:00Z", + "stats": { + "complexity_mean": 2, "complexity_median": 2, "complexity_p95": 2, "complexity_max": 2, + "halstead_vol_mean": 25.85, "halstead_vol_median": 25.85, "halstead_vol_p95": 25.85, "halstead_vol_sum": 25.85, + "delivered_bugs_sum": 0.0086, + "comment_score_mean": 1, "comment_score_min": 1, "doc_coverage_mean": 1, + "cohesion_mean": 1, "cohesion_min": 1, + "files_analyzed": 1, "total_functions": 1, "max_complexity": 2 + } + } + ], + "aggregate": { + "total_ticks": 1, + "total_files_analyzed": 1, + "complexity_median_mean": 2, + "complexity_p95_mean": 2, + "halstead_vol_median_mean": 25.85, + "total_delivered_bugs": 0.0086, + "comment_score_mean_mean": 1, + "min_comment_score": 1, + "cohesion_mean_mean": 1, + "min_cohesion": 1 + } +} +``` --- ## See also -- [Understanding code quality over time](../explanation/quality.md) — the mental model, per-file metrics, and architecture. +- [Understanding code quality over time](../explanation/quality.md) — the mental model, per-file metrics, and aggregation. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/analyzers/sentiment.md b/site/analyzers/sentiment.md index abdee1a1..40c6db57 100644 --- a/site/analyzers/sentiment.md +++ b/site/analyzers/sentiment.md @@ -1,10 +1,12 @@ # Sentiment analyzer reference -The sentiment analyzer classifies **comment sentiment** across Git history, -extracting new or changed comments via UAST parsing and classifying them as -positive, negative, or neutral. +The sentiment analyzer classifies **comment sentiment** across Git history: +for every commit it extracts the comments the commit **introduced or +changed** (the difference between the file's old and new UAST comment sets), +scores them with VADER, and reports one score per tick that carries such +comments. -For the conceptual model — how comments are filtered and scored, the +For the conceptual model — how comments are diffed, filtered and scored, the multilingual lexicon, and the software-engineering domain adjustments — see [Understanding comment sentiment analysis](../explanation/sentiment.md). To run it, see the [Quick start](../getting-started/quickstart.md). @@ -13,23 +15,45 @@ it, see the [Quick start](../getting-started/quickstart.md). ## Configuration options -| Option | Type | Default | Description | +| Flag | Type | Default | Description | |---|---|---|---| -| `CommentSentiment.MinLength` | `int` | `20` | Minimum character length for a comment to be analyzed. Comments shorter than this are skipped. | -| `CommentSentiment.Gap` | `float` | `0.5` | Sentiment score threshold. Values must be in the range (0, 1). Higher values require stronger sentiment signal to classify as positive/negative. | - -```yaml -# .codefang.yml -history: - sentiment: - min_comment_length: 20 - gap: 0.5 -``` +| `--min-comment-len` | `int` | `20` | Minimum comment length in **characters** (after delimiters are stripped). Shorter comments are not scored. `0` disables the length filter; a negative value is rejected with `Error: invalid --min-comment-len: must be >= 0, got <n>`. | + +The classification thresholds are fixed: a tick scores **positive** at +`>= 0.6`, **negative** at `<= 0.4`, **neutral** in between; a negative tick +is a **HIGH**-risk low-sentiment period at `<= 0.2` and **MEDIUM** otherwise; +a trend is **improving** / **declining** when the fitted endpoints differ by +more than `0.1`. + +!!! note "`.codefang.yml` sentiment keys" + The configuration loader parses `history.sentiment.*` keys + (`min_comment_length`, `gap`, `neutralizer_weight`, `max_weight_ratio`, + the thresholds), but the `codefang` command line does not apply them — + `--min-comment-len` is the only sentiment setting the CLI honors. The + keys are accepted so an existing file keeps loading; they have no effect. + +The analyzer needs UAST support for a file's language to extract its +comments; files in unsupported languages are skipped (shell scripts fall back +to line-wise `#` comment extraction). -!!! tip "Tuning the gap" - A gap of `0.5` is conservative -- only strongly positive or negative comments are flagged. Lower it to `0.3` for more sensitivity, or raise to `0.7` to capture only the most extreme sentiments. +--- -The analyzer requires UAST support to extract comments and is automatically enabled when the UAST pipeline is available. +## Tick semantics + +- A tick carries a `time_series` entry **only when** at least one of its + commits introduced or changed a comment that passes the filters. A tick of + commits without such comments has no score: it is absent from the time + series, from the trend fit and from `low_sentiment_periods`, and it is not + classified. +- `aggregate.total_ticks`, `positive_ticks`, `neutral_ticks` and + `negative_ticks` count the scored ticks; `aggregate.total_commits` counts + **every** commit of the window, scored tick or not; `total_comments` counts + the scored comments. +- `trend.start_sentiment` and `trend.end_sentiment` are the least-squares + line through the scored ticks evaluated at the first and last scored tick, + **clamped to `[0, 1]`**; `change_percent` is computed from the clamped + endpoints (`0` when the start endpoint is `0`). +- Every reported sentiment lies in `[0, 1]` (`0.5` is neutral). --- @@ -50,8 +74,8 @@ The analyzer requires UAST support to extract comments and is automatically enab "classification": "positive" }, { - "tick": 1, - "start_time": "2024-01-16T09:00:00Z", + "tick": 2, + "start_time": "2024-01-17T09:00:00Z", "end_time": "2024-01-17T18:30:00Z", "sentiment": 0.35, "comment_count": 8, @@ -76,17 +100,27 @@ The analyzer requires UAST support to extract comments and is automatically enab } ], "aggregate": { - "total_ticks": 10, + "total_ticks": 9, "total_comments": 245, "total_commits": 89, "average_sentiment": 0.56, "positive_ticks": 4, - "neutral_ticks": 3, + "neutral_ticks": 2, "negative_ticks": 3 } } ``` +Tick 1 above had commits but no new or changed comment, so it has no entry; +its commits are still part of `total_commits`. + +### NDJSON and time series + +`--format ndjson` emits one record per commit whose `data.Comments` array +holds the comments that commit introduced or changed (empty for a code-only +commit). `--format timeseries` emits per commit `{"comment_count": n, +"sentiment": s}` with `sentiment` present only when `n > 0`. + ### Plot output The HTML plot includes: @@ -108,15 +142,15 @@ The terminal renderer provides a colored, Unicode-rich summary: Summary ──────────────────────────────────────────────────────────────── Average Sentiment: [██████████░░░░░░░░░░] 5/10 😐 - Total Ticks: 10 + Total Ticks: 9 Total Comments: 245 Total Commits: 89 Distribution ──────────────────────────────────────────────────────────────── - 😊 Positive (≥0.6) ████████░░░░░░░░░░░░ 40% (4) - 😐 Neutral ██████░░░░░░░░░░░░░░ 30% (3) - 😟 Negative (≤0.4) ██████░░░░░░░░░░░░░░ 30% (3) + 😊 Positive (≥0.6) █████████░░░░░░░░░░░ 44% (4) + 😐 Neutral ████░░░░░░░░░░░░░░░░ 22% (2) + 😟 Negative (≤0.4) ███████░░░░░░░░░░░░░ 33% (3) Trend ──────────────────────────────────────────────────────────────── @@ -126,7 +160,7 @@ The terminal renderer provides a colored, Unicode-rich summary: Sentiment Timeline ──────────────────────────────────────────────────────────────── - ▇▆▅▄▃▂▃▄▅▆ + ▇▆▅▄▃▂▃▄▅ neg pos ``` diff --git a/site/analyzers/shotness.md b/site/analyzers/shotness.md index ad1edd52..3dc1531b 100644 --- a/site/analyzers/shotness.md +++ b/site/analyzers/shotness.md @@ -11,51 +11,127 @@ the [Quick start](../getting-started/quickstart.md). --- +## What an entity is + +An entity is a declaration the structural DSL selects **anywhere** in a +file's UAST — by default every node carrying the `Function` role, which +covers top-level functions, class methods (Java, C#, TypeScript, JavaScript, +Python, …), Go receiver methods and nested functions. Each entity is +identified by three things: + +| Part | Source | Example | +|---|---|---| +| `type` | the UAST node type | `Function`, `Method`, `Class` | +| `name` | the **qualified** name: the names of the enclosing declarations, then the receiver type (Go-style methods), then the node's own name from the name DSL | `A.f`, `K.Inner.im`, `outer.inner`, `A.String` | +| `file` | the path at the commit being walked | `pkg/core/engine.go` | + +Qualification uses the enclosing `Class`, `Struct`, `Interface`, `Enum`, +`Function` and `Method` declarations (a Java method `f` in class `A` is +`A.f`; a Python `def inner` inside `def outer` is `outer.inner`), and the +receiver type of a method declared as `func (a *A) String()` (`A.String`). +Two `String()` methods on two receivers are therefore two entities. A node +whose name DSL yields nothing (an anonymous lambda) is not an entity. + +Rust `impl` methods are qualified only by their enclosing module-level +declarations: the mapping does not expose the `impl` target, so `fn m` in +`impl A` is the entity `m`. Mappings that set no `props.name` on +declarations (the real-world set in the +[shape contract](../architecture/uast.md#shape-contract) does) yield no +entities for that language. + +## Entity lifecycle + +The walk visits the commit window oldest-first and records, per commit, what +the commit did to each entity; every output format is a replay of those +records. + +| Event | Effect | +|---|---| +| A file is added | every entity of the file is born with `change_count` 1 and coupled with the commit's other touched entities. | +| A file is modified | a diff hunk **touches** every entity whose line span it overlaps (deleted lines on the Before tree, inserted lines on the After tree); each touched entity counts **one** change for the commit, however many hunks hit it, and the commit's touched entities are coupled pairwise. | +| A declaration disappears from a file | the entity **ends**: it is dropped from the report together with every coupling that referenced it (with `--shotness-keep-ended` it stays, marked `"ended": true`, with its counts and couplings). Renaming a function in place (`f` → `f2`) ends `f` and starts `f2`. | +| A file is renamed (`git mv`, with or without edits) | every live entity of the file **moves** under the new path: counts and couplings are preserved and the old keys disappear. Ended entities keep the path they ended under. | +| A file is deleted | every entity of the file ends. | +| A merge commit | skipped: its diff against the first parent re-shows the side line that was already walked. Under `--first-parent` a merge is an ordinary commit and counts once. | + +The report therefore lists exactly the entities that exist at the newest +commit of the window, each with the number of window commits that changed +it. An entity removed and later re-added under the same name is a new +entity. + +`--shotness-keep-ended` (config key `history.shotness.keep_ended`) keeps +every entity the window saw instead: the ones that no longer exist are +marked `"ended": true` and keep their counts and couplings, the aggregate +adds `ended_nodes`, and an entity removed and declared again under the same +name and file is live again with its history. Hotspots and `hot_nodes` +count only live entities, and the plot pages show only live ones. On long +histories this grows the report: on 31,923 kubernetes commits the shotness +report goes from 256 MB to 859 MB (18,499 ended entities, couplings +1.06 M → 3.45 M). + +Bounds shared with every history analyzer: blobs larger than 256 KiB are +not parsed (the file's entities are neither born nor touched by that +commit), and a commit whose raw tree diff exceeds `--max-changes-per-commit` +(default 10 000) is dropped before any analyzer sees it. A commit touching +more than 500 entities counts the touches but skips the pairwise coupling +update (a mass refactor is coupling noise). + +--- + ## Configuration options -| Option | Type | Default | Description | +| Option | CLI flag | Default | Description | |---|---|---|---| -| `Shotness.DSLStruct` | `string` | `filter(.roles has "Function")` | UAST DSL query to select which code structures to track. | -| `Shotness.DSLName` | `string` | `.props.name` | UAST DSL expression to extract the name from each matched node. | +| `Shotness.DSLStruct` | `--shotness-dsl-struct` | `rfilter(.roles has "Function")` | UAST DSL query selecting the tracked declarations, run against each parsed file. | +| `Shotness.DSLName` | `--shotness-dsl-name` | `.props.name` | UAST DSL query extracting a declaration's own name, run against each selected node and each enclosing declaration. | -```yaml -# .codefang.yml -history: - shotness: - dsl_struct: 'filter(.roles has "Function")' - dsl_name: '.props.name' -``` +Both expressions are compiled before the walk starts: an expression that +does not parse is a flag error (`invalid --shotness-dsl-struct "rfilter(": +DSL parse error: …`) and no report is produced. An empty value means the +default. The DSL grammar — `filter` over the direct children, `rfilter` / +`rmap` over the whole subtree, field access such as `.props.name` — is +documented under [`uast query`](../guide/cli-reference.md#uast-query). -The `--shotness-dsl-struct` and `--shotness-dsl-name` CLI flags set these options for a single run. +The structural query must be recursive to see methods: `filter(...)` looks +at the file node's direct children only, so +`--shotness-dsl-struct 'filter(.roles has "Function")'` tracks top-level +functions and nothing inside a class. ### Custom DSL examples === "Track classes instead of functions" - ```yaml - dsl_struct: 'filter(.roles has "Class")' - dsl_name: '.props.name' + ```bash + codefang run -a history/shotness --shotness-dsl-struct 'rfilter(.roles has "Class")' . ``` -=== "Track both functions and methods" + Nested classes are qualified by their enclosing class (`K.Inner`). - ```yaml - dsl_struct: 'filter(.roles has "Function" or .roles has "Method")' - dsl_name: '.props.name' +=== "Track interfaces" + + ```bash + codefang run -a history/shotness --shotness-dsl-struct 'rfilter(.roles has "Interface")' . ``` -=== "Track interfaces" +=== "Name entities by node type" - ```yaml - dsl_struct: 'filter(.roles has "Interface")' - dsl_name: '.props.name' + ```bash + codefang run -a history/shotness --shotness-dsl-name '.type' . ``` + Every selected node (and every enclosing declaration) is named by its + type, so all methods of a Java class collapse into one `Class.Method` + entity — a way to measure a whole class body as one unit. + +Methods carry the `Function` role, so the default already tracks functions +and methods together. + --- ## Output formats -The shotness analyzer supports four output formats: JSON, YAML, text, and plot. +The shotness analyzer supports json, yaml, text, ndjson, timeseries, +compact and plot output. === "Text" @@ -77,17 +153,17 @@ The shotness analyzer supports four output formats: JSON, YAML, text, and plot. Hottest Functions ────────────────────────────────────────────────────────── - processPayment (engine [████████████████████░] 1.0 (42 changes) - validateInput (engine. [████████████████░░░░░] 0.8 (34 changes) + Engine.processPayment [████████████████████░] 1.0 (42 changes) + Engine.validateInput ( [████████████████░░░░░] 0.8 (34 changes) Risk Assessment ────────────────────────────────────────────────────────── - processPayment (engine HIGH (42 changes) - validateInput (engine. HIGH (34 changes) + Engine.processPayment HIGH (42 changes) + Engine.validateInput ( HIGH (34 changes) Strongest Couplings ────────────────────────────────────────────────────────── - processPayment ↔ validateInput 85% (12 co-changes) + Engine.processPayment ↔ Engine.validateInput 85% (12 co-changes) handleRequest ↔ parseBody 72% (8 co-changes) ``` @@ -97,8 +173,8 @@ The shotness analyzer supports four output formats: JSON, YAML, text, and plot. { "node_hotness": [ { - "name": "processFile", - "type": "Function", + "name": "Engine.processFile", + "type": "Method", "file": "pkg/core/engine.go", "change_count": 42, "coupled_nodes": 3, @@ -107,7 +183,7 @@ The shotness analyzer supports four output formats: JSON, YAML, text, and plot. ], "node_coupling": [ { - "node1_name": "processFile", + "node1_name": "Engine.processFile", "node1_file": "pkg/core/engine.go", "node2_name": "validate", "node2_file": "pkg/core/engine.go", @@ -117,8 +193,8 @@ The shotness analyzer supports four output formats: JSON, YAML, text, and plot. ], "hotspot_nodes": [ { - "name": "processFile", - "type": "Function", + "name": "Engine.processFile", + "type": "Method", "file": "pkg/core/engine.go", "change_count": 42, "risk_level": "HIGH" @@ -139,22 +215,22 @@ The shotness analyzer supports four output formats: JSON, YAML, text, and plot. ```yaml node_hotness: - - name: processFile - type: Function + - name: Engine.processFile + type: Method file: pkg/core/engine.go change_count: 42 coupled_nodes: 3 hotness_score: 1.0 node_coupling: - - node1_name: processFile + - node1_name: Engine.processFile node1_file: pkg/core/engine.go node2_name: validate node2_file: pkg/core/engine.go co_changes: 15 coupling_strength: 0.36 hotspot_nodes: - - name: processFile - type: Function + - name: Engine.processFile + type: Method file: pkg/core/engine.go change_count: 42 risk_level: HIGH @@ -167,12 +243,20 @@ The shotness analyzer supports four output formats: JSON, YAML, text, and plot. hot_nodes: 2 ``` +=== "NDJSON / timeseries" + + `--format ndjson` emits one line per commit that touched an entity, with + `data.NodesTouched` keyed by entity key (`Method_A.f_A.java`) and a + `CountDelta` of 1 per touched entity. `--format timeseries` reports per + commit `nodes_touched` and `coupling_pairs` (`C(n, 2)`). A pure `git mv` + commit touches nothing and appears in neither. + === "Plot" Generates an interactive HTML dashboard with three visualizations: - 1. **Code Hotness TreeMap**: Hierarchical file → function view sized by change frequency - 2. **Function Coupling Matrix**: Heatmap showing co-change frequency between functions + 1. **Code Hotness TreeMap**: Hierarchical file → entity view sized by change frequency + 2. **Function Coupling Matrix**: Heatmap showing co-change frequency between entities 3. **Top Hot Functions**: Bar chart comparing self-changes vs coupled changes --- @@ -183,36 +267,39 @@ The shotness analyzer supports four output formats: JSON, YAML, text, and plot. | Field | Type | Description | |---|---|---| -| `name` | string | Function/method name | -| `type` | string | UAST node type (e.g., "Function") | -| `file` | string | Source file path | -| `change_count` | int | Number of commits that modified this node | -| `coupled_nodes` | int | Number of other nodes that co-changed with this node | -| `hotness_score` | float | Normalized score [0, 1] relative to the hottest node | +| `name` | string | Qualified entity name (`Class.method`, `Receiver.method`) | +| `type` | string | UAST node type (e.g. `Function`, `Method`) | +| `file` | string | Source file path at the newest commit of the window | +| `change_count` | int | Number of commits that modified this entity | +| `coupled_nodes` | int | Number of other entities that co-changed with this entity | +| `hotness_score` | float | Normalized score [0, 1] relative to the hottest entity | +| `ended` | bool | Only with `--shotness-keep-ended`: the declaration (or its file) was removed and not declared again by the newest commit | ### Node coupling | Field | Type | Description | |---|---|---| -| `node1_name` / `node2_name` | string | Names of the coupled nodes | -| `node1_file` / `node2_file` | string | File paths of the coupled nodes | -| `co_changes` | int | Number of commits where both nodes changed | +| `node1_name` / `node2_name` | string | Names of the coupled entities | +| `node1_file` / `node2_file` | string | File paths of the coupled entities | +| `co_changes` | int | Number of commits where both entities changed | | `coupling_strength` | float | Normalized strength [0, 1] | ### Aggregate | Field | Type | Description | |---|---|---| -| `total_nodes` | int | Total tracked nodes | -| `total_changes` | int | Sum of all node change counts | +| `total_nodes` | int | Live entities at the end of the window (with `--shotness-keep-ended`: every entity the window saw) | +| `total_changes` | int | Sum of all entity change counts | | `total_couplings` | int | Number of unique coupling pairs | -| `avg_changes_per_node` | float | Mean changes per node | +| `avg_changes_per_node` | float | Mean changes per entity | | `avg_coupling_strength` | float | Mean coupling strength across all pairs | -| `hot_nodes` | int | Nodes with change count ≥ 10 (MEDIUM or HIGH risk) | +| `hot_nodes` | int | Live entities with change count ≥ 10 (MEDIUM or HIGH risk) | +| `ended_nodes` | int | Only with `--shotness-keep-ended`: entities marked `ended` | --- ## See also - [Understanding structural hotness](../explanation/shotness.md) — the mental model, algorithm, architecture, interpretation, and limitations. +- [`uast query`](../guide/cli-reference.md#uast-query) — the DSL the two options are written in. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/analyzers/typos.md b/site/analyzers/typos.md index 131853a3..82c0158d 100644 --- a/site/analyzers/typos.md +++ b/site/analyzers/typos.md @@ -13,7 +13,7 @@ it, see the [Quick start](../getting-started/quickstart.md). | Option | Type | Default | Description | |---|---|---|---| -| `TyposDatasetBuilder.MaximumAllowedDistance` | `int` | `4` | Maximum Levenshtein distance between two lines to consider them a typo-fix candidate. Lower values produce fewer but higher-confidence results. | +| `TyposDatasetBuilder.MaximumAllowedDistance` | `int` | `4` | Maximum Levenshtein distance between the two identifiers of a typo-fix pair (also the bound on the whole-line prefilter). Lower values produce fewer but higher-confidence results. | ```yaml # .codefang.yml @@ -27,37 +27,58 @@ history: - **Distance 3-4** (default): Good balance. Catches transposition errors and short misspellings. - **Distance 5+**: Lower confidence. May produce false positives from intentional renames. -The analyzer requires UAST support to extract identifiers from source code. It is automatically enabled when the UAST pipeline is available. The `--typos-max-distance` CLI flag sets `MaximumAllowedDistance` for a single run. +The analyzer requires UAST support to extract identifiers from source code: only files the parser supports are examined. The `--typos-max-distance` CLI flag sets `MaximumAllowedDistance` for a single run. + +--- + +## What counts as a typo fix + +A typo fix is **one identifier that a commit changed into a different, +Levenshtein-close identifier on the same line**: + +- the removed and the added line are aligned (equal-sized delete/insert + runs) and are within the distance bound of each other as whole lines; +- the identifiers the two lines share cancel, whatever their position + (`v := recieve()` → `v := receive()` keeps `v` out of the way); +- exactly one identifier remains on each side, both non-empty and + different, and the two are within the distance bound. + +Not reported: a literal change next to an unchanged identifier +(`#define N 4` → `#define N 17`), a rename to something unrelated +(`foo` → `completelyDifferent`), a line whose identifiers changed in more +than one place, prose in files the parser does not support. --- ## Example output +```bash +codefang run -a history/typos --format json ./path/to/repo +``` + === "JSON" ```json { - "typos": [ + "aggregate": { + "total_typos": 2, + "unique_patterns": 1, + "affected_files": 1, + "affected_commits": 1 + }, + "file_typos": [ + { "file": "a.go", "typo_count": 2, "fixed_typos": 2 } + ], + "patterns": [ + { "wrong": "recieve", "correct": "receive", "frequency": 2 } + ], + "typo_list": [ { "wrong": "recieve", "correct": "receive", - "file": "pkg/api/handler.go", - "commit": "a1b2c3d4e5f6...", - "line": 42 - }, - { - "wrong": "calcualte", - "correct": "calculate", - "file": "pkg/math/stats.go", - "commit": "f6e5d4c3b2a1...", - "line": 15 - }, - { - "wrong": "reponse", - "correct": "response", - "file": "pkg/api/client.go", - "commit": "1a2b3c4d5e6f...", - "line": 88 + "file": "a.go", + "line": 3, + "commit": "a1b2c3d4e5f6..." } ] } @@ -66,21 +87,56 @@ The analyzer requires UAST support to extract identifiers from source code. It i === "YAML" ```yaml - typos: - - wrong: recieve - correct: receive - file: pkg/api/handler.go - line: 42 - - wrong: calcualte - correct: calculate - file: pkg/math/stats.go - line: 15 - - wrong: reponse - correct: response - file: pkg/api/client.go - line: 88 + codefang (v2): + version: ... + hash: ... + history/typos: + aggregate: + total_typos: 2 + unique_patterns: 1 + affected_files: 1 + affected_commits: 1 + file_typos: + - file: a.go + typo_count: 2 + fixed_typos: 2 + patterns: + - wrong: recieve + correct: receive + frequency: 2 + typo_list: + - wrong: recieve + correct: receive + file: a.go + line: 3 + commit: a1b2c3d4e5f6... ``` +The report is one value for every machine format: `json` marshals it +compactly, `yaml` renders it under the `history/typos:` header, `bin` +wraps the json bytes in the CFB1 envelope. + +--- + +## Output structure + +| Key | Description | +|---|---| +| `typo_list` | Every distinct `wrong → correct` pair once, attributed to its chronologically first fix: `file`, **1-based** `line` in the fixed file, `commit`. The order is first-seen order. | +| `patterns` | The pairs fixed more than once, with the number of occurrences (`frequency`), most frequent first (ties by `wrong`, then `correct`). | +| `file_typos` | Occurrences per file (`typo_count`; `fixed_typos` is the same number — every detected occurrence is a fix), most affected first (ties by file name). | +| `aggregate` | `total_typos` (occurrences), `unique_patterns` (distinct pairs), `affected_files`, `affected_commits`. | + +Every list is present and `[]` when empty. A pair fixed on two lines of +one file is one `typo_list` entry, one `patterns` entry with `frequency: 2`, +`typo_count: 2` for the file and `total_typos: 2`. + +The per-commit `ndjson` records carry the occurrences of each commit +(`Wrong`, `Correct`, `File`, `Commit` as 20 bytes, 1-based `Line`). + +The output is a pure function of the repository and the flags: the same +bytes under any CPU count. + --- ## See also diff --git a/site/architecture/overview.md b/site/architecture/overview.md index f1505d7b..64b70e95 100644 --- a/site/architecture/overview.md +++ b/site/architecture/overview.md @@ -39,48 +39,56 @@ codefang run -a history/burndown,history/devs --limit 50 . | Crate | Description | |-------|-------------| | `bins/codefang` | CLI entry point for the analyzer. Subcommands `run`, `render`, `version`, `completion`. Delegates to `cf-commands`. | -| `bins/uast` | CLI entry point for the UAST parser. Subcommands `parse`, `query`, `diff`, `explore`, `analyze`, `validate`, `mapping`, `lsp`, `server`. | +| `bins/uast` | CLI entry point for the UAST parser. Subcommands `parse`, `diff`, `query`, `explore`, `analyze`, `validate`, `mapping`, `completion`, `version`. | ### Core libraries (workspace crates) The Rust port is a workspace of small, single-responsibility crates (`cf-*`). -The most important ones: +The ones the binaries link: | Crate | Description | |-------|-------------| -| `cf-uast` | UAST parser engine: Tree-sitter integration, DSL engine, language mappings, pre-compiled matchers. | -| `cf-uast-node`, `cf-uast-spec` | UAST node model and the schema it conforms to. | -| `cf-commands` | The `codefang` command bodies (`run`, `render`) and analyzer wiring. | -| `cf-framework`, `cf-pipeline`, `cf-streaming` | Pipeline orchestration: runner, coordinator, blob/diff/UAST stages, memory-bounded streaming. | -| `cf-gitlib` | Git operations via the `git2` crate (vendored libgit2): repository, commit, tree, changes, batch blob/diff worker. | -| `cf-alg`, `cf-alg-stats`, `cf-alg-mapx`, `cf-alg-lru`, … | Generic algorithms, statistics, map/slice helpers, and supporting data structures used across analyzers. | -| `cf-gojson`, `cf-goyaml`, `cf-gosort` | Go-byte-compatible serialization encoders — the only path report output may use, so output stays byte-identical to the frozen oracle. | +| `cf-uast` | UAST parser engine: Tree-sitter integration, rule lowering, grammar selection by extension. | +| `cf-uast-node`, `cf-uast-mapping`, `cf-uast-mappings`, `cf-uast-spec` | UAST node model, the mapping model and macro, the compiled-in language mappings, the schema. | +| `cf-commands` | The `codefang` command tree and bodies: the analyzer registry, the shared static walk, the shared history core, every output format. | +| `cf-gitlib` | Git operations via the `git2` crate (vendored libgit2): repository, revwalk, tree diff with rename detection, blobs, line diffs. | +| `cf-analyzers-plumbing`, `cf-identity` | The shared history tick assigner (`HistoryTicks`) and author identity detector. | +| `cf-langpath`, `cf-pathpolicy`, `cf-pathfilter` | Language detection (an enry / Linguist port) and the vendored / generated path policy shared by both phases. | +| `cf-alg`, `cf-alg-stats`, `cf-alg-mapx`, `cf-alg-levenshtein`, … | Generic algorithms, statistics and map/slice helpers used across analyzers. | +| `cf-gojson`, `cf-goyaml`, `cf-reportutil`, `cf-analyze` | The report encoders (JSON, YAML, the CFB1 binary envelope) and the unified `codefang.run.v1` model — the only path report output may use, so machine-format bytes stay stable across releases. | +| `cf-renderer`, `cf-terminal`, `cf-plotpage` | Terminal `text` / `compact` rendering and the HTML `plot` pages. | | `cf-safeconv` | Safe numeric conversions (clamp / checked / extract) used across the workspace. | -| `cf-persist`, `cf-checkpoint`, `cf-storage` | Codec-based persistence, checkpointing for crash recovery, atomic file writes. | -| `cf-config` | Configuration system: typed config, loader, compiled defaults, validation. | -| `cf-mcp` | Model Context Protocol server exposing analysis as agent tools. | -| `cf-observability` | OpenTelemetry integration: tracing, RED metrics, structured logging. | -| `cf-budget`, `cf-cache`, `cf-meminfo` | Memory-budget solver, blob/hash caches, memory introspection for auto-tuning. | -| Analyzer crates (`cf-complexity`, `cf-cohesion`, `cf-halstead`, `cf-comments`, `cf-imports`, `cf-clones`, `cf-composition`, `cf-burndown-core`, `cf-couples`, `cf-devs`, `cf-file-history`, `cf-sentiment`, `cf-shotness`, `cf-typos`, `cf-anomaly`, `cf-quality`) | One crate per analyzer's logic. | +| Analyzer crates (`cf-complexity`, `cf-cohesion`, `cf-halstead`, `cf-comments`, `cf-imports`, `cf-clones`, `cf-composition`, `cf-analyzer-burndown`, `cf-burndown-core`, `cf-couples`, `cf-devs`, `cf-file-history`, `cf-sentiment`, `cf-shotness`, `cf-typos`, `cf-anomaly`, `cf-quality`) | One crate per analyzer's logic. | + +`codefang` also links `cf-mcp` (the `codefang mcp` server), `cf-streaming`, +`cf-budget`, `cf-framework`, `cf-units` (the history memory budget), +`cf-meminfo` (heap release), `cf-persist` (the UAST results spill) and +`cf-config` (the configuration file). Neither binary links `cf-checkpoint`, +`cf-storage`, `cf-spillstore`, `cf-cache`, `cf-observability`, +`cf-plumbing`, `cf-analyzers-common`, `cf-alg-bloom`, +`cf-alg-cms`, `cf-alg-interval` or `cf-alg-lru` (`cargo tree -p codefang`, +`cargo tree -p uast`). ### Analyzers -#### Shared components (`cf-analyzers-plumbing`) +#### Shared history core -The plumbing crate provides the shared pipeline components that all history -analyzers depend on. These run as "core" analyzers in the pipeline before any -leaf analyzers consume their output. +Every history analyzer runs through one shared core in `cf-commands` +(`crates/cf-commands/src/handlers/history/mod.rs`): one commit window, one +tree diff with rename detection, one change filter, one identity and tick +assignment and one chronological reduce. See +[History pipeline](streaming-pipeline.md). | Component | Purpose | |-----------|---------| -| Tree-diff analyzer | Computes per-commit tree diffs via libgit2 | -| Blob-cache analyzer | Caches blob content for efficient re-reads | -| File-diff analyzer | Computes file-level diffs from blobs | -| Identity detector | Maps commit authors to canonical identities | -| Language detection | Detects file languages | -| Ticks-since-start | Assigns tick indices to commits for time-series | -| Line-stats calculator | Computes per-commit line addition/deletion stats | -| UAST-changes analyzer | Parses UAST for changed files | +| `HistoryRun` | The opened repository, the oldest-first commit window and the change filter | +| `walk_chunks` + worker pool (`history/chunks.rs`, `history/pool.rs`) | The pure per-commit pass (tree diff, line stats, blob reads, UAST parses) on a worker pool at most one chunk ahead of the in-order fold | +| `Stamper` | The ONE identity + tick assignment, one commit at a time in window order | +| Memory budget (`history/budget.rs`) | `--memory-budget` / `--chunk-size` → each walk's chunk size and worker count | +| `HistoryFilter` | `--languages`, vendored / generated policy, the generated-code marker | +| `IdentityDetector` (`cf-analyzers-plumbing`) | Maps commit authors to canonical developers | +| `HistoryTicks` (`cf-analyzers-plumbing`) | Assigns monotonic tick indices (`--tick-size`) | +| Shared UAST pass (`uast_walk.rs`) | Parses each change's two sides once, one change at a time, for every selected UAST-based analyzer | #### Static analyzers @@ -91,10 +99,12 @@ leaf analyzers consume their output. | Halstead | `static/halstead` | Halstead software science metrics | | Cohesion | `static/cohesion` | Class/module cohesion metrics | | Imports | `static/imports` | Import graph and dependency analysis | +| Clones | `static/clones` | Near-duplicate function detection | +| Composition | `static/composition` | File categories (source, test, docs, config, …) | -The analysis layer also contains the static analysis service, analyzer -registry, output formatting (JSON, text, compact, YAML, plot, binary, -timeseries), and cross-format conversion logic. +`cf-commands` also holds the analyzer registry, the shared static walk and +the output formatting (JSON, YAML, binary, text, compact, NDJSON, +timeseries, plot). #### History analyzers @@ -133,21 +143,19 @@ flowchart TD end subgraph uast_engine["cf-uast (Tree-sitter)"] - DETECT[Language Detection<br/><em>enry</em>] - PARSE[DSL Parser] + DETECT[Grammar selection<br/><em>file extension</em>] + PARSE[Parser<br/><em>compiled-in mappings</em>] NODES[UAST Nodes] end - subgraph framework["cf-framework"] - COORD[Coordinator<br/><em>worker pool</em>] - BLOB_PIPE[Blob Pipeline] - DIFF_PIPE[Diff Pipeline] - UAST_PIPE[UAST Pipeline] - RUNNER[Runner<br/><em>chunk processing</em>] + subgraph core["cf-commands history core"] + WINDOW[Commit window<br/><em>--limit / --since / --head</em>] + PREP[Per-commit pass<br/><em>worker pool, one chunk ahead</em>] + STAMP[Identity + ticks<br/><em>Stamper</em>] + REDUCE[Chronological reduce] end subgraph analyzers["analyzer crates"] - PLUMB[Plumbing<br/><em>core analyzers</em>] STATIC[Static Analyzers<br/><em>complexity, cohesion,<br/>halstead, comments, imports</em>] HISTORY[History Analyzers<br/><em>burndown, couples, devs,<br/>sentiment, anomaly, ...</em>] end @@ -161,21 +169,14 @@ flowchart TD TS_OUT[TimeSeries] end - REPO --> OPEN --> COMMITS - COMMITS --> COORD - COORD --> BLOB_PIPE --> BLOBS - COORD --> DIFF_PIPE --> TREEDIFF - COORD --> UAST_PIPE --> PARSE + REPO --> OPEN --> COMMITS --> WINDOW --> PREP + PREP --> TREEDIFF + PREP --> BLOBS + BLOBS --> PARSE SRC --> DETECT --> PARSE --> NODES - - BLOBS --> PLUMB - TREEDIFF --> PLUMB - NODES --> PLUMB - - PLUMB --> HISTORY - SRC --> STATIC - - RUNNER --> COORD + NODES --> PREP + PREP --> STAMP --> REDUCE --> HISTORY + NODES --> STATIC STATIC --> FORMAT HISTORY --> FORMAT @@ -208,12 +209,10 @@ flowchart LR direction TB H1[Open Git repository via libgit2] H2[Walk commit history] - H3[Coordinator pipelines<br/><em>blob, diff, UAST workers</em>] - H4[Core plumbing analyzers<br/><em>tree_diff, blob_cache,<br/>identity, ticks, ...</em>] - H5[Leaf history analyzers<br/><em>burndown, couples, devs,<br/>sentiment, anomaly, ...</em>] - H6[Streaming chunks +<br/>hibernate/boot cycles] + H3[Parallel per-commit pass<br/><em>tree diff, line diffs,<br/>UAST parses</em>] + H4[Identity + tick stamps] + H5[Chronological reduce per analyzer<br/><em>burndown, couples, devs,<br/>sentiment, anomaly, ...</em>] H1 --> H2 --> H3 --> H4 --> H5 - H2 --> H6 end CMD["codefang run -a static/*,history/*"] @@ -227,76 +226,66 @@ flowchart LR ### Static mode -1. Reads source files from the filesystem. -2. Parses each file into a UAST using Tree-sitter with DSL-based mappings. -3. Runs selected static analyzers (complexity, cohesion, halstead, comments, imports). +1. Walks the source tree once per analyzer (`walk_source_files`), applying + the vendored / generated policy, `--languages` and a 64 MiB per-file cap. +2. Parses each file into a UAST using Tree-sitter and the compiled-in + mappings. +3. Runs the selected static analyzers (complexity, cohesion, halstead, + comments, imports, clones, composition); up to four analyzers run + concurrently. 4. Produces per-file and aggregate metrics. -Static analysis is fast, parallelized across files, and requires no Git history. +Static analysis requires no Git history. ### History mode -1. Opens the Git repository via libgit2 through the `git2` crate (supports both normal and bare repos). -2. Loads the commit history (optionally filtered by `--limit`, `--since`, `--first-parent`). -3. The **Coordinator** orchestrates a worker pool with three pipeline stages: blob loading, diff computation, and UAST parsing. -4. **Core plumbing analyzers** (tree diff, blob cache, identity detection, tick assignment, line stats, language detection, UAST changes) process each commit first. -5. **Leaf history analyzers** consume the plumbing output and accumulate their state using the generic aggregator framework or custom memory-efficient data structures. -6. For large repositories, the **streaming pipeline** splits commits into memory-bounded chunks with hibernate/boot cycles and optional double-buffered pipelining. The `BaseHistoryAnalyzer` manages state serialization transparently. -7. **Checkpointing** after each chunk enables crash recovery. +1. Opens the Git repository via libgit2 through the `git2` crate. +2. Selects the commit window (`--limit` newest commits, `--since`, `--head`, + `--first-parent`) and consumes it oldest-first. +3. Diffs every commit against its first parent with rename detection and + applies the shared change filter and `--max-changes-per-commit`. +4. Computes each commit's pure products (line stats, blob reads, UAST + parses) in parallel across all cores. +5. Assigns every commit its author id and tick once, then each analyzer + reduces the products in chronological order on one thread — so the report + is the same on any CPU count. ### Combined mode -When both static and history analyzers are selected, Codefang runs both phases -sequentially, encodes each phase to an internal binary format, then decodes and -merges the results into a single output document in the requested format. +When both static and history analyzers are selected, Codefang computes every +analyzer's report (up to four concurrently), encodes each into the internal +CFB1 binary envelope, then decodes and merges them into a single +`codefang.run.v1` document in the requested format. --- ## Pipeline architecture -The history analysis pipeline is built around a **Runner** that coordinates -the full lifecycle: - -```text -Initialize --> ProcessChunk (x N) --> FinalizeWithAggregators -``` - -Each `ProcessChunk` call: - -1. Feeds commits to the **Coordinator** worker pool. -2. The Coordinator dispatches work across three parallel pipeline stages (blob, diff, UAST). -3. Collected `CommitData` is fed sequentially to each analyzer's `Consume` method. -4. Between chunks, hibernatable analyzers serialize their state to compact form and reboot. - -The **Runner** supports two execution strategies: - -- **Single-pass**: All commits in one chunk (small repos or unlimited memory). -- **Streaming**: Memory-bounded chunks with hibernate/boot cycles, planned by the `streaming.Planner`. See [Streaming Pipeline](streaming-pipeline.md) for details. +There is no runner or coordinator: each history analyzer walks the commit +window in chunks (`--chunk-size`; one chunk by default) — a parallel +per-commit pass over the chunk followed by a single-threaded chronological +fold into the analyzer's reduce state — and several analyzers of one run are +computed concurrently. See [History pipeline](streaming-pipeline.md) for the +commit window, the change filter, identity and ticks, and how reports are +serialized. --- -## Configuration layers - -Configuration follows a clear priority chain: - -```text -CLI flags > Environment variables > .codefang.yaml > Compiled defaults -``` - -The `cf-config` crate handles loading and merging. Environment variables use the `CODEFANG_` prefix with -underscore-separated nesting (e.g., `CODEFANG_PIPELINE_WORKERS=8`). +## Configuration -See the [Configuration reference](../guide/configuration.md) for the full -config file structure and validation rules. +A run is configured by the `codefang run` command-line flags (see the +[CLI reference](../guide/cli-reference.md)); the `cf-config` crate (typed +config, loader, compiled defaults) is not linked into the binary. See the +[Configuration reference](../guide/configuration.md). --- ## Observability -Every layer of the pipeline is instrumented with OpenTelemetry: - -- **Tracing**: Hierarchical spans from `codefang.run` through coordinator pipeline, chunk processing, individual analyzers, git operations, and UAST parsing. -- **Metrics**: RED (Rate, Errors, Duration) metrics plus analysis-specific counters (commits, chunks, cache hit rates). -- **Logging**: Structured `slog` logging with automatic trace context injection. - -See [Observability](../operations/observability.md) for the full instrumentation guide. +The default `codefang` binary exports no traces or metrics: +`cf-observability` is compiled only with `cf-commands`' non-default +`runtime` feature. A run reports on stderr — the resolved selection and the +elapsed time as `progress:` lines (`--silent` turns them off), each skipped +file once as a `warning:` line, and every failure as one `Error: <cause>` +line — and writes report bytes only to stdout (or to `--output` for +`plot`). diff --git a/site/architecture/streaming-pipeline.md b/site/architecture/streaming-pipeline.md index 6ad689fa..f0c60f86 100644 --- a/site/architecture/streaming-pipeline.md +++ b/site/architecture/streaming-pipeline.md @@ -1,494 +1,329 @@ --- -title: Streaming Pipeline -description: Chunk-based streaming execution for analyzing large repositories with bounded memory, including double-buffered pipelining and checkpoint recovery. +title: History Pipeline +description: How codefang walks a repository's history — commit selection, tree diffs with rename detection, the parallel per-commit pass and the single chronological reduce that makes every history report deterministic. --- -# Streaming pipeline +# History pipeline -Large Git repositories (tens or hundreds of thousands of commits) can easily -exhaust available memory if all analyzer state is accumulated in a single pass. -The streaming pipeline solves this by splitting commit history into -memory-bounded **chunks**, processing each chunk independently with -**hibernate/boot cycles** between them. - ---- - -## The problem - -History analyzers accumulate state as they process commits. For example: - -- **Burndown** tracks per-line ownership matrices that grow with repository size. -- **Couples** builds file co-change adjacency matrices. -- **Devs** accumulates per-developer statistics across languages. - -For a 100k-commit repository, a naive single-pass approach may require 10+ GiB -of memory. Many CI environments and containers are limited to 2-8 GiB. - ---- - -## The solution: chunk-based processing - -The streaming pipeline: - -1. **Plans** chunk boundaries based on memory budget and per-analyzer growth rates. -2. **Processes** each chunk by feeding commits to analyzers. -3. **Hibernates** analyzers between chunks (serialize state to compact form). -4. **Boots** analyzers for the next chunk (restore from compact state). -5. **Checkpoints** after each chunk for crash recovery. +Every `history/*` analyzer runs through one shared core +(`crates/cf-commands/src/handlers/history/mod.rs`): one way to select the +commit window, one tree diff, one change filter, one identity and tick +assignment, and one chronological reduce. This page follows a run through +that core in the order it executes. ```mermaid flowchart LR - subgraph chunk1["Chunk 1"] - direction TB - C1_INIT[Initialize] - C1_PROCESS["Process commits<br/>0..499"] - C1_CHECKPOINT[Checkpoint] - C1_INIT --> C1_PROCESS --> C1_CHECKPOINT - end - - subgraph hibernate1["Hibernate / Boot"] - H1[Serialize state] - B1[Restore state] - H1 --> B1 - end - - subgraph chunk2["Chunk 2"] - direction TB - C2_PROCESS["Process commits<br/>500..999"] - C2_CHECKPOINT[Checkpoint] - C2_PROCESS --> C2_CHECKPOINT - end - - subgraph hibernate2["Hibernate / Boot"] - H2[Serialize state] - B2[Restore state] - H2 --> B2 - end - - subgraph chunk3["Chunk 3"] - direction TB - C3_PROCESS["Process commits<br/>1000..1499"] - C3_FINALIZE[FinalizeWithAggregators] - C3_PROCESS --> C3_FINALIZE - end - - chunk1 --> hibernate1 --> chunk2 --> hibernate2 --> chunk3 + W["Commit window<br/><em>--limit / --since / --head</em>"] --> D["Tree diff<br/><em>rename detection</em>"] + D --> F["Change filter<br/><em>languages, vendored,<br/>generated, size cap</em>"] + F --> P["Per-commit products<br/><em>parallel, pure</em>"] + P --> S["Identity + tick stamps"] + S --> R["Chronological reduce<br/><em>one thread, oldest first</em>"] + R --> O["Report value"] + O --> E["json / yaml / bin / text / compact /<br/>ndjson / timeseries / plot"] ``` ---- - -## The planner - -The `streaming.Planner` calculates optimal chunk boundaries. It lives in -`internal/streaming/planner.go`. - -### Parameters - -| Parameter | Value | Description | -|-----------|-------|-------------| -| `MinChunkSize` | 50 | Minimum commits per chunk to amortize hibernation cost | -| `MaxChunkSize` | 3,000 | Safety cap on commits per chunk | -| `BaseOverhead` | 400 MiB | Fixed memory for Go runtime + libgit2 + caches | -| `DefaultStateGrowthPerCommit` | 500 KiB | Conservative fallback per-commit growth rate (= `DefaultWorkingStateSize` + `DefaultAvgTCSize`) | -| `DefaultWorkingStateSize` | 400 KiB | Fallback per-commit working state estimate | -| `DefaultAvgTCSize` | 100 KiB | Fallback per-commit TC payload estimate | -| `SafetyMarginPercent` | 50% | Added to growth rate for transient allocations | -| `UsablePercent` | 95% | Budget fraction available after GC slack | -| `WorkStatePercent` | 60% | Fraction of remaining budget for analyzer working state | -| `AggStatePercent` | 30% | Fraction of remaining budget for aggregator spill budget | -| `ChunkMemPercent` | 10% | Fraction of remaining budget for in-flight data | - -### Budget decomposition: P + W + A + S - -The scheduler decomposes the memory budget into four explicit regions: - -```text -B = P + W + A + S - -usable = budget * 0.95 (S = 5% slack for GC headroom) -remaining = usable - pipelineOverhead (P = pipeline overhead) -workState = remaining * 0.60 (W = analyzer working state) -aggState = remaining * 0.30 (A = aggregator state / spill budget) -chunkMem = remaining * 0.10 (reserved for in-flight CommitData + TCs) -``` - -The `ComputeSchedule()` function in `internal/streaming/planner.go` performs this -decomposition and returns a `Schedule` containing chunk boundaries, chunk size, -buffering factor, and the aggregator spill budget. - -### Chunk size calculation - -The planner determines chunk size from the **working state** portion of the budget: - -```text -growth = working_state_per_commit * 1.5 (safety margin) -chunk_size = clamp(workState / growth, MinChunkSize, MaxChunkSize) -``` - -Where: - -- **`workState`** is 60% of the remaining budget after pipeline overhead and slack. -- **`working_state_per_commit`** is the sum of all selected leaf analyzers' declared `WorkingStateSize()` values. Each leaf analyzer declares `WorkingStateSize()` (analyzer-internal data structures) and `AvgTCSize()` (per-commit TC payload). Only `WorkingStateSize()` drives chunk sizing; `AvgTCSize()` is used separately for aggregator budget estimation. - -### Memory sizing methods - -Each `HistoryAnalyzer` declares two per-commit memory estimates: - -```go -// WorkingStateSize returns estimated bytes of analyzer-internal -// working state accumulated per commit (maps, treaps, matrices). -WorkingStateSize() int64 - -// AvgTCSize returns estimated bytes of TC payload emitted per commit. -AvgTCSize() int64 -``` - -The planner sums `WorkingStateSize() + AvgTCSize()` across all selected leaf -analyzers. Plumbing analyzers return 0 for both and are excluded from the sum. - -### Example - -For a 10,000-commit repo with 4 GiB budget, 400 MiB pipeline overhead, and -1.5 MiB/commit working state growth: - -```text -usable = 4 GiB * 0.95 = 3,891 MiB -remaining = 3,891 - 400 = 3,491 MiB -workState = 3,491 * 0.60 = 2,095 MiB -aggState = 3,491 * 0.30 = 1,047 MiB (AggSpillBudget) -growth = 1.5 MiB * 1.5 = 2.25 MiB/commit -chunk_size = 2,095 / 2.25 = 931 commits -chunks = ceil(10,000 / 931) = 11 chunks -``` +Before any of this runs, one preflight (`handlers::preflight`) validates the +path, opens the repository, resolves `--since`, reads `--people-dict` and +checks every numeric flag and the format; a failure prints `Error: <cause>` +and nothing is written to stdout. --- -## Buffered chunk pipelining - -When the memory budget is sufficient and multiple chunks are needed, the -pipeline enables **buffered pipelining** to overlap the pipeline stage -of upcoming chunks with the analyzer consumption stage of the current chunk. -The scheduler determines the **buffering factor** (1, 2, or 3) based on -the memory budget. - -### The insight +## 1. Commit selection -Processing a chunk has two phases: +The window is computed by `load_history_commit_hashes` +(`crates/cf-commands/src/handlers.rs`) from a libgit2 revwalk that starts at +`HEAD` and is ordered by committer time with topological constraints, newest +first. -1. **Pipeline phase**: The Coordinator runs git operations (blob loading, tree - diffs, UAST parsing) through its worker pool. This is I/O-bound. -2. **Consume phase**: Analyzers consume the collected `CommitData` and update - their state. This is CPU/memory-bound. - -These two phases use different resources and can overlap. - -### Buffering factor selection - -The `ComputeSchedule()` function iterates buffering factors from `MaxBuffering` -(default 3) down to 1, selecting the highest factor where `ChunkSize >= -MinChunkSize`. Only the working state region (60% of remaining budget) is -divided among buffering slots; the aggregator spill budget is unaffected. - -```go -for bf = maxBuffering; bf >= 1; bf-- { - chunkSize = workState / (bf * effectiveGrowth) - if chunkSize >= MinChunkSize { - return bf, chunkSize - } -} -``` +| Flag | Effect on the window | +|------|----------------------| +| `--since <time or revision>` | Keeps the commits whose **author** time is at or after the cutoff. A revision (`v1.2.0`, `HEAD~100`, a SHA) uses that commit's author time. Author time can be skewed against the committer-time order, so the cutoff is a skip filter over the whole walk, never a stop condition. A cutoff newer than `HEAD` gives an empty window and empty reports. | +| `--limit N` | Keeps the **N newest** commits that pass `--since` (the `git log -n` convention); `0` keeps all. | +| `--head` | The window is exactly `[HEAD]` — byte-identical to `--limit 1` for every analyzer and format. | +| `--first-parent` | libgit2 first-parent simplification: side branches are not walked, and a merge is treated as an ordinary single-parent commit. | -| Budget | Typical Factor | Behavior | -|--------|----------------|----------| -| 8 GiB | 2-3 | Double or triple buffering | -| 4 GiB | 2-3 | Double or triple buffering | -| 2 GiB | 1-2 | Single or double buffering | -| 512 MiB | 1 | Single buffering (budget too tight) | -| Unlimited (0) | 3 | Maximum parallelism | +The selected hashes are then **reversed**: every analyzer consumes the window +oldest-first, because tick assignment, identity ids and burndown's line +survival depend on history order. -### How double-buffering works +`history/burndown` always walks first-parent (`HistoryRun::open_first_parent`): +its line-survival tracking follows the main line of development. This is +burndown's own walk; analyzers selected next to it keep the user's +`--first-parent` setting. In a `--limit`, `--since` or `--head` window, +burndown seeds its tracked lines from the window's base tree (the parent of +the first in-window commit), so a commit's added and removed lines are the +lines it changed. -```text - Time ───────────────────────────────────────────> - -Chunk 1: |==== Pipeline ====|==== Consume ====| -Chunk 2: |==== Pipeline ====|==== Consume ====| -Chunk 3: |==== Pipeline ====|==== Consume ====| - - ^^^^^^^^^^^^^^^^ - Pipeline 2 overlaps with Consume 1 -``` - -1. **Chunk 1** runs normally (no prefetch available yet). -2. While Chunk 1's analyzers **consume** data, Chunk 2's **pipeline** runs - concurrently in a background goroutine (`startPrefetch`). -3. When Chunk 1's consume phase finishes, Chunk 2's pipeline data is already - available -- analyzers can consume it immediately without waiting for I/O. -4. The pattern repeats for subsequent chunks. - -### How triple-buffering works - -With `BufferingFactor >= 3`, the scheduler produces smaller chunks (workState -divided by 3), and the prefetch loop naturally overlaps more pipeline phases. -The existing double-buffer loop handles triple-buffering semantics: at each -iteration it prefetches the next chunk, so with more (smaller) chunks, the -overlap covers a larger fraction of total processing time. - -### Memory budget split - -The scheduler handles the memory budget split through the buffering factor -iteration. With `BufferingFactor = N`, the working state region is divided -among N concurrent slots: - -```text -workState = remaining * 0.60 -chunkSize = workState / (N * effectiveGrowth) -``` - -This results in smaller chunk sizes (more chunks), but the pipeline overlap -compensates by eliminating I/O wait time between chunks. - -### Activation conditions - -The scheduler automatically selects the buffering factor. The pipeline uses -double-buffered processing when `BufferingFactor >= 2` and falls back to -sequential chunk processing otherwise. Iterator mode (commit-at-a-time loading) -always uses single-buffering since it cannot prefetch. +A shallow clone yields an empty walk (`cf_gitlib::Repository::log`). --- -## Three-metric adaptive feedback - -After each chunk, the `AdaptivePlanner` examines three independent metrics and -re-plans remaining chunks if any metric diverges from its prediction by more -than 25%. - -### Tracked metrics - -| Metric | Source | Purpose | -|--------|--------|---------| -| **Working state growth** | `HeapInuse` delta minus aggregator delta | Drives chunk resizing | -| **TC payload size** | TC count x declared `AvgTCSize` | Detects data volume changes | -| **Aggregator state growth** | `EstimatedStateSize()` delta | Detects accumulation spikes | - -Each metric is tracked by its own exponential moving average (EMA) with -`alpha = 0.3` (~3-chunk half-life). The `ReplanObservation` struct carries -all three per-commit observations to `Replan()`. +## 2. Tree diff with rename detection -### Replan logic +Each commit is diffed against its first parent (`commit_tree_changes`); a +root commit lists every file of its tree as an insertion. The diff uses +libgit2's tree diff followed by `git_diff_find_similar` with renames enabled +(libgit2's default 50 % similarity, the `git diff -M` behaviour), so a moved +file is ONE change from the old path to the new path instead of a delete plus +an add (`cf_gitlib::Repository::diff_tree_to_tree`). That keeps a file's +identity across renames: burndown moves the file's line history, couples +carries its co-change row, file-history carries its commit list, and devs +counts a pure `git mv` as zero added and zero removed lines. -1. Update all three EMAs with the chunk's observations (clamped to 1 KiB floor). -2. Compute the predicted effective growth rate: `declared * 1.5` (safety margin). -3. If any EMA diverges from predicted by more than 25%, trigger a replan. -4. The **working state growth** EMA drives chunk resizing (as before). TC and - aggregator metrics are informational triggers only. -5. Processed chunks are never modified (checkpoint safety). - -### Telemetry - -`AdaptiveStats` exposes per-metric final rates: - -- `FinalWorkGrowth` — smoothed working state growth per commit. -- `FinalTCSize` — smoothed TC payload size per commit. -- `FinalAggGrowth` — smoothed aggregator state growth per commit. +Commits whose trees are identical produce no changes. Type changes and +unreadable deltas are skipped. --- -## Hibernate / boot cycles - -Between chunks, the pipeline calls `Hibernate()` on all hibernatable analyzers, -then `Boot()` to restore them for the next chunk. - -```go -type Hibernatable interface { - Hibernate() error // Serialize state to compact form - Boot() error // Restore from compact state -} -``` - -Hibernation compacts in-memory data structures. For example, the burndown -analyzer may convert dense matrices to sparse representations. Some analyzers -support disk-backed hibernation for very large state. - -The hibernate/boot cycle adds overhead per chunk boundary, which is why -`MinChunkSize` (50 commits) exists to amortize this cost. +## 3. The change filter + +`HistoryFilter` (`crates/cf-commands/src/handlers.rs`) decides, per change, +whether any analyzer sees it. It uses the same path policy as the static +phase: + +- **Vendored paths** (detected by the Linguist rules: `vendor/`, + `node_modules/`, `third_party/`, `testdata/`, minified bundles, …) are + excluded unless `--include-vendored` is set. +- **Generated files** are excluded unless `--include-generated` is set: by + name (`*.pb.go`, `zz_generated_*.go`, `*_pb2.py`, `*.min.js`, …) and by a + generated-code marker (`DO NOT EDIT`, `Code generated`, …) in the first + 512 bytes of the blob (`GENERATED_MARKER_HEADER_BYTES`). +- **`--extra-excluded-prefixes`** adds path prefixes to exclude. +- **`--languages`** keeps only the listed languages. The language is detected + from the path AND the blob content, so `.h` resolves by content and an + extensionless script by its shebang. An absent flag, an empty value or + `all` means no restriction. + +**`--max-changes-per-commit N`** (default 10 000 when `0`) drops a commit +whose raw tree diff — before the filter — has more than `N` changes. The +commit is removed before identity and ticks are assigned, so it consumes no +author id and advances no tick, and every history analyzer applies the same +rule (`Gated` / `gated_product` in `history/mod.rs`): all of them see the +same commit set, whether an analyzer runs alone or next to others. Couples additionally keeps its own +changeset cap (1 000 files): a commit above it still owns the files it +touched but adds no co-change pairs. --- -## Checkpointing - -After each fully processed chunk (except the last), the pipeline saves a -checkpoint to disk. If the process is interrupted (OOM kill, pod eviction, -timeout), the next run with `--resume` automatically restarts from the last -completed chunk. - -### Checkpoint contents - -| Field | Description | -|-------|-------------| -| `TotalCommits` | Total commits in the repository | -| `ProcessedCommits` | Number of commits processed so far | -| `CurrentChunk` | Index of the last completed chunk | -| `TotalChunks` | Total planned chunk count | -| `LastCommitHash` | Hash of the last processed commit | -| `AggregatorSpills` | Per-aggregator spill directory path and spill count | -| Analyzer state | Serialized state of all checkpointable analyzers | +## 4. Per-commit products in parallel + +The expensive, order-independent work — tree diffs, line diffs, blob reads, +language detection, UAST parses — runs on a worker pool +(`prepare_in_order`, `history/pool.rs`): + +- a fixed set of worker threads lives for the whole walk; each opens its OWN + `Repository` once (libgit2 handles cannot cross threads) and keeps one + thread-local UAST parser (`UAST_PARSER`), so setup is paid once per thread; +- workers claim short runs of consecutive commits (up to 32) from a shared + cursor: consecutive commits share most of their trees, which the claiming + worker's libgit2 object cache then still holds (`history/anomaly` on + 31,923 kubernetes commits: 65 s claiming one commit at a time, 32 s with + runs); +- a worker with nothing left to claim takes the unstarted back half of + another worker's run, so a cluster of slow commits (vendoring, mass + renames) is shared out instead of serialized behind one worker; +- the function run per commit is **pure**: its result depends only on that + commit, never on another commit's result; +- the calling thread folds the products in window order as they arrive; a + fatal git error on any commit aborts the run, and a panicking commit + panics the run with its own message. + +### Chunks + +A worker may claim a commit only while it is less than one chunk ahead of +the fold, so at most one chunk of products is alive at once +(`walk_chunks`, `history/chunks.rs`). The chunk is a sliding bound, not a +barrier: no worker waits for the slowest commit of a chunk. Each kept commit +is stamped and folded into the analyzer's reduce state in window order and +its product is freed. The fold sees the same commits in the same order +whatever the chunk size or worker count, so the output is byte-identical for +every `--chunk-size` and `--memory-budget` +(`bins/codefang/tests/chunked_history_e2e.rs`). Every history walk folds +this way. Burndown first stamps every kept commit (the value packing needs +the last tick before its first line is attributed), then prepares each +change's facts on the pool — the blob line counts, the binary check, the +change filter and the line diff against the From blob — and folds them into +its per-file treaps in commit order; a precomputed diff is used only while +the From path's tracked length matches the From blob, exactly the +sequential rule. The raw-count pre-pass keeps the tree diffs of commits +with 256 or more changes for the main pass, so their rename detection runs +once (`history/burndown` on 31,923 kubernetes commits: 59.4 s → 25.9 s). + +After every chunk of folded commits, and when the walk ends, free heap pages +go back to the operating system (`cf_meminfo::release_free_heap`, +`malloc_trim(0)` on glibc). glibc otherwise keeps every worker's allocator +arena at its largest transient working set: on the 7,973-commit kubernetes +prefix a forced trim dropped `history/sentiment` from 1777 MB to 679 MB +resident mid-run. + +### The memory budget + +`--memory-budget` (default: half the physical RAM, at most 2 GiB) plans each +walk (`history/budget.rs`): + +1. `--chunk-size` wins when given; the walk then runs one worker per core. +2. The history walks that run at once (the selected history analyzers, the + five UAST analyzers counting once, at most four) share the budget. +3. libgit2 gets a fixed part first: a quarter of the budget for its object + cache, shared by every repository handle (its 256 MiB default made the + parallel walks evict each other's trees: the five non-UAST walks over + 31,923 kubernetes commits ran 183 s with it, 109 s with 512 MiB), and + 7.5 % for mapped pack windows (the Go split; without a cap libgit2 keeps + up to 8 GiB mapped, every touched page resident). Both are set once, + before the first repository opens. +4. A fixed base (128 MiB: the binary and the embedded parsers) is paid once. + The walks that run at once share the rest (the pre-computed UAST pass + runs alone and gets all of it); half of a walk's share is reserved for + worker working sets. Each walk has a measured cost per worker and per + in-flight product (`DEVS_COST`, `ANOMALY_COST`, …, recorded next to each + walk with the measurement it comes from), so the worker count is as many + as the reserve holds (at least one, at most the cores) and the chunk is + what `cf_streaming::Planner` fits in the other half (at least two + products per worker, at most the window). + +`-v` prints each walk's plan, for example +`progress: history/devs plan: 3000 commits per chunk, 11 chunks, 24 workers (budget 2048 MiB)`. + +The budget bounds what a walk holds in flight; the analyzers' own reduce +state (per-commit records the per-commit formats emit, per-file and +per-developer aggregates) still grows with the window. + +### Blob size caps + +| Cap | Value | Applies to | +|-----|-------|------------| +| `MAX_BLOB_SIZE` (`uast_walk.rs`) | 256 KiB | A larger blob is not parsed into a UAST (imports, quality, sentiment, shotness, typos skip it). Line-based analyzers still count it. | +| Content classification (`file_history.rs`) | first 8 KiB | File-history classifies a file by its path and its blob header (binary, generated). | +| Generated marker | first 512 bytes | The change filter above. | + +### The shared UAST walk + +The five UAST-based analyzers (`history/imports`, `quality`, `sentiment`, +`shotness`, `typos`) run as ONE chunked pass, whether one or several of them +are selected (`crates/cf-commands/src/handlers/uast_walk.rs`): each commit's +diff is computed once, and each change's two sides are read and parsed at +most once and shared by every selected analyzer (`ChangeParses`). A worker +holds the parses of one change at a time, never a whole commit's: the +largest kubernetes commits parse megabytes of source, and holding them all +was what drove the UAST analyzers' memory. The pass applies the same flags +to every analyzer (drop gate, `--first-parent`, the shotness DSL), so an +analyzer's report does not depend on which others are selected. The pass is +memoized for the process with only its finished per-analyzer records, so +every output path of the run (several formats, the plot pages) reuses it. + +When other history walks run after the pass, the finished records would +share memory with them while nobody reads them until rendering (on 31,923 +kubernetes commits: 2.2 GB of the 4.9 GB peak). The memo then spills each +analyzer's records to an unlinked temporary file in `TMPDIR` +(`handlers/spill.rs`, bincode through `cf_persist::GobCodec`), and each +consumer reads back only its own analyzer when it renders. The files have no +name, so their space is reclaimed when the process exits. On a host whose +`/tmp` is `tmpfs`, the spill leaves the process but stays in RAM; point +`TMPDIR` at a disk to free it. `-v` prints the size of every spill. -The checkpoint format is versioned (currently **v2**). Checkpoints saved by -older versions are rejected with a warning, and the run starts fresh. - -On resume, aggregators are recreated and pointed at their saved spill -directories so that TCs accumulated before the interruption are preserved. -This ensures resumed runs produce identical output to uninterrupted runs. - -### Checkpointable interface - -```go -type Checkpointable interface { - CheckpointSave(w io.Writer) error - CheckpointLoad(r io.Reader) error -} -``` +--- -!!! info "Full Coverage Required" - Checkpoint resume only activates when **all** analyzers in the pipeline - implement `Checkpointable`. If any analyzer lacks support, checkpointing - is disabled with a warning. +## 5. Identity and ticks -### CLI flags +`Stamper` (`history/chunks.rs`) assigns each kept commit its author id and +its tick, one commit at a time, oldest-first, as the walk folds it. Both +depend only on the commits stamped before, never on the chunk boundaries, so +all analyzers agree on who and when. -| Flag | Default | Description | -|------|---------|-------------| -| `--checkpoint` | `true` | Enable checkpointing | -| `--resume` | `true` | Resume from checkpoint if available | -| `--checkpoint-dir` | `~/.codefang/checkpoints` | Checkpoint storage directory | -| `--clear-checkpoint` | `false` | Clear existing checkpoint before run | +**Identity** (`IdentityDetector`, `cf-analyzers-plumbing`). By default a +commit whose author email OR name matches a known developer is that +developer, and the other token is registered too, so `alice <a@x>`, +`Alice Smith <a@x>` and `Alice Smith <a@y>` are one person. Ids are assigned +in first-seen order, oldest commit first. -### Crash recovery flow +| Flag | Effect | +|------|--------| +| `--exact-signatures` | Match on the full `name <email>` signature instead. | +| `--people-dict <file>` | Map signatures to people from a file (an unreadable file is an error). | +| `--anonymize` | Report developers as `Developer-A`, `Developer-B`, … | -```mermaid -flowchart TD - START[codefang run] --> CHECK{Checkpoint exists?} - CHECK -->|No| FRESH[Start from chunk 0] - CHECK -->|Yes| VALIDATE{Validate checkpoint} - VALIDATE -->|Invalid| FRESH - VALIDATE -->|Valid| RESUME["Resume from chunk N+1"] - FRESH --> PROCESS[Process chunks] - RESUME --> PROCESS - PROCESS --> COMPLETE{All chunks done?} - COMPLETE -->|Yes| CLEAR[Clear checkpoint] - CLEAR --> FINALIZE[FinalizeWithAggregators] - COMPLETE -->|Interrupted| SAVED[Checkpoint on disk] - SAVED --> |Next run| CHECK -``` +**Ticks** (`HistoryTicks`). The first commit's committer time, floored to the +tick size, is tick 0; every commit's tick is +`max(floor((time - tick0) / size), previous tick)`, so ticks never go +backwards even when committer clocks do. `--tick-size` sets the size in +hours (default 24, must be greater than 0). The assignment is a pure function +of the commit times: no wall clock is consulted. --- -## Full pipeline timeline - -The following diagram shows the complete lifecycle of a streaming analysis -run with buffered pipelining (double-buffer shown) and checkpointing: - -```text -Time ──────────────────────────────────────────────────────────────────────> - -Phase: INIT CHUNK 1 CHUNK 2 CHUNK 3 FINALIZE - ┌───┐ ┌─────────────────────┐ ┌─────────────────────┐ ┌──────────┐ ┌───────┐ -Main: │INI│ │Pipeline│ Consume │ │Consume (prefetched)│ │Pipe│Consu│ │REPORT │ - └───┘ └────────┴─────────────┘ └────────────────────┘ └────┴─────┘ └───────┘ - │ │ │ -Background: │ ┌────────────┐ │ ┌────────────┐ │ -(prefetch) └──│Pipeline C2 │ └───│Pipeline C3 │ │ - └────────────┘ └────────────┘ │ - │ -Checkpoint: [save] [save] │ - │ │ │ - v v │ -Disk: [chunk_0.ckpt] [chunk_1.ckpt] [clear on success] - -Hibernate: [hib][boot] [hib][boot] -``` - -### Legend - -| Symbol | Meaning | -|--------|---------| -| `INI` | `runner.Initialize()` -- set up analyzers | -| `Pipeline` | Coordinator worker pool: blob, diff, UAST stages | -| `Consume` | Analyzers process CommitData sequentially | -| `[hib][boot]` | Hibernate/boot cycle between chunks | -| `[save]` | Checkpoint saved to disk | -| `REPORT` | `runner.FinalizeWithAggregators()` -- generate output via aggregator path | +## 6. The chronological reduce + +Each analyzer then folds its per-commit products into its report on ONE +thread, in window order: burndown applies each commit's line edits to its +per-file treaps, couples accumulates co-change and ownership matrices over +one exact set of seen files, file-history maintains per-path commit lists, +shotness runs its per-function state machine, typos keeps each pair's first +fix. + +The split is deliberate. State that depends on history order — line +survival, rename chains, first-seen identities, "first occurrence" lists — +must be applied oldest-first, and keeping all parallelism in the pure +per-commit stage makes every report a pure function of the repository and +the flags: the same bytes on 2 CPUs or 64. The end-to-end test +`output_does_not_depend_on_the_host_cpu_count` +(`bins/codefang/tests/history_core_e2e.rs`) pins this by running under +`taskset -c 0-1`. + +A merge commit (when not walking first-parent) is diffed against its first +parent like any commit; each analyzer decides what a merge contributes — for +example devs attributes no line stats to it, and couples counts only the +files no earlier commit of the window changed. --- -## Configuration recommendations - -### Memory budget by repository size +## 7. Several analyzers in one run -| Repository Size | Recommended `--memory-budget` | Expected Chunks | -|-----------------|-------------------------------|-----------------| -| < 1k commits | `2GiB` | 1 (no streaming) | -| 1k -- 10k commits | `4GiB` | 2 -- 10 | -| 10k -- 100k commits | `4GiB` -- `8GiB` | 10 -- 100+ | -| 100k+ commits | `8GiB` | Many, checkpointing essential | +Each analyzer computes its own walk (except the shared UAST walk above). The +analyzers of one selection are independent, so up to four are computed +concurrently (`ANALYZER_CONCURRENCY`), and their outputs are always written +in a fixed order. -### Tuning tips - -!!! tip "Let the planner decide" - In most cases, setting `--memory-budget` is sufficient. The planner - automatically calculates optimal chunk sizes based on the selected - analyzers' declared growth rates. +--- -- **More analyzers = smaller chunks**: Each analyzer adds to the aggregate - growth rate, reducing the number of commits that fit in each chunk. -- **Burndown is the heaviest**: The burndown analyzer has the highest - per-commit growth rate. If you do not need burndown, your chunks will be - larger and runs faster. -- **Checkpointing adds ~5% overhead**: The serialization cost per chunk - boundary is small but non-zero. -- **Buffered pipelining shines on I/O-bound repos**: Repositories with large - blobs benefit most from overlapping pipeline and consume phases. The - scheduler automatically selects double or triple buffering when the budget - allows. +## 8. Serialization -### Reference benchmarks +The reduce produces one report value per analyzer; every format encodes that +value, through `cf-gojson` / `cf-goyaml` so key order and number formatting +are stable. -Measured on the kubernetes repository (56K first-parent commits, burndown -analyzer, `--memory-budget 4GB`): +| Format | One analyzer | Several analyzers | +|--------|--------------|-------------------| +| `json`, `yaml` | The analyzer's report. | ONE `codefang.run.v1` document: `{"version", "metadata", "analyzers": [{"id", "mode", "report"}]}` — the same shape as a mixed static + history run. | +| `bin` | A CFB1 envelope around the JSON value. | One envelope per analyzer. | +| `text` | A version header, `<id>:`, then the analyzer's human summary. | The header once, one section per analyzer. | +| `compact` | One line: id, `Info`, the key figures. | One line per analyzer. | +| `ndjson` | One line per per-commit record: `{"hash", "tick", "author_id", "timestamp", "analyzer", "data"}`. | Every analyzer's records merged by commit position. | +| `timeseries` | A `codefang.timeseries.v1` document: per commit `{hash, timestamp, author, tick, <analyzer>: …}`. | The per-commit values of every analyzer merged into one entry per commit. | +| `timeseries --ndjson` | The document's `commits`, one line per commit. | Same, merged. | +| `plot` | `<page>.html`, `index.html` and `report.json` in `--output`. | One page per analyzer, one index. | -| Metric | Value | -|--------|-------| -| Peak RSS | 6.5 -- 7.1 GiB | -| Wall time | 2m 19s -- 2m 44s | -| Chunks | ~20 (adaptive) | -| NDJSON first line (10K commits) | 1.3s | +Every whole-selection renderer builds its complete output before anything is +written: if an analyzer cannot produce its report, the run prints +`Error: <cause>` and stdout stays empty. --- -## Observability - -The streaming pipeline emits OpenTelemetry spans and metrics for monitoring: +## Observing a run -### Spans +Unless `--silent` is set, the run prints two progress lines on stderr: the +resolved selection (`progress: resolved analyzers: static=… history=… +output_format=…`) and the elapsed time (`progress: run finished in …`). +Report bytes only ever go to stdout (or to `--output` for `plot`). -| Span | Attributes | -|------|------------| -| `codefang.analysis` | `analysis.chunks`, `analysis.chunk_size`, `analysis.double_buffered` | -| Per-chunk events | `chunk.index`, `chunk.offset`, `chunk.size`, `chunk.duration_ms` | -| `checkpoint.saved` | `chunk` index | -| `checkpoint.resumed` | `chunk` index | - -### Metrics +--- -| Metric | Type | Description | -|--------|------|-------------| -| `codefang.analysis.commits.total` | Counter | Total commits analyzed | -| `codefang.analysis.chunks.total` | Counter | Total chunks processed | -| `codefang.analysis.chunk.duration.seconds` | Histogram | Per-chunk processing duration | -| `codefang.analysis.cache.hits.total` | Counter | Cache hits (blob, diff) | -| `codefang.analysis.cache.misses.total` | Counter | Cache misses (blob, diff) | +## What the pipeline does not do -The analysis span also records aggregate pipeline timing (blob, diff, UAST -stage durations) and cache statistics, with the dominant stage identified -automatically. +The `codefang` binary spills only the finished UAST results; it does not +hibernate a walk's in-progress state between chunks, has no +checkpoint/resume and no prefetch buffering, and it exports no tracing spans +or metrics. The workspace crates that model those ideas (`cf-checkpoint`, +`cf-spillstore`, `cf-observability`) are not linked into it. The memory budget bounds what the walks hold in flight; the reduce +state of very long windows still grows with the window, so on a very large +repository narrow it with `--limit` or `--since`, or select fewer analyzers. diff --git a/site/architecture/uast.md b/site/architecture/uast.md index 3e1afc37..72e825ea 100644 --- a/site/architecture/uast.md +++ b/site/architecture/uast.md @@ -8,8 +8,8 @@ description: Deep dive into the Universal Abstract Syntax Tree parser, Tree-sitt The **Universal Abstract Syntax Tree** (UAST) is Codefang's language-agnostic code representation. It normalizes the wildly different ASTs produced by language-specific parsers into a common schema with consistent node types, -roles, and properties -- enabling analyzers to work across 60+ languages -with a single implementation. +roles, and properties -- enabling analyzers to work across the 68 +compiled-in languages with a single implementation. --- @@ -53,48 +53,60 @@ backend. Tree-sitter provides: - **Incremental parsing** -- only re-parses changed regions - **Error recovery** -- produces partial trees even for malformed code -- **60+ language grammars** compiled into the binary via [go-sitter-forest](https://github.com/alexaandru/go-sitter-forest) +- **68 language grammars** compiled into the binary: the grammar C sources are + vendored under `crates/cf-uast/vendor/` (taken from + [go-sitter-forest](https://github.com/alexaandru/go-sitter-forest)) and + built by `crates/cf-uast/build.rs` - **Zero runtime dependencies** -- all grammars are statically linked The parsing flow: ```mermaid flowchart LR - SRC["Source Code<br/>(any language)"] --> DETECT["Language Detection<br/><em>go-enry</em>"] + SRC["Source Code<br/>(any language)"] --> DETECT["Grammar selection<br/><em>file extension</em>"] DETECT --> TS["Tree-sitter Parser<br/><em>language-specific grammar</em>"] TS --> CST["Concrete Syntax Tree<br/><em>Tree-sitter nodes</em>"] CST --> DSL["DSL Mapping Engine"] DSL --> UAST["Universal AST<br/><em>normalized nodes</em>"] ``` -### Language detection - -File language is detected using the [go-enry](https://github.com/go-enry/go-enry) -library, which uses filename, extension, and content heuristics (the same -algorithm as GitHub's Linguist). You can override detection with the -`--language` / `-l` flag. - -### Performance optimizations - -The UAST parser includes several performance optimizations for high-throughput -analysis: - -- **Parser pool**: `sync.Pool` of Tree-sitter parsers to avoid re-allocation across files. -- **Pre-interned types and roles**: DSL rule strings are interned at load time, eliminating repeated allocations. -- **Per-parse string interning**: Short strings (32 bytes or less) are deduplicated within each parse call. -- **O(1) rule lookup**: Rule index by node type replaces linear scan. -- **Batch child reading**: Uses unsafe batch reads for nodes with 8+ children, avoiding per-child CGO overhead. -- **Zero-copy text comparison**: `unsafeNodeText` provides zero-allocation string views for condition evaluation. -- **Cursor pooling**: Tree-sitter cursors are pooled and reused across recursive calls within a single parse. -- **Arena allocator**: Node allocation uses a per-parse arena (`node.Allocator`) to reduce GC pressure. +### Grammar selection + +The parser (`cf_uast::Parser`) picks the grammar from the file's +**extension** (lower-cased), looked up in the registry of compiled-in +mappings behind a small bloom filter (`crates/cf-uast/src/loader.rs`). There +are no content heuristics: a file without a mapped extension (for example a +`Makefile`) is not parsed. `uast parse -l <language>` forces the grammar. + +The `codefang` language filter (`--languages`) and its per-language +breakdowns are a separate step: they use `cf-langpath`, a port of the +enry / Linguist cascade over the file name AND its content (shebangs, +`.h` disambiguation). + +### Performance characteristics + +- **Lazy per-language setup**: each language's parser state (tree-sitter + language, resolved rules, pattern matcher) is built on its first parse and + then shared (`LazyDslParser`, a `OnceLock`). +- **O(1) rule lookup**: rules are indexed by the tree-sitter node type they + match (`rule_dispatch`), so lowering never scans the rule list. +- **One parser facade per worker thread** in history walks: the parallel + per-commit pass keeps a thread-local `cf_uast::Parser` + (`crates/cf-commands/src/handlers/history/mod.rs`), so registry and loader + setup is paid once per thread. +- **Parse watchdog**: every tree-sitter parse has a 30 s timeout + (`PARSE_TIMEOUT_MICROS`); a fresh tree-sitter parser is created per parse. --- ## DSL mapping engine -The DSL (Domain-Specific Language) is how Tree-sitter's language-specific -concrete syntax tree nodes are mapped to universal UAST node types. Each -language has a `.uast` mapping file that declares the transformation rules. +The mapping rules are how Tree-sitter's language-specific concrete syntax +tree nodes are mapped to universal UAST node types. The compiled-in languages +keep their rules as Rust tables, one module per language in +`crates/cf-uast-mappings/src/` (built with the `uast_language!` macro); the +text DSL below expresses the same rule model and is the format for custom +mappings (`uast mapping`, `cf_uast::Parser::with_map`). ### DSL syntax overview @@ -138,7 +150,7 @@ Each rule has four parts: | `type` | UAST node type | `"Function"`, `"Class"`, `"Import"` | | `token` | Token extraction source | `"@name"`, `"self"`, `"text"`, `"fields.name"`, `"child:identifier"`, `"descendant:identifier"` | | `roles` | Comma-separated semantic roles | `"Declaration", "Function"` | -| `props` | Key-value properties | `name: "@name", visibility: "public"` | +| `props` | Key-value properties: `@capture`, `descendant:<kind>`, or a literal | `name: "@name", operator: "?:"` | | `children` | Child capture reference | `"@body"` | ### Token sources @@ -166,7 +178,12 @@ public_method <- (method_declaration ``` Conditions support `==` and `!=` operators, comparing fields, captures, or -child types against string literals. +child types against string literals. A dotted path navigates the tree-sitter +structure — `value.type == "arrow_function"` compares the `value` field's node +kind — and a leading `parent` segment steps to the enclosing node: +`parent.type == "variable_declarator"` selects the rule only when the node is +the value of a declarator. Several rules may share one node kind; the first +whose conditions pass wins and an unconditional rule is the catch-all. ### Inheritance @@ -220,6 +237,66 @@ syntax. A single node can have multiple roles. --- +## Shape contract + +Analyzers read the UAST, never the language. The guarantees below hold for +every language; the language-agnostic ones are implemented once in the +lowering (`crates/cf-uast/src/lowering.rs`) and pinned by +`crates/cf-uast/tests/lowering_contract.rs`, the per-language ones by +`crates/cf-uast-mappings/tests/shape_contract.rs`. + +### Operators + +Every `BinaryOp`, `UnaryOp` and `Assignment` node (plain and compound) and +every conditional expression carries `props.operator`: the operator's source +spelling (`+`, `&&`, `and`, `not in`, `=`, `+=`, `++`). It is resolved, in +order, from the rule's own `operator` prop, the grammar's `operator` / `op` / +`operation` field, the space-joined repeated `operators` field (a Python chain +`a < b <= c` reports `< <=`), and — for the three operator types only — the +first anonymous child token. Ternaries are `If` nodes with role `Condition` +and `operator` `?:` (`if-else` for Python's `x if c else y`). The node +`token` of an operator node is not part of the contract; read `operator`. + +### Tokens + +`Identifier` and `Literal` nodes always carry their source text as `token` +(including string quotes). A `Comment` is a **leaf** whose `token` is the +comment text and whose span never includes the trailing line terminator; +documentation comments (`///`, `//!`, `/** */`, Python docstrings) carry the +role `Doc`. The `token` of a declaration node, when a table sets one, is the +declared name (C/C++ functions, Java types, JS/TS class-field functions); +some tables inherited from the DSL corpus still set the full declaration +text (Go, Rust and Java functions) — slice the source through `pos` instead +of relying on either. + +### Names + +Every `Function`, `Method`, `Class`, `Struct`, `Interface` and `Enum` +declaration carries `props.name`, and there is exactly **one** such node per +declaration: bodies are `Block`s, declarators and parameter-type lists are +structural, and a function-typed parameter (`func(int) int`) or a prototype +is never a `Function`. Named function values (`const f = () => {}`, +`handler = () => {}`) are a single `Function` / `Method` named after the +binding; an anonymous function value is a `Lambda`. + +### Branches and loops + +An `If`'s children are its condition expression(s), the consequence `Block` +(role `Body`) and, when present, the alternative — the child lowered from the +tree-sitter `alternative` field — which carries the role `Else`. An +else-if chain is therefore an `If` **directly** under an `If` with roles +`Condition, Else`; a final else is a `Block` with roles `Body, Else` +(`Else` lands on whatever the alternative lowers to, including a bare +statement in C/Java/JS). Grammars with an `else_clause` wrapper (C, C++, +Rust, JavaScript, TypeScript) leave it unmapped so the wrapper collapses; +Python's `elif_clause` / `else_clause` set the roles explicitly. Each loop +statement is exactly one `Loop`; where the table knows the loop kind it is +`props.kind` (`for`, `while`, `do-while`, `for-in`, `loop`). Every +`case` / `default` label is one `Case` with role `Branch`; `Switch` is the +statement only. + +--- + ## Writing custom mappings To add or customize language mappings: @@ -286,7 +363,7 @@ uast parse --language rust main.rs uast explore main.rs # Query specific node types -uast query -e 'filter(.type == "Function")' main.rs +uast query 'rfilter(.type == "Function")' main.rs ``` ### Step 3: iterate @@ -297,19 +374,18 @@ mapping rules to convert them to UAST types. ### Pre-compiled mappings -For production performance, language mappings are pre-compiled into the binary. -The DSL is parsed once at startup and the resulting `PatternMatcher` caches -compiled Tree-sitter queries for O(1) lookup during parsing. - -The `DSLParser` pre-interns all Type and Role strings from the mapping rules, -so repeated files of the same language share the same string values without -allocation. +The built-in language mappings are compiled into the binary as Rust tables +(`crates/cf-uast-mappings`); nothing is read from disk at run time. Each +language's rules are resolved once, on its first parse, together with its +`PatternMatcher` and the node-type rule index. --- ## UAST CLI commands -The `uast` binary provides five commands for working with Universal ASTs. +The `uast` binary's commands for working with Universal ASTs are below; it +also provides `analyze`, `validate`, `mapping`, `completion` and `version` +(`uast --help`). ### `uast parse` @@ -337,8 +413,8 @@ cat main.go | uast parse - # Parse from stdin Query UAST nodes using filter expressions. ```bash -uast query -e 'filter(.type == "Function")' main.go -uast query -i ast.json -e 'filter(.roles has "Declaration")' +uast query 'rfilter(.type == "Function")' main.go +uast query 'rfilter(.roles has "Declaration")' -i ast.json uast query --interactive main.go # REPL mode ``` @@ -389,22 +465,22 @@ flowchart TB EXPLORE_CMD[explore] end - subgraph parser["pkg/uast"] - LOADER[Loader<br/><em>pre-compiled mappings</em>] + subgraph parser["cf-uast"] + LOADER[Loader<br/><em>compiled-in mappings</em>] PARSER[Parser<br/><em>language router</em>] - DSL[DSLParser<br/><em>per-language</em>] - PM[PatternMatcher<br/><em>cached TS queries</em>] - POOL[Parser Pool<br/><em>sync.Pool</em>] + DSL[LazyDslParser<br/><em>per language, built on first use</em>] + PM[PatternMatcher<br/><em>rule index by node type</em>] + POOL[Tree-sitter parser<br/><em>per parse, 30 s timeout</em>] end subgraph treesitter["Tree-sitter"] - TS_GRAMMAR[Language Grammar<br/><em>60+ languages</em>] + TS_GRAMMAR[Language Grammar<br/><em>68 languages</em>] TS_PARSER[Tree-sitter Parser] CST[Concrete Syntax Tree] end - subgraph enry["go-enry"] - LANG_DETECT[Language Detection] + subgraph enry["Grammar selection"] + LANG_DETECT[File extension lookup] end subgraph output_nodes["Output"] @@ -441,13 +517,19 @@ The UAST system is used by both static and history analyzers: - `static/comments` -- measures `Comment` node density relative to code - `static/imports` -- extracts `Import` nodes for dependency graphs -**History analyzers** receive UAST diffs through the plumbing layer: +**History analyzers** parse the changed blobs of every commit: -- `history/sentiment` -- analyzes sentiment of `Comment` nodes across commits +- `history/sentiment` -- analyzes sentiment of the `Comment` nodes a commit + introduced or changed - `history/shotness` -- tracks change frequency of `Function` nodes -- `history/typos` -- detects identifier typos in UAST diffs +- `history/typos` -- detects identifiers a commit renamed into a + Levenshtein-close spelling +- `history/imports` -- records the imports each developer adds - `history/quality` -- tracks UAST-based quality metrics over time -The `plumbing.UASTChangesAnalyzer` parses both the before and after versions -of changed files, providing history analyzers with UAST diff information -for each commit. +Each commit's changed files are parsed in the parallel per-commit pass of the +[history pipeline](streaming-pipeline.md), one change at a time: both sides +of a change are parsed at most once, shared by every selected UAST analyzer, +and dropped before the next change (`ChangeParses` in +`crates/cf-commands/src/handlers/uast_walk.rs`); blobs over 256 KiB are not +parsed. One pass parses for all the selected UAST analyzers. diff --git a/site/contributing/index.md b/site/contributing/index.md index a9c75719..daf5a4e1 100644 --- a/site/contributing/index.md +++ b/site/contributing/index.md @@ -252,9 +252,8 @@ produce clean changelogs and enable automated versioning. ``` feat(analyzers): add anomaly detection analyzer - Implements Z-score and IQR based anomaly detection for commit-level - metrics. Integrates with the streaming pipeline and supports - checkpointing for crash recovery. + Implements Z-score anomaly detection over per-tick commit metrics, + with a configurable trailing window and threshold. Closes #142 ``` @@ -329,7 +328,7 @@ The project `Makefile` provides a comprehensive set of targets for development: Found a bug? Please [open a GitHub Issue](https://github.com/Sumatoshi-tech/codefang/issues/new) with the following information: -1. **Codefang version** -- output of `codefang --version` +1. **Codefang version** -- output of `codefang version` 2. **Operating system and architecture** -- e.g., `Linux x86_64`, `macOS arm64` 3. **Steps to reproduce** -- minimal command or configuration to trigger the bug 4. **Expected behavior** -- what you expected to happen @@ -337,9 +336,9 @@ with the following information: 6. **Repository** (if applicable) -- a public repository or minimal reproducer !!! tip "Debugging tips" - Run with `--debug-trace` and set `OTEL_EXPORTER_OTLP_ENDPOINT` to capture - a full trace of the analysis run. This helps maintainers diagnose issues - faster. + Run with `-v` to print the configuration file and the resolved analyzers + on stderr, and with `--profile` to log memory samples; attach that stderr + output to the report. This helps maintainers diagnose issues faster. --- diff --git a/site/explanation/anomaly.md b/site/explanation/anomaly.md index f8f1d7d8..b48558b4 100644 --- a/site/explanation/anomaly.md +++ b/site/explanation/anomaly.md @@ -16,27 +16,29 @@ that deviate significantly from the historical baseline. ### Z-score analysis -For each time tick, the analyzer computes Z-scores for six metrics using a trailing sliding window: +For each time tick, the analyzer computes Z-scores for six metrics using a trailing window: | Metric | Description | |---|---| | **Net churn** | `lines_added - lines_removed` | -| **Files changed** | Number of files modified in the tick | +| **Files changed** | Number of **distinct** files touched in the tick (a file modified by several commits counts once; a deleted file counts) | | **Lines added** | Total lines added across all commits in the tick | | **Lines removed** | Total lines removed across all commits in the tick | | **Language diversity** | Number of distinct programming languages modified | | **Author count** | Number of distinct developers active in the tick | -A Z-score measures how many standard deviations a value is from the rolling mean. When any metric's absolute Z-score exceeds the threshold, the tick is flagged as anomalous. +A Z-score measures how many standard deviations a value is from the rolling mean. When any metric's absolute Z-score strictly exceeds the threshold, the tick is flagged as anomalous. !!! info "Z-score interpretation" - **|Z| < 2.0**: Within normal variation - **|Z| 2.0 - 3.0**: Unusual, worth investigating - **|Z| > 3.0**: Highly anomalous, likely a significant event -### Sliding window +### Trailing window -The Z-score is computed against a trailing window of the previous N ticks (default 20). This means the "normal" baseline adapts to gradual trends, and only sudden deviations are flagged. +The Z-score of a tick is computed against the previous `window_size` ticks (default 20) — the ticks that exist when the history is shorter — using the mean and the population standard deviation of that window. The "normal" baseline therefore adapts to gradual trends, and only sudden deviations are flagged. + +A window needs at least two observations: the first tick has no window and the second has a single observation, so both score `0`. When a window of two or more identical values is followed by a different value, there is no variance to scale by and the signed sentinel `±100` is assigned. ### Anomaly records @@ -46,22 +48,20 @@ Each detected anomaly includes: - **Z-scores**: Per-metric Z-scores showing which dimension is anomalous - **Max absolute Z-score**: The highest Z-score across all metrics (used for severity ranking) - **Raw metrics**: The actual values for that tick -- **Files**: List of files changed in the anomalous tick +- **Files**: The distinct files changed in the anomalous tick -Anomalies are sorted by severity (highest absolute Z-score first). +Anomalies are sorted by severity (highest absolute Z-score first, ties by tick). When nothing is flagged the list is empty, never absent. --- ## Architecture -The anomaly analyzer follows the **TC/Aggregator pattern**: - -1. **Consume phase**: For each commit, `Consume()` extracts file changes, line stats, language diversity, and author ID, returning per-commit metrics as a `TC{Data: *CommitAnomalyData}`. The analyzer retains no per-commit state. -2. **Aggregation phase**: An `anomaly.Aggregator` collects TCs, merges `CommitAnomalyData` into per-tick `TickMetrics` (aggregating files, lines, languages, and unique authors), and produces `TICK` results. -3. **Z-score detection**: `ticksToReport()` runs sliding-window Z-score analysis on six dimensions over the aggregated tick metrics to detect anomalies. -4. **Serialization phase**: `SerializeTICKs()` converts aggregated TICKs into JSON, YAML, binary, or HTML plot output via `ComputeAllMetrics()`. +The math lives in the `cf-anomaly` crate; the history walk in `cf-commands` supplies the per-commit inputs: -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +1. **Per-commit product**: for every commit the walk takes the filtered tree diff against the first parent, the files it names (a delete names the deleted file), the line stats (skipped for merges) and the language of every touched blob — a pure function of the commit, computed in parallel across cores. +2. **Chronological reduce**: the shared history core stamps every commit with its identity and tick in oldest-first order; `cf_anomaly::aggregate_commits_to_ticks` merges the commits of a tick (lines add up; files, languages and authors merge as sets). +3. **Z-score detection**: `cf_anomaly::detect_anomalies_from_ticks` runs the trailing-window Z-scores on the six dimensions with the validated `Config` (`--anomaly-window`; threshold 2.0). +4. **Serialization**: `compute_all_metrics` produces one value tree (anomalies, time series, aggregate) that json, yaml and bin encode; the plot path adds the cross-analyzer enrichment over the co-selected analyzers' time series. --- @@ -78,10 +78,10 @@ This separation enables streaming output, budget-aware memory spilling, and deco ## Limitations -- **Cold start**: The first `window_size` ticks have no or limited history for Z-score computation. Anomalies during this period are less reliable. +- **Cold start**: The first ticks have little history: the first two score `0` on every metric, and the following ones are scored against a window shorter than `window_size`. Anomalies early in a history are less reliable. - **Gradual drift**: Z-scores detect sudden deviations, not gradual trends. A slow but steady increase in churn will update the rolling mean and never trigger an anomaly. -- **Zero-variance sentinel**: When the window has zero standard deviation (all values identical) and the current value differs, a sentinel Z-score of 100 is assigned. This ensures detection but may inflate severity rankings. -- **Tick granularity**: Metrics are aggregated per tick (default 24-hour window). Sub-tick anomalies are not detected. +- **Zero-variance sentinel**: When the window (two or more observations) has zero standard deviation and the current value differs, a sentinel Z-score of `±100` is assigned. This ensures detection but may inflate severity rankings. +- **Tick granularity**: Metrics are aggregated per tick (default 24 hours, `--tick-size`). Sub-tick anomalies are not detected, and days without commits are not ticks. - **Not causal**: The analyzer detects statistical anomalies, not root causes. Investigation is required to determine whether an anomaly represents a real problem. - **Multi-dimensional**: An anomaly is flagged when *any* single metric exceeds the threshold. This means false positive rates increase with the number of metrics (currently six). diff --git a/site/explanation/burndown.md b/site/explanation/burndown.md index 3b6cc438..de0ce531 100644 --- a/site/explanation/burndown.md +++ b/site/explanation/burndown.md @@ -1,45 +1,67 @@ # Understanding code burndown This page explains the mental model behind the burndown analyzer: what code -survival means, the per-file and per-developer breakdowns, and how the -line-tracking algorithm works. For configuration keys and the output schema, -see the [Burndown reference](../analyzers/burndown.md). +survival means, how the per-file and per-developer breakdowns are defined, +and how the line-tracking algorithm works. For the flags and the output +schema, see the [Burndown reference](../analyzers/burndown.md). --- ## What it measures -The burndown analyzer tracks **code survival over time** by following every -line of code through Git history. It produces burndown charts showing how code -written at different points in time persists, is modified, or is deleted. -Optionally, it tracks per-file and per-developer breakdowns. +The burndown analyzer follows every line of code through Git history and +answers one question per time band: **of the lines written in this band, +how many are still alive at each later sample?** The result is the classic +burndown chart — stacked areas showing how much code from each era remains. ### Global code survival -A time-series matrix where each row is a sampling point and each column is an age band. The value at `[sample][band]` is the number of lines that were last edited during that band and still survive at that sample point. +A matrix where each row is a sample and each column is an age band. The +value at `[sample][band]` is the number of lines last written in that band +that still exist at the sample. Three figures summarise it: -This matrix produces the classic **burndown chart**: stacked area plots showing how much code from each era remains. +- **peak lines** — the sum over bands of each band's maximum, i.e. the lines + ever written that lived until at least one sample boundary; +- **current lines** — the live lines at the last sample; +- **survival rate** — `current / peak`, never above 100 %. -### Per-file burndown +A line that is written and deleted inside one sampling period never reaches +a sample boundary and is invisible to the matrix (it still shows up in the +per-commit `LinesAdded` / `LinesRemoved` of the ndjson and timeseries +formats). -When `--burndown-files` is enabled, the analyzer produces a separate survival matrix for each file, enabling file-level burndown charts. +### Per-file survival (`--burndown-files`) -### Per-developer burndown +The same matrix per file, reduced to `current_lines`, `peak_lines` and +`survival_rate`. Files are keyed by their path at the end of the window: +renames are followed (a pure `git mv` keeps the file's history), and a file +deleted inside the window stays in the list with zero current lines. +Binary files are never tracked. -When `--burndown-people` is enabled, the analyzer tracks which developer last edited each line. This reveals: +### Per-developer survival and interactions (`--burndown-people`) -- **Developer survival rates**: How much of each developer's code persists -- **Interaction matrix**: Which developers modify each other's code +Every line also remembers who wrote it. This gives: -### File ownership +- **developer survival** — per developer, the lines they wrote that are + still alive (`current_lines`), the lines they ever wrote that reached a + sample (`peak_lines`) and the ratio. Every developer the shared identity + detector knows appears, even with zeros; +- **interactions** — the overwrite matrix: one entry per `(author, + modifier)` pair counting the lines written by `author` that `modifier` + deleted or replaced inside the window. `is_self_modify` marks a developer + rewriting their own code. -When both `--burndown-files` and `--burndown-people` are enabled, the analyzer computes per-file ownership by iterating the live line segments in each file's internal tree. Each segment stores a packed `[author|tick]` value, from which the author ID is extracted to produce a `file -> author -> line_count` mapping. +### File ownership (both flags) -!!! note "Ownership requires developer tracking" - File ownership data is only available when `--burndown-people` is enabled. Without developer tracking, no author information is stored in the line segments, so file ownership will be empty. +With both flags on, each file carries `ownership` — live lines per +developer id — and the top owner (most lines; the smaller id on a tie) with +its percentage of the file's owned lines. A deleted file has an empty +ownership map and no top owner. -!!! warning "Memory usage" - Per-file and per-developer tracking significantly increases memory usage. For repositories with more than 100k commits, consider enabling hibernation (on by default). +!!! note "Memory" + Per-file tracking keeps one sparse history per path ever tracked; + per-developer tracking keeps one per developer. Both are small compared + to the line treaps the global curve needs anyway. --- @@ -47,51 +69,115 @@ When both `--burndown-files` and `--burndown-people` are enabled, the analyzer c ### Algorithm overview -1. **Commit traversal**: Commits are processed sequentially in topological order. For each commit, the analyzer diffs the parent tree against the current tree to find inserted, deleted, and modified lines. - -2. **Line tracking**: Each line in every file is tracked using an augmented balanced binary tree (treap). Each tree node stores a contiguous range of lines with a packed value encoding `[author_id | creation_tick]`. When lines are inserted or deleted, the tree is split and merged to maintain the correct line-to-value mapping. - -3. **Sparse history accumulation**: At each commit, the analyzer records deltas in a sparse history structure: `map[currentTick]map[creationTick]lineCountDelta`. This captures how many lines created at `creationTick` are alive at `currentTick`. - -4. **Tick aggregation**: Sparse deltas from individual commits are accumulated into tick-level snapshots by a streaming aggregator. At each sampling boundary, the aggregator emits a `TickResult` containing the full sparse history state. - -5. **Dense matrix conversion**: The sparse history is converted to a dense matrix (`DenseHistory`) via `groupSparseHistory()`. This involves: - - **Tick normalization**: Map arbitrary tick values to sorted indices - - **Granularity grouping**: Collapse adjacent creation ticks into age bands of width `granularity` - - **Forward-fill**: For each age band, carry forward the last known value across sampling gaps to produce a complete matrix - -6. **Metrics computation**: The dense matrix is used to compute aggregate statistics (survival rate, peak lines, current lines), per-developer survival, and the interaction matrix. +1. **First-parent walk.** Burndown always follows the first parent, whatever + `--first-parent` says, because line survival is a property of the main + line of development. A merge commit is consumed as the change set it + introduces on that line: the lines it deletes are debited at the merge's + own tick, the lines it brings in are credited to it. The user's + `--first-parent` flag is never forced on any other analyzer. + +2. **Window base.** In a `--limit`, `--since` or `--head` window the tracked + state is seeded from the window's base tree — the first in-window + commit's parent — at tick 0 before the first commit is applied. A + commit's `LinesAdded` / `LinesRemoved` are therefore the lines it really + changed (the same delta devs reports for that commit), and the base + lines form band 0 of the global curve as "older than the window" code + with no author. A window starting at a root commit has no base. + +3. **Line tracking.** Each tracked file is an implicit treap of line + segments. A segment's value packs `(author, tick)`: the tick field is + exactly as wide as the window's last tick needs and the author takes + the remaining bits of the 32-bit value (without `--burndown-people` the + whole value is the tick), so no tick is ever reserved and a 45-year or + an hourly history runs like any other. Every edit splits and merges the + treap in `O(log N)`. + +4. **Deltas.** Each `File::update` reports `(current, previous, delta)`: + an insertion `(now, now, +n)`, a deletion `(now, written, −n)` per + deleted segment. The per-commit reduce turns the reports into the + commit's sparse global history (`current tick → written tick → delta`), + the per-file and per-developer histories (keyed by the author of the + affected lines) and the interaction cells (a negative report whose + author differs from, or equals, the committing developer). A path that + is re-inserted — a length mismatch after a dropped commit, a text file + turning binary and back — is debited first, so no line is ever counted + twice or leaked. + +5. **Dense matrix.** The sparse histories are laid out on the sampling + grid: band `j` holds the lines written in ticks `[j·granularity, + (j+1)·granularity)`, sample `i` is the state after the last tick of + `[i·sampling, (i+1)·sampling)`, and samples with no recorded tick repeat + the previous row. The grid runs to the newest commit's tick, so the last + sample is always the end of the window. + +6. **Figures.** Peak, current and survival per matrix; ownership from the + live segments of each file at the end of the walk. ### Key parameters -- **Granularity** controls the width of each age band (in ticks). Higher values produce fewer, wider bands. -- **Sampling** controls how frequently snapshots are taken (in ticks). Higher values reduce the number of data points. -- **Tick size** defaults to a 24-hour window. All commits within the same day share one tick. +- **`--granularity`** — ticks per age band. Wider bands, fewer columns. +- **`--sampling`** — ticks per sample. Wider periods, fewer rows; a + non-positive value means the default (30), and the two are independent. +- **`--tick-size`** — hours per tick (24 by default). All commits inside one + tick share a band. +- **`--max-changes-per-commit`** — applied exactly as devs applies it: a + commit whose raw tree diff exceeds the cap is dropped before the ticks + and identities are assigned, so burndown and devs see the same commit + set. A dropped commit's edits are never applied; the next commit that + touches an affected file finds its tracked state out of sync, debits it + and re-inserts the file whole at that commit's tick. + +### Age labels + +The text report groups the last sample's bands into `< 1 month`, `1-3 +months`, `3-6 months`, `6-12 months` and `> 12 months` by the band's age at +the end of the window: band `i` of `n` is `(n − i) · granularity · +tick_hours / 24 / 30` months old, so the last band is the youngest. The plot +labels its series the same way (`3mo`, `2y`, …) unless the history spans two +calendar years, in which case the bands are regrouped by year. --- ## Use cases -- **Project health monitoring**: Track the overall code survival rate. A declining rate may indicate churn or instability. -- **Developer contribution analysis**: Understand whose code persists and who rewrites existing code. -- **Code age visualization**: Generate burndown charts showing how much ancient code remains in the codebase. -- **Refactoring impact**: Measure how much code a refactoring effort actually replaced. -- **Team dynamics**: The interaction matrix reveals collaboration patterns -- who reviews and modifies whose code. +- **Project health monitoring**: track the overall survival rate. A falling + rate means the code base is being rewritten faster than it grows. +- **Developer contribution analysis**: `developer_survival` shows whose code + persists; `interactions` shows who rewrites whose code. +- **Code age visualization**: the burndown chart shows how much ancient code + remains. +- **Refactoring impact**: run with `--since` on the refactoring window and + read how much of the base tree (band 0) the window replaced. +- **File-level hot spots**: `file_survival` ranks files by surviving lines; + a low survival rate with a high peak is a file that keeps being rewritten. --- ## Limitations -- **Sequential processing**: Burndown tracks cumulative per-line state across all commits and must process commits sequentially. It cannot be parallelized across commits (though per-file processing within a commit is parallelized via goroutines). -- **Memory intensive**: Every line in every file is tracked throughout history. Large repositories (100k+ commits, 10k+ files) can require several GB of RAM. Use hibernation options to manage memory. -- **Binary files excluded**: Binary files are automatically skipped since they cannot be meaningfully diff'd line-by-line. -- **Rename tracking**: File renames are tracked using Git's rename detection. If Git does not detect a rename (e.g., content changed significantly), the file appears as a deletion + insertion. -- **Tick resolution**: The default 24-hour tick means that all commits within the same day share one tick. Sub-day granularity is not supported. -- **File ownership without developer tracking**: File ownership data requires `--burndown-people`. Without it, the `file_survival` array will have empty ownership maps. +- **Sequential processing**: line survival is cumulative state, so commits + are applied one after another (only the per-commit tree diffs used for + the cap are computed in parallel). +- **Memory**: every live line of every tracked file is a treap segment. + Large repositories need memory in proportion to their line count. +- **Binary files excluded**: a binary blob is never tracked; a text file + turning binary is debited at that commit. +- **Rename tracking**: renames come from Git's similarity detection. A move + with heavy edits appears as a deletion plus an insertion. +- **Tick resolution**: all commits inside one tick share a band; sub-tick + ordering inside a band is not represented. +- **Unknown authors**: lines older than the window (the seeded base tree) + have no author. They count in the global curve and in per-file survival, + but not in developer survival, interactions or ownership. +- **Identity capacity**: with `--burndown-people` the developer id shares + the 32-bit segment value with the tick. A window of `T` ticks leaves + `32 − ⌈log₂(T + 1)⌉` bits for developers — 2²³ for a 500-tick window, + 2¹⁷ for a 45-year daily one. Ids beyond that fold into the "unknown" + slot. --- ## See also -- [Burndown reference](../analyzers/burndown.md) — configuration keys and output schema. +- [Burndown reference](../analyzers/burndown.md) — flags and output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/explanation/clones.md b/site/explanation/clones.md index 59331958..23aa9397 100644 --- a/site/explanation/clones.md +++ b/site/explanation/clones.md @@ -2,72 +2,132 @@ This page explains the mental model behind the clone detection analyzer: what a clone pair is, the clone-type taxonomy, and why the two-stage MinHash + LSH -design keeps cross-file detection fast. For configuration keys and the output -schema, see the [Clones reference](../analyzers/clones.md). +design keeps cross-file detection fast. For the output schema and the metric +definitions, see the [Clones reference](../analyzers/clones.md). --- ## What it measures The clone detection analyzer finds **duplicate and near-duplicate functions** -across your entire codebase using MinHash signatures and Locality-Sensitive -Hashing (LSH). It operates on the UAST representation and detects clones -**cross-file** — not just within a single file. +across your entire codebase. It operates on the UAST representation and +detects clones **cross-file** — not just within a single file. ### Clone pairs -A clone pair is two functions whose AST structure is similar enough to be considered duplicated code. Each pair includes a **similarity score** (0.0–1.0) and a **clone type** classification. +A clone pair is two functions whose AST structure is similar enough to be +considered duplicated code. Each pair carries a **similarity** (the exact +Jaccard index of the two functions' shingle sets, 0.5–1.0) and a **clone +type**. ### Clone types -| Type | Similarity | Description | +The classification follows the standard taxonomy (Roy & Cordy) and is decided +from two fingerprints per function — one over its *structure* (the sequence of +AST node types) and one over its *tokens* (the identifiers, literals and +operators at the leaves) — plus the exact shingle similarity: + +| Type | Rule | Similarity | |---|---|---| -| **Type-1 (Exact)** | = 1.0 | Identical AST structure and tokens | -| **Type-2 (Renamed)** | ≥ 0.8 | Identical structure, different variable/function names | -| **Type-3 (Near-miss)** | ≥ 0.5 | Similar but modified structure | +| **Type-1 (Exact)** | identical structure **and** identical tokens | 1.0 | +| **Type-2 (Renamed)** | identical structure, different identifiers / literals | 1.0 | +| **Type-3 (Near-miss)** | different structure, shingle Jaccard ≥ 0.5 | 0.5 – 1.0 | + +So a function copied verbatim is Type-1; the same copy with every variable +renamed, a constant changed or an operator swapped is Type-2; a copy with a +statement added or removed is Type-3 with a similarity that says how much of +the structure survived. Comments are ignored entirely: a copy that only +gained comments is still Type-1. ### Clone ratio -The ratio of detected clone pairs to total functions, computed globally across all files: +The share of functions that take part in at least one clone pair, computed +globally across all files: ```text -clone_ratio = total_clone_pairs / total_functions +clone_ratio = functions in ≥ 1 pair / total_functions ``` +It is bounded to `[0, 1]` (a pair-count ratio is not: three copies of one +function form three pairs). Both the status message and the `N/10` score are +keyed off it: + !!! info "Interpretation" - - **0.0** (green): No duplication detected - - **≤ 0.1** (yellow): Low duplication — few clone pairs - - **≤ 0.3** (red): Moderate duplication — consider refactoring - - **> 0.3**: High duplication — significant refactoring recommended + - **0.0** — `No code clones detected`, 10/10 + - **≤ 0.1** — `Low duplication` + - **≤ 0.3** — `Moderate duplication - consider refactoring clone pairs` + - **> 0.3** — `High duplication - significant refactoring recommended` + + The score is `1 − clone_ratio`, so the message and the score always agree. --- ## Methodology -### Stage 1: per-file signature extraction +### Stage 1: per-file profiles and signatures For each source file, the analyzer: -1. **Parses** the file into a UAST and finds all function/method nodes. -2. **Shingles** each function by performing a pre-order traversal of the AST subtree and extracting sliding windows of `k` consecutive node types (k=5 by default). For example, a function with nodes `[Function, Block, Assignment, Identifier, Call, Return]` produces shingles like `Function|Block|Assignment|Identifier|Call` and `Block|Assignment|Identifier|Call|Return`. -3. **MinHash** compresses each function's shingle set into a fixed-size signature of 128 hash values. Two functions with identical structure produce identical signatures; similar functions produce similar signatures. - -Signatures are exported per-file along with function names. At this stage, no clone detection occurs — comparison is deferred to the aggregation stage. +1. **Parses** the file into a UAST and finds every function, method and + closure (UAST `Lambda`). Closures are named after their enclosing function + and ordinal (`Handle.func#2`), or `func#N` at file level. +2. **Profiles** each function: a pre-order traversal of its subtree (comment + nodes skipped) yields the node-type sequence, a *structure* fingerprint + over that sequence, and a *token* fingerprint over the leaf + `(type, token)` pairs. Functions with fewer than 20 nodes are counted but + not profiled further. +3. **Shingles** the type sequence into sliding windows of `k = 5` consecutive + node types. For example, `[Function, Block, Assignment, Identifier, Call, + Return]` produces `Function|Block|Assignment|Identifier|Call` and + `Block|Assignment|Identifier|Call|Return`. Each shingle is hashed; the + sorted, deduplicated hash set is kept — it is the exact set the next stage + verifies against. +4. **MinHash** compresses that shingle set into a fixed-size signature of 128 + hash values. + +No comparison happens at this stage; it is deferred to the aggregation stage. ### Stage 2: cross-file clone detection After all files have been processed, the aggregator: -1. **Qualifies** function names with their source file path (`path/to/file.go::FuncName`) so that same-named functions across files are distinguishable. -2. **Builds a global LSH index** (16 bands, 8 rows per band) from all collected signatures. LSH hashes each signature's bands into buckets — functions whose signatures share any bucket are candidate clones. -3. **Queries** the LSH index for each function, retrieves candidates above the similarity threshold (0.5), computes exact Jaccard similarity from the full MinHash signatures, and classifies each pair by clone type. -4. **Computes metrics** globally: `clone_ratio = total_pairs / total_functions`. - -This two-stage design means clone detection works across file boundaries. A controller duplicated in `pkg/a/handler.go` and `pkg/b/handler.go` is detected even though each file is parsed independently. +1. **Qualifies** function names with their source file path + (`path/to/file.go::FuncName`) so that same-named functions across files + are distinguishable. +2. **Builds a global LSH index** (32 bands × 4 rows) from all signatures. + LSH hashes each signature's bands into buckets — functions whose + signatures share any bucket are candidate clones. With these parameters a + pair with Jaccard `s` becomes a candidate with probability + `1 − (1 − s⁴)³²`: ≈ 0.87 at the 0.5 threshold, > 0.99 at 0.64, and + < 0.06 at 0.2. (The previous 16 × 8 layout had a recall of 0.06 at 0.5, + which is why near-miss pairs went unreported.) +3. **Verifies every candidate pair exactly**: equal structure fingerprints + mean similarity 1.0 — Type-1 if the token fingerprints match, Type-2 + otherwise; different structure means the exact Jaccard of the two shingle + sets, reported as Type-3 when it reaches 0.5 and dropped otherwise. The + `similarity` in the report is therefore always exact, never the MinHash + estimate, and the lower S-curve never adds false positives. +4. **Counts** every pair, keeps the best 1000 (similarity descending, then + name) as the detail list, and computes `clone_ratio` from the functions + that appear in at least one pair. + +Every unordered pair is enumerated exactly once (each function is queried for +the candidates that come after it in the index), so the work is linear in the +number of candidate pairs and nothing is allocated per pair: 4000 identical +functions — 8 million Type-1 pairs — are counted in a few seconds. + +This two-stage design means clone detection works across file boundaries. A +controller duplicated in `pkg/a/handler.go` and `pkg/b/handler.go` is detected +even though each file is parsed independently. ### Why MinHash + LSH? -Pairwise comparison of N functions is O(N²). For a project with 100K functions this is infeasible. MinHash compresses each function into a 1 KB signature (128 hashes × 8 bytes), and LSH reduces candidate pair generation to near-linear time by only comparing functions that hash into the same bucket. The combination provides probabilistic guarantees on recall while keeping the analysis fast. +Pairwise comparison of N functions is O(N²). For a project with 100K +functions this is infeasible. MinHash compresses each function into a 1 KB +signature (128 hashes × 8 bytes), and LSH reduces candidate pair generation to +near-linear time by only comparing functions that hash into the same bucket. +The exact verification step then makes the result precise: LSH only decides +*which* pairs are looked at, never what is reported about them. --- @@ -84,7 +144,7 @@ Pairwise comparison of N functions is O(N²). For a project with 100K functi ## Methodology references - **MinHash**: Broder, A. Z. (1997). *On the resemblance and containment of documents*. Proceedings of the Compression and Complexity of Sequences. MinHash estimates Jaccard similarity between sets using compact fixed-size signatures. -- **LSH (Locality-Sensitive Hashing)**: Indyk, P. & Motwani, R. (1998). *Approximate nearest neighbors: towards removing the curse of dimensionality*. STOC. Banded LSH reduces candidate pair generation from O(N²) to near-linear time. +- **LSH (Locality-Sensitive Hashing)**: Indyk, P. & Motwani, R. (1998). *Approximate nearest neighbors: towards removing the curse of dimensionality*. STOC. Banded LSH reduces candidate pair generation from O(N²) to near-linear time. - **AST-based clone detection**: Jiang, L. et al. (2007). *DECKARD: Scalable and accurate tree-based detection of code clones*. ICSE. Tree-based shingling captures structural similarity independent of identifier names. - **Clone taxonomy**: Roy, C. K. & Cordy, J. R. (2007). *A survey on software clone detection research*. The Type-1/Type-2/Type-3 classification follows the standard clone taxonomy. @@ -93,10 +153,10 @@ Pairwise comparison of N functions is O(N²). For a project with 100K functi ## Limitations - **Language scope**: Works with any language supported by the UAST parser. Unsupported files are silently skipped. -- **Structural only**: Detection is based on AST node types, not token values. Two functions with identical structure but completely different semantics (e.g., add vs multiply with the same nesting shape) may appear as clones. -- **Minimum function size**: Functions smaller than the shingle window (5 AST nodes) produce no shingles and are excluded from clone detection. They are still counted in `total_functions`. -- **Probabilistic recall**: LSH is a probabilistic algorithm. Clone pairs with similarity just above the threshold (0.5) may occasionally be missed. High-similarity pairs (Type-1 and Type-2) have near-perfect recall. -- **Generated code**: The analyzer does not distinguish hand-written code from generated code. Consider excluding generated directories. +- **Structural similarity**: Type-3 similarity is based on AST node types, not token values. Two functions with the same nesting shape but different semantics (e.g. add vs multiply) are reported as Type-2 clones, which is what the taxonomy defines. +- **Minimum function size**: Functions with fewer than 20 UAST nodes (comments excluded) are counted in `total_functions` but never paired. +- **Probabilistic candidate recall**: LSH is a probabilistic candidate stage. A pair whose similarity is exactly at the 0.5 threshold is proposed with probability ≈ 0.87; pairs at 0.64 and above are proposed practically always, and Type-1/Type-2 pairs always. Every proposed pair is verified exactly, so the stage never produces false positives. +- **Generated code**: The analyzer does not distinguish hand-written code from generated code; generated files are excluded by the default path policy (`--include-generated` adds them). - **No Type-4 clones**: Semantic clones (different structure, same behavior) are not detected. The analyzer operates purely on syntactic structure. --- diff --git a/site/explanation/cohesion.md b/site/explanation/cohesion.md index 291fc8f8..d322fc8d 100644 --- a/site/explanation/cohesion.md +++ b/site/explanation/cohesion.md @@ -1,24 +1,51 @@ # Understanding cohesion metrics This page explains the mental model behind the cohesion analyzer: what -LCOM-HS measures, how the variable-sharing ratio captures communicational -cohesion, and how to read the scores. For configuration keys and the output -schema, see the [Cohesion reference](../analyzers/cohesion.md). +counts as a variable, what LCOM-HS measures, how the variable-sharing ratio +captures communicational cohesion, how scores are combined across files, and +how to read them. For the output schema, see the +[Cohesion reference](../analyzers/cohesion.md). --- ## What it measures -The cohesion analyzer computes **LCOM-HS (Henderson-Sellers)** and **variable -sharing ratio** metrics to identify files and modules with low internal -cohesion. Low cohesion indicates functions that are poorly related to each -other — a strong signal for refactoring. +The cohesion analyzer computes **LCOM-HS (Henderson-Sellers)** per file and a +**variable sharing ratio** per function to identify files whose functions are +poorly related to each other — a strong signal for refactoring. + +Both metrics are built on one relation: *which function uses which +variable*. Everything below follows from how that relation is extracted. + +### What counts as a variable + +A function *uses* a variable when its body contains an identifier that reads +or writes it. The analyzer works lexically on the UAST — no symbol +resolution — with these rules: + +| Counted | Not counted | +|---|---| +| Local variables (`x := 1`, `let m = …`, `const total = …`) — by the declared **name**, never the whole statement | Type names (`int`, `string`, `Config`, `i32`, `Self`, `number`) | +| Parameters (`cfg`, `items`, `n`) | The function's own name and its return-type annotation | +| Fields and attributes (`s.a` counts `a`; `self.x` counts `x`; `this.b` counts `b`) | Callees: `helper()`, `string(v)`, the `Println` in `fmt.Println(x)`, the `Validate` in `cfg.Validate()` | +| The object of a member access (`cfg` in `cfg.Name`, `fmt` in `fmt.Println`) | The receiver: `self`, `this`, and a Go receiver such as `s` in `func (s *S) M()` | +| Variables of a closure, attributed to the enclosing function | Literals (`1`, `"x"`, `nil`, `true`) | + +Each function's variables form a **set**: using `x` three times is one use of +`x`. + +!!! note "Lexical means lexical" + `fmt` in `fmt.Println(x)` is indistinguishable from a variable without + symbol resolution, so it counts. Two different variables that share a + name across functions count as the same variable. Both are documented + limitations, not bugs. ### LCOM-HS (lack of cohesion of methods — Henderson-Sellers) -LCOM-HS is the industry-standard cohesion metric used by NDepend, JArchitect, and CppDepend. +LCOM-HS is the industry-standard cohesion metric used by NDepend, JArchitect, +and CppDepend. -**Formula:** +**Formula (per file):** ```text LCOM = 1 - sum(mA) / (m × a) @@ -26,54 +53,113 @@ LCOM = 1 - sum(mA) / (m × a) Where: -- `m` = number of functions in the file -- `a` = number of distinct variables across all functions -- `mA` = for each variable, the count of functions that reference it -- `sum(mA)` = sum of access counts across all variables +- `m` = number of functions in the file **that use at least one variable** +- `a` = number of distinct variables across those functions +- `mA` = for each variable, the number of functions that use it +- `sum(mA)` = the number of (function, variable) usage pairs + +**Range:** 0.0 (perfect cohesion: every function uses every variable) to +1.0 (no cohesion: no two functions share a variable). -**Range:** 0.0 (perfect cohesion) to 1.0 (no cohesion). +Functions that use no variables are not part of the usage matrix (there is +nothing they could share), so trivial helpers never penalize a file. When +fewer than two functions use a variable — a single-function file, or a file +of parameterless helpers — there is nothing to compare, and `LCOM = 0`. -!!! info "Interpretation" - - **LCOM ≤ 0.3** (green): Excellent cohesion — functions share most variables. - - **LCOM ≤ 0.6** (yellow): Moderate cohesion — some functions may be loosely related. - - **LCOM > 0.6** (red): Poor cohesion — functions share few variables and may belong in separate files. +**Worked example** — `f` uses `{x, y}`, `g` uses `{x}`: + +```text +m = 2, a = 2 ({x, y}), sum(mA) = 2 (x) + 1 (y) = 3 +LCOM = 1 - 3 / (2 × 2) = 0.25 +``` ### Cohesion score -A convenience inversion of LCOM-HS: `cohesion_score = 1.0 - LCOM`. Higher is better. +A convenience inversion of LCOM-HS: `cohesion_score = 1.0 - LCOM`. Higher is +better. It drives the section score (`6/10` means `0.6`) and the summary +message: -| Cohesion Score | Assessment | +| Cohesion score | Summary | |---|---| -| ≥ 0.7 | Excellent | -| ≥ 0.4 | Good | -| ≥ 0.3 | Fair | -| < 0.3 | Poor | +| ≥ 0.7 | Excellent overall cohesion across all analyzed code | +| ≥ 0.4 | Good overall cohesion with room for improvement | +| ≥ 0.3 | Fair overall cohesion - consider refactoring some functions | +| < 0.3 | Poor overall cohesion - significant refactoring recommended | ### Function-level cohesion (variable sharing ratio) -For each function, the analyzer computes what fraction of its variables are shared with at least one other function in the same file. This measures **communicational cohesion** (Yourdon-Constantine level 5). +For each function, the analyzer computes what fraction of its variables is +shared with **at least one other function in the same file**. This measures +**communicational cohesion** (Yourdon-Constantine level 5). **Formula:** ```text -function_cohesion = shared_vars / total_unique_vars +function_cohesion = shared_vars / unique_vars ``` Where: -- `shared_vars` = variables in this function that also appear in at least one other function -- `total_unique_vars` = total distinct variables in this function -- Functions with no variables receive a score of 1.0 (trivial, no penalty) +- `unique_vars` = the function's distinct variables +- `shared_vars` = those of them that at least one *other* function of the + file also uses (a repeat inside the same function is not sharing) +- Functions with no variables score 1.0 (trivial, no penalty) +- Every function of a file with fewer than two variable-using functions + scores 1.0 (nothing to compare against) **Range:** 0.0 (completely isolated function) to 1.0 (all variables shared). -| Function Cohesion | Assessment | +| Function cohesion | Assessment | |---|---| | ≥ 0.6 | Excellent — function shares most variables with the module | | ≥ 0.4 | Good — reasonable sharing with room for improvement | | ≥ 0.3 | Fair — consider refactoring | | < 0.3 | Poor — function is isolated from the module | +Functions below **Excellent** are listed as issues, worst first. The +distribution counts every function in exactly one of the four buckets, so it +always sums to **Total Functions**. + +In the worked example above, `f` shares `x` but not `y` (`0.5`, Good) and `g` +shares its only variable (`1.0`, Excellent). + +### Across files + +A variable is a per-file notion, so LCOM is never recomputed over a whole +tree. Instead: + +- **LCOM Score** = the function-weighted mean of the per-file LCOM values (a + file with five functions weighs five times a file with one); +- **Cohesion Score** = `1 - LCOM Score`; +- **Avg Cohesion** = the mean per-function cohesion over every function; +- **Total Functions** = every function found. Same-named functions are kept + (methods are reported as `Type.name`, e.g. `A.String` and `B.String`). + +Files without functions — struct-only files, empty files, Markdown — carry +**no cohesion data**: they are excluded from every denominator. A tree in +which no file has a function reports `No cohesion data available` with score +`0/10` (and exit code 0). Use `--per-file` to see each file's own LCOM. + +--- + +## Per-language notes + +- **Go** — `type_identifier` nodes (`int`, `Config`) are typed and excluded; + receivers are excluded and name the method (`S.M1`); closures (`func() + {…}` literals) belong to the enclosing function. +- **Python** — `self` is excluded; methods are qualified by their class + (`C.m1`); decorators are outside the function. Type annotations are plain + identifiers in Python's UAST and therefore count. +- **JavaScript / TypeScript** — `this` is excluded; class methods are + qualified (`A.m1`); TypeScript type annotations are excluded. +- **Java** — `this` is excluded; methods, including constructors, are + qualified by their class (`A.m1`, `A.A`); primitive types are excluded. +- **Rust** — `self` is excluded; `impl` blocks do not qualify method names + (`m1`, not `S.m1`). Identifier nodes in the Rust mapping carry no token; + their names are recovered from the enclosing function's source span. +- Any language supported by the UAST works; results are best where variable + declarations and function definitions are explicit. + --- ## Use cases @@ -94,9 +180,9 @@ Where: ## Limitations -- **Language scope**: Works with any language supported by the UAST. Best results with languages that have explicit variable declarations and function definitions. +- **Lexical names**: The analyzer uses identifier spellings. Different variables with the same name across functions count as shared; package qualifiers (`fmt`) count as variables. +- **Constructors and initializers**: A constructor that initializes every field (`S { a: 1, b: 2 }`, `this.a = …`) uses every field and ties the other methods together, raising the file's cohesion. - **Accessor methods**: Simple getters/setters may inflate cohesion since each touches only one shared field. -- **Variable naming**: The analyzer uses lexical variable names. Different variables with the same name across functions will be counted as shared. - **Single-function files**: Files with only one function always receive perfect cohesion (LCOM = 0.0, cohesion = 1.0) since there are no other functions to compare against. - **Trivial functions**: Functions with no variables receive a cohesion score of 1.0 to avoid penalizing simple utility functions. @@ -104,5 +190,5 @@ Where: ## See also -- [Cohesion reference](../analyzers/cohesion.md) — configuration keys and output schema. +- [Cohesion reference](../analyzers/cohesion.md) — output schema and thresholds. - [Quick start](../getting-started/quickstart.md) — run your first analysis. diff --git a/site/explanation/comments.md b/site/explanation/comments.md index 53888e5b..9db29b57 100644 --- a/site/explanation/comments.md +++ b/site/explanation/comments.md @@ -1,59 +1,151 @@ # Understanding documentation coverage This page explains the mental model behind the comments analyzer: what -documentation coverage means, how the analyzer distinguishes comment types, -and what a healthy comment density looks like. For configuration keys and the -output schema, see the [Comments reference](../analyzers/comments.md). +documentation coverage means, how a comment is attached to a declaration, and +how the score is built. For the output schema, see the +[Comments reference](../analyzers/comments.md). --- ## What it measures -The comments analyzer evaluates **documentation coverage** by examining comment -presence, placement, and density across your source code. It identifies -functions and classes that lack documentation and measures overall -documentation quality. +The comments analyzer looks at two things in every parsed file: -### Documentation coverage +- **Documentation coverage** — the share of declarations (functions, methods, + classes, interfaces, structs) that carry a doc comment. +- **Comment placement** — where each comment sits: directly above a + declaration, inside a body, or attached to nothing. -The percentage of public functions, methods, and classes that have an associated documentation comment. A comment is "associated" when it immediately precedes the declaration. +Both are summed over the whole analyzed tree: coverage is +*documented declarations / all declarations* and the good-comment ratio is +*good comments / all comments*. A file without declarations (a Markdown +README, a file of constants) adds nothing to either denominator, so it cannot +dilute the result. -### Comment placement quality +### Which comments document a declaration -Not all comments are equally useful. The analyzer distinguishes: +A comment block documents a declaration when it **ends on the line directly +above the declaration**. There may be no blank line in between. Attribute and +decorator lines (`#[derive(...)]`, `@dataclass`, annotations) belong to the +declaration, so a doc comment above them still counts. -- **Doc comments**: Comments immediately before a function/class declaration -- **Inline comments**: Comments within function bodies -- **Orphan comments**: Comments not associated with any code structure +=== "Go" -### Comment density + ```go + // Add adds. ← above Add: documents it + func Add(a, b int) int { + // inline comment ← inline (inside Add) + return a + b + } -The ratio of comment lines to total lines of code. Provides a quick overall measure of how well-documented a file is. + // Mul multiplies. ← unassociated: a blank line separates it + + func Mul(a, b int) int { return a * b } + ``` -!!! info "Healthy ratios" - A comment density between **15-30%** is generally considered healthy. Below 10% suggests under-documentation; above 40% may indicate stale or redundant comments. +=== "Rust" + + ```rust + /// Documents foo. ← above foo + #[inline] ← attribute lines belong to the declaration + pub fn foo() -> i32 { 1 } + + //! Crate docs ← unassociated unless directly above an item + ``` + +=== "Python" + + ```python + # documents K ← above K, through the decorator line + @dataclass + class K: + """Class docstring.""" ← docstring: documents K + + # documents m ← above m (inside K's span, but directly above m) + def m(self): ... + ``` + +=== "Java" + + ```java + /** Javadoc for A. */ ← above A + public class A { + /** Javadoc for foo. */ ← above foo + public int foo() { + return 1; // trailing ← inline (inside foo) + } + } + ``` + +Adjacent comment lines form one block, so a multi-line `//` comment counts as +several comments that share one placement. Comment text is not analyzed: +`// do the thing` documents a function as well as a paragraph does. + +### Placement categories + +| Placement | Meaning | Counts as | +|---|---|---| +| `above` | Ends on the line directly above a declaration (attribute lines included) | good — documents the declaration | +| `inline` | Lies inside a declaration's span; attributed to the innermost one | bad | +| `unassociated` | Everything else: file headers, license blocks, comments after the last declaration, comments separated from a declaration by a blank line | bad | + +### Score and status + +The section score is the good-comment ratio, shown as `N/10`, and the status +is its threshold label: + +| Good ratio | Status | +|---|---| +| `>= 0.8` | Excellent comment quality and placement | +| `>= 0.6` | Good comment quality with room for improvement | +| `>= 0.4` | Fair comment quality - consider improving placement | +| below | Poor comment quality - significant improvement needed | + +A tree with neither comments nor declarations reports `No comments found`. +Coverage is reported alongside the score (`Doc Coverage`) and drives the +Documented / Undocumented distribution and the issue list; it is not folded +into the score. --- ## Use cases -- **Documentation enforcement**: Require minimum documentation coverage in CI/CD pipelines. -- **Onboarding assessment**: Measure how well-documented a codebase is before new team members join. -- **API surface quality**: Ensure all exported/public functions have doc comments. -- **Stale comment detection**: Identify files with unusually high comment density that may contain outdated comments. +- **Documentation enforcement**: require a minimum `Doc Coverage` in CI. +- **Onboarding assessment**: measure how well-documented a codebase is before + new team members join. +- **API surface quality**: the issue list names every undocumented + declaration with its file; `--per-file` breaks it down per file. +- **Placement hygiene**: a low good ratio with high coverage means most + comments live inside bodies or float between declarations. --- ## Limitations -- **Comment quality**: The analyzer measures comment *presence* and *placement*, not comment *quality*. A comment saying `// do the thing` counts the same as a thorough description. -- **Language conventions**: Documentation comment conventions vary by language (e.g., `///` in Rust, `"""` in Python, `//` in Go). The UAST normalizes these, but edge cases in less common languages may be missed. -- **Auto-generated docs**: Comments generated by tools (e.g., protobuf stubs) are counted the same as hand-written documentation. -- **Multilingual comments**: Comment text is not analyzed for language quality or correctness. +- **Comment quality**: presence and placement are measured, not content. +- **Docstrings**: a Python docstring (the string that is the first statement + of a `def` / `class` body) is a `Comment` with the `Doc` role in the UAST + and documents its declaration (placement `docstring`). A string literal + that appears later in a body is an `inline` comment of that body, and a + module docstring is `unassociated`. +- **Declaration names**: a declaration's name is its UAST `name` property or + its name identifier; a declaration the mapping leaves nameless — an + anonymous function expression, for instance — is reported as `unknown` + (a nameless class / struct / interface node is not counted at all). + Same-named declarations are tracked separately, so two `String` methods on + different receivers are documented independently. +- **Unparsable files**: a file with a supported extension that the parser + cannot handle (Markdown) is listed as an empty file with zero counts. +- **Auto-generated docs**: comments generated by tools (protobuf stubs) count + like hand-written ones. +- **Attribute roles**: attribute / decorator lines bridge the gap only when + the language mapping gives them the `Attribute` role (Rust attributes and + Python decorators do). --- ## See also -- [Comments reference](../analyzers/comments.md) — configuration keys and output schema. +- [Comments reference](../analyzers/comments.md) — output schema and + definitions. - [Quick start](../getting-started/quickstart.md) — run your first analysis. diff --git a/site/explanation/complexity.md b/site/explanation/complexity.md index 7fae0e6a..3f0e73df 100644 --- a/site/explanation/complexity.md +++ b/site/explanation/complexity.md @@ -1,9 +1,9 @@ # Understanding complexity metrics This page explains the mental model behind the complexity analyzer: what -each metric means, why the analyzer measures three dimensions, and how -cyclomatic and cognitive complexity differ. For configuration keys and the -output schema, see the [Complexity reference](../analyzers/complexity.md). +each metric means, the exact counting rules, how cyclomatic and cognitive +complexity differ, and how the rules are validated. For the output schema +and the thresholds, see the [Complexity reference](../analyzers/complexity.md). --- @@ -11,20 +11,23 @@ output schema, see the [Complexity reference](../analyzers/complexity.md). The complexity analyzer measures three dimensions of code complexity from source code: **cyclomatic complexity**, **cognitive complexity**, and -**nesting depth**. It operates on the UAST representation of your source -files. +**nesting depth**, plus the **lines of code** of each function. It operates +on the UAST representation of your source files, with the file's source text +at hand so operators, identifiers and line counts can be recovered where a +language mapping does not carry them. ### Cyclomatic complexity (McCabe, 1976) -Counts the number of linearly independent paths through a function's control flow graph. - -For Go code, the analyzer follows the same practical counting model used by `gocyclo`: +Counts the number of linearly independent paths through a function's +control-flow graph. The same rules apply to every language: - Base score is `1` -- `if`, `for` / `range` add `+1` -- Non-default `case` adds `+1` -- Each `&&` and `||` adds `+1` -- `default` case does **not** add complexity +- `+1` per `if`, `else if` / `elif`, and conditional (ternary) expression +- `+1` per loop (`for`, `while`, `do`, `loop`, `for … in`, `range`) +- `+1` per non-default `case` / match arm (`default`, `_`, `else` and + Python's `case _:` add nothing) +- `+1` per `catch` / `except` +- `+1` per logical operator (`&&`, `||`, `and`, `or`) !!! info "Interpretation" - **1-10**: Simple, low risk @@ -34,29 +37,143 @@ For Go code, the analyzer follows the same practical counting model used by `goc ### Cognitive complexity (SonarSource, 2017) -Measures how difficult code is for a human to read and understand. Unlike cyclomatic complexity, it penalizes nested structures more heavily and rewards linear sequences of logic. +Measures how difficult code is for a human to read and understand. Unlike +cyclomatic complexity, it penalizes nested structures more heavily and +rewards linear sequences of logic. + +- **Structural increments**: `if`, conditional expressions, + `switch`/`match`, loops and `catch` each add `1` **plus the current + nesting level**, and raise the nesting level for what they contain. +- **Flat increments**: `else` and `else if` add `1` each and never a nesting + penalty; an `else if` chain stays at the level of its first `if`. +- **Sequences of logical operators**: `+1` per run of like operators — + `a && b && c` is one, `a && b || c` is two. A sequence is counted once + wherever it appears (conditions, returns, assignments, call arguments). +- **Jumps to a label**: `break label`, `continue label` and `goto` add `1`; + a plain `break`/`continue` adds nothing. +- **Direct recursion**: a call to the function itself adds `1`. +- **Nesting only**: lambdas and anonymous function literals (closures, + callbacks) add nothing themselves but raise the nesting level for what + they contain. `try` adds nothing; its `catch` is a structural increment. +- The function's own body is nesting level 0 — including an arrow or + function-expression wrapper such as `const f = (x) => { … }`. +- A **named nested function** (Python `def` inside `def`, an inner `fn`, + `const g = function () {}`) is its own unit: it is reported separately and + excluded from the enclosing function's metrics. -Key differences from cyclomatic complexity: +### Nesting depth -- Nesting increments add to the score (deeper nesting = higher penalty) -- `else if` adds structural complexity, but avoids extra nesting penalty vs deeply nested `if` -- Logical operator sequences account for readability, not just path count -- Direct recursion adds a penalty +The maximum depth of nested control-flow blocks within a function: each +`if`/`else` chain (one level for the whole chain), loop, `switch`/`match` +and `try`/`catch` statement (one level for the whole statement) opens a +level. Conditional expressions and lambdas are not blocks and do not count. +Deep nesting is a strong signal for refactoring. -### Nesting depth +### Lines of code and density -Tracks the maximum depth of nested control structures within each function. Deep nesting is a strong signal for refactoring. +Lines of code are the non-blank lines of the function's span that carry +something other than comment text (comment-only lines and blank lines are +skipped; a trailing comment does not blank a code line). The lines of a +nested named function belong to that function. `complexity_density` is +cyclomatic complexity divided by lines of code. --- -## Validation against golden implementations +## Counting rules + +### What is a function + +A `Function`/`Method` node with a body: a block, an expression body, or an +anonymous function/lambda it wraps (JS declarations). Declarations without a +body — C prototypes and declarators, Go function types and interface +methods, Rust trait signatures, Java abstract and interface methods — are +never reported. Anonymous function literals inside a function belong to +that function (they raise nesting); at the top level they are reported as +`anonymous` only when the mapping exposes them as functions. + +Names come from the mapping's `name` property, else from the `Name`-role +identifier (through a C declarator), falling back to the identifier's source +text; a multi-line token is never a name. + +### Worked example + +```go +func decisions(a, b int, xs []int) int { + r := 0 + if a > 0 && b > 0 { // cyclomatic +1 (if) +1 (&&) | cognitive +1 (if) +1 (&& sequence) + r++ + } else if a < 0 || b < 0 { // cyclomatic +1 (if) +1 (||) | cognitive +1 (else if) +1 (|| sequence) + r-- + } else { // | cognitive +1 (else) + r = 2 + } + for _, x := range xs { // cyclomatic +1 | cognitive +1 + r += x + } + switch r { // | cognitive +1 + case 1: // cyclomatic +1 + r = 10 + case 2: // cyclomatic +1 + r = 20 + default: // nothing + r = 0 + } + return r +} +``` + +Cyclomatic `1 + 7 = 8`, cognitive `7`, nesting depth `1` (nothing is nested +inside another block), lines of code `22`. + +Nesting changes the cognitive score but not the cyclomatic one: + +```go +func nested(a, b, c bool) int { + if a { // cognitive +1 (nesting 0) + if b { // cognitive +2 (nesting 1) + if c { // cognitive +3 (nesting 2) + return 1 + } + } + } + return 0 +} +``` + +Cyclomatic `4`, cognitive `6`, nesting depth `3`. + +### Operator recovery + +A binary operator is read from the node's `operator` property when the +language mapping provides one, else from the node's own token when that is +a bare operator symbol, else from the source text between the two operands. +Python's `and`/`or` are recognised through the same source-text rule even +where the mapping lowers them to a generic node. -The complexity analyzer is continuously validated against battle-tested Go references: - -- **Cyclomatic parity target:** `gocyclo` (v0.6.0) -- **Cognitive parity target:** `gocognit` (v1.2.1) +--- -For stabilization, we run a controlled methodology sample through both references and `codefang`, then align discrepancies until parity is reached. +## Validation + +The counting rules are pinned by hand-computed fixtures, not by another +tool's output: + +- `crates/cf-complexity/tests/fixtures/` holds Go, Python, JavaScript, Rust, + Java and C sources (else-if chains, nested `else`, boolean sequences, + `if` with an init statement, `try`/`catch`, lambdas and closures, + `switch` with several cases, `default`/`_` arms, recursion, labelled + jumps, nested functions, declarations without bodies, generics). Every + function's cyclomatic, cognitive, nesting and line count is asserted + from the parsed source (`crates/cf-complexity/tests/fixtures.rs`). +- `bins/codefang/tests/complexity_e2e.rs` drives the real binary over the + same sources and asserts the exact numbers in `json`, `yaml`, `bin` and + `text`, that `--per-file` entries sum to the aggregate, that every format + survives 20 000 nesting levels, and that output is deterministic. +- Unit tests pin each rule on hand-built trees (`crates/cf-complexity/src`). + +One expectation is recorded but not yet enforced: Java `switch` arms and +the Java ternary are lowered by the present mapping in a shape the analyzer +cannot count (`java_decisions_method` is `#[ignore]`d until the UAST mapping +contract lands); every other Java construct is covered. --- @@ -72,13 +189,14 @@ For stabilization, we run a controlled methodology sample through both reference ## Limitations - **Language coverage**: Only languages supported by the UAST parser are analyzed. Unsupported files are silently skipped. -- **Generated code**: The analyzer does not distinguish hand-written code from generated code. Consider excluding generated directories. +- **Mapping fidelity**: the analyzer counts what the language mapping exposes. Where a mapping lowers a construct to a generic node (with the present mapping: Java `switch` arms and the Java ternary), that construct is not counted until the mapping is fixed. +- **Generated code**: The analyzer does not distinguish hand-written code from generated code. Consider excluding generated directories (the default path policy already skips vendored and generated paths). - **Macros and metaprogramming**: Complexity within macros or template metaprogramming may not be fully captured, since the UAST represents the source as written, not as expanded. -- **Cognitive complexity model**: The cognitive complexity scoring follows the SonarSource specification. Other tools may use slightly different weightings. +- **Cognitive complexity model**: The cognitive complexity scoring follows the SonarSource specification. Other tools may use slightly different weightings (for example, `gocognit` does not count `goto`). --- ## See also -- [Complexity reference](../analyzers/complexity.md) — configuration keys and output schema. +- [Complexity reference](../analyzers/complexity.md) — output schema and thresholds. - [Quick start](../getting-started/quickstart.md) — run your first analysis. diff --git a/site/explanation/couples.md b/site/explanation/couples.md index c198c04d..2fa07bcb 100644 --- a/site/explanation/couples.md +++ b/site/explanation/couples.md @@ -1,9 +1,9 @@ # Understanding file and developer coupling This page explains the mental model behind the couples analyzer: what file and -developer coupling mean, how coupling strength is computed, and how the -aggregation pipeline is structured. For configuration keys and the output -schema, see the [Couples reference](../analyzers/couples.md). +developer coupling mean, how coupling strength is computed, and how the walk +counts renames, merges and large commits. For the output schema, see the +[Couples reference](../analyzers/couples.md). --- @@ -16,30 +16,62 @@ collaborating (or competing). ### File coupling -A square matrix where each cell `[i][j]` counts the number of commits in which files `i` and `j` both appeared. High co-change counts indicate tight coupling between files. +A square matrix where each cell `[i][j]` is the number of commits in the +window in which files `i` and `j` both changed. The matrix is symmetric and +the diagonal `[i][i]` is file `i`'s own change count. High co-change counts +indicate tight coupling between files. **Coupling strength** is computed using the code-maat formula: ```text -strength = co_changes / average(revisions_file1, revisions_file2) +strength = co_changes / average(changes_file1, changes_file2) ``` -Where `revisions_fileN` is the diagonal element of the file matrix (the file's self-change count). The result is capped at 1.0. This normalizes coupling by the average activity of both files, so a pair of files that change together 10 times out of 20 average revisions scores 50%. +Where `changes_fileN` is the diagonal element of the file matrix. The result +is capped at 1.0. This normalizes coupling by the average activity of both +files, so a pair of files that change together 10 times out of 20 average +revisions scores 50%. !!! info "Changeset size filter" - Commits touching more than **1000 files** are excluded from coupling analysis. Mass changes (e.g., formatting, license updates, dependency bumps) produce noise rather than meaningful coupling signal. + Commits touching more than **1000 files** contribute no coupling + context. Mass changes (formatting, license updates, dependency bumps) + produce noise rather than meaningful coupling signal. Such a commit still + counts for ownership: its author owns every file it touched, and its + ndjson record keeps the commit's hash with an empty `CouplingFiles` + list and the full `AuthorFiles` map. ### Developer coupling -A matrix where each cell `[i][j]` counts the number of times developers `i` and `j` committed to the same file. Developer coupling strength uses the same code-maat formula normalized by each developer's individual commit activity. +A matrix where each cell `[i][j]` sums, over every file both developers +changed, `min(changes_i, changes_j)` — the number of file changes the two +developers share. The diagonal is a developer's own file-change count, and +the coupling strength uses the same code-maat formula over those counts. ### File ownership -For each tracked file, the analyzer reports its line count and the number of distinct contributors. Single-owner files are flagged as bus-factor risks. +For each file present at the window head, the analyzer reports its line +count (newlines in the blob at the head) and the number of distinct +contributors who changed it in the window. Single-owner files are flagged as +bus-factor risks. ### Rename tracking -The analyzer tracks file renames to avoid counting a renamed file as both a deletion and an insertion, which would break coupling chains. +The tree diff detects renames (a pure `git mv` and a move with edits above +the similarity threshold). When a rename is walked, the old path's whole +accumulated history — its co-change row and column, its change count and its +owners — moves to the new path, so the report shows ONE row under the new +name. A rename that also edits the file counts that commit as a change of the +new path. Renames chain (`a → b → c` ends up under `c`). + +### Every change counts once + +The walk visits commits oldest-first and keeps one set of files already +changed in the window. A merge commit (when `--first-parent` is off) lists, +against its first parent, every file the merged branch changed; those changes +were already counted at the branch's own commits, so the merge contributes +only the files no earlier commit of the window changed, and it records no +deletions. Under `--first-parent` the branch is not walked and the merge is +an ordinary commit that changes those files. --- @@ -56,15 +88,26 @@ The analyzer tracks file renames to avoid counting a renamed file as both a dele ## Architecture -The couples analyzer follows the **TC/Aggregator pattern**: +The analyzer runs on the shared history core (one commit window, one +identity detector, one tick assigner, one change filter) in three steps: -1. **Consume phase**: Each commit produces a `TC{Data: *CommitData}` containing the coupling context (list of co-changed files), per-file author touch counts (always 1 per file per commit), rename pairs, and whether the author's commit count was incremented. Commits exceeding `CouplesMaximumMeaningfulContextSize` (1000 files) are skipped, producing an empty coupling context. +1. **Per-commit products** (parallel, pure): the filtered tree diff of every + commit against its first parent. -2. **Aggregation phase**: The `Aggregator` accumulates the file co-occurrence matrix using `SpillStore[map[string]int]`, along with per-person file touch counts (`people`), commit counts (`peopleCommits`), and rename tracking. When memory pressure exceeds `SpillBudget`, the file coupling matrix is spilled to disk via gob encoding. `Collect()` merges spilled data using additive merge semantics. +2. **Chronological reduce**: every commit, oldest-first, yields a payload — + the coupling context (co-changed files, empty above the cap), the + author's file touches and the rename pairs — which the in-memory + aggregator sums into the file co-occurrence matrix and the per-developer + file touches, applying each rename as it happens. -3. **Serialization phase**: `ticksToReport()` reconstructs the full report (PeopleMatrix, PeopleFiles, Files, FilesLines, FilesMatrix, ReversedPeopleDict) from aggregated TICKs, then `ComputeAllMetrics()` produces the final typed output for any format (text, plot, JSON, YAML, binary). +3. **Report**: the accumulated state is reduced to the files present in the + newest commit of the window (with their line counts), and the typed + metrics (`file_coupling`, `developer_coupling`, `file_ownership`, + `aggregate`) are rendered for every format (text, plot, JSON, YAML, + binary, ndjson, timeseries). -**Working state** (`merges`, `seenFiles`) stays in the analyzer for merge-mode dedup across commits. **Accumulated output** (file couplings, people maps, renames) is owned entirely by the aggregator. +The output is a pure function of the repository and the flags: it does not +depend on the host CPU count. --- @@ -86,14 +129,14 @@ The aggregate `avg_coupling_strength` is the mean of all per-pair coupling stren ## Limitations -- **Large commits excluded**: Commits touching more than 1000 files are skipped to avoid noise from mass changes (formatting, license headers, dependency updates). -- **No temporal decay**: All commits are weighted equally. A coupling from long ago counts the same as a recent one. Consider filtering by date range for recent coupling analysis. -- **Merge commits**: Merge commits are processed only once (first encounter) to avoid double-counting changes that appear in multiple branches. -- **File deletions**: Deleted files are included in the coupling matrix during the analysis window but may not appear in the final output if they no longer exist at HEAD. +- **Large commits excluded from coupling**: Commits touching more than 1000 files feed no coupling context (they still count for ownership). +- **No temporal decay**: All commits are weighted equally. A coupling from long ago counts the same as a recent one. Consider `--since` or `--limit` for recent coupling analysis. +- **Merge commits**: A conflict resolution made inside a merge commit touches a file an earlier commit already changed, so it is not counted as a separate change. A merge whose branch commits fall outside the window is the first commit of the window that changes those files, so they count at the merge. +- **Head-only output**: Only files present in the newest commit of the window appear; files deleted inside the window are dropped from the matrices and the ownership list, and a file recreated after a deletion keeps the deleted file's counts under the same path. --- ## See also -- [Couples reference](../analyzers/couples.md) — configuration keys and output schema. +- [Couples reference](../analyzers/couples.md) — output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/explanation/developers.md b/site/explanation/developers.md index 10ce0576..f9455e74 100644 --- a/site/explanation/developers.md +++ b/site/explanation/developers.md @@ -2,7 +2,7 @@ This page explains the mental model behind the developers analyzer: what per-developer statistics it computes, how the bus factor is derived, and how -the aggregation pipeline is structured. For configuration keys and the output +the aggregation pipeline is structured. For the flags and the output schema, see the [Developers reference](../analyzers/developers.md). --- @@ -10,93 +10,149 @@ schema, see the [Developers reference](../analyzers/developers.md). ## What it measures The developers analyzer computes **per-developer contribution statistics** -across Git history, including commit counts, line changes, language breakdown, -bus factor risk, and activity time series. +across Git history: commit counts, line changes, language breakdown, bus +factor risk and activity time series. ### Developer statistics -For each developer (identified by the identity detector): - -- **Commits**: Total number of commits -- **Lines added / removed / changed**: Aggregate line statistics -- **Net lines**: `added - removed`, showing net contribution -- **Languages**: Breakdown of line changes per programming language -- **Active period**: First and last ticks of activity, number of active ticks +For each developer (identified by the shared identity detector, so +`--people-dict`, `--exact-signatures` and `--anonymize` apply): + +- **Commits**: the number of commits attributed to the developer. +- **Lines added / removed / changed**: per commit, every file change is + line-diffed against the first parent. A deleted run immediately followed + by an inserted run counts as *changed* lines (the shorter of the two), the + rest as added or removed. A new file adds its line count, a deleted file + removes it, an empty (0-byte) blob is a 0-line text side, and a pure + rename is a 0 / 0 / 0 change that still registers the file's language. + Binary blobs carry no line statistics. +- **Net lines**: `added − removed`. +- **Languages**: the same statistics per language, detected from the path + and the blob content (`Other` when no language matches). +- **Active period**: first and last ticks of activity, number of distinct + active ticks. + +### Which commits count + +The devs walk visits every commit of the window in the user's walk mode +(`--first-parent` or the full DAG, unlike burndown which always follows +the first parent). A merge commit counts as a commit for its author but +contributes no line statistics — its lines belong to the commits it merges. +A commit whose filtered tree diff is empty (a trivial merge, an +`--allow-empty` commit, a commit touching only excluded paths) is skipped +unless `--empty-commits` is set, in which case it counts as a commit with +zero lines. Commits whose raw tree diff exceeds `--max-changes-per-commit` +are dropped before any analyzer sees them. ### Language statistics Aggregated across all developers: -- **Total lines per language**: Lines added (for backward compatibility) -- **Total contribution per language**: Lines added + removed, used for bus factor and ownership calculations -- **Contributors per language**: Which developers contribute to each language, measured by total contribution (added + removed) +- **Total lines per language**: lines added. +- **Total contribution per language**: lines added + removed, the measure + the bus factor and the ownership shares use. +- **Contributors per language**: each developer's contribution. ### Bus factor -Knowledge concentration risk per language, following the [CHAOSS Contributor Absence Factor](https://chaoss.community/kb/metric-bus-factor/) methodology. - -For each language, the analyzer computes: +Knowledge-concentration risk per language, following the [CHAOSS +Contributor Absence Factor](https://chaoss.community/kb/metric-bus-factor/) +methodology. For each language: -- **Bus factor number**: The smallest number of contributors responsible for 50% of total contributions (added + removed). This is the CHAOSS standard metric. -- **Total contributors**: Number of unique contributors to that language. -- **Primary/secondary owner**: The top two contributors and their ownership percentages. -- **Risk level**: Based on primary owner concentration. +- **Bus factor number**: the smallest number of contributors responsible + for 50 % of the total contribution. +- **Total contributors**: unique contributors to that language. +- **Primary / secondary owner**: the top two contributors with their + shares. The secondary owner is reported whenever the language has at + least two contributors — developer id 0 included. +- **Risk level**: from the primary owner's share. !!! danger "Risk levels" - - **CRITICAL** (>= 90%): A single developer owns nearly all the code - - **HIGH** (>= 80%): Very concentrated ownership - - **MEDIUM** (>= 60%): Moderate concentration - - **LOW** (< 60%): Healthy distribution + - **CRITICAL** (>= 90%): a single developer owns nearly all the code + - **HIGH** (>= 80%): very concentrated ownership + - **MEDIUM** (>= 60%): moderate concentration + - **LOW** (< 60%): healthy distribution -A **project-level bus factor** is also computed across all developers and all languages, reported in the aggregate section. +A **project-level bus factor** over all developers and languages is +reported in the aggregate section. ### Activity time series -Per-tick commit counts broken down by developer. Shows contribution velocity over time. +Per-tick commit counts broken down by developer, with the tick's first and +last committer time. ### Code churn (line velocity) -Per-tick lines added and removed. This measures raw line velocity — the volume of code changes over time. High values may indicate refactoring, feature development, or instability. +Per-tick lines added and removed. High values may indicate refactoring, +feature development or instability. !!! note "Terminology" - In academic literature, "code churn" specifically refers to recently-written code that is quickly rewritten. This analyzer measures the broader concept of line velocity (total additions and removals per time period). + In academic literature, "code churn" specifically refers to + recently-written code that is quickly rewritten. This analyzer measures + the broader concept of line velocity (total additions and removals per + time period). + +### Aggregate + +`total_developers` and `active_developers` are exact counts; +`estimated_total_developers` and `estimated_active_developers` are the +same cardinalities estimated by a HyperLogLog sketch (precision 14, about +0.8 % standard error) — they are the figures a streaming or merged run can +maintain without the exact sets, and they equal the exact counts on small +inputs. `analysis_period_ticks` is the newest tick of the window. --- ## Architecture -The developers analyzer is built on the **BaseHistoryAnalyzer** and **GenericAggregator** foundations: - -1. **Consume phase**: `Consume()` extracts author ID, line stats, and language breakdown, delegating state storage to the generic aggregator framework. The analyzer retains minimal internal state. -2. **Aggregation phase**: The `GenericAggregator` automatically handles per-commit memory spilling and per-tick grouping using pure function hooks (`extractTC`, `mergeState`, `buildTick`), eliminating custom state management boilerplate. -3. **Metrics & UI phase**: A pure function pipeline (`ComputeAllMetrics`) generates the output structures for JSON, YAML, and text, while declarative chart builders (`plotpage.BuildBarChart`, `plotpage.BuildLineChart`) render the HTML visualizations. +1. **Per-commit pre-pass** (parallel): the tree diff against the first + parent, the cap check, the change filter, the per-change line + statistics and the per-blob language. +2. **Chronological reduce**: the shared identity / tick stamps key every + commit's `CommitDevData`, grouped into per-tick / per-developer buckets. +3. **Metrics**: a pure function pipeline (`compute_all_metrics`) produces + the developers, languages, bus factor, activity, churn and aggregate + sections; the json / yaml / bin / text / plot formats are encodings of + that one value, and the timeseries formats list every kept commit in + tick order. -This unified approach significantly reduces boilerplate while maintaining full support for streaming output, decoupled aggregation, and budget-aware memory spilling. +The output is a pure function of the repository and the flags: two runs +produce identical bytes. --- ## Use cases -- **Team assessment**: Understand who contributes what, in which languages, and when. -- **Bus factor analysis**: Identify languages or components where a single developer departure would create critical knowledge gaps. The CHAOSS bus factor number tells you how many people need to leave before 50% of knowledge is lost. -- **Activity monitoring**: Track developer engagement over time. Declining activity may signal burnout or attrition risk. -- **Language migration tracking**: Monitor the adoption of a new language by watching language statistics over time. -- **Onboarding evaluation**: Measure how quickly new team members ramp up by comparing their activity curves. -- **Code churn analysis**: Detect periods of high line velocity that may correlate with instability or deadline pressure. +- **Team assessment**: who contributes what, in which languages, and when. +- **Bus factor analysis**: languages where a single departure would create + a critical knowledge gap. +- **Activity monitoring**: declining activity may signal attrition risk. +- **Language migration tracking**: watch language statistics over time. +- **Onboarding evaluation**: compare the activity curves of new team + members. +- **Code churn analysis**: periods of high line velocity. --- ## Limitations -- **Identity resolution**: Developer identity is determined by the identity detector (email-based by default). Multiple email addresses for the same person will appear as separate developers unless a mailmap is configured. -- **Merge commits**: By default, merge commits are processed only once (first encounter). Trivial merges are skipped unless `ConsiderEmptyCommits` is enabled. -- **Line attribution**: Lines are attributed to the commit author, not the committer. In workflows with heavy rebasing, this may differ from expectations. -- **Contribution measurement**: Contributions are measured as lines added + lines removed. This gives fair credit to refactoring work but does not distinguish between code complexity or quality. -- **Active developer threshold**: "Active developers" are those with commits in the most recent 90-day window (when tick size is known). Falls back to the recent 30% of the analysis period when tick size is unavailable. This threshold is not configurable. +- **Identity resolution**: identities are merged by the shared detector + (a name or an e-mail seen together links the two). Use `--people-dict` + for explicit aliases and `--exact-signatures` to keep every + `name <email>` pair apart. +- **Merge commits**: counted as commits, never as lines. +- **Line attribution**: lines are attributed to the commit author, not the + committer. +- **Contribution measurement**: `added + removed` credits refactoring work + but does not distinguish complexity or quality. +- **Active developer threshold**: "active developers" are those with + commits in the most recent 90 days (`90 · 24 / tick_size_hours` ticks, + clamped at the start of the window). The threshold is not configurable. --- ## See also -- [Developers reference](../analyzers/developers.md) — configuration keys and output schema. +- [Developers reference](../analyzers/developers.md) — flags and output + schema. - [Quick start](../getting-started/quickstart.md) — run developer analysis. diff --git a/site/explanation/file-history.md b/site/explanation/file-history.md index cb303d4c..81c4f1a5 100644 --- a/site/explanation/file-history.md +++ b/site/explanation/file-history.md @@ -1,8 +1,8 @@ # Understanding file history analysis This page explains the mental model behind the file history analyzer: what -per-file lifecycle data it records, how hotspots and churn are derived, and how -rename detection maintains continuity. For configuration keys and the output +per-file lifecycle data it records, how hotspots, churn and the composition +are derived, and how rename detection maintains continuity. For the output schema, see the [File history reference](../analyzers/file-history.md). --- @@ -11,16 +11,22 @@ schema, see the [File history reference](../analyzers/file-history.md). The file history analyzer tracks the **lifecycle of every file** through Git history, recording which commits touched each file, which developers modified -it, and aggregating line statistics per contributor. It supports rename -detection to maintain continuity across file moves. +it, and aggregating line statistics per contributor. It follows renames to +maintain continuity across file moves. ### Per-file commit history -For each file present in the repository at HEAD, the analyzer records the ordered list of commits that created, modified, or deleted it. +For each file present at the window head, the analyzer records the list of +commits in the window that created, modified or deleted it (a deletion +followed by a re-creation starts a new list: git treats that as a new file). ### Per-file contributor breakdown -For each file, a map of developer IDs to line statistics (added, removed, changed). This shows who contributed what to each file. +For each file, a map of developer IDs to line statistics (added, removed, +changed), attributed commit by commit to the commit's author. A run of +removed lines immediately followed by added lines counts as *changed* +lines. Merge commits contribute no line stats (their diff against the first +parent repeats the merged branch's changes). ### Code hotspots @@ -34,11 +40,38 @@ Files with high commit counts are flagged as hotspots with risk levels: ### File churn -A composite score combining commit frequency and line change volume. High-churn files may indicate instability or areas of active development. +A composite score combining commit frequency and line change volume: +`commit_count + (added + removed + changed) / 100`. High-churn files may +indicate instability or areas of active development. + +### File composition + +Every changed file is classified by the shared classifier over its path +**and** its blob content (the first 8 KiB): `binary` (NUL bytes), `image`, +`vendor`, `generated` (generated paths or a generated-code marker in the +content), `documentation`, `configuration`, `dotfile`, else `source`. + +- `composition` counts the **distinct files changed in the window** per + category: a file edited in eight commits is one file. A single-file + repository therefore reports `source 1 (100%)`, not the number of edits. +- `composition_ts` counts the distinct files changed in **each tick** + (`--tick-size`, 24 h by default), with the tick's `start_time` / + `end_time` — the first and last committer time of the tick's commits. + +A file's latest classification wins (text that became binary counts as +binary), and a rename retires the old path, so the moved file is counted +once. The composition covers every file changed in the window, including +files deleted before the window head; the per-file sections cover only the +files at the head. ### Rename support -When Git detects a file rename (e.g., `old/path.go` to `new/path.go`), the analyzer transfers the full history from the old path to the new path, maintaining a continuous record. +When Git detects a file rename (a pure `git mv`, or a move with edits above +the similarity threshold), the analyzer transfers the full commit list from +the old path to the new path and appends the rename commit, maintaining a +continuous record; the rename's own line stats (the edit, if any) are +attributed to the new path. The ndjson record of the commit carries the +`FromPath` / `ToPath` pair. --- @@ -49,20 +82,21 @@ When Git detects a file rename (e.g., `old/path.go` to `new/path.go`), the analy - **Onboarding guides**: New team members can see which files each developer owns to know who to ask about specific code. - **Refactoring ROI**: Identify files with both high churn and many contributors -- refactoring these produces the largest payoff. - **Risk assessment**: Files with a single contributor and high commit counts are both hot and concentrated -- a bus factor risk. +- **Change mix over time**: The composition time series shows whether a period's changes went into source, documentation, configuration or generated files. --- ## Limitations -- **HEAD-only output**: Only files present at HEAD are included in the final output. Files that were deleted before the last commit are tracked during analysis but excluded from results. -- **Merge handling**: Merge commits are processed only once to avoid double-counting. Non-merge context changes in merge commits are skipped. -- **Rename detection**: Depends on Git's rename detection heuristics. If a file is both renamed and heavily modified in the same commit, Git may not detect the rename. +- **HEAD-only per-file output**: Only files present in the newest commit of the window are listed in `file_churn`, `file_contributors`, `hotspots` and the aggregate. Files deleted inside the window are tracked during analysis (and counted in the composition) but excluded from the per-file results. +- **Merge handling**: A merge commit appends to the commit lists of the files it lists against its first parent and counts them in its tick's composition, but contributes no line stats. Under `--first-parent` a merge is an ordinary commit. +- **Rename detection**: Depends on Git's rename detection heuristics (50 % similarity). A file that is renamed and rewritten beyond that threshold in one commit is a deletion plus an insertion. - **Churn score formula**: The churn score is `commit_count + (added + removed + changed) / 100`. This is a simple heuristic, not a rigorous statistical measure. -- **No time-series**: Unlike the burndown analyzer, file history does not produce time-series data. It provides aggregate per-file statistics only. +- **Binary blobs**: A change whose blob is binary contributes no line stats (it still counts as a commit and as a `binary` file in the composition). --- ## See also -- [File history reference](../analyzers/file-history.md) — configuration keys and output schema. +- [File history reference](../analyzers/file-history.md) — output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/explanation/halstead.md b/site/explanation/halstead.md index 64032360..134ca62a 100644 --- a/site/explanation/halstead.md +++ b/site/explanation/halstead.md @@ -1,19 +1,19 @@ # Understanding Halstead metrics This page explains the mental model behind the Halstead analyzer: what -operators and operands are, which derived metrics matter most, and how the -counting policy keeps results stable across languages. For configuration keys -and the output schema, see the [Halstead reference](../analyzers/halstead.md). +operators and operands are, how codefang finds them in the UAST, how a +codebase is aggregated, and which derived metrics matter most. For the +output schema and the exact thresholds, see the +[Halstead reference](../analyzers/halstead.md). --- ## What it measures -The Halstead analyzer computes **Halstead complexity metrics** (1977) based on -operator and operand counts extracted from the UAST. These metrics provide an -objective, quantitative assessment of program size and complexity. - -The Halstead model treats a program as a sequence of **operators** (keywords, symbols, function calls) and **operands** (variables, literals, constants). From these counts, it derives: +The Halstead model (1977) treats a program as a sequence of **operators** +(symbols and keywords that *do* something: `+`, `:=`, `return`, `if`, a +call's parentheses, a member access) and **operands** (the things they act +on: variables, literals, names). From four counts it derives: | Metric | Formula | Description | |---|---|---| @@ -23,47 +23,132 @@ The Halstead model treats a program as a sequence of **operators** (keywords, sy | **N2** | -- | Total number of operands | | **Vocabulary** (n) | n1 + n2 | Distinct tokens used | | **Length** (N) | N1 + N2 | Total tokens in the program | -| **Volume** (V) | N * log2(n) | Information content in bits | -| **Difficulty** (D) | (n1/2) * (N2/n2) | Error proneness | -| **Effort** (E) | D * V | Mental effort to implement | +| **Volume** (V) | N · log2(n) | Information content in bits | +| **Difficulty** (D) | (n1/2) · (N2/n2) | Error proneness | +| **Effort** (E) | D · V | Mental effort to implement | | **Time** (T) | E / 18 | Estimated implementation time (seconds) | | **Bugs** (B) | V / 3000 | Estimated delivered bugs | !!! info "Key insight" - **Volume** measures the size of the implementation. **Difficulty** captures how error-prone it is. Their product, **Effort**, is the best single number for comparing overall complexity. + **Volume** measures the size of the implementation. **Difficulty** + captures how error-prone it is. Their product, **Effort**, is the best + single number for comparing overall complexity. + +### A worked example -### Counting policy +```go +func hal(a int, b int) int { + c := a + b + return c * 2 +} +``` -Codefang computes Halstead metrics from UAST with a lexical-first policy: +| | Tokens | Count | +|---|---|---| +| operators | `:=`, `+`, `*`, `return` | n1 = 4, N1 = 4 | +| operands | `a`, `b`, `c`, `c`, `2` | n2 = 4, N2 = 5 | -- Counts lexical operands (`Identifier`, `Literal`, `Field`) and operator nodes/roles. -- Excludes structural wrappers and declaration-only artifacts from operand counts. -- Uses explicit operator properties when available, then token extraction fallback. +n = 8, N = 9, V = 9 · log2(8) = **27**, D = (4/2) · (5/4) = **2.5**, +E = **67.5**, T = 3.75 s, B = 0.009. The function's name, its parameters +and its return type are its *signature* — they are declared, not operated +on — so `a` and `b` count once each (their uses in the body), not three +times. + +--- -This improves stability across languages and avoids pseudo-operands like structural `Parameter` nodes. +## Counting policy + +Codefang classifies UAST nodes inside each function body with one table +(the full version is in the [reference](../analyzers/halstead.md#classification-table)): + +- **Operands** are leaf `Identifier` and `Literal` nodes, named by their + text. A quoted string is one operand with its quotes; a qualified name + such as `fmt.Println` is two operands (`fmt`, `Println`) plus the + member-access operator `.`. +- **Operators** are, in order of preference: the node's explicit + `operator` property when the language mapping exposes it; the fixed + operator of calls (`()`), indexing (`[]`) and slicing (`[:]`); otherwise + the node's **residue** — its source text with every child blanked out. + For `c := a + b` the declaration's residue is ` := ` and the binary + expression's is ` + `; for `return c * 2` it is `return `; for `r++` it + is `++`. Residue is tokenized into symbol runs (always operators) and + words (operators only when they are keywords: `return`, `if`, `for`, + `let`, `int`, `new`, …). Brackets, commas, semicolons and a lone `:` are + punctuation, not operators; the conditional `? :` counts as one `?:`. +- A leaf identifier with the `Type` role inside a body (`int` in + `int c = 0;`, `var x int`) is a type-specifier **operator**, following + the classic convention for declaration keywords. +- **Skipped**: the function's signature (name, parameters, modifiers, + return type), parameter subtrees anywhere, comments and docstrings. +- **Closures and lambdas** belong to the function that contains them (the + same rule the complexity analyzer uses); nested *named* functions and + methods are separate records. + +Text is recovered from the file bytes by byte span, so languages whose +mappings leave literals or identifiers without a token (Rust, Java, Go +literals) are counted correctly. When the analyzer runs without the file +(the `history/quality` path), it falls back to the node's own verbatim +token or the enclosing function's token; a node whose text cannot be +recovered is left uncounted — a node is never counted under its type name, +and a statement's text is never an operand. + +### Aggregation: union, not average + +A file, and the whole codebase, is aggregated as the **union of its +functions' multisets**: an operator used in ten functions is *one* +distinct operator (n1) with its occurrences summed (N1); the derived +measures are then computed from the four union counts exactly as for a +single function. Alongside the union, the report carries per-function +statistics — mean and maximum volume, difficulty and effort, and the +number and share of **high-effort functions** (effort ≥ 10000). + +The status message, the health score and the section score are keyed off +that share: a codebase where fewer than 5% of functions are high-effort +is "well-structured", up to 15% "acceptable", up to 30% "consider +refactoring", beyond that "significant refactoring recommended"; the +health score falls linearly from 100 through 90, 70 and 40 at those +boundaries to 0 when every function is high-effort. Note that the *union* +difficulty of a large codebase grows with code size (every operand reuse +raises `N2 / n2`), which is why the health score is based on the +per-function distribution rather than on the union D or E. --- ## Use cases -- **Effort estimation**: Use the Effort metric to compare the relative complexity of different modules or features. -- **Bug prediction**: The Bugs metric provides a rough upper bound on expected defects. Modules with high estimated bugs warrant more thorough testing. -- **Code review guidance**: Functions with high Difficulty scores are more error-prone and deserve extra scrutiny. -- **Language comparison**: Halstead metrics allow cross-language comparisons since they are based on abstract operator/operand counts. +- **Effort estimation**: compare the Effort of modules or features to rank + them by the mental work they represent. +- **Bug prediction**: the Bugs metric is a rough upper bound on expected + defects; functions with high estimated bugs warrant more thorough testing. +- **Code review guidance**: functions with high Difficulty are more + error-prone and deserve extra scrutiny; `--per-file` shows which files + concentrate them. +- **Language comparison**: the counts are abstract operator / operand + counts, so trends compare across languages within one toolchain. --- ## Limitations -- **Cross-tool variance**: Language-specific tools (for example, Python- or ESTree-only analyzers) can differ in tokenization and counting policy. Compare trends within one toolchain rather than mixing absolute numbers across tools. -- **Operator classification**: UAST-based classification targets cross-language consistency, not byte-for-byte parity with each language parser. -- **Estimation accuracy**: The Bugs and Time formulas are empirical approximations from the 1970s. Treat them as relative indicators, not precise predictions. -- **Macro expansion**: Halstead metrics count tokens as written, not as expanded. Heavy use of macros or code generation can skew results. -- **Comments excluded**: Comments and whitespace are excluded from Halstead counts (by design). +- **Cross-tool variance**: language-specific tools (for example, Python- + or ESTree-only analyzers) differ in tokenization and counting policy. + Compare trends within one toolchain rather than mixing absolute numbers + across tools. +- **Mapping coverage**: the counts are as good as the UAST mapping of the + language. A construct the mapping lowers to a bare wrapper without spans + is not counted; keyword operators come from a fixed whitelist, so an + unlisted keyword is dropped rather than mislabelled. +- **Estimation accuracy**: the Bugs and Time formulas are empirical + approximations from the 1970s. Treat them as relative indicators, not + precise predictions. +- **Macro expansion**: Halstead metrics count tokens as written, not as + expanded. Heavy use of macros or code generation can skew results. +- **Comments excluded**: comments and whitespace are excluded from + Halstead counts (by design). --- ## See also -- [Halstead reference](../analyzers/halstead.md) — configuration keys and output schema. +- [Halstead reference](../analyzers/halstead.md) — classification table, thresholds and output schema. - [Quick start](../getting-started/quickstart.md) — run your first analysis. diff --git a/site/explanation/imports.md b/site/explanation/imports.md index 5ae6572d..0b2938a3 100644 --- a/site/explanation/imports.md +++ b/site/explanation/imports.md @@ -2,26 +2,37 @@ This page explains the mental model behind the imports analyzer: what it measures in static and history modes, and how the history-mode aggregation -pipeline works. For configuration keys and the output schema, see the -[Imports reference](../analyzers/imports.md). +pipeline works. For the per-language extraction rules, the classification +table and the output schema, see the [Imports reference](../analyzers/imports.md). --- ## What it measures The imports analyzer extracts **import and dependency information** from source -code using UAST parsing. It operates in both static mode (single-file analysis) -and history mode (tracking import usage per developer over time). +code using UAST parsing. It operates in both static mode (per-file analysis +aggregated over a tree) and history mode (tracking import usage per developer +over time). ### Static mode -- **Import list**: All imports/dependencies declared in each file -- **Language detection**: Automatically detects the language and normalizes import paths -- **Dependency graph**: Maps which files depend on which packages +- **Import targets, not statements**: each file contributes the modules, + packages or files its import statements name — `strings` for Go's + `str "strings"`, `react` for `import { useState } from 'react'`, + `std::collections::HashMap` and `std::collections::HashSet` for one Rust + `use` with a brace group. Aliases, specifier lists and keywords are never + imports. Each target counts once per file. +- **Usage across files**: the aggregate reports every unique import with the + number of files importing it, so the most-shared dependencies surface + first. +- **Classification by the importing file's language**: `relative`, + `stdlib` or `external`, using per-language standard-library tables (a Go + `errors` and a Python `datetime` are both stdlib; a JavaScript `react` is + external). ### History mode -Tracks import usage across Git history, producing a per-developer, per-language, per-tick breakdown of which dependencies each developer introduces or modifies. This reveals: +Tracks import usage across Git history, producing a per-developer, per-language, per-tick breakdown of which dependencies each developer introduces or modifies: every commit's changed files (the After version of each Insert/Modify) are parsed, their imports are extracted with the file's language, and each import is counted under the commit's author, the file's language and the commit's tick. This reveals: - **Dependency adoption timeline**: When new libraries were introduced - **Developer expertise signals**: Which developers work with which dependencies @@ -29,15 +40,26 @@ Tracks import usage across Git history, producing a per-developer, per-language, --- -## Architecture (history mode) +## How extraction works + +The extractor walks the parsed tree with one rule per language (Go, Python, +JavaScript/TypeScript, Rust, Java, C/C++, C#, PHP, Ruby, Kotlin) and a +generic rule for everything else. A rule recognizes an import node by its +shape — a statement token to parse, a quoted literal child, an identifier +child, or a `require`/`import` call — and once a node matches, its subtree +is never visited again. That consuming walk is what keeps nested +specifier nodes (`React`, `{ useState, useEffect }`, `* as ns`) out of the +result. -The imports history analyzer follows the **TC/Aggregator pattern**: +--- + +## Architecture (history mode) -1. **Consume phase**: For each commit, `Consume()` extracts imports from changed files via parallel UAST parsing and returns them as `TC{Data: []ImportEntry}`. Each `ImportEntry` carries a language and import path. The analyzer retains no per-commit state; only the UAST parser is kept as working state. -2. **Aggregation phase**: An `imports.Aggregator` collects TCs into a 4-level `Map` (author -> language -> import -> tick -> count) using `SpillStore[Map]`. The `AuthorID` and `Tick` from each TC index the entries correctly. -3. **Serialization phase**: `SerializeTICKs()` merges all tick data back into the full `Map` with metadata (`author_index`, `tick_size`), then delegates to `Serialize()` for JSON, YAML, binary, or HTML plot output. +The map and its report live in the `cf-imports` crate; the history walk in `cf-commands` supplies the per-commit inputs: -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +1. **Per-commit product**: for every Insert/Modify change the walk parses the After blob (files the parser supports, up to 256 KiB) and runs `cf_imports::extract_imports` with the parser's language for the file and the blob bytes — the same extractor and per-language rules as `static/imports`. The product is the commit's list of `(language, import)` entries, a pure function of the commit, computed in parallel across cores. +2. **Chronological reduce**: the shared history core stamps every commit with its author id and tick in oldest-first order; `add_entries_to_map` folds each commit's entries into the 4-level map `author -> language -> import -> tick -> count`, which merges additively. +3. **Report**: `cf_imports::HistoryReport` carries the tick size, the author index and the map keyed by developer label; its one value tree is what json, yaml and bin encode, and `aggregate_import_counts` / `top_imports` derive the plot's top-20 bars from the same map. --- @@ -53,15 +75,18 @@ This separation enables streaming output, budget-aware memory spilling, and deco ## Limitations -- **Language support**: Only languages with UAST parser support are analyzed. Unsupported file types are silently skipped. -- **Dynamic imports**: Runtime or dynamic imports (e.g., Python's `importlib.import_module()`, JavaScript's `import()`) are not detected since they are not present in the static UAST. +- **Language support**: Only languages with UAST parser support are analyzed. Unsupported file types are silently skipped. Languages without a dedicated rule use the generic statement rule, which covers the common `import` / `from … import` / `use` forms. +- **Dynamic imports**: JavaScript `import('m')` and `require('m')` are detected only when the argument is a string literal; computed module names (`require(name)`) and Python's `importlib.import_module()` are not present as imports in the UAST. +- **Re-exports**: `export { x } from './m'` is not an import. +- **Kotlin**: the Kotlin grammar lowers imports without tokens, so the statement is read back from the file bytes; both the static and the history path provide them. - **Transitive dependencies**: Only direct imports are reported. The analyzer does not resolve transitive dependency trees. -- **File size threshold**: Files exceeding `MaxFileSize` (default 1 MB) are skipped to avoid excessive memory usage during parallel extraction. -- **History mode overhead**: History mode creates a UAST parser per fork, which increases memory usage. Tune `Goroutines` based on available memory. +- **Blob size threshold**: In history mode, blobs above 256 KiB are not parsed. +- **Counts are per change, not per file**: history mode counts the imports of every Insert/Modify change's After version, so a file rewritten in several commits contributes its imports once per commit. +- **History mode overhead**: Every changed file of every commit is parsed; one UAST parser per worker thread is reused across the commits that thread processes. --- ## See also -- [Imports reference](../analyzers/imports.md) — configuration keys and output schema. +- [Imports reference](../analyzers/imports.md) — extraction rules, classification, configuration keys and output schema. - [Quick start](../getting-started/quickstart.md) — run your first analysis. diff --git a/site/explanation/quality.md b/site/explanation/quality.md index f8ef8285..394424e5 100644 --- a/site/explanation/quality.md +++ b/site/explanation/quality.md @@ -2,53 +2,85 @@ This page explains the mental model behind the code quality analyzer: which static metrics it tracks per commit and how it aggregates them per tick. For -configuration keys and the output schema, see the +the output schema and the exact per-field definitions, see the [Quality reference](../analyzers/quality.md). --- ## What it measures -The code quality analyzer tracks **complexity, Halstead metrics, comment -quality, and cohesion** across Git history. For each commit, it runs four -static analyzers on UAST-parsed changed files and produces per-file quality -metrics. +The code quality analyzer answers one question per tick: *what did the code +touched in this period look like?* For every commit in the window it takes +the files the commit changed, parses each one's new version into a UAST, and +runs the four static analyzers on it — exactly the analyzers behind +`static/complexity`, `static/halstead`, `static/comments` and +`static/cohesion`. The per-file numbers are therefore the numbers those +commands print for the same file. -### Per-file metrics +### Per-file sample -For each changed file in a commit, the analyzer computes: +For each parsed file in a commit, the analyzer records: | Category | Metrics | |---|---| -| **Complexity** | Cyclomatic complexity, cognitive complexity, max single-function complexity, function count | -| **Halstead** | Volume, effort, delivered bugs | -| **Comments** | Overall comment score, documentation coverage | -| **Cohesion** | Cohesion score | - -### Statistical aggregation - -Per-tick statistics are computed from per-file arrays: - -- **Mean, median, P95, max** for complexity and Halstead volume -- **Sum** for delivered bugs and Halstead volume -- **Min** for comment score and cohesion (worst-case tracking) -- **Total** files analyzed and functions counted +| **Complexity** | summed cyclomatic complexity, summed cognitive complexity, max single-function complexity, function count | +| **Halstead** | volume, effort, delivered bugs (over the file's functions, with operator and operand tokens recovered from the file bytes) | +| **Comments** | good-comment ratio, documentation coverage | +| **Cohesion** | cohesion score — only for files with at least one function | + +The sample is computed by `cf_quality::analyze_file(root, source)`, which +feeds both the UAST and the file bytes to the component analyzers. The one +deliberate difference from the static commands is a discovery depth cap: +functions and comments nested more than ten levels below the file root are +not discovered (each discovered function is analyzed in full). + +### Which files count + +- A file counts when its parse succeeded. Unsupported languages, binaries, + blobs above 256 KiB and files whose parse fails contribute nothing — they + are not "analyzed files" and they do not pad the statistics with zeros. +- A parsed file without functions is an analyzed file: its complexity and + Halstead scalars are zero (there is nothing to measure) and its comment + scalars are whatever the comments analyzer reports. It carries **no + cohesion sample**, mirroring the static cohesion analyzer's "no data". +- Deleted files contribute nothing. + +### Per-tick aggregation + +Commits are bucketed by committer time into ticks (`--tick-size`, default +24 hours). A tick's statistics are computed over the concatenated samples of +its commits — mean, median, P95 and max for complexity and Halstead volume, +sums for delivered bugs and volume, mean and minimum for the comment score, +mean and minimum for cohesion. + +A tick in which nothing was analyzed does not exist in the report: it is not +a tick with zero complexity, it is a tick with no data. The aggregate is +computed over the ticks that do exist, and the cohesion aggregates over the +ticks that hold cohesion data, so an aggregate minimum can never exceed the +matching aggregate mean. + +### Large commits + +Every surviving change of a commit is analyzed; there is no per-commit file +cap other than `--max-changes-per-commit`, which drops a commit entirely when +its raw tree diff exceeds the limit. --- ## Architecture -The quality analyzer follows the **TC/Aggregator pattern**: - -1. **Consume phase**: For each commit, `Consume()` runs four static analyzers (complexity, Halstead, comments, cohesion) on each changed file's UAST, returning per-file metrics as a `TC{Data: *TickQuality}`. The analyzer retains no per-commit state. -2. **Aggregation phase**: A `quality.Aggregator` collects TCs, merges `TickQuality` data by time bucket (tick), and produces `TICK` results. -3. **Serialization phase**: `SerializeTICKs()` converts aggregated TICKs into JSON, YAML, binary, or HTML plot output via `ComputeAllMetrics()`. - -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +1. **Walk**: the shared history core yields the commit window, identity and + tick stamps; a parallel pre-pass computes each commit's filtered tree diff + and parses the changed blobs once (one parser per worker thread). +2. **Sample**: `cf_quality::analyze_file` turns each parsed file into a + `FileSample`; a commit's samples form its `TickQuality`. +3. **Reduce**: samples are merged per tick in chronological order and + `cf_quality::compute_all_metrics` derives the time series and the + aggregate. The output is a pure function of the repository and the flags. --- ## See also -- [Quality reference](../analyzers/quality.md) — configuration keys and output schema. +- [Quality reference](../analyzers/quality.md) — output schema and field definitions. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/explanation/sentiment.md b/site/explanation/sentiment.md index 138984b1..e57f3a9f 100644 --- a/site/explanation/sentiment.md +++ b/site/explanation/sentiment.md @@ -1,39 +1,51 @@ # Understanding comment sentiment analysis This page explains the mental model behind the sentiment analyzer: how it -extracts and filters comments, how VADER and the multilingual lexicon score -them, and the software-engineering domain adjustments that keep technical -language from skewing the result. For configuration keys and the output schema, -see the [Sentiment reference](../analyzers/sentiment.md). +finds the comments a commit introduced or changed, how it filters them, how +VADER and the multilingual lexicon score them, and the software-engineering +domain adjustments that keep technical language from skewing the result. For +the flags and the output schema, see the +[Sentiment reference](../analyzers/sentiment.md). --- ## What it measures -The sentiment analyzer classifies **comment sentiment** across Git history. For -each commit, it extracts new or changed comments via UAST parsing, filters out -noise, and classifies the remaining comments as positive, negative, or neutral. -This reveals how developer sentiment evolves over time. +The sentiment analyzer tracks the **emotional tone of the comments developers +write** over Git history. For each commit it looks at the comments that +commit added or edited, filters out noise, scores the rest, and reports one +score per tick that received such comments. This reveals how developer +sentiment evolves over time — without re-counting the unchanged comments of +every touched file. ### Per-commit comment extraction -For each commit, the analyzer: - -1. Computes the UAST diff between the old and new file versions -2. Extracts comment nodes from the new UAST -3. Merges adjacent comment lines into single blocks -4. Filters out noise (short comments, license headers, function signatures) -5. Returns the filtered comments as a `TC` payload (`sentiment.CommitResult`) +For every Insert or Modify change of a commit that survives the shared change +filter, the analyzer: + +1. Parses the **new** version of the file into a UAST and collects its + `Comment` nodes; for a Modify it parses the **old** version too (both + parses go through the per-commit parse cache, so each blob is parsed once). +2. Merges adjacent comment lines into blocks on each side (a block comment, a + run of `//` lines, a Javadoc / Doxygen block whose ` * ` continuation + markers are stripped). +3. Filters each side's blocks (see below). +4. Keeps the **multiset difference new − old**: a block whose text also + occurs in the old version cancels one occurrence and is not counted. A + commit that only adds code to a commented file therefore contributes no + comment; an edited comment counts once, as new text; a brand-new file + contributes all of its comments. ### Comment filtering Comments are filtered using several heuristics: -- **Minimum length**: Comments shorter than the threshold are skipped -- **Letter ratio**: At least 60% of characters must be Unicode letters (filters out commented-out code) -- **First character**: Must start with a Unicode letter or digit (supports all scripts) -- **License detection**: Comments matching license/copyright patterns (including UK "licence" spelling) are excluded -- **Function name removal**: Inline function references like `doThing()` are stripped before analysis +- **Minimum length**: comments shorter than `--min-comment-len` characters (default + 20, after delimiters are stripped) are skipped; `0` disables the check +- **Letter ratio**: at least 60% of the bytes must be Unicode letters (filters out commented-out code) +- **First character**: must be a Unicode letter or digit (supports all scripts) +- **License detection**: comments matching license/copyright patterns (including UK "licence" spelling) are excluded +- **Function name removal**: inline function references like `doThing()` are stripped before analysis ### Multilingual support @@ -45,18 +57,29 @@ The analyzer supports comments in any language that uses Unicode letters: - **Latin**: English, French, German, Spanish, etc. - All other Unicode letter scripts -Comment filtering uses Unicode-aware regex patterns (`\p{L}` for letters, `\p{N}` for digits) rather than ASCII-only ranges, ensuring comments in non-English languages are not silently dropped. +Comment filtering uses Unicode-aware character classes (`\p{L}` for letters, +`\p{N}` for digits) rather than ASCII-only ranges, ensuring comments in +non-English languages are not silently dropped. !!! info "Multilingual sentiment scoring" Sentiment scoring uses VADER's English lexicon as the base, extended with **93,000+ multilingual word entries** from the Chen-Skiena lexicon dataset (ACL 2014) covering **32 languages**. Non-ASCII words from the dataset are injected into VADER's lexicon at startup, enabling basic sentiment scoring for comments in Russian, Chinese, Japanese, Korean, Arabic, and 27 other languages. VADER's grammatical rules (negation, intensifiers) still operate on English syntax, so scoring accuracy for non-English comments is lower than for English — but significantly better than no coverage. -### Sentiment classification +### Sentiment scoring -Filtered comments are classified as positive, negative, or neutral using **VADER** (Valence Aware Dictionary and sEntiment Reasoner) via the [GoVader](https://github.com/jonreiter/govader) library, enhanced with **software engineering domain adjustments**. +Filtered comments are scored with **VADER** (Valence Aware Dictionary and +sEntiment Reasoner), implemented in `cf-sentiment` over the vendored VADER +lexicon and emoji table, enhanced with **software engineering domain +adjustments**. #### VADER base scoring -VADER is a lexicon and rule-based sentiment analyzer designed for social media and short text. It handles negations, intensifiers, and punctuation. The compound score (-1 to 1) is mapped to our [0, 1] range. +VADER is a lexicon and rule-based sentiment analyzer designed for social +media and short text. It handles negations, intensifiers, contrastive `but`, +idioms, ALLCAPS emphasis, `!` / `?` emphasis and emoji. The compound score in +`[-1, 1]` is mapped to the reported `[0, 1]` range (`score = (compound + 1) / +2`, so `0.5` is neutral). The implementation reproduces the published VADER +example scores (for instance `VADER is smart, handsome, and funny!` → +compound `0.8439`). #### Multilingual lexicon extension @@ -81,56 +104,72 @@ step is required. #### SE-domain adjustments -VADER frequently misclassifies technical terms that sound negative in natural language but are neutral in software engineering. The analyzer applies domain-specific adjustments: - -**Neutralized terms** (pushed toward neutral): -`kill`, `abort`, `fatal`, `terminate`, `dead`, `destroy`, `panic`, `deprecated`, `obsolete`, `execute`, `exploit`, `conflict`, `revert`, `reject`, `critical` - -**Genuinely negative in SE** (pushed toward negative): -`hack`, `hacky`, `kludge`, `workaround`, `spaghetti`, `nightmare`, `technical debt` - -#### Comment length weighting - -Longer comments carry more weight in the sentiment score, as they tend to contain more meaningful sentiment signal. Weight is capped at 3x the average to prevent a single long comment from dominating. +VADER scores some words negative that are neutral in software engineering, +and it does not know some words that are negative there. The scorer matches +the lists below as **whole words** (or whole phrases) — `deadline` is not +`dead`, `skill` is not `kill`, `hacker` is not `hack` — and adjusts the +comment's compound score: -#### Trend analysis +- every **neutralizer** hit contributes `(0 − compound) × 0.8`; +- every **negative term** hit contributes its shift; +- the mean contribution is added to the compound, which is then clamped to + `[-1, 1]`. -Sentiment trend is computed using **linear regression** (least squares) rather than a simple first-to-last comparison. This makes the trend robust to outliers and intermediate noise. - ---- - -## SE-domain lexicon - -The following terms have special handling in the sentiment scorer: - -### Technical terms neutralized (not negative) - -These terms are common in SE but trigger VADER's negative scoring: +**Neutralized terms** (whole-word hits pull the comment toward neutral). The +list holds exactly the terms the VADER lexicon scores negative that are +neutral in code comments; a word VADER does not score cannot be misclassified +by it and is deliberately not listed: | Term | Context | |---|---| -| `kill` | Process management | -| `abort` | Transaction/operation cancellation | -| `fatal` | Error severity levels | -| `terminate` | Process/thread lifecycle | -| `panic` | Error handling (Go, Rust) | -| `deprecated` | API lifecycle | -| `execute` | Command/query execution | -| `conflict` | Merge conflicts | -| `critical` | Severity levels | +| `kill`, `killed`, `killing`, `kills` | process management | +| `fatal` | error severity levels | +| `dead` | dead code, dead letters, dead-lock | +| `destroy`, `destroyed` | object lifecycle | +| `panic` | error handling (Go, Rust) | +| `obsolete` | API lifecycle | +| `exploit` | security | +| `conflict`, `conflicts` | merge conflicts | +| `reject`, `rejected` | request handling, promises | +| `critical` | severity levels, critical sections | +| `argument`, `arguments` | function arguments | +| `block`, `blocked` | blocks, blocking calls | +| `interrupt`, `interrupted` | signals, threads | +| `trap` | signal / instruction traps | +| `dump` | core dumps, data dumps | + +**Genuinely negative in SE** (whole-word hits push the comment toward +negative; none of these is scored by the VADER lexicon): + +| Term | Shift | Indication | +|---|---|---| +| `hack` | −0.3 | quick-and-dirty solutions | +| `hacky` | −0.4 | quick-and-dirty solutions | +| `kludge` | −0.5 | inelegant fixes | +| `workaround` | −0.2 | avoiding the root cause | +| `technical debt` | −0.3 | accumulated shortcuts | +| `spaghetti` | −0.4 | poor code structure | +| `nightmare` | −0.4 | maintenance difficulty | + +Words VADER already scores negative (`awful`, `terrible`, `horrible`, `debt`, +`bug`, …) are not listed: they keep their lexicon valence. -### Terms with genuine negative sentiment +#### Comment length weighting -These terms indicate real frustration or code quality issues: +Longer comments carry more weight in a tick's score, as they tend to contain +more meaningful sentiment signal. A comment's weight is its length in characters +over the tick's average comment length, capped at 3× to prevent a single long +comment from dominating. -| Term | Indication | -|---|---| -| `hack` / `hacky` | Quick-and-dirty solutions | -| `kludge` | Inelegant fixes | -| `workaround` | Avoiding root cause | -| `spaghetti` | Poor code structure | -| `nightmare` | Maintenance difficulty | -| `technical debt` | Accumulated shortcuts | +#### Tick scores, trend and periods + +A tick's score is the length-weighted mean of its comments' scores. Only +ticks with at least one scored comment receive a score; a tick of code-only +commits is not "neutral" and not "negative" — it has no data and is absent +from the time series. The trend is the least-squares line through the scored +ticks, evaluated at the first and last scored tick and clamped to `[0, 1]`; +the low-sentiment periods are the scored ticks at or below the negative +threshold, worst first. --- @@ -147,15 +186,16 @@ These terms indicate real frustration or code quality issues: ## Limitations - **Non-English scoring accuracy**: While 32 languages have lexicon coverage via the Chen-Skiena dataset, VADER's grammatical rules (negation handling, intensifiers, punctuation effects) are English-specific. Non-English comments get word-level sentiment from the lexicon but miss syntactic nuances. -- **UAST dependency**: Requires UAST parsing support for the target language. Files in unsupported languages are skipped. +- **UAST dependency**: Requires UAST parsing support for the target language. Files in unsupported languages are skipped (shell scripts fall back to line-wise `#` comments). - **Sarcasm**: Sarcasm, irony, and context-dependent meaning can mislead the classifier. Comments like "great, another production outage" may be scored as positive. - **Comment extraction**: Only comments that appear in the UAST are analyzed. Preprocessor directives, build file comments, and non-code files are excluded. -- **CPU intensive**: The sentiment analyzer performs UAST parsing for every modified file in every commit. For large repositories, this is significantly slower than non-UAST analyzers. It benefits from parallel execution via the framework's worker pool. -- **Minimum length filter**: The default minimum of 20 characters filters out many short but potentially meaningful comments (e.g., `// FIXME`, `// HACK`). Lower `MinLength` to capture these, at the cost of more noise. +- **Whole-comment adjustment**: an SE-domain hit adjusts the whole comment's compound score, not just the matched word, so a positive remark that also mentions `kill` is pulled toward neutral. +- **CPU intensive**: The sentiment analyzer parses both versions of every modified file in every commit. For large repositories, this is significantly slower than non-UAST analyzers; the per-commit pre-pass runs in parallel. +- **Minimum length filter**: The default minimum of 20 characters filters out many short but potentially meaningful comments (e.g., `// FIXME`, `// HACK`). Lower `--min-comment-len` to capture these, at the cost of more noise. --- ## See also -- [Sentiment reference](../analyzers/sentiment.md) — configuration keys and output schema. +- [Sentiment reference](../analyzers/sentiment.md) — flags, tick semantics and output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/explanation/shotness.md b/site/explanation/shotness.md index 10c63b55..d12dd29d 100644 --- a/site/explanation/shotness.md +++ b/site/explanation/shotness.md @@ -2,8 +2,8 @@ This page explains the mental model behind the shotness analyzer: what node-level change frequency and coupling mean, how coupling strength is -computed, and how the aggregation pipeline is structured. For configuration -keys and the output schema, see the +computed, and how the walk is structured. For configuration keys, the entity +lifecycle table and the output schema, see the [Shotness reference](../analyzers/shotness.md). --- @@ -15,13 +15,22 @@ individual code entities (functions, methods, classes) across Git history. Unlike the couples analyzer which operates at file granularity, shotness operates at the UAST node level, providing fine-grained co-change analysis. -### Node change frequency +### Entity change frequency -For each code entity matched by the DSL query (functions by default), the analyzer counts how many commits modified lines within that entity's span. Entities that change frequently are "hot" -- they are likely volatile, complex, or central to the system. +For each declaration the structural DSL selects (every function-role node by +default — top-level functions, class methods, nested functions), the analyzer +counts how many commits modified lines within that entity's span. Entities +that change frequently are "hot" -- they are likely volatile, complex, or +central to the system. An entity is identified by its type, its **qualified +name** (`Class.method`, `Receiver.method`, `outer.inner`) and its file, so +same-named methods on different types never merge. -### Node co-change coupling +### Entity co-change coupling -When two code entities are modified in the same commit, their coupling counter is incremented. This produces a fine-grained coupling matrix at the function level, which is more precise than file-level coupling from the couples analyzer. +When two entities are modified in the same commit, their coupling counter is +incremented. This produces a fine-grained coupling matrix at the entity +level, which is more precise than file-level coupling from the couples +analyzer. ### Coupling strength @@ -35,7 +44,7 @@ This ensures the result is always in [0, 1] and provides a meaningful confidence ### Risk classification -Nodes are classified into risk levels based on absolute change counts: +Entities are classified into risk levels based on absolute change counts: | Risk Level | Threshold | Meaning | |---|---|---| @@ -45,25 +54,48 @@ Nodes are classified into risk levels based on absolute change counts: ### How it works -For each commit: - -1. Parse the before and after versions of each changed file into UAST -2. Apply the `dsl_struct` query to select target nodes (e.g., functions) -3. Apply the `dsl_name` query to extract the name of each node -4. Map diff hunks to nodes using line-range overlaps -5. Emit a per-commit TC (Transient Commit result) with touched node deltas and coupling pairs - -After all commits are processed, the Aggregator accumulates TCs into a final report with sorted nodes and a sparse co-change matrix. +For each commit of the window, oldest first: + +1. Compute the tree diff against the first parent (the whole tree for a + root commit); drop the commit if the raw diff exceeds + `--max-changes-per-commit`; apply the language / vendor / generated + filter. +2. Parse the Before and After versions of each changed file into a UAST + (blobs above 256 KiB are skipped). +3. Run the structural query (`dsl_struct`) over each tree to select the + declarations, name each one with the name query (`dsl_name`) and qualify + it with its enclosing declarations and receiver type. +4. Line-diff the two versions and map every hunk to the entities whose + line span it overlaps. +5. Record the commit's **effects**: the entities it touched (one change + each, coupled pairwise), the entities whose declaration or file + disappeared (ended), and the entities a file rename moved. + +Steps 1–4 are pure per-commit work and run in parallel across cores; step 5 +runs sequentially in commit order over the live entity state, so the output +is a pure function of the repository and the flags, never of the host. +Merge commits are skipped (their diff re-shows the side line already +walked) unless the walk is `--first-parent`. ### Architecture -The shotness analyzer follows the **TC/Aggregator** pattern: - -- **Consume phase**: Per-commit processing builds working state (`nodes`, `files` maps for deletion/rename tracking) and emits a `TC{Data: *CommitData}` with node touch deltas and coupling pairs. -- **Aggregation phase**: The `Aggregator` accumulates node counts and coupling matrices from the TC stream. It supports disk-backed spilling via `SpillStore` for memory-bounded operation. -- **Serialization phase**: `SerializeTICKs()` converts aggregated tick data into the `Nodes`/`Counters` report consumed by `ComputeAllMetrics()` and plot generation. - -The `nodes` map remains in the analyzer as working state because `handleDeletion`, `handleInsertion`, `handleModification`, and `applyRename` read and mutate it during `Consume()`. The aggregator maintains its own separate accumulation of counts and couplings. +- **Pre-pass** (`cf-commands`, shared with the other UAST history + analyzers): the tree diff, the per-commit parse cache and the entity + extraction produce one state-independent *product* per change. +- **Reduce** (`cf-commands` → `cf-shotness`): the reducer turns each + commit's products into `EntityEffect` records (touched / ended / moved + from) over the live `ShotnessState`, resolving file-level events into the + entities currently declared in the file. +- **Report** (`cf-shotness`): replaying the effect records in walk order + over an empty state yields the live entity set; its key-sorted + `Nodes`/`Counters` matrix feeds `compute_all_metrics` (hotness, coupling, + hotspots, aggregate), the plot sections and the text report. The ndjson + and timeseries formats list the same per-commit records. + +There is one source of truth — the effect records and the state transition +that replays them — so a multi-analyzer run (which shares the walk with +imports, quality, sentiment and typos) and a standalone run produce the +same report. --- @@ -98,16 +130,17 @@ The `nodes` map remains in the analyzer as working state because `handleDeletion ## Limitations -- **UAST required**: Only languages with UAST parser support are analyzed. Files in unsupported languages are skipped entirely. -- **CPU intensive**: The analyzer performs UAST parsing on both the before and after versions of every changed file in every commit. This makes it one of the most expensive analyzers. It benefits from parallel execution. -- **Name collisions**: If two functions in different files have the same name, they are tracked as distinct nodes (the file path is part of the key). However, if a file is renamed, the analyzer updates all associated nodes. -- **Shallow extraction within a file**: When multiple structural nodes in the same file share the same extracted name (e.g., nested functions with identical names), only one is tracked. The last one encountered wins. Qualified paths (e.g., `OuterClass.innerMethod`) are not built. -- **DSL limitations**: The DSL query must match nodes that have position information (`Pos` field) in the UAST. Nodes without position data cannot be mapped to diff hunks. -- **Large functions**: A change anywhere within a function's line range counts as a change to that function. Very large functions (hundreds of lines) will have inflated change counts. +- **UAST required**: Only languages with UAST parser support are analyzed. Files in unsupported languages, binary blobs, blobs above 256 KiB and files that fail to parse are skipped for that commit. +- **CPU intensive**: The analyzer parses both the before and after versions of every changed file in every commit. The per-commit work is spread across cores; the reduce is sequential. +- **The report is the present**: an entity that no longer exists at the newest commit of the window (its declaration was removed or its file deleted) is dropped, together with its couplings. An in-place rename of a declaration (`f` → `f2`) is an end plus a birth: the history of `f` does not carry over. A file rename does carry everything over. `--shotness-keep-ended` keeps the past instead: ended entities stay with their counts and couplings, marked `ended`, while hotspots and the plot pages still show only live entities. +- **Qualification follows the mapping**: Rust `impl` methods are not qualified by the `impl` target, and a language whose mapping sets no declaration names yields no entities. Two selected nodes in one file that resolve to the same type and qualified name are one entity. +- **Position required**: a selected node without position information cannot be mapped to diff hunks and is never touched. +- **Large entities**: A change anywhere within an entity's line range counts as a change to that entity. Very large functions (hundreds of lines) will have inflated change counts, and a change inside a method also touches the enclosing class when classes are selected. +- **Merge commits** are not counted (see the lifecycle table in the reference), so a side line's work is attributed to its own commits. --- ## See also -- [Shotness reference](../analyzers/shotness.md) — configuration keys and output schema. +- [Shotness reference](../analyzers/shotness.md) — configuration keys, the entity lifecycle and the output schema. - [Quick start](../getting-started/quickstart.md) — run history analysis. diff --git a/site/explanation/typos.md b/site/explanation/typos.md index 6ed0ceae..1852cf61 100644 --- a/site/explanation/typos.md +++ b/site/explanation/typos.md @@ -17,15 +17,17 @@ and their corrections by analyzing UAST identifier changes across Git history. For each commit, the analyzer: -1. Computes the diff between the old and new versions of each changed file -2. Identifies delete/insert hunk pairs of equal size (same number of lines) -3. Compares corresponding lines using **Levenshtein distance** -4. For line pairs within the distance threshold, extracts UAST identifiers from the old and new versions -5. If exactly one identifier changed between the two versions, it records a typo-fix pair +1. Computes the line diff between the old and new versions of each changed file (both versions must be text and parse with the UAST) +2. Aligns delete/insert hunk pairs of equal size (line *i* of each) +3. Keeps the aligned line pairs whose whole lines are within the distance threshold — a cheap prefilter that bounds the UAST work +4. Extracts the UAST identifiers of each kept line pair and diffs them as multisets: identifiers present on both lines cancel, whatever their position +5. Records a typo-fix pair when exactly one identifier remains on each side, both are non-empty and different, and the two identifiers are within the distance threshold of each other + +The distance is therefore applied twice: to the whole line (prefilter) and to the identifier pair (the rule). A literal change next to an unchanged identifier leaves nothing after cancellation and is never a typo; a rename to an unrelated name fails the pair bound. ### Levenshtein distance -The Levenshtein distance is the minimum number of single-character edits (insertions, deletions, substitutions) needed to transform one string into another. A small distance between the old and new line suggests a typo fix rather than a semantic change. +The Levenshtein distance is the minimum number of single-character edits (insertions, deletions, substitutions) needed to transform one string into another. A small distance between the old and new identifier suggests a typo fix rather than a semantic change. !!! info "Example" - `recieve` to `receive` -- distance 2 (transposed characters) @@ -36,13 +38,11 @@ The Levenshtein distance is the minimum number of single-character edits (insert ## Architecture -The typos analyzer follows the **TC/Aggregator pattern**: - -1. **Consume phase**: For each commit, `Consume()` computes diffs, identifies line pairs within Levenshtein distance, and extracts UAST identifier changes. Per-commit typos are returned as `TC{Data: []Typo}`. The analyzer retains no per-commit state; only the `lcontext` (Levenshtein context) is kept as working state. -2. **Aggregation phase**: A `typos.Aggregator` collects TCs into a `SliceSpillStore[Typo]`. `FlushTick()` deduplicates typos by `wrong|correct` key (keeping the first occurrence), returning a `TickData` with the unique set. -3. **Serialization phase**: `SerializeTICKs()` assembles all tick data into an `analyze.Report{"typos": allTypos}`, then delegates to `ComputeAllMetrics()` for JSON, YAML, binary, or HTML plot output. +The detection rules and the metrics live in the `cf-typos` crate; the history walk in `cf-commands` supplies the git and UAST inputs: -This separation enables streaming output, budget-aware memory spilling, and decoupled aggregation. +1. **Per-commit product**: for every Modify change the walk reads both blobs, runs the libgit2 line diff and both UAST parses, and asks `cf_typos::candidate_line_pairs` for the aligned line pairs within the prefilter and `cf_typos::identifier_pair` for the one identifier each pair changed. The product is the commit's list of occurrences (file, 1-based line, commit); it is a pure function of the commit, so it is computed in parallel across cores. +2. **Chronological reduce**: the shared history core stamps every commit with its identity and tick in oldest-first order, and the occurrences are concatenated in that order — the report never depends on how the work was distributed. +3. **Metrics**: `cf_typos::metrics_report_value` derives `typo_list` (each distinct pair once, first fix wins), `patterns` (occurrence counts above one), `file_typos` and `aggregate` from the occurrence list, as one value tree that json, yaml and bin encode. --- @@ -59,10 +59,10 @@ This separation enables streaming output, budget-aware memory spilling, and deco ## Limitations - **UAST required**: Only languages with UAST parser support are analyzed. Identifiers in unsupported languages are not extracted. -- **Single-identifier changes only**: The analyzer only records a typo when exactly one identifier changes between the old and new lines. Multi-identifier changes are skipped to avoid false positives. +- **Single-identifier changes only**: The analyzer only records a typo when exactly one identifier differs between the old and new lines after the shared identifiers cancel. Multi-identifier changes are skipped to avoid false positives. - **Equal-length hunks only**: Only delete/insert hunk pairs with the same number of lines are considered. A typo fix that also adds or removes lines will be missed. - **False positives**: Intentional identifier renames with small Levenshtein distance (e.g., `idx` to `jdx`) will be reported as typos. -- **Deduplication**: Typo pairs are deduplicated by the `wrong|correct` key. The same typo fixed in multiple commits is reported only once. +- **One list entry per pair**: `typo_list` reports each `wrong → correct` pair once, at its first fix; the number of times the pair was fixed is in `patterns` (when above one) and in the per-file and aggregate counts. - **CPU intensive**: Like all UAST-based analyzers, the typos analyzer parses both file versions for every changed file in every commit. --- diff --git a/site/getting-started/first-analysis.md b/site/getting-started/first-analysis.md index ef6fbe99..dc2a945d 100644 --- a/site/getting-started/first-analysis.md +++ b/site/getting-started/first-analysis.md @@ -260,11 +260,12 @@ the universal formats; some also support format-specific extras. === "Plot (HTML)" ```bash - codefang run -a history/burndown --format plot . > report.html + codefang run -a history/burndown --format plot -o report . ``` - Opens as an interactive chart in any browser. Multiple analyzers produce - a multi-section dashboard with tabs. + Writes `report/index.html` plus one interactive page per analyzer + (`report/burndown.html`, …) and `report/report.json`; `-o` is required. + Open `report/index.html` in any browser. === "Time Series" @@ -291,50 +292,46 @@ the universal formats; some also support format-specific extras. ## Configuration file -Instead of passing flags every time, create a `.codefang.yaml` file in your -repository root (or `$HOME`): +Instead of passing flags every time, create a `.codefang.yaml` file in the +directory you run `codefang` from (or in `$HOME`); `--config <file>` points +at any other file. Every key is the default for one `run` flag: ```yaml title=".codefang.yaml" -# Analyzers to run by default (empty = all registered). +# Analyzers to run when -a is not given (omitted = all). analyzers: - static/complexity - static/halstead - history/burndown -# Pipeline resource tuning. -pipeline: - workers: 4 # parallel workers (0 = auto) - memory_budget: "4GiB" # auto-tune caches to fit this budget - blob_cache_size: "512MiB" - # History analyzer settings. history: + tick_size: 24 # --tick-size, hours per tick burndown: - granularity: 30 # days per time bucket - sampling: 30 - track_files: false - track_people: false + granularity: 30 # --granularity, ticks per age band + sampling: 30 # --sampling, ticks per sample + track_files: false # --burndown-files + track_people: false # --burndown-people devs: - anonymize: false + anonymize: false # --anonymize sentiment: - min_comment_length: 20 - gap: 0.5 - -# Checkpoint for crash recovery on large repos. -checkpoint: - enabled: true - resume: true + min_comment_length: 20 # --min-comment-len ``` !!! tip "Precedence order" - CLI flags override environment variables, which override the configuration - file. Environment variables use the `CODEFANG_` prefix with underscores - for nesting: + An explicit CLI flag overrides a `CODEFANG_*` environment variable, which + overrides the configuration file, which overrides the compiled default. + Environment variables use the `CODEFANG_` prefix with the key upper-cased + and `.` replaced by `_`: ```bash - CODEFANG_PIPELINE_WORKERS=8 codefang run -a history/burndown . + CODEFANG_HISTORY_BURNDOWN_GRANULARITY=60 codefang run -a history/burndown . ``` + A key that codefang does not read (for example the Go-era `pipeline:` + tuning tree or `checkpoint:`) is ignored with one + `warning: ignoring unsupported config keys: …` line on stderr. See + [Configuration](../guide/configuration.md) for every key. + --- ## Useful flags for history analysis @@ -345,34 +342,29 @@ checkpoint: | `--since` | `--since 2024-01-01` | Only commits at/after this date, duration, or revision | | `--head` | `--head` | Snapshot of HEAD only (fast; ignores the two flags above) | | `--first-parent` | `--first-parent` | Skip merge-commit side branches | -| `--workers N` | `--workers 8` | Parallel pipeline workers | -| `--memory-budget` | `--memory-budget 2GB` | Auto-tune caches | -| `--checkpoint` | `--checkpoint` | Enable crash recovery (on by default) | -| `--silent` | `--silent` | Suppress progress output | +| `--quiet` / `-q` | `-q` | Suppress the `progress:` lines on stderr (alias `--silent`) | +| `--output` / `-o` | `-o report.json` | Write the report to a file instead of stdout | --- -## Converting between formats +## One run per format -You can convert an existing report to a different format without re-running -the analysis: +A report is rendered in the format chosen by `--format` when the analysis +runs; there is no command that converts a saved report into another format. +To get several formats, run the analysis once per format (bound the window +with `--limit` or `--since` to keep each run short): ```bash -# Run once and save as binary -codefang run -a history/burndown --format bin . > report.bin +# Machine-readable JSON +codefang run -a history/burndown --format json -o report.json . -# Convert to YAML later -codefang run --input report.bin --format yaml +# The same analysis as YAML +codefang run -a history/burndown --format yaml -o report.yaml . -# Convert to an HTML plot -codefang run --input report.bin --format plot > dashboard.html +# Interactive HTML pages in ./dashboard +codefang run -a history/burndown --format plot -o dashboard . ``` -!!! note "Supported input formats" - The `--input` flag accepts **JSON** and **binary** files. Use - `--input-format auto` (the default) to let Codefang detect the format, or - specify `--input-format json` or `--input-format bin` explicitly. - --- ## Next steps diff --git a/site/getting-started/quickstart.md b/site/getting-started/quickstart.md index 4fc324c8..51ba3f2f 100644 --- a/site/getting-started/quickstart.md +++ b/site/getting-started/quickstart.md @@ -120,7 +120,7 @@ column shows how many of those lines survive into subsequent windows. ## 4. Developer analysis -See per-developer contribution statistics with a single commit snapshot: +See per-developer contribution statistics for the latest commit: ```bash codefang run -a history/devs --head --format yaml . @@ -129,24 +129,37 @@ codefang run -a history/devs --head --format yaml . ??? example "Expected output (truncated)" ```yaml - devs: - developers: - - name: "Alice" - commits: 142 - added: 12400 - removed: 3200 - changed: 8900 - - name: "Bob" - commits: 87 - added: 6300 - removed: 1800 - changed: 4100 + codefang (v2): + version: 0 + hash: <unknown> + history/devs: + aggregate: + total_commits: 1 + total_lines_added: 2 + total_lines_removed: 0 + total_developers: 1 + active_developers: 1 + developers: + - id: 0 + name: alice + email: alice@example.com + commits: 1 + lines_added: 2 + lines_removed: 0 + lines_changed: 0 + net_lines: 2 + languages: + - language: Go + added: 2 + removed: 0 + changed: 0 ``` !!! info "The `--head` flag" - `--head` tells the history pipeline to analyze only the HEAD commit's - state rather than walking the full history. It is much faster and useful - for snapshot metrics like developer totals. + `--head` analyzes exactly one commit — `HEAD`, diffed against its first + parent — and ignores `--limit` and `--since`. It is the same window + `--limit 1` selects, so the two produce identical reports. Use it for a + fast snapshot of the latest change; drop it to walk the full history. --- @@ -188,16 +201,16 @@ invocation: Generate a self-contained HTML report with interactive charts: ```bash -codefang run -a history/burndown,history/devs --format plot . +codefang run -a history/burndown,history/devs --format plot -o report . ``` -This writes an HTML page to **stdout**. Redirect it to a file and open in your -browser: +`-o` names the output **directory** (required with `--format plot`). It +receives `index.html`, one page per analyzer (`burndown.html`, `devs.html`) +and `report.json`. Open the index in your browser: ```bash -codefang run -a history/burndown --format plot . > burndown.html -open burndown.html # macOS -xdg-open burndown.html # Linux +xdg-open report/index.html # Linux +open report/index.html # macOS ``` --- @@ -205,8 +218,8 @@ xdg-open burndown.html # Linux ## 7. Pipe into AI Any `--format json` output is machine-friendly and can be fed to an LLM or an -agent. Codefang also provides a **Model Context Protocol** server (the `cf-mcp` -crate) so agents can query analysis results programmatically; see the +agent. Codefang also provides a **Model Context Protocol** server (`codefang mcp`) +so agents can query analysis results programmatically; see the [MCP Integration](../integrations/mcp.md) guide for how to wire it up. !!! note "AI-ready output" diff --git a/site/glossary.md b/site/glossary.md index f2f6cf40..2c4c004a 100644 --- a/site/glossary.md +++ b/site/glossary.md @@ -32,64 +32,47 @@ normal and bare repositories via libgit2. The raw content of a file at a specific Git object hash. History analysis reads blobs to compute diffs and parse code. -## Blob cache - -A core plumbing analyzer (`BlobCacheAnalyzer`) that caches blob content so the -pipeline can re-read the same file content without fetching it from Git again. -It wraps an LRU cache. - -## Blob pipeline - -One of the three parallel stages the Coordinator runs. The blob pipeline loads -blob content from Git for the commits in a chunk. - ## Burndown A history analysis that tracks code age and survival: how many lines written in a given period are still present in the codebase over time. The result is often shown as a burndown chart. -## Checkpoint +## Change filter -A serialized snapshot of analyzer state written after each streaming chunk so an -interrupted run can recover instead of restarting from the first commit. +The per-run rule that decides which changed files a history analyzer sees: +`--languages`, the vendored and generated path policy (`--include-vendored`, +`--include-generated`, `--extra-excluded-prefixes`), the generated-content +marker in the first 512 bytes, and `--max-changes-per-commit`. -## Chunk +## Chronological reduce -A memory-bounded batch of commits. The streaming pipeline splits a large commit -history into chunks so memory stays within budget. +The single-threaded, oldest-first fold of every commit's product into an +analyzer's report. Doing it in one fixed order is what makes history output +byte-identical across runs and machines. ## Cohesion -A static metric measuring how strongly the members of a class or module belong -together. Codefang reports cohesion using metrics such as LCOM. +A static metric measuring how strongly the functions of a file belong together, +computed as LCOM over the variables each function reads or writes. Codefang +reports `1 − LCOM` as the cohesion score. -## Coordinator +## Commit window -The component that orchestrates a worker pool across the three pipeline stages -(blob, diff, and UAST) for each chunk of commits. +The commits a history run analyzes: the newest `--limit` commits, or every +commit authored since `--since`, or just `HEAD` with `--head`, delivered to the +analyzers oldest-first. ## Couples A history analysis that detects co-change coupling: files that tend to change together in the same commits. Useful for risk-aware refactoring. -## Diff pipeline - -One of the three parallel stages the Coordinator runs. The diff pipeline computes -per-commit tree and file diffs. - ## Halstead A family of static software-science metrics (volume, difficulty, effort, and related measures) derived from the operators and operands in code. -## Hibernate/boot cycle - -The state-management cycle of the streaming pipeline. Between chunks a -hibernatable analyzer serializes (hibernates) its state to a compact form, then -reboots to resume. This keeps memory bounded across a long history walk. - ## History mode The analysis mode that walks a Git repository's commit history and runs history @@ -97,32 +80,16 @@ analyzers. Contrast with static mode, which reads source files from disk. ## Identity detection -A core plumbing step (`IdentityDetector`) that maps commit authors to canonical -identities so a single developer with multiple names or emails counts as one -person. +The step (`IdentityDetector`) that maps commit authors to canonical developer +ids, so one developer with several names or emails counts as one person. By +default a known email or name unifies the two; `--exact-signatures` and +`--people-dict` change the rule. -## Leaf analyzer +## Per-commit product -A history analyzer that consumes the output of the core plumbing analyzers — for -example burndown, couples, or devs. Leaf analyzers run after plumbing in the -pipeline. - -## Pipeline stage - -One of the three parallel work types the Coordinator dispatches per chunk: blob -loading, diff computation, and UAST parsing. - -## Plumbing - -The shared core analyzers that all history analyzers depend on (tree diff, blob -cache, identity detection, tick assignment, line stats, language detection, UAST -changes). Plumbing runs before any leaf analyzer. - -## Runner - -The component that coordinates the full history-analysis lifecycle: initialize, -process each chunk, then finalize with aggregators. It supports single-pass and -streaming execution strategies. +The order-independent work for one commit (tree diff with rename detection, line +diffs, blob reads, UAST parses) computed in parallel before the chronological +reduce. Its result depends only on that commit. ## Sentiment @@ -138,17 +105,11 @@ functions are touched most often (structural hotness). The analysis mode that reads source files from disk, parses them into UASTs, and runs static analyzers. It requires no Git history. -## Streaming pipeline - -The execution strategy that processes a large history in memory-bounded chunks -with hibernate/boot cycles and optional double-buffered pipelining, planned by -the streaming planner. - ## Tick -A discrete time index assigned to commits so analyses can build time series. The -`TicksSinceStart` plumbing analyzer assigns a tick to each commit relative to the -start of the history. +A discrete time index assigned to commits so analyses can build time series. +The first commit's committer time, floored to the tick size (`--tick-size`, +default 24 hours), is tick 0; ticks never go backwards. ## Typos diff --git a/site/guide/cli-reference.md b/site/guide/cli-reference.md index 469a2fbb..f6d6b9b4 100644 --- a/site/guide/cli-reference.md +++ b/site/guide/cli-reference.md @@ -2,7 +2,9 @@ Codefang ships two binaries: **`codefang`**, the analyzer engine, and **`uast`**, the Universal Abstract Syntax Tree parser. This page is the exhaustive reference for -every command, sub-command, and flag. +every command, sub-command, and flag. Every flag listed by `codefang run --help` +is documented here and is read by the pipeline; a flag that is accepted but +has no effect is a bug. --- @@ -11,6 +13,17 @@ every command, sub-command, and flag. The main analysis binary. It runs static and history analyzers against source code and Git repositories. +#### Global flags + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--verbose` | `-v` | `bool` | `false` | Print run details on stderr: `progress: config file: <path>` (or `none`) and `progress: analyzers: <resolved ids>` | +| `--quiet` (alias `--silent`) | `-q` | `bool` | `false` | Suppress the `progress:` lines on stderr; warnings and errors still print. `-v` and `-q` together are a usage error. | +| `--profile` | | `bool` | `false` | Memory watchdog: logs `MEM t=<s> RSS=<MiB> threads=<n>` to stderr at start and every 2 s, writes `/proc/self/maps` to `$TMPDIR/maps_baseline.txt` at start and `maps_spike_<MiB>MiB.txt` on the first RSS spike above 4096 MiB (Linux) | + +Global flags are accepted before or after the subcommand +(`codefang -q run …` ≡ `codefang run -q …`). + ### `codefang run` The primary entry point. Runs selected static and/or history analyzers on a @@ -26,7 +39,8 @@ If `path` is omitted the current directory (or the value of `--path`) is used. | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--analyzers` | `-a` | `[]string` | `nil` | Analyzer IDs or glob patterns. Comma-separated. | +| `--analyzers` | `-a` | `[]string` | all | Analyzer IDs or glob patterns. Comma-separated. Omitted ⇒ every analyzer. | +| `--list-analyzers` | | `bool` | `false` | Print every analyzer ID (one per line) and exit | Analyzer IDs follow a `<category>/<name>` convention. Globs are supported: @@ -50,8 +64,8 @@ codefang run -a '*' . ??? info "Available Analyzer IDs" **Static analyzers:** - `static/complexity`, `static/comments`, `static/halstead`, - `static/cohesion`, `static/imports` + `static/clones`, `static/cohesion`, `static/comments`, `static/complexity`, + `static/composition`, `static/halstead`, `static/imports` **History analyzers:** `history/anomaly`, `history/burndown`, `history/couples`, @@ -63,61 +77,95 @@ codefang run -a '*' . | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--format` | | `string` | `json` | Output format: `json`, `text`, `compact`, `yaml`, `plot`, `bin`, `timeseries` | -| `--verbose` | `-v` | `bool` | `false` | Enable detailed output (RSS/memory stats in progress logs, full static report details) | -| `--silent` | | `bool` | `false` | Suppress progress output on stderr | -| `--no-color` | | `bool` | `false` | Disable colored static output | +| `--format` | | `string` | `json` | Output format: `json`, `yaml`, `bin`, `text`, `compact`, `ndjson`, `timeseries`, `plot` | +| `--ndjson` | | `bool` | `false` | With `--format timeseries`: emit one JSON line per commit instead of the merged document. With any other format the run fails with `Error: invalid --ndjson: requires --format timeseries, got --format <f>`. | +| `--output` | `-o` | `string` | `""` | Write the report to this file (created or truncated) instead of stdout. With `--format plot` it is the output directory for the HTML pages, and required. | +| `--per-file` | `-F` | `bool` | `false` | Add one `files` entry per analyzed file to every static section. **`--format json` only**: with any other format the flag is ignored and the run prints `--per-file applies to --format json only; ignored for <format>` on stderr (once). | +| `--no-color` | | `bool` | `false` | Disable ANSI color in `text` / `compact` output (same as `NO_COLOR=1`) | + +Which formats a selection accepts: + +| Selection | Accepted formats | +|-----------|------------------| +| static only | `json`, `yaml`, `bin`, `text`, `compact`, `plot` | +| history only | `json`, `yaml`, `bin`, `text`, `compact`, `ndjson`, `timeseries` (+ `--ndjson`), `plot` | +| mixed static + history | `json`, `yaml`, `bin`, `ndjson`, `timeseries`, `plot` | + +`text` is supported by the six history analyzers with a text renderer +(sentiment, shotness, burndown, couples, devs, file-history); selecting any +other history analyzer with `--format text` fails with +`serialization error for <Name>: unsupported format: text`. ```bash # Human-readable table codefang run -a 'static/*' --format text . +# One summary line per history analyzer +codefang run -a 'history/*' --format compact . + # Interactive HTML charts -codefang run -a 'history/*' --format plot . +codefang run -a 'history/*' --format plot --output ./report . + +# The report in a file instead of stdout +codefang run -a static/complexity --format json -o complexity.json . # Unified time-series JSON codefang run -a 'history/devs,history/sentiment' --format timeseries . ``` -#### Path & input flags - -| Flag | Short | Type | Default | Description | -|------|-------|------|---------|-------------| -| `--path` | `-p` | `string` | `.` | Folder or repository path to analyze | -| `--input` | | `string` | `""` | Input report path for cross-format conversion | -| `--input-format` | | `string` | `auto` | Input format: `auto`, `json`, `bin` | +!!! note "Several analyzers, one document" + A `json` or `yaml` run that selects more than one analyzer — a mixed + static+history selection, or several history analyzers — always emits + **one** document: + + ```json + { + "version": "codefang.run.v1", + "metadata": { "...": "run provenance" }, + "analyzers": [ + { "id": "history/devs", "mode": "history", "report": { "...": "the devs report" } }, + { "id": "history/burndown", "mode": "history", "report": { "...": "the burndown report" } } + ] + } + ``` -!!! tip "Format Conversion" + A single analyzer emits its own report as the document root. Several + **static** analyzers in `json`/`text` merge their sections into one report + with an `overall_score`. - Use `--input` to convert a previously generated report to a different - format without re-running analysis: +#### Path flags - ```bash - codefang run -a 'history/*' --format bin . > report.bin - codefang run -a 'history/*' --input report.bin --format plot - ``` +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--path` | `-p` | `string` | `.` | Folder or repository path to analyze (the positional `path` wins when both are given) | #### Git history flags | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--limit` | `int` | `0` | Analyze at most N commits — the N **newest** (`0` = no limit) | -| `--since` | `string` | `""` | Only analyze commits at/after this time or revision | -| `--first-parent` | `bool` | `false` | Follow only first parent of merge commits | -| `--head` | `bool` | `false` | Analyze only HEAD commit (ignores `--limit` and `--since`) | +| `--limit` | `int` | `0` | Analyze at most N commits — the N **newest** (`0` = no limit). Negative values are rejected. | +| `--since` | `string` | `""` | Only analyze commits **authored** at/after this time or revision | +| `--first-parent` | `bool` | `false` | Follow only the first parent of merge commits | +| `--head` | `bool` | `false` | Analyze only the HEAD commit (ignores `--limit` and `--since`) | +| `--tick-size` | `int` | `24` | Width of one tick in hours (must be `> 0`). Burndown bands, devs activity, anomaly windows and `commits_by_tick` are bucketed by tick. | +| `--max-changes-per-commit` | `int` | `0` | Drop commits whose raw tree diff exceeds this many changes (`0` = default 10000; negative values are rejected). Dropped commits are invisible to every analyzer; burndown re-inserts an affected file whole the next time it is touched. Raise this on monorepos with legitimate large commits. | +| `--chunk-size` | `int` | planned from the budget | Commits per history chunk: worker threads prepare the per-commit work (diffs, line stats, parses) of at most this many commits ahead of the walk, which folds each commit into its report in commit order and frees it (must be `>= 1`). Overrides the chunk size `--memory-budget` plans; the walk then runs one worker per core. The output is byte-identical for every chunk size. | +| `--memory-budget` | `size` | half the RAM, at most 2 GiB | Memory a history run plans against, e.g. `512MB`, `2GB`, `1.5GiB` (minimum `256MiB`). The history walks running at once share it; each walk sizes its chunk and its worker count from its share and its measured per-worker and per-commit cost, and libgit2 gets a fixed part of it (25 % for its object cache, 7.5 % for mapped pack files). Free heap pages go back to the OS after every chunk. `-v` prints each walk's plan. The output never depends on the budget. | These flags bound the **history** phase only — static analyzers read the -working tree, so neither one affects them. +working tree, so none of them affects a static run. The window is selected **newest-first** (`--limit 10` = the ten most recent commits, like `git log -n 10`) and then handed to the analyzers in chronological oldest-first order, because ticks, burndown line survival and author identity all depend on that order. `--limit` and `--since` compose: -`--since` picks the window, `--limit` caps its newest end. +`--since` picks the window, `--limit` caps its newest end. `--head` is the +single-commit window `[HEAD]` — the same window `--limit 1` selects, so the two +produce byte-identical reports for every analyzer and format. The `--since` flag accepts these forms, in precedence order: -- **Go duration**: `24h`, `168h` (7 days), `1h30m` — units are `h`/`m`/`s` +- **Duration**: `24h`, `168h` (one week), `1h30m` — units are `h`/`m`/`s` only. There is **no** `d` or `w` unit and `m` means **minutes**, so a week is `168h`, not `7d` - **Date**: `2025-01-01` (midnight UTC) @@ -125,8 +173,13 @@ The `--since` flag accepts these forms, in precedence order: - **Git revision**: `v1.2.0`, `HEAD~100`, or a bare SHA — that commit's author time becomes the cutoff +The cutoff compares the commit's **author** time, and every commit authored +at/after it is kept regardless of its position in the committer-time walk +(a commit authored after the cutoff but committed before it is still +included; one authored before the cutoff but committed after it is not). + ```bash -# Last 7 days only +# Everything authored in the last 168h (one week) codefang run -a history/devs --since 168h . # Since a specific date @@ -137,6 +190,9 @@ codefang run -a history/couples --since v1.2.0 . # Only the latest 500 commits codefang run -a history/couples --limit 500 . + +# Hourly ticks +codefang run -a history/devs --tick-size 1 --format timeseries . ``` !!! note "Empty windows and bad values" @@ -146,30 +202,72 @@ codefang run -a history/couples --limit 500 . neither a time form nor a resolvable revision fails immediately with `Error: cannot resolve --since "<value>"`, empty stdout, exit 1. -!!! note "Burndown and `--first-parent`" - - The burndown analyzer automatically enables `--first-parent` when selected. - This is required for correct line-tracking across merge commits. +!!! note "Burndown always walks first-parent" + The burndown analyzer tracks line survival along the main line of + development, so **its own** walk always follows the first parent of merge + commits, whatever `--first-parent` says. This never leaks into the other + analyzers: selecting burndown next to devs, couples or file-history leaves + their walks exactly as the user's `--first-parent` flag configures them. + +!!! note "Renames" + The tree diff detects renames (libgit2 `find_similar`, the `git diff -M` + default of 50 % similarity). A pure `git mv` is one change whose old and + new paths differ and whose blob is unchanged, so devs reports 0 added / + 0 removed lines for it and burndown, couples, file-history and shotness + keep the file's identity across the move. + +!!! note "Deterministic output" + A history report is a pure function of the repository and the flags. It + does not depend on the host's CPU count, the wall clock, or the number of + threads the walk happens to use. #### Language filtering | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--languages` | `[]string` | `[all]` | Restrict analysis to the given Linguist languages; comma-separated. `all` (default) disables the filter. Applies to **both** history and static phases. | - -**History phase** — the filter is pushed down into libgit2's `pathspec` -at the tree-diff stage, so non-matching files are skipped before the -diff is computed. On a polyglot repo a narrow filter can reduce wall -time by 30–40 %. A content-based language check still runs as the -authoritative pass for ambiguous extensions (`.h`, `.pl`, `.m`, `.r`). - -**Static phase** — the filter is applied at the directory walker -before the UAST parser or raw-file analyzers -see the file. It's path-based only: the parser's own language router -remains the final authority for how a matched file is parsed (e.g. a -`.h` under `--languages c++` is still parsed as C). Both phases read -from the same `langpath.Globs` helper, so the flag value has one -meaning across `-a 'static/*'`, `-a 'history/*'`, and `-a '*'` runs. +| `--languages` | `[]string` | `[all]` | Restrict analysis to the given Linguist languages; comma-separated. `all` (default), an absent flag and an explicit empty value (`--languages ""`) all disable the filter. Applies to **both** history and static phases with one meaning. In the static phase each language resolves to its Linguist filename globs, matched against the file's **basename** (`*.go`, `*.py`, `Dockerfile`, …). | + +**History phase** — each changed file is classified with the full enry +cascade over its basename **and** its blob content, so ambiguous extensions +(`.h`, `.m`, `.pl`, `.r`) and extensionless scripts (shebang) resolve +correctly; changes whose language is not in the set are dropped before any +analyzer sees them. + +**Static phase** — the filter is applied by the ONE directory walk every +static analyzer shares, before the UAST parser or raw-file analyzers see the +file: the language globs are matched against the basename, so the parser's +own language router remains the final authority for how a matched file is +parsed. Both phases read the same `langpath` tables, so the flag value has +one meaning across `-a 'static/*'`, `-a 'history/*'`, and `-a '*'` runs. + +#### The static walk + +Every `static/*` analyzer reads the same walk of the path you pass +(a directory or a single file): + +- directories are visited in lexical order, depth-first; `.git` and any + directory carrying a `CACHEDIR.TAG` (Cargo's `target/`) are skipped; + symlinked directories are never followed (a symlink loop cannot recurse + and the walk cannot leave the root); symlinks to regular files are read; +- a file is admitted when the UAST parser supports its extension, its + basename matches `--languages`, and its **root-relative** path passes the + vendor / generated / `--extra-excluded-prefixes` policy; +- each admitted file is read **once**; a file larger than **64 MiB** or one + that cannot be read is skipped with one stderr line + (`warning: skipped <path>: larger than 64 MiB` / `: unreadable`) for the + whole run, however many analyzers are selected, and counted once in the + end-of-run `progress: skipped N unreadable/oversized/unparseable files` + line; +- the generated-content marker (`DO NOT EDIT`, `Code generated`, + `@generated`, … in the first 512 bytes) is applied to the bytes just read + unless `--include-generated`; +- a single-file path analyzes that one file and stamps its **name** + (`a.go`, never `.`) on every per-file entry and issue location. + +`static/composition` is the designed exception: it exists to report +vendored / generated / binary shares, so it classifies **every** regular +file the walk reaches (reading only the first 8 KiB of each); only +`--languages` and `--extra-excluded-prefixes` narrow it. Language names are [Linguist keys](https://github.com/github/linguist/blob/master/lib/linguist/languages.yml) and common aliases resolve automatically: @@ -178,13 +276,14 @@ and common aliases resolve automatically: # Canonical names (any case, whitespace is trimmed) codefang run -a 'history/devs' --languages go,python,typescript . -# Aliases resolve via enry +# Aliases resolve via the Linguist tables codefang run -a 'history/devs' --languages golang,js,ts . -# Unknown language fails fast at configure time instead of silently -# returning an empty report: +# Unknown language fails fast instead of silently returning an empty report: codefang run -a 'history/devs' --languages notalang . -# → Error: failed to configure TreeDiff: tree-diff pathspec: unknown language: "notalang" +# → Error: history --languages: unknown language: "notalang" +codefang run -a 'static/complexity' --languages notalang . +# → Error: static --languages: unknown language: "notalang" ``` Filename-only languages (e.g. `Dockerfile`, `Makefile`) are also supported: @@ -193,9 +292,6 @@ Filename-only languages (e.g. `Dockerfile`, `Makefile`) are also supported: codefang run -a 'history/devs' --languages dockerfile . ``` -See `specs/optimize-lang/PROPOSAL.md` for the architecture and acceptance-gate -numbers. - #### Vendor & generated exclusion By default, Codefang excludes **vendored dependencies** and **auto-generated @@ -205,11 +301,13 @@ single-language analyser (`go vet`, `eslint`, `ruff`, `rubocop`, `scalafix`, | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--include-vendored` | `bool` | `false` | Re-include vendored dependencies (detected by enry / Linguist) in analysis. Cross-language: covers `vendor/`, `node_modules/`, `third_party/`, `testdata/`, minified bundles, and more. | -| `--include-generated` | `bool` | `false` | Re-include auto-generated files in analysis. Covers `*.pb.go`, `zz_generated_*.go`, `*_pb2.py`, `*.min.js`, and any file whose first 512 bytes contain a generated-file marker (`DO NOT EDIT`, `Code generated`, etc.). | -| `--extra-excluded-prefixes` | `[]string` | `[]` | Additional UNIX path prefixes to exclude on top of enry heuristics (e.g. `".venv/,target/,build/"`). | +| `--include-vendored` | `bool` | `false` | Re-include vendored dependencies (detected by the Linguist vendor patterns over the **root-relative** path) in analysis. Cross-language: covers `vendor/`, `node_modules/`, `third_party/`, `testdata/`, minified bundles, and more. `static/composition` counts vendored files regardless. | +| `--include-generated` | `bool` | `false` | Re-include auto-generated files in analysis. Covers `*.pb.go`, `zz_generated_*.go`, `*_pb2.py`, `*.min.js` (path heuristics) **and** any file whose first 512 bytes contain a generated-file marker (`DO NOT EDIT`, `Code generated`, `@generated`, …) — the static walk reads every admitted file and applies the marker to it; the history filter applies it to the blob header. `static/composition` counts generated files regardless. | +| `--extra-excluded-prefixes` | `[]string` | `[]` | Additional UNIX path prefixes to exclude on top of the built-in heuristics (e.g. `".venv/,target/,build/"`), matched against the **root-relative** path (`gen/` excludes `<root>/gen/…` whatever the root spelling). Always excludes, even with the include flags set, and narrows `static/composition` too. | -All three flags apply **identically** to both static and history phases. +All three flags apply **identically** to both static and history phases: the +history change filter reads the blob header for the generated-file marker +exactly as the static walk reads the bytes of each admitted file. ```bash # default: your own code only @@ -218,117 +316,151 @@ codefang run -a '*' . # include vendored deps (node_modules/, vendor/, …) codefang run -a '*' --include-vendored . -# restore pre-codefang-2026-04 behavior (include everything) +# include everything codefang run -a '*' --include-vendored --include-generated . -# skip extras that enry doesn't know about (e.g. Python venv, Rust target/) +# skip extras the heuristics do not know about (e.g. Python venv, Rust target/) codefang run -a '*' --extra-excluded-prefixes '.venv/,target/' . ``` -!!! warning "Breaking change in 2026-04" +!!! warning "Removed spellings" + The hidden `--skip-blacklist` and `--blacklisted-prefixes` flags were + accepted and ignored; they are removed and fail as unknown arguments. + Drop `--skip-blacklist` (the default already excludes) and write + `--blacklisted-prefixes X,Y` as `--extra-excluded-prefixes X,Y`. - Earlier versions of Codefang analysed vendored and generated files by - default (they needed the confusingly-named `--skip-blacklist=true` to be - excluded). Starting from 2026-04, defaults flip: vendor / generated - are **excluded by default**. To restore the old behavior: - - ```bash - codefang run ... --include-vendored --include-generated - ``` - - The deprecated `--skip-blacklist` and `--blacklisted-prefixes` flags - still work with a cobra deprecation warning and will be removed in - the next minor release. Map them to: - - - `--skip-blacklist` → no-op (the new default already excludes) - - `--blacklisted-prefixes X,Y` → `--extra-excluded-prefixes X,Y` - -See `specs/exclude-vendored/PROPOSAL.md` for the full cross-phase design -and `specs/frds/FRD-20260419-exclude-vendored.md` for implementation -details. - -#### Pipeline tuning flags +#### Developer identity | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--workers` | `int` | `0` | Parallel workers (`0` = CPU count via `GOMAXPROCS`) | -| `--buffer-size` | `int` | `0` | Internal pipeline channel size (`0` = `workers * 2`) | -| `--commit-batch-size` | `int` | `0` | Commits per processing batch (`0` = default 100) | -| `--blob-cache-size` | `string` | `""` | Max blob cache size (e.g. `256MB`, `1GB`; empty = 1 GB) | -| `--diff-cache-size` | `int` | `0` | Max diff cache entries (`0` = default 10000) | -| `--blob-arena-size` | `string` | `""` | Memory arena for blob loading (e.g. `4MB`; empty = 4 MB) | -| `--memory-budget` | `string` | `""` | Memory budget for auto-tuning (e.g. `512MB`, `2GB`). Applies to both history and static phases — auto-derives worker count, spill thresholds, and Go memory limit | +| `--people-dict` | `path` | `""` | File with one person per line, `|`-separated identity tokens (names and emails, matched case-insensitively). With a dictionary, an author matching no line is reported as `<unmatched>`. An unreadable file is an error. | +| `--exact-signatures` | `bool` | `false` | Match the whole `name <email>` signature instead of merging authors that share a name **or** an email. Yields considerably more identities. | +| `--anonymize` | `bool` | `false` | Replace developer names in every report with `Developer-A`, `Developer-B`, … | -```bash -# Large repository with constrained memory -codefang run -a 'history/*' --workers 4 --memory-budget 2GB . +By default an author is identified by **either** the lowercased email or the +lowercased name, and a hit on one token registers the other: `alice <a@x>`, +`Alice Smith <a@x>` and `Alice Smith <a@y>` are one developer. One identity +detector serves every history analyzer, so devs, burndown, couples, +file-history and anomaly agree on author ids. -# Static analysis with memory budget (auto-tunes workers and spill thresholds) -codefang run -a 'static/*' --memory-budget 512MB . +``` +# people.txt +Alice|alice@example.com|alice@other.org +Bob|bob@example.com +``` -# High-throughput with large caches -codefang run -a 'history/*' --blob-cache-size 2GB --diff-cache-size 50000 . +```bash +codefang run -a history/devs --people-dict people.txt . +codefang run -a history/devs --anonymize --format text . ``` -#### GC tuning flags +#### Analyzer options + +| Flag | Type | Default | Applies to | Description | +|------|------|---------|------------|-------------| +| `--typos-max-distance` | `int` | `4` | `history/typos` | Maximum Levenshtein distance between the two identifiers of a typo-fix pair (also bounds the whole-line prefilter); must be `> 0` | +| `--granularity` | `int` | `30` | `history/burndown` | Ticks per age band of the survival matrix (must be `> 0`; `Error: invalid --granularity: must be > 0, got <v>`) | +| `--sampling` | `int` | `30` | `history/burndown` | Ticks per sample row of the survival matrix (must be `> 0`; independent of `--granularity`) | +| `--burndown-files` | `bool` | `false` | `history/burndown` | Per-file survival (`file_survival`, `tracked_files`, ndjson `FileDeltas`); with `--burndown-people` also per-file ownership | +| `--burndown-people` | `bool` | `false` | `history/burndown` | Per-developer survival and the interaction matrix (`developer_survival`, `interactions`, `tracked_developers`, ndjson `PeopleDeltas` / `MatrixDeltas`, the text "Top Developers" section) | +| `--anomaly-window` | `int` | `20` | `history/anomaly` | Trailing window size in ticks for the rolling mean and standard deviation (must be `>= 2`; a shorter history uses every earlier tick) | +| `--anomaly-threshold` | `float` | `2.0` | `history/anomaly` | Z-score threshold, in standard deviations, above which a tick is flagged (must be `> 0`) | +| `--empty-commits` | `bool` | `false` | `history/devs` | Count commits whose filtered diff is empty (trivial merges, `--allow-empty` commits, commits touching only excluded paths) as commits with zero lines | +| `--shotness-dsl-struct` | `string` | `rfilter(.roles has "Function")` | `history/shotness` | UAST DSL query selecting the tracked declarations anywhere in each file (`filter(...)` would see the top level only); validated before the walk, an invalid expression is a flag error | +| `--shotness-dsl-name` | `string` | `.props.name` | `history/shotness` | UAST DSL query extracting a declaration's own name (applied to selected nodes and their enclosing declarations); validated before the walk | +| `--shotness-keep-ended` | `bool` | `false` | `history/shotness` | Keep entities whose declaration or file was removed, marked `"ended": true` with their counts and couplings (adds `ended_nodes`); hotspots and plot pages still show only live entities. Grows the report on long histories | +| `--min-comment-len` | `int` | `20` | `history/sentiment` | Minimum comment length in characters to analyze; `0` disables the length filter, a negative value is rejected (`Error: invalid --min-comment-len: …`) | + +#### Configuration | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--gogc` | `int` | `0` | GC target percentage (`0` = Go default 100) | -| `--ballast-size` | `string` | `"0"` | GC ballast allocation (`0` = disabled) | +| `--config` | `string` | `""` | Configuration file. Without it `./.codefang.yaml`, then `$HOME/.codefang.yaml` is read when present. An explicit file that cannot be read is an error. | -#### Checkpoint & resume flags - -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--checkpoint` | `bool` | `true` | Enable checkpointing for crash recovery | -| `--checkpoint-dir` | `string` | `""` | Checkpoint directory (default: `~/.codefang/checkpoints`) | -| `--resume` | `bool` | `true` | Resume from checkpoint if available | -| `--clear-checkpoint` | `bool` | `false` | Clear existing checkpoint before run | +Every key of the file — and its `CODEFANG_*` environment variable — sets +the **default** of one flag; the precedence is explicit flag > environment +> config file > compiled default. `--config /dev/null` runs with the +compiled defaults only. Keys nothing reads are ignored with one +`warning: ignoring unsupported config keys: …` line. See +[Configuration](configuration.md) for the key → flag table. ```bash -# Disable checkpointing entirely -codefang run -a history/burndown --checkpoint=false . +codefang run -a history/anomaly --config ./ci.codefang.yaml . +``` -# Resume a previously interrupted run -codefang run -a 'history/*' --resume . +No checkpoint store, incremental cache or trace exporter is linked into the +binary, so the Go-era `--checkpoint`, `--checkpoint-dir`, `--resume`, +`--clear-checkpoint`, `--cache-dir`, `--no-cache` and `--debug-trace` flags +are removed; passing one fails as an unknown argument. A run always analyzes +the whole requested window — bound it with `--limit` / `--since`. + +#### Errors + +Every user error prints `Error: <message>` to stderr, leaves stdout empty and +exits 1: + +| Situation | Message | +|-----------|---------| +| the path does not exist | `path not found: <path>` | +| a history analyzer on a directory that is not a git repository | `cannot open repository: <path>` | +| an unknown `--format` | `unsupported format: <value>` | +| a known format the selection cannot render (e.g. static-only `ndjson`, mixed `compact`) | `format <value> is not supported for <analyzer>` | +| an unknown analyzer id | `unknown analyzer id: <id>` | +| `--limit`, `--tick-size`, `--max-changes-per-commit`, `--chunk-size`, `--granularity`, `--sampling`, `--typos-max-distance`, `--anomaly-window`, `--anomaly-threshold`, `--min-comment-len` out of range | `invalid --<flag>: must be … , got <value>` | +| a `--memory-budget` that is not a size, or below `256MiB` | `invalid --memory-budget: "<value>" is not a size (examples: 512MB, 2GB, 1.5GiB)` / `… is below the minimum 256 MiB` | +| `--ndjson` without `--format timeseries` | `invalid --ndjson: requires --format timeseries, got --format <f>` | +| `--verbose` with `--quiet` | `--verbose and --quiet cannot be used together` | +| an unreadable `--config` file | `cannot read --config <path>: …` | +| a malformed config file or an out-of-range key | `invalid config <path>: <key> must be …, got <value>` (or the parser's message naming the key and line) | +| a malformed `CODEFANG_*` variable | `invalid environment variable <NAME>: …` | +| `-o` cannot be created | `cannot write --output <path>: …` | +| an unresolvable `--since` | `cannot resolve --since "<value>": …` | +| an unknown `--languages` token | `static --languages: unknown language: "<token>"` / `history --languages: …` | +| an unreadable `--people-dict` | `cannot read --people-dict <path>: …` | +| an analyzer produced nothing for an otherwise valid run | `no report produced for <analyzer> --format <format>` | + +A history `--format text` selection that includes an analyzer without a text +renderer writes the header and the section name first, then fails with +`serialization error for <Name>: unsupported format: text`. + +### `codefang version` + +Prints `codefang <version> (commit: <hash>, built: <date>)`: + +- `<version>` — the crate version (or `CF_VERSION` / `GIT_VERSION` set at + build time); +- `<hash>` — the commit the binary was built from: `CF_COMMIT` / + `GIT_COMMIT` / `GIT_HASH` set at build time, else the `HEAD` of the + checkout it was built in, else `unknown`; +- `<date>` — `CF_DATE` / `SOURCE_DATE_EPOCH` set at build time, else + `unknown` (the build never reads the wall clock). -# Start fresh, clearing old checkpoint data -codefang run -a 'history/*' --clear-checkpoint . +```bash +codefang version ``` -#### Profiling & debug flags +The former `codefang render <store-dir>` subcommand only ever printed +"not available" — there is no stored-report format to render from — and is +removed; `codefang run --format plot -o <dir>` writes the HTML pages +directly. -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--profile` | `bool` | `false` | Start pprof HTTP server on `localhost:6060` and memory watchdog (heap dumps on RSS spikes) | -| `--cpuprofile` | `string` | `""` | Write CPU profile to file | -| `--heapprofile` | `string` | `""` | Write heap profile to file | -| `--debug-trace` | `bool` | `false` | Enable 100% OpenTelemetry trace sampling | - -`--profile` is a **global flag** (available on all subcommands). It enables: +### `codefang completion <shell>` -- A pprof HTTP server on `localhost:6060` for live heap/goroutine/CPU inspection -- A memory watchdog that logs RSS every 2s and dumps heap profiles to `/tmp` when RSS exceeds 4 GiB +Generates the shell-completion script for `bash`, `fish`, `powershell` or +`zsh` on stdout, from the live command tree (every flag on this page). +`--no-descriptions` leaves the help texts out of the script. ```bash -# CPU profile a large run -codefang run -a 'history/*' --cpuprofile cpu.prof . - -# Live pprof + memory watchdog -codefang run -a 'static/*' --profile --path ~/sources/kubernetes - -# Full debug tracing -codefang run -a 'history/*' --debug-trace . +codefang completion bash > ~/.local/share/bash-completion/completions/codefang ``` --- -### MCP server +### `codefang mcp` -The Model Context Protocol server lives in the `cf-mcp` crate. It exposes -Codefang analysis capabilities as tools that AI agents can discover and invoke -over stdio: +Serves the Model Context Protocol over stdio (newline-delimited JSON-RPC) until +the client closes stdin. It takes no flags and exposes three tools AI agents +discover with `tools/list` and invoke with `tools/call`: | Tool | Description | |------|-------------| @@ -336,15 +468,17 @@ over stdio: | `codefang_history` | Git history analysis (burndown, couples, devs, sentiment, etc.) | | `uast_parse` | Parse source code into Universal AST | -See the [MCP Integration](../integrations/mcp.md) guide for how to launch and -configure the server with an MCP-compatible client. +Each tool result is the report the CLI renders for the same input. See the +[MCP Integration](../integrations/mcp.md) guide for the tool reference. --- ## `uast` The UAST binary parses source files into a Universal Abstract Syntax Tree and -provides querying, diffing, and exploration utilities. +provides querying, diffing, and exploration utilities. The root command has +no flags of its own; every flag below belongs to its subcommand and is read by +it (`uast <command> --help` lists the same set). ### `uast parse` @@ -358,16 +492,19 @@ uast parse [files...] [flags] |------|-------|------|---------|-------------| | `--language` | `-l` | `string` | `""` | Force language detection override | | `--output` | `-o` | `string` | `""` | Output file (default: stdout) | -| `--format` | `-f` | `string` | `json` | Output format: `json`, `compact`, `tree` | -| `--progress` | `-p` | `bool` | `false` | Show progress for multiple files | +| `--format` | `-f` | `string` | `json` | Output format: `json` (pretty), `compact` (one line per input), `none` (parse only) | | `--all` | | `bool` | `false` | Parse all source files in the codebase recursively | +With no files (and no `--all`), or for a `-` file argument, the source is read +from stdin and parsed as Go unless `-l` names the language. With `-o`, every +input's document is written to the one file, in input order. + ```bash # Parse a single file uast parse main.go -# Parse all Go files with progress -uast parse -p *.go +# Parse all Go files, one compact document per line +uast parse -f compact *.go # Force language detection uast parse -l go main.c @@ -394,15 +531,24 @@ uast query <expression> [files...] [flags] | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| `--input` | `-i` | `string` | `""` | Input file (UAST JSON or source code) | -| `--output` | `-o` | `string` | `""` | Output file (default: stdout) | +| `--input` | `-i` | `string` | `""` | Input file (UAST JSON or source code), queried before any positional files | +| `--output` | `-o` | `string` | `""` | Output file (default: stdout); every input's result is written to it, in input order | | `--format` | `-f` | `string` | `json` | Output format: `json`, `compact`, `count` | -| `--interactive` | `-t` | `bool` | `false` | Interactive query mode | +| `--interactive` | `-t` | `bool` | `false` | Interactive query mode over one file (`--input`, else the single positional file, else source read from stdin) | + +With neither files nor `--input`, or for a `-` file argument, the query runs +over a UAST JSON tree read from stdin. ```bash -# Find all functions +# Find the top-level functions (filter looks at the file's direct children) uast query "filter(.type == 'Function')" main.go +# Find every function-role node anywhere in the tree, methods included +uast query 'rfilter(.roles has "Function")' main.go + +# The name of every declaration in the tree +uast query "rmap(.props.name)" main.go + # Find exported items uast query "filter(.roles has 'Exported')" *.go @@ -412,6 +558,9 @@ uast query "reduce(count)" main.go # Query from piped UAST JSON uast parse main.go | uast query "filter(.type == 'Call')" - +# The same query over an --input file +uast query "filter(.type == 'Call')" -i main.go + # Count format uast query -f count "filter(.type == 'Function')" main.go @@ -421,14 +570,29 @@ uast query -t main.go ??? info "DSL Syntax Reference" + A query is a pipeline of stages joined by `|`. `filter` and `map` look + at their input set only — a `filter(...)` at the start of a query runs + over the root's **direct children** — while `rfilter` and `rmap` apply + the same predicate / expression to every node of each input's subtree in + pre-order (the input itself first). + | Expression | Description | |-----------|-------------| - | `filter(.type == "Function")` | Filter by node type | - | `filter(.type == "Call")` | Find function calls | - | `filter(.type == "Identifier")` | Find identifiers | - | `filter(.type == "Literal")` | Find literal values | - | `filter(.roles has "Exported")` | Filter by role annotation | - | `reduce(count)` | Count matching nodes | + | `filter(.type == "Function")` | Direct children of the root whose type is `Function` | + | `rfilter(.type == "Call")` | Every function call anywhere in the tree | + | `rfilter(.type == "Identifier")` | Every identifier | + | `rfilter(.roles has "Function")` | Every function-role node (functions and methods) | + | `filter(.roles has "Exported")` | Direct children carrying a role | + | `rfilter(.type == "Class") \| map(.props.name)` | The names of the classes | + | `rmap(.props.name)` | The `name` property of every node that has one | + | `map(.children)` | The children of each input node | + | `reduce(count)` | Count the matching nodes | + + Field access resolves `children`, `token`, `id`, `roles`, `type`, + `props`, `first`, `last`; any other name reads that property + (`.props.name` ≡ `.name`). Comparisons: `==`, `!=`, `<`, `<=`, `>`, + `>=`, `has` (membership in `roles`). Builtins: `contains`, + `startsWith`, `endsWith`, `has`, `count`, `first`, `last`, `sort`. --- @@ -513,3 +677,68 @@ uast analyze -f json *.go # Generate an HTML report uast analyze -o report.html -f html *.go ``` + +--- + +### `uast validate` + +Validate a UAST JSON document against the UAST JSON Schema. Exit codes: `0` +valid, `1` the document violates the schema, `2` an error (unreadable input, +malformed JSON, unreadable schema). + +```bash +uast validate <file.json|-> [flags] +``` + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--schema` | | `string` | `""` | Path to a UAST JSON schema; empty validates against the schema embedded in the binary | + +```bash +# Validate a parsed tree against the embedded schema +uast parse -o tree.json main.go +uast validate tree.json +``` + +--- + +### `uast mapping` + +Grammar helpers for UAST mapping authors: summarize and classify a +tree-sitter `node-types.json`, generate a `.uastmap` DSL skeleton from it, or +dump the raw tree-sitter tree of source files. + +```bash +uast mapping [files...] [flags] +``` + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--node-types` | | `string` | `""` | Path to `node-types.json` (required unless `--show-treesitter`) | +| `--format` | | `string` | `text` | Output format of the node-types summary: `text`, `json` | +| `--generate` | | `bool` | `false` | Print a `.uastmap` DSL skeleton generated from `node-types.json` | +| `--show-treesitter` | | `bool` | `false` | Print the raw tree-sitter tree (named nodes, positions, byte ranges, text) of each input file | +| `--language` | | `string` | `""` | Tree-sitter language name (e.g. `go`): required by `--show-treesitter`, written into the `--generate` header | +| `--extensions` | | `string` | `""` | Comma-separated file extensions written into the `--generate` header | + +```bash +# Classify a grammar's node types +uast mapping --node-types node-types.json --format json + +# Generate a mapping skeleton +uast mapping --node-types node-types.json --generate --language go --extensions .go + +# Dump the tree-sitter tree of a file +uast mapping --show-treesitter --language go main.go +``` + +--- + +### `uast completion <shell>` + +Generates the completion script for `bash`, `zsh`, `fish` or `powershell` on +stdout, covering every subcommand and flag listed on this page. + +### `uast version` + +Prints `uast <version> (commit: <hash>, built: <date>)`. diff --git a/site/guide/configuration.md b/site/guide/configuration.md index d4bdeac2..02207593 100644 --- a/site/guide/configuration.md +++ b/site/guide/configuration.md @@ -1,8 +1,9 @@ # Configuration -Codefang uses a layered configuration system built on [Viper](https://github.com/spf13/viper). -Settings can come from a YAML config file, environment variables, or CLI flags, -and they merge with a well-defined priority order. +A `.codefang.yaml` file and `CODEFANG_*` environment variables set the +**defaults** of `codefang run` flags (`crates/cf-config`). Every key on this +page maps to exactly one flag; a key that is not listed here is not read, +and a file that sets one prints a single warning naming it. --- @@ -10,425 +11,172 @@ and they merge with a well-defined priority order. ### Search order -When no explicit `--config` flag is provided, Codefang searches for a -`.codefang.yaml` file in the following locations (first match wins): - | Priority | Location | |----------|----------| -| 1 | Current working directory (`./.codefang.yaml`) | -| 2 | User home directory (`$HOME/.codefang.yaml`) | - -### Merge priority +| 1 | The file named by `--config <file>` (it must be readable) | +| 2 | `./.codefang.yaml` (the working directory) | +| 3 | `$HOME/.codefang.yaml` | -When the same setting is specified in multiple sources, the highest-priority -source wins: +The first file found is the only one read. A missing file at priority 2 or +3 is not an error; an explicit `--config` that cannot be read is: ```text -CLI flags > Environment variables > Config file > Compiled defaults +Error: cannot read --config ./missing.yaml: No such file or directory (os error 2) ``` -!!! example "Override Example" +`--config /dev/null` runs with the compiled defaults only, whatever the +working directory or `$HOME` holds. - A config file sets `pipeline.workers: 2`, but the command line passes - `--workers 8`. The effective value is **8** because CLI flags have the - highest priority. +### Precedence -### Explicit config path +```text +explicit flag > CODEFANG_* environment > config file > compiled default +``` -Use `--config` to point at a specific file: +Each configured value becomes the default of its flag, so a flag given on +the command line always wins: ```bash -codefang run -a 'history/*' --config /etc/codefang/production.yaml . +# .codefang.yaml sets history.anomaly.window_size: 30 — this run uses 10 +codefang run -a history/anomaly --anomaly-window 10 . ``` ---- +`-v` names the file a run used (`progress: config file: ./.codefang.yaml`, +or `none`). -## Environment variables +### Environment variables -All configuration keys can be set via environment variables using the -`CODEFANG_` prefix. Nested keys use `_` as a separator. +Every key has a variable: `CODEFANG_` + the key upper-cased with `.` +replaced by `_`. `CODEFANG_ANALYZERS` takes a comma- or space-separated list. -| Config Key | Environment Variable | -|-----------|---------------------| -| `pipeline.workers` | `CODEFANG_PIPELINE_WORKERS` | -| `pipeline.memory_budget` | `CODEFANG_PIPELINE_MEMORY_BUDGET` | +| Config key | Environment variable | +|------------|----------------------| | `history.burndown.granularity` | `CODEFANG_HISTORY_BURNDOWN_GRANULARITY` | -| `history.sentiment.gap` | `CODEFANG_HISTORY_SENTIMENT_GAP` | -| `checkpoint.enabled` | `CODEFANG_CHECKPOINT_ENABLED` | +| `history.devs.anonymize` | `CODEFANG_HISTORY_DEVS_ANONYMIZE` | +| `history.anomaly.window_size` | `CODEFANG_HISTORY_ANOMALY_WINDOW_SIZE` | +| `analyzers` | `CODEFANG_ANALYZERS` | ```bash -# Set workers via environment -export CODEFANG_PIPELINE_WORKERS=4 -export CODEFANG_PIPELINE_MEMORY_BUDGET=2GiB -codefang run -a 'history/*' . +export CODEFANG_HISTORY_BURNDOWN_GRANULARITY=60 +export CODEFANG_ANALYZERS=history/burndown,history/devs +codefang run . ``` ---- +A variable whose value does not fit its key fails the run: +`Error: invalid environment variable CODEFANG_HISTORY_TICK_SIZE: …`. + +--- + +## Supported keys + +| Key | Type | Flag it defaults | Compiled default | Rule | +|-----|------|------------------|------------------|------| +| `analyzers` | list of strings | `-a` / `--analyzers` | every analyzer | ids or globs, as `-a` | +| `pipeline.max_changes_per_commit` | int | `--max-changes-per-commit` | `0` (= 10000) | `>= 0` | +| `pipeline.memory_budget` | size (`2GB`, `512MiB`) or bytes | `--memory-budget` | half the RAM, at most 2 GiB | `>= 256MiB` | +| `pipeline.chunk_size` | int (commits) | `--chunk-size` | planned from the budget | `>= 1` | +| `history.tick_size` | int (hours) | `--tick-size` | `24` | `> 0` | +| `history.burndown.granularity` | int (ticks) | `--granularity` | `30` | `> 0` | +| `history.burndown.sampling` | int (ticks) | `--sampling` | `30` | `> 0` | +| `history.burndown.track_files` | bool | `--burndown-files` | `false` | | +| `history.burndown.track_people` | bool | `--burndown-people` | `false` | | +| `history.devs.consider_empty_commits` | bool | `--empty-commits` | `false` | | +| `history.devs.anonymize` | bool | `--anonymize` | `false` | | +| `history.sentiment.min_comment_length` | int (characters) | `--min-comment-len` | `20` | `>= 0` (`0` disables the filter) | +| `history.shotness.dsl_struct` | string | `--shotness-dsl-struct` | `rfilter(.roles has "Function")` | empty = default | +| `history.shotness.dsl_name` | string | `--shotness-dsl-name` | `.props.name` | empty = default | +| `history.shotness.keep_ended` | bool | `--shotness-keep-ended` | `false` | | +| `history.typos.max_distance` | int | `--typos-max-distance` | `4` | `> 0` | +| `history.anomaly.threshold` | float | `--anomaly-threshold` | `2.0` | `> 0` | +| `history.anomaly.window_size` | int (ticks) | `--anomaly-window` | `20` | `>= 2` | + +What each flag does is documented in the [CLI reference](cli-reference.md). +A key left out, or set to an empty value (`window_size:`), keeps the +compiled default. A value that breaks its rule fails the run before any +analysis, naming the file and the key: + +```text +Error: invalid config ./.codefang.yaml: history.anomaly.window_size must be >= 2, got 1 +``` + +A value of the wrong type fails the same way with the parser's message, +which names the key and the line. -## Full configuration reference +### Full file -Below is the complete `.codefang.yaml` file with all fields set to their -compiled defaults: +Every supported key, set to its compiled default: ```yaml -analyzers: [] +analyzers: [] # empty = every analyzer pipeline: - workers: 0 # 0 = auto (GOMAXPROCS) - memory_budget: "" # e.g. "4GiB" - blob_cache_size: "" # e.g. "1GB" (default when empty) - diff_cache_size: 0 # 0 = default (10000) - blob_arena_size: "" # e.g. "4MB" (default when empty) - commit_batch_size: 0 # 0 = default (100) - gogc: 0 # 0 = Go default (100) - ballast_size: "0" # "0" = disabled - memory_limit: "" # e.g. "8GiB" - worker_timeout: "" # e.g. "60s" - # Advanced pipeline tuning. - uast_spill_threshold: 32 - intra_commit_parallel_threshold: 4 - max_intra_commit_workers: 4 - max_uast_blob_size: 262144 # 256 KiB - uast_parse_timeout: "10s" - max_changes_per_commit: 10000 - max_diff_batch_size: 1000 - memory_budget_ratio: 50 - memory_budget_cap: "2GiB" - memory_limit_ratio: 75 - # Extended pipeline tuning. - uast_spill_trim_interval: 16 - native_trim_interval: 10 - max_streaming_buffering: 3 - drain_prefetch_timeout: "30s" - sampler_interval: "2s" - worker_ratio: 100 - uast_worker_ratio: 40 - leaf_worker_divisor: 3 - min_leaf_workers: 4 - buffer_size_multiplier: 2 - budget_limit_ratio: 95 - system_ram_limit_ratio: 90 - diff_job_buffer_multiplier: 10 - static_max_workers: 8 - malloc_trim_interval: 50 - static_memory_limit_ratio: 90 + max_changes_per_commit: 0 # 0 = the default cap (10000) + # memory_budget: 2GB # unset = half the RAM, at most 2 GiB + # chunk_size: 1000 # unset = planned from the budget history: - couples: - coupling_threshold_high: 10 - ownership_few_threshold: 3 - ownership_moderate_threshold: 5 - batch_coupling_threshold: 100 - hll_precision: 10 - top_k_per_file: 100 - min_edge_weight: 2 + tick_size: 24 burndown: granularity: 30 sampling: 30 track_files: false track_people: false - hibernation_threshold: 1000 - hibernation_to_disk: true - hibernation_directory: "" - debug: false - goroutines: 0 devs: consider_empty_commits: false anonymize: false - bus_factor_threshold: 0.5 - risk_threshold_critical: 90.0 - risk_threshold_high: 80.0 - risk_threshold_medium: 60.0 - active_threshold_ratio: 0.7 - default_active_days: 90 - hll_precision: 14 - file_history: - hotspot_threshold_critical: 50 - hotspot_threshold_high: 30 - hotspot_threshold_medium: 15 - imports: - goroutines: 4 - max_file_size: 1048576 - max_dependency_risk_rows: 30 sentiment: min_comment_length: 20 - gap: 0.5 - neutralizer_weight: 0.8 - max_weight_ratio: 3.0 - positive_threshold: 0.6 - negative_threshold: 0.4 - trend_threshold: 0.1 - low_sentiment_risk_thresh: 0.2 - clones: - max_clone_pairs: 1000 - num_hashes: 128 - num_bands: 16 - num_rows: 8 - shingle_size: 5 - similarity_type2: 0.8 - similarity_type3: 0.5 - threshold_ratio_yellow: 0.1 - threshold_ratio_red: 0.3 - threshold_pairs_yellow: 5 - threshold_pairs_red: 20 shotness: - dsl_struct: 'filter(.roles has "Function")' + dsl_struct: 'rfilter(.roles has "Function")' dsl_name: ".props.name" + keep_ended: false typos: max_distance: 4 anomaly: threshold: 2.0 window_size: 20 - -checkpoint: - enabled: true - dir: "" - resume: true - clear_prev: false -``` - ---- - -## Section reference - -### `analyzers` - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `analyzers` | `[]string` | `[]` | Default list of analyzer IDs to run when `-a` is not provided on the command line. Accepts the same IDs and glob patterns as the `-a` flag. | - -```yaml -analyzers: - - static/complexity - - history/burndown - - history/devs ``` ---- - -### `pipeline` - -Resource and tuning knobs for the analysis pipeline. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `workers` | `int` | `0` | Number of parallel workers. `0` uses `GOMAXPROCS`. | Must be >= 0 | -| `memory_budget` | `string` | `""` | Total memory budget for auto-tuning cache sizes (e.g. `"512MB"`, `"2GiB"`). Empty means no budget-based tuning. | Valid byte-size string or empty | -| `blob_cache_size` | `string` | `""` | Maximum size of the blob cache (e.g. `"256MB"`, `"1GB"`). Empty uses the built-in default of 1 GB. | Valid byte-size string or empty | -| `diff_cache_size` | `int` | `0` | Maximum number of entries in the diff cache. `0` uses the built-in default of 10000. | Must be >= 0 | -| `blob_arena_size` | `string` | `""` | Memory arena allocation for blob loading (e.g. `"4MB"`). Empty uses the built-in default of 4 MB. | Valid byte-size string or empty | -| `commit_batch_size` | `int` | `0` | Number of commits processed per batch. `0` uses the built-in default of 100. | Must be >= 0 | -| `gogc` | `int` | `0` | Go garbage collector target percentage. `0` uses the Go default of 100. Higher values reduce GC frequency at the cost of memory. | Must be >= 0 | -| `ballast_size` | `string` | `"0"` | GC ballast allocation size. `"0"` disables ballast. Useful for reducing GC pauses in memory-rich environments. | Valid byte-size string | -| `memory_limit` | `string` | `""` | Hard memory limit passed to the Go runtime (`GOMEMLIMIT`). Empty means no limit. | Valid byte-size string or empty | -| `worker_timeout` | `string` | `""` | Maximum duration a single worker may run before being terminated (e.g. `"60s"`, `"5m"`). Empty means no timeout. | Valid Go duration string or empty | -| `uast_spill_threshold` | `int` | `32` | File changes per commit before UAST trees are spilled to disk to cap memory. | Must be >= 0 | -| `intra_commit_parallel_threshold` | `int` | `4` | Minimum file changes in a commit before intra-commit parallel UAST parsing is used. | Must be >= 0 | -| `max_intra_commit_workers` | `int` | `4` | Maximum goroutines for parallel UAST parsing within a single commit. | Must be >= 0 | -| `max_uast_blob_size` | `int` | `262144` | Maximum blob size in bytes for UAST parsing (256 KiB). Larger files are skipped. | Must be >= 0 | -| `uast_parse_timeout` | `string` | `"10s"` | Per-file timeout for UAST parsing. Prevents pathological tree-sitter behavior. | Valid Go duration string or empty | -| `max_changes_per_commit` | `int` | `10000` | Commits with more file changes than this are skipped entirely. | Must be >= 0 | -| `max_diff_batch_size` | `int` | `1000` | Maximum number of diff requests batched together for efficiency. | Must be >= 0 | -| `memory_budget_ratio` | `int` | `50` | Percentage of system RAM to use as the auto-detected memory budget. | Must be 0–100 | -| `memory_budget_cap` | `string` | `"2GiB"` | Maximum auto-detected memory budget. | Valid byte-size string or empty | -| `memory_limit_ratio` | `int` | `75` | Percentage of system RAM to use as Go's soft memory limit. | Must be 0–100 | -| `uast_spill_trim_interval` | `int` | `16` | How often to call `MallocTrim` during UAST spill-mode parsing (every N commits). | Must be >= 0 | -| `native_trim_interval` | `int` | `10` | How often to call `malloc_trim` within a chunk (every N commits). | Must be >= 0 | -| `max_streaming_buffering` | `int` | `3` | Maximum buffering factor for streaming (1=single, 2=double, 3=triple). | Must be >= 1 | -| `drain_prefetch_timeout` | `string` | `"30s"` | Timeout for abandoned prefetch goroutines before they are leaked. | Valid Go duration string | -| `sampler_interval` | `string` | `"2s"` | Pipeline sampler polling interval for memory triage logging. | Valid Go duration string | -| `worker_ratio` | `int` | `100` | Percentage of CPU cores to use for pipeline workers. | Must be 0–100 | -| `uast_worker_ratio` | `int` | `40` | Percentage of CPU cores to use for UAST pipeline workers. | Must be 0–100 | -| `leaf_worker_divisor` | `int` | `3` | Leaf worker count = `NumCPU / divisor`. | Must be >= 1 | -| `min_leaf_workers` | `int` | `4` | Minimum number of leaf workers when enabled. | Must be >= 1 | -| `buffer_size_multiplier` | `int` | `2` | Buffer size = `workers * multiplier`. | Must be >= 1 | -| `budget_limit_ratio` | `int` | `95` | Percentage of memory budget used as Go's soft memory limit. | Must be 0–100 | -| `system_ram_limit_ratio` | `int` | `90` | Memory limit cap as percentage of system RAM. | Must be 0–100 | -| `diff_job_buffer_multiplier` | `int` | `10` | Scales the diff job queue buffer relative to pipeline buffer size. | Must be >= 1 | -| `static_max_workers` | `int` | `8` | Maximum concurrent workers for static analysis phase. | Must be >= 1 | -| `malloc_trim_interval` | `int` | `50` | Files between `malloc_trim` calls in static analysis. `-1` disables. | Must be >= -1 | -| `static_memory_limit_ratio` | `int` | `90` | Percentage of budget applied as Go's memory limit during static phase. | Must be 0–100 | - -!!! tip "Memory Budget Auto-Tuning" - - When `memory_budget` is set, Codefang automatically calculates optimal - values for `blob_cache_size`, `diff_cache_size`, and `blob_arena_size`. - Explicit values for those fields take precedence over the auto-tuned ones. - ---- - -### `history.couples` - -Controls the file coupling and ownership analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `coupling_threshold_high` | `int` | `10` | Minimum co-change count for a file pair to be classified as "high" coupling. | Must be >= 0 | -| `ownership_few_threshold` | `int` | `3` | Maximum number of contributors for a file to be in the "few owners" bucket. | Must be >= 0 | -| `ownership_moderate_threshold` | `int` | `5` | Maximum number of contributors for a file to be in the "moderate owners" bucket. | Must be >= 0 | -| `batch_coupling_threshold` | `int` | `100` | Maximum number of file pairs per commit considered for coupling. Limits quadratic growth on large commits. | Must be >= 0 | -| `hll_precision` | `int` | `10` | HyperLogLog precision for contributor count sketches. Higher = more accurate, more memory. Valid range: 4–18. | Must be 4–18 when set | -| `top_k_per_file` | `int` | `100` | Maximum coupling pairs per file in store output. | Must be >= 0 | -| `min_edge_weight` | `int` | `2` | Minimum co-change count for a coupling edge to be included. | Must be >= 0 | - ---- - -### `history.burndown` - -Controls the burndown (code ownership aging) analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `granularity` | `int` | `30` | Time granularity in days for burndown bands. | Must be > 0 | -| `sampling` | `int` | `30` | Sampling interval in days for burndown snapshots. | Must be > 0 | -| `track_files` | `bool` | `false` | Track per-file burndown data. Increases memory usage. | -- | -| `track_people` | `bool` | `false` | Track per-person burndown data. Increases memory usage. | -- | -| `hibernation_threshold` | `int` | `1000` | Number of file entries before hibernation activates. | -- | -| `hibernation_to_disk` | `bool` | `true` | Spill hibernated state to disk instead of keeping in memory. | -- | -| `hibernation_directory` | `string` | `""` | Directory for hibernated state files. Empty uses a temp directory. | -- | -| `debug` | `bool` | `false` | Enable verbose debug output for the burndown analyzer. | -- | -| `goroutines` | `int` | `0` | Parallel goroutines for burndown computation. `0` uses a sensible default. | -- | - ---- - -### `history.devs` - -Controls the developer activity analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `consider_empty_commits` | `bool` | `false` | Include empty (no-diff) commits in developer statistics. | -- | -| `anonymize` | `bool` | `false` | Replace developer names with anonymous identifiers in output. | -- | -| `bus_factor_threshold` | `float64` | `0.5` | Cumulative ownership fraction at which the bus factor count stops. | Must be 0.0–1.0 | -| `risk_threshold_critical` | `float64` | `90.0` | Ownership percentage above which a file is at critical risk (single-owner). | Must be 0–100 | -| `risk_threshold_high` | `float64` | `80.0` | Ownership percentage above which a file is at high risk. | Must be 0–100 | -| `risk_threshold_medium` | `float64` | `60.0` | Ownership percentage above which a file is at medium risk. | Must be 0–100 | -| `active_threshold_ratio` | `float64` | `0.7` | Fraction of the most-active developer's commits required to consider a developer "active". | Must be 0.0–1.0 | -| `default_active_days` | `int` | `90` | Lookback window in days for determining whether a developer is currently active. | Must be >= 0 | -| `hll_precision` | `int` | `14` | HyperLogLog precision for developer count sketches. Higher = more accurate, more memory. Valid range: 4–18. | Must be 4–18 when set | - ---- - -### `history.file_history` +!!! example "Custom shotness targets" -Controls the file history (churn and hotspot) analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `hotspot_threshold_critical` | `int` | `50` | Commit count above which a file is classified as a critical hotspot. | Must be >= 0 | -| `hotspot_threshold_high` | `int` | `30` | Commit count above which a file is classified as a high hotspot. | Must be >= 0 | -| `hotspot_threshold_medium` | `int` | `15` | Commit count above which a file is classified as a medium hotspot. | Must be >= 0 | - ---- - -### `history.imports` - -Controls the import/dependency history analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `goroutines` | `int` | `4` | Number of parallel goroutines for import extraction. | Must be > 0 | -| `max_file_size` | `int` | `1048576` | Maximum file size in bytes to analyze for imports (1 MiB default). | Must be > 0 | -| `max_dependency_risk_rows` | `int` | `30` | Maximum number of rows in the dependency risk table in plot output. | Must be >= 0 | - ---- - -### `history.sentiment` - -Controls the comment sentiment analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `min_comment_length` | `int` | `20` | Minimum comment character length to include in sentiment analysis. | Must be > 0 | -| `gap` | `float64` | `0.5` | Sentiment classification gap threshold. Comments with scores within this gap of neutral are considered neutral. | Must be between 0.0 and 1.0 | -| `neutralizer_weight` | `float64` | `0.8` | How strongly SE-domain adjustments affect the final score. `0` = no effect, `1` = full adjustment. | Must be 0.0–1.0 | -| `max_weight_ratio` | `float64` | `3.0` | Maximum weight ratio for comment length weighting. Prevents single long comments from dominating. | Must be > 0 | -| `positive_threshold` | `float64` | `0.6` | Sentiment score at or above this is classified as "positive". | Must be 0.0–1.0 | -| `negative_threshold` | `float64` | `0.4` | Sentiment score at or below this is classified as "negative". | Must be 0.0–1.0 | -| `trend_threshold` | `float64` | `0.1` | Minimum change in sentiment needed to classify a trend as "improving" or "declining". | Must be >= 0 | -| `low_sentiment_risk_thresh` | `float64` | `0.2` | Sentiment at or below this is flagged as HIGH risk (vs MEDIUM). | Must be 0.0–1.0 | - ---- - -### `history.clones` - -Controls the clone detection analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `max_clone_pairs` | `int` | `1000` | Maximum number of clone pairs reported in the aggregated result. | Must be >= 0 | -| `num_hashes` | `int` | `128` | MinHash signature size. More hashes = better accuracy, more memory. | Must be > 0 | -| `num_bands` | `int` | `16` | Number of LSH bands. `num_bands * num_rows` must equal `num_hashes`. | Must be > 0 | -| `num_rows` | `int` | `8` | Number of rows per LSH band. | Must be > 0 | -| `shingle_size` | `int` | `5` | Token shingle window size for MinHash input. | Must be > 0 | -| `similarity_type2` | `float64` | `0.8` | Minimum Jaccard similarity for Type-2 (renamed) clone detection. | Must be 0.0–1.0 | -| `similarity_type3` | `float64` | `0.5` | Minimum Jaccard similarity for Type-3 (near-miss) clone detection. | Must be 0.0–1.0 | -| `threshold_ratio_yellow` | `float64` | `0.1` | Clone ratio above which a yellow warning is issued. | Must be 0.0–1.0 | -| `threshold_ratio_red` | `float64` | `0.3` | Clone ratio above which a red warning is issued. | Must be 0.0–1.0 | -| `threshold_pairs_yellow` | `int` | `5` | Clone pair count above which a yellow warning is issued. | Must be >= 0 | -| `threshold_pairs_red` | `int` | `20` | Clone pair count above which a red warning is issued. | Must be >= 0 | - ---- - -### `history.shotness` - -Controls the shotness (function co-change) analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `dsl_struct` | `string` | `filter(.roles has "Function")` | DSL expression to identify structural elements (functions, methods) for co-change tracking. | Valid UAST DSL expression | -| `dsl_name` | `string` | `.props.name` | DSL expression to extract the name of each structural element. | Valid UAST DSL path expression | - -!!! example "Custom Shotness Targets" - - Track co-change at the class level instead of functions: + Track co-change at the class level instead of functions (`rfilter` + walks the whole tree; `filter` would see only the root's direct + children): ```yaml history: shotness: - dsl_struct: 'filter(.type == "Class")' + dsl_struct: 'rfilter(.type == "Class")' dsl_name: ".props.name" ``` --- -### `history.typos` - -Controls the typo detection analyzer. - -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `max_distance` | `int` | `4` | Maximum Levenshtein edit distance for two identifiers to be considered a potential typo pair. | Must be > 0 | - ---- - -### `history.anomaly` - -Controls the temporal anomaly detection analyzer. +## Unsupported keys -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `threshold` | `float64` | `2.0` | Z-score threshold for flagging a commit as anomalous. Lower values are more sensitive. | Must be > 0 | -| `window_size` | `int` | `20` | Sliding window size (in commits) for computing the rolling mean and standard deviation. | Must be >= 2 | +A key the binary does not read never fails a run. The file loads, the +supported keys apply, and stderr carries ONE line listing the rest in +document order: ---- - -### `checkpoint` - -Controls checkpoint and resume behavior for long-running history analyses. +```text +warning: ignoring unsupported config keys: pipeline.workers, checkpoint.enabled, history.burndown.goroutines +``` -| Field | Type | Default | Description | Validation | -|-------|------|---------|-------------|------------| -| `enabled` | `bool` | `true` | Enable periodic checkpointing for crash recovery. | -- | -| `dir` | `string` | `""` | Directory for checkpoint files. Empty uses `~/.codefang/checkpoints`. | Valid directory path or empty | -| `resume` | `bool` | `true` | Automatically resume from an existing checkpoint if one is found. | -- | -| `clear_prev` | `bool` | `false` | Delete any existing checkpoint data before starting a new run. | -- | +The keys earlier releases documented and the Rust binary does not read: -!!! warning "Checkpoint Directory Permissions" +| Section | Keys | +|---------|------| +| `pipeline` | every key but `max_changes_per_commit`, `memory_budget` and `chunk_size`: `workers`, `blob_cache_size`, `diff_cache_size`, `blob_arena_size`, `commit_batch_size`, `gogc`, `ballast_size`, `memory_limit`, `worker_timeout`, the spill / trim / ratio / buffering tuning keys, `static_max_workers`, `malloc_trim_interval`, … (Go-runtime and goroutine-pool tuning) | +| `checkpoint` | `enabled`, `dir`, `resume`, `clear_prev` (no checkpoint store is linked into the binary) | +| `history.burndown` | `hibernation_threshold`, `hibernation_to_disk`, `hibernation_directory`, `debug`, `goroutines` | +| `history.couples` | every key (`coupling_threshold_high`, `ownership_*_threshold`, `batch_coupling_threshold`, `hll_precision`, `top_k_per_file`, `min_edge_weight`) | +| `history.devs` | `bus_factor_threshold`, `risk_threshold_*`, `active_threshold_ratio`, `default_active_days`, `hll_precision` | +| `history.file_history` | every key (`hotspot_threshold_*`) | +| `history.imports` | every key (`goroutines`, `max_file_size`, `max_dependency_risk_rows`) | +| `history.sentiment` | every key but `min_comment_length` (`gap`, `neutralizer_weight`, `max_weight_ratio`, `*_threshold`, `low_sentiment_risk_thresh`) | +| `history.clones` | every key | - The checkpoint directory must be writable by the user running Codefang. - When using the default (`~/.codefang/checkpoints`), the directory is - created automatically on first use. +Delete them from your file to silence the warning. --- @@ -441,31 +189,22 @@ Controls checkpoint and resume behavior for long-running history analyses. - history/burndown - history/devs - pipeline: - workers: 2 - memory_budget: "1GiB" - - checkpoint: - enabled: false + history: + devs: + anonymize: true ``` === "Large Repository" ```yaml title=".codefang.yaml" pipeline: - workers: 8 - memory_budget: "8GiB" - gogc: 50 - commit_batch_size: 200 + max_changes_per_commit: 50000 # keep legitimate mass commits history: + tick_size: 168 # one tick per week burndown: - hibernation_to_disk: true - goroutines: 4 - - checkpoint: - enabled: true - dir: "/tmp/codefang-checkpoint" + granularity: 4 + sampling: 4 ``` === "Security Audit" diff --git a/site/guide/data-analytics.md b/site/guide/data-analytics.md index 19920337..928163bc 100644 --- a/site/guide/data-analytics.md +++ b/site/guide/data-analytics.md @@ -13,10 +13,10 @@ Analyze a repository and produce DWH-ready output: ```bash # JSON for small-to-medium repos (< 5K files, < 10K commits) -codefang run --format json --per-file --memory-budget 4GB /path/to/repo > report.json +codefang run --format json --per-file /path/to/repo > report.json -# NDJSON for large repos (streaming, one line per analyzer) -codefang run --format ndjson --per-file --memory-budget 8GB /path/to/repo > report.ndjson +# NDJSON for large repos (one line per analyzer) +codefang run --format ndjson /path/to/repo > report.ndjson # Limit history depth for faster iteration codefang run --format json --per-file --limit 5000 /path/to/repo > report.json @@ -89,32 +89,31 @@ cat report.ndjson | clickhouse-client --query "INSERT INTO codefang_raw FORMAT J --- -## Memory budget +## Bounding a history run -**Always set `--memory-budget`** for repos with history analysis. Without it, -the streaming pipeline uses a conservative 2GB default that may OOM on large -repos. +`--memory-budget` (default: half the RAM, at most 2 GiB) bounds what the +history walks hold in flight: each walk plans its chunk size and worker count +from its share, and `-v` prints the plan. The output never depends on the +budget. The analyzers' own aggregates still grow with the commit window, so +on a very large repository also bound the window: -| Machine RAM | Recommended Budget | Handles | -|-------------|-------------------|---------| -| 8 GB | `--memory-budget 2GB` | Repos up to ~10K commits | -| 16 GB | `--memory-budget 4GB` | Repos up to ~30K commits | -| 32 GB | `--memory-budget 8GB` | Repos up to ~60K commits | -| 64 GB | `--memory-budget 16GB` | Repos up to ~150K commits | +| Flag | Effect | +|------|--------| +| `--limit N` | Only the N newest commits | +| `--since <time or revision>` | Only commits authored at/after the cutoff (`4380h`, `2025-01-01`, `v1.2.0`) | +| `--first-parent` | Skip the side branches of merges | +| `--max-changes-per-commit N` | Drop commits whose tree diff exceeds N changes (default 10000) — vendoring dumps, mass renames | +| `--languages go,python` | Only changes in these languages reach the analyzers | +| `--head` | The single HEAD commit | -The budget controls the streaming chunk planner — larger budgets mean fewer, -bigger chunks and faster processing. The actual RSS will be ~2x the budget -due to Go runtime overhead and native memory. +Measured on the hercules repository, `codefang run -a 'history/*' --limit 500` +peaks at 752 MB resident memory and finishes in 4.7 s wall time. ```bash -# 64GB machine, kubernetes-sized repo (~56K commits) -codefang run --format ndjson --per-file --memory-budget 8GB ~/sources/kubernetes +# The newest 5000 commits of a kubernetes-sized repository +codefang run --format ndjson --limit 5000 ~/sources/kubernetes > report.ndjson ``` -!!! warning "Without `--memory-budget`" - The default 2GB budget may cause the process to be killed by the OS OOM - killer on large repos. Always set this flag explicitly. - --- ## Commit limiting @@ -126,14 +125,14 @@ Use `--limit N` to analyze only the most recent N commits. This is useful for: - **Memory control**: Fewer commits = less memory, faster processing ```bash -# Last 1000 commits (fast, ~2 min) +# Last 1000 commits codefang run --format json --per-file --limit 1000 /repo > recent.json -# Last 10000 commits (moderate, ~15 min) -codefang run --format json --per-file --limit 10000 --memory-budget 4GB /repo > report.json +# Last 10000 commits +codefang run --format json --per-file --limit 10000 /repo > report.json -# Full history (slow, may take hours for large repos) -codefang run --format json --per-file --memory-budget 8GB /repo > full.json +# Full history +codefang run --format json --per-file /repo > full.json ``` --- @@ -385,7 +384,7 @@ codefang run \ ### Full analysis (everything) ```bash -codefang run --format ndjson --per-file --memory-budget 8GB /repo +codefang run --format ndjson /repo ``` **Produces**: All 17 analyzers. Use NDJSON for large repos. @@ -394,28 +393,24 @@ codefang run --format ndjson --per-file --memory-budget 8GB /repo ## Performance tuning -### Static analysis workers - -Control parallelism for the UAST parsing phase: - -```bash -# Use all CPUs (default: min(NumCPU, 8)) -codefang run --static-workers 16 --format json /repo -``` +### Static analysis -More workers = faster static phase but higher peak memory. +The static phase is one walk of the working tree shared by every `static/*` +analyzer; each admitted file is read once. Narrow it with `--languages` and +`--extra-excluded-prefixes`; vendored and generated files are excluded by +default (`--include-vendored` / `--include-generated` re-include them). ### History analysis -The streaming pipeline auto-tunes chunk sizes based on `--memory-budget`. -No manual tuning needed. Key parameters: +The history phase has no tuning flags beyond the commit window (see +[Bounding a history run](#bounding-a-history-run)): | Parameter | Flag | Default | Effect | |-----------|------|---------|--------| -| Memory budget | `--memory-budget` | 2GB | Controls chunk size | | Commit limit | `--limit` | 0 (all) | Analyze at most N commits — the N **newest** | -| First parent | `--first-parent` | false | Skip merge commits | +| First parent | `--first-parent` | false | Skip merge side branches | | Since | `--since` | none | Time- or revision-based filtering | +| Change cap | `--max-changes-per-commit` | 0 (= 10000) | Drop oversized commits | ```bash # Analyze only the last 6 months (4380 h), first-parent only @@ -436,156 +431,40 @@ Duration units are `h`/`m`/`s` only — there is no `d`/`w`, and `m` is --- -## Incremental analysis & checkpointing - -Codefang supports two persistence mechanisms for long-running analysis: -**incremental caching** (skip already-processed commits) and **checkpointing** -(crash recovery). - -### Incremental cache - -The incremental cache stores analysis results keyed by repository root SHA and -branch. On subsequent runs, only new commits since the last cached position -are processed. - -!!! warning "History-only mode required" - The incremental cache currently works with history-only runs - (`-a 'history/*'`). In the default combined mode (static + history), - the cache directory is accepted but may not produce cache files. - For incremental DWH loads, run history and static phases separately. - -```bash -# History-only run with cache (incremental) -codefang run -a 'history/*' --format json --memory-budget 8GB \ - --cache-dir ~/.codefang/cache /repo > history.json - -# Static run (always full, no caching needed — fast) -codefang run -a 'static/*' --format json --per-file /repo > static.json - -# Force full re-analysis (ignore cache) -codefang run -a 'history/*' --format json --memory-budget 8GB \ - --cache-dir ~/.codefang/cache --no-cache /repo > history-full.json -``` - -| Flag | Default | Description | -|------|---------|-------------| -| `--cache-dir` | none | Directory for incremental cache storage | -| `--no-cache` | false | Force full re-analysis, ignore existing cache | - -The cache stores a metadata file (`cache.json`) with head SHA, branch, commit -count, and analyzer IDs. If the root SHA changes (force-push or history -rewrite), the cache is automatically invalidated. - -!!! tip "Ideal for daily DWH loads" - Point `--cache-dir` to a persistent directory on your CI machine. - Each daily run only processes the new commits since yesterday, - cutting analysis time from hours to minutes. - -### Checkpointing (crash recovery) - -For very long runs (e.g., full kubernetes at ~3 hours), checkpointing saves -progress periodically so a crash doesn't lose all work. - -```bash -# Enable checkpointing (on by default) -codefang run --format json --memory-budget 8GB \ - --checkpoint --checkpoint-dir ~/.codefang/checkpoints /repo - -# Resume from checkpoint after crash -codefang run --format json --memory-budget 8GB \ - --resume --checkpoint-dir ~/.codefang/checkpoints /repo - -# Clear old checkpoint and start fresh -codefang run --format json --memory-budget 8GB \ - --clear-checkpoint /repo -``` - -| Flag | Default | Description | -|------|---------|-------------| -| `--checkpoint` | true | Enable periodic checkpointing | -| `--checkpoint-dir` | `~/.codefang/checkpoints` | Directory for checkpoint files | -| `--resume` | true | Resume from checkpoint if available | -| `--clear-checkpoint` | false | Clear existing checkpoint before run | - -The checkpoint stores: - -- Current chunk position (which commits have been processed) -- Aggregator spill state (intermediate results on disk) -- Repository hash (for validation on resume) - -!!! info "Auto-cleanup on success" - Checkpoint files are **automatically deleted** after a successful run. - They only persist if the process crashes mid-analysis. This is by design — - checkpoints are for crash recovery, not persistent storage. - -!!! warning "Checkpoint vs Cache" - **Checkpoint** = crash recovery within a single run (temporary, auto-cleaned on success). - **Cache** = incremental analysis across runs (persistent, reused on next invocation). - For DWH pipelines, you want **both**: `--cache-dir` for incremental loads and - `--checkpoint` for resilience. - -### Production pipeline example +## Recurring loads -A daily cron job that incrementally analyzes a repository: +There is no incremental cache and no checkpoint: every run analyzes the whole +requested window. For a recurring (for example daily) load, bound each run to +the commits that are new since the previous one with `--since`, and load the +static snapshot separately: ```bash #!/bin/bash REPO=/opt/repos/kubernetes -CACHE_DIR=/var/lib/codefang/cache -CHECKPOINT_DIR=/var/lib/codefang/checkpoints OUTPUT_DIR=/var/lib/codefang/output +DAY=$(date +%Y%m%d) # Pull latest cd "$REPO" && git pull --ff-only -# Static analysis (always full, fast) -codefang run \ - -a 'static/*' \ - --format ndjson \ - --per-file \ - "$REPO" > "$OUTPUT_DIR/static-$(date +%Y%m%d).ndjson" +# Static snapshot of the working tree +codefang run -a 'static/*' --format json --per-file \ + -o "$OUTPUT_DIR/static-$DAY.json" "$REPO" -# History analysis (incremental via cache) -codefang run \ - -a 'history/*' \ - --format ndjson \ - --memory-budget 8GB \ - --cache-dir "$CACHE_DIR" \ - --checkpoint-dir "$CHECKPOINT_DIR" \ - "$REPO" > "$OUTPUT_DIR/history-$(date +%Y%m%d).ndjson" +# History of the last 24 hours, one line per commit and analyzer +codefang run -a 'history/*' --format ndjson --since 24h \ + -o "$OUTPUT_DIR/history-$DAY.ndjson" "$REPO" # Load into ClickHouse -cat "$OUTPUT_DIR/report-$(date +%Y%m%d).ndjson" \ - | clickhouse-client --query "INSERT INTO codefang_raw FORMAT JSONEachRow" -``` - -### Advanced tuning for history pipeline - -Fine-tune the history streaming pipeline for specific hardware: - -```bash -codefang run \ - --memory-budget 8GB \ - --commit-batch-size 200 \ - --blob-cache-size 2GB \ - --diff-cache-size 20000 \ - --blob-arena-size 8MB \ - --tmp-dir /fast-ssd/tmp \ - --format ndjson /repo +clickhouse-client --query "INSERT INTO codefang_raw FORMAT JSONEachRow" \ + < "$OUTPUT_DIR/history-$DAY.ndjson" ``` -| Flag | Default | Description | -|------|---------|-------------| -| `--commit-batch-size` | 100 | Commits per processing batch | -| `--blob-cache-size` | 1GB | Max blob cache (LRU, keeps hot files in memory) | -| `--diff-cache-size` | 10000 | Max diff cache entries | -| `--blob-arena-size` | 4MB | Memory arena for blob loading | -| `--tmp-dir` | system temp | Directory for spill files (use fast SSD) | -| `--keep-store` | false | Keep temp ReportStore after rendering (for debugging) | - -!!! tip "SSD for tmp-dir" - The streaming pipeline spills intermediate data to disk when memory - pressure is high. Point `--tmp-dir` to a fast SSD for best performance. +A window-bounded history run starts from the tree of the commit just before +the window, so a commit's lines added / removed are the lines it changed, as +in a full-history run. Whole-history aggregates (totals, bus factor, coupling +strength) describe the window only, and burndown counts the lines that +existed before the window as its oldest band. --- @@ -640,17 +519,15 @@ GROUP BY repo_id, week; ## Troubleshooting -### OOM kills +### High memory on long histories -**Symptom**: Process killed during history analysis. -**Fix**: Set `--memory-budget` explicitly. +**Symptom**: the process is killed or swaps during history analysis. +**Fix**: bound the window — `--limit`, `--since`, `--first-parent`, +`--max-changes-per-commit`, `--languages`. ```bash -# Check available RAM -free -h - -# Set budget to ~25% of available RAM -codefang run --memory-budget 4GB --format ndjson /repo +# The newest 10000 commits only +codefang run --limit 10000 --format ndjson /repo ``` ### Empty history analyzers @@ -659,9 +536,9 @@ Some analyzers require specific conditions: | Analyzer | Requirement | |----------|-------------| -| `burndown` (developer/file survival) | Enable via config: `Burndown.TrackPeople: true`, `Burndown.TrackFiles: true` | -| `history/imports` | Requires UAST-enabled pipeline mode | -| `history/typos` | Requires UAST-enabled pipeline mode | +| `burndown` (developer/file survival) | `--burndown-people` / `--burndown-files` (config: `history.burndown.track_people` / `track_files`) | +| `history/imports` | Only files in a language the UAST parser supports contribute | +| `history/typos` | Only files in a language the UAST parser supports contribute | ### Large file coupling tables diff --git a/site/guide/output-formats.md b/site/guide/output-formats.md index b52e255b..e2353766 100644 --- a/site/guide/output-formats.md +++ b/site/guide/output-formats.md @@ -30,14 +30,16 @@ codefang run -a static/complexity --format text . Human-readable table output with optional color. This is the most readable format for terminal review. Static analyzers render section headers, aligned -columns, and summary lines. History analyzers render a version header followed -by structured key-value data. +columns, and summary lines. History analyzers print the `codefang (v2):` +version header once, then one section per analyzer: its id (`history/devs:`) +followed by a summary box with the key figures and the top items (top +contributors, most coupled files, typo fixes, anomalous ticks, …). -Use `--verbose` (`-v`) to expand full detail in static reports. Use `--no-color` -to strip ANSI escape codes (useful when piping to a file). +Use `--no-color` (or `NO_COLOR=1`) to strip ANSI escape codes (useful when +piping to a file). ```bash -codefang run -a static/complexity --format text -v . +codefang run -a static/complexity --format text --no-color . ``` ??? example "Example Output" @@ -178,7 +180,6 @@ codefang run --format json . - CI/CD pipelines that parse results programmatically - Loading into data warehouses (ClickHouse, BigQuery, Snowflake) - - Cross-format conversion input (`--input`) - Building BI dashboards from function-level metrics --- @@ -312,14 +313,17 @@ A unified chronological JSON array that merges data from **all selected history analyzers** into a single stream keyed by commit. Each entry contains commit metadata plus per-analyzer data for that commit. -This format is only meaningful for history analyzers. It requires at least one -analyzer that implements the `CommitTimeSeriesProvider` interface (anomaly, -devs, quality, sentiment). +This format is only meaningful for history analyzers. Every history analyzer +contributes its per-commit data (typos, which has no per-commit summary, +contributes its name only); a static-only selection rejects the format. ```bash codefang run -a history/devs,history/sentiment --format timeseries . ``` +`--format timeseries --ndjson` emits the document's `commits` array, one JSON +line per commit, with every selected analyzer merged into that line. + ??? example "Example Output" ```json @@ -446,61 +450,54 @@ open ./report/index.html ## Format comparison -The following table summarizes which formats are available for which analyzer -categories: +The following table summarizes which formats each selection accepts (the +same matrix `codefang run` validates before any analysis starts): -| Format | Static Analyzers | History Analyzers | Mixed Runs | -|--------|:---------------:|:-----------------:|:----------:| -| `text` | :material-check: | -- | -- | -| `compact` | :material-check: | -- | -- | +| Format | Static only | History only | Mixed static + history | +|--------|:-----------:|:------------:|:----------------------:| | `json` | :material-check: | :material-check: | :material-check: | | `yaml` | :material-check: | :material-check: | :material-check: | -| `ndjson` | :material-check: | :material-check: | :material-check: | -| `plot` | :material-check: | :material-check: | :material-check: | +| `bin` | :material-check: | :material-check: | :material-check: | +| `text` | :material-check: | :material-check: | :material-check: | +| `compact` | :material-check: | :material-check: | :material-check: | +| `ndjson` | -- | :material-check: | :material-check: | | `timeseries` | -- | :material-check: | :material-check: | +| `plot` | :material-check: | :material-check: | :material-check: | -!!! note "Mixed Runs" - - When both static and history analyzers are selected (`-a '*'`), the format - must be one of the **universal formats**: `json`, `yaml`, `plot`, or - `timeseries`. The `text` and `compact` formats are only available when - running static analyzers alone. - -!!! info "Memory optimization" +!!! note "Mixed runs" + A mixed static+history selection renders `json`, `yaml`, `bin` and + `ndjson` as one unified document. `text` prints the static sections and + then the history text; `compact` prints every analyzer's compact line, + static first. `timeseries` (with or without `--ndjson`) is the history + time series: static analyzers have no per-commit data, so they are named + once on stderr (`warning: static analyzers have no per-commit time + series; omitted: …`). - The `text` and `compact` formats use **summary-only aggregation**: only - running sums and averages are kept in memory, while per-function detail - data is not collected. This dramatically reduces memory usage on large - codebases (up to 97% heap reduction). The `json`, `yaml`, `plot`, and - `binary` formats use **full aggregation** since they need per-item data - for serialization. +A format the selection cannot render fails before the walk with +`Error: format <value> is not supported for <analyzer>`. `--ndjson` is a +modifier of `--format timeseries` only; with any other format it is rejected +with `Error: invalid --ndjson: …`. --- -## Cross-format conversion +## Saving a report -You can convert a previously generated report to a different format without -re-running analysis. First, generate a binary (or JSON) report, then convert: +Every format except `plot` is written to **stdout** by default; `-o <file>` +(`--output <file>`) writes the same bytes to that file instead (created or +truncated). With `--format plot`, `-o` names the output **directory** and is +required. -```bash -# Step 1: Generate binary report -codefang run -a 'history/*' --format bin . > report.bin +There is no conversion of a saved report into another format: the format is +chosen when the analysis runs. Run once per format you need, bounding the +history window with `--limit` / `--since` to keep each run short: -# Step 2: Convert to interactive plot -codefang run -a 'history/*' --input report.bin --format plot > report.html +```bash +# JSON for the warehouse +codefang run -a 'history/*' --format json -o report.json . -# Step 3: Convert the same data to YAML -codefang run -a 'history/*' --input report.bin --format yaml +# YAML of the same window +codefang run -a 'history/*' --format yaml -o report.yaml . -# Step 4: Convert to unified time-series -codefang run -a 'history/*' --input report.bin --format timeseries +# Interactive pages +codefang run -a 'history/*' --format plot -o ./report . ``` - -The `--input-format` flag controls how the input file is parsed. It defaults to -`auto` which detects the format from the file content: - -| Value | Description | -|-------|-------------| -| `auto` | Detect from content (binary magic bytes or JSON) | -| `json` | Force JSON parsing | -| `bin` | Force binary parsing | diff --git a/site/how-to/analyze-a-bare-repo.md b/site/how-to/analyze-a-bare-repo.md index ebf99259..c538cde5 100644 --- a/site/how-to/analyze-a-bare-repo.md +++ b/site/how-to/analyze-a-bare-repo.md @@ -40,12 +40,12 @@ checking out a working tree. -type d > /tmp/repo_paths.txt ``` -4. Run a single bare repo with a memory budget so the streaming pipeline stays - bounded on large histories: +4. Run a single bare repo; bound the commit window with `--limit` on large + histories: ```bash codefang run /data/extract/repositories/@hashed/ab/cd/abcdef.git \ - -a 'history/*' --memory-budget 4GiB --workers 4 --format json --silent + -a 'history/*' --limit 5000 --format json --silent ``` 5. For periodic scans, analyze only new commits with `--since` rather than diff --git a/site/how-to/analyze-a-monorepo.md b/site/how-to/analyze-a-monorepo.md index 869f3c47..f368c789 100644 --- a/site/how-to/analyze-a-monorepo.md +++ b/site/how-to/analyze-a-monorepo.md @@ -47,15 +47,17 @@ generated noise. codefang run -a '*' --extra-excluded-prefixes '.venv/,target/,build/' --format json . ``` -5. Constrain memory on a large monorepo so the streaming pipeline auto-tunes - worker count and spill thresholds rather than consuming all available RAM: +5. Bound the history window on a large monorepo — there is no memory or + worker knob; a history run costs what its commit window costs. Limit the + range with `--since` or `--limit`, skip merge side branches with + `--first-parent`, and drop mass-change commits (vendoring dumps, + generated-code regenerations) with `--max-changes-per-commit`: ```bash - codefang run -a 'history/*' --memory-budget 4GB --workers 4 --format json --silent . + codefang run -a 'history/*' --limit 5000 --first-parent --format json --silent . ``` -6. If you need only a subset of the history, limit the commit range with - `--since` or `--limit` to keep the run bounded: +6. If you need only a period of the history, select it with `--since`: ```bash codefang run -a 'history/devs' --since 2025-01-01 --format json . diff --git a/site/how-to/attach-an-mcp-client.md b/site/how-to/attach-an-mcp-client.md index 27d8a040..94536ee0 100644 --- a/site/how-to/attach-an-mcp-client.md +++ b/site/how-to/attach-an-mcp-client.md @@ -60,20 +60,11 @@ agent can run code analysis as a tool. } ``` -5. If a call misbehaves, enable debug logging to stderr by adding the `--debug` - argument to the server registration: - - ```json - { - "mcpServers": { - "codefang": { - "command": "codefang", - "args": ["mcp", "--debug"], - "env": {} - } - } - } - ``` +5. If a call misbehaves, reproduce it on the command line: the tool result + equals what `codefang run` prints for the same input (for example + `codefang run -a history/devs --format json --limit 500 /home/user/projects/myapp`), + and the server writes its own failures to stderr, which most clients + show in their MCP log. ## Result diff --git a/site/how-to/read-a-burndown-chart.md b/site/how-to/read-a-burndown-chart.md index aae95923..25166948 100644 --- a/site/how-to/read-a-burndown-chart.md +++ b/site/how-to/read-a-burndown-chart.md @@ -54,9 +54,9 @@ you can see how much code from each era still survives at the latest sample. the visual form of the same matrix: ```bash - codefang run -a history/burndown --format plot . > burndown.html - xdg-open burndown.html # Linux - open burndown.html # macOS + codefang run -a history/burndown --format plot -o burndown . + xdg-open burndown/index.html # Linux + open burndown/index.html # macOS ``` 6. Extract just the tick axis for scripting or a custom plot: diff --git a/site/how-to/run-in-ci.md b/site/how-to/run-in-ci.md index 9399e1e1..739fd95d 100644 --- a/site/how-to/run-in-ci.md +++ b/site/how-to/run-in-ci.md @@ -74,12 +74,14 @@ detects issues. echo "${{ steps.codefang.outputs.report }}" ``` -5. If a large repository runs out of memory in CI, constrain it with a memory - budget by running the binary directly inside the job, or in Docker: +5. If a large repository runs out of memory or time in CI, bound the history + window — `--limit`, `--since`, `--first-parent`, + `--max-changes-per-commit` — by running the binary directly inside the + job, or in Docker: ```bash docker run --rm -v "$(pwd):/workspace:ro" \ - codefang run -a 'history/*' --memory-budget 2GiB --format json --silent /workspace + codefang run -a 'history/*' --limit 2000 --format json --silent /workspace ``` ## Result diff --git a/site/index.md b/site/index.md index 0c8fa656..db6acc85 100644 --- a/site/index.md +++ b/site/index.md @@ -86,11 +86,11 @@ analysis of entire repositories. One tool, one config, complete understanding. Capabilities ────────────────────────────────────────────────────────────────────────────── UAST Parsing ████████████████████ 60+ languages via Tree-sitter - Static Analysis ████████████████████ 5 analyzers (complexity, cohesion, …) - History Analysis ████████████████████ 9 analyzers (burndown, devs, …) - Output Formats ████████████████████ JSON, YAML, text, compact, plot, binary - Streaming Pipeline ████████████████████ Bounded-memory, checkpointed - Observability ████████████████████ OpenTelemetry traces + metrics + Static Analysis ████████████████████ 7 analyzers (complexity, cohesion, …) + History Analysis ████████████████████ 10 analyzers (burndown, devs, …) + Output Formats ████████████████████ JSON, YAML, NDJSON, time series, text, compact, plot, binary + History Pipeline ████████████████████ Parallel per-commit work, one chronological reduce + Determinism ████████████████████ Byte-identical output across runs and machines ``` --- @@ -212,7 +212,7 @@ codefang (v2): [Overview](architecture/overview.md) · [UAST System](architecture/uast.md) · - [Streaming Pipeline](architecture/streaming-pipeline.md) + [History Pipeline](architecture/streaming-pipeline.md) - :material-puzzle-outline:{ .lg .middle } **Integrations** diff --git a/site/integrations/ai-agents.md b/site/integrations/ai-agents.md index 31968236..0406e105 100644 --- a/site/integrations/ai-agents.md +++ b/site/integrations/ai-agents.md @@ -222,9 +222,9 @@ flowchart LR | Metric | Source | Typical Baseline | |--------|--------|-----------------| -| Comment density | `static/comments` | 15-25% of lines | +| Documentation coverage | `static/comments` | >= 60% of functions documented | | Max cyclomatic complexity | `static/complexity` | 10-15 per function | -| Cohesion score | `static/cohesion` | > 0.5 (LCOM4) | +| Cohesion score | `static/cohesion` | >= 0.6 (1 − LCOM, function-weighted) | | Halstead difficulty | `static/halstead` | < 30 per function | ### Example agent prompt diff --git a/site/integrations/docker-and-actions.md b/site/integrations/docker-and-actions.md index 462e1b2d..d65c278f 100644 --- a/site/integrations/docker-and-actions.md +++ b/site/integrations/docker-and-actions.md @@ -83,16 +83,15 @@ docker run --rm \ --format json --silent /workspace ``` -#### With observability +#### Writing the report to a file -Export traces to an OTLP collector: +`-o` writes the report into the mounted directory instead of stdout (the +mount must then be writable): ```bash docker run --rm \ - -e OTEL_EXPORTER_OTLP_ENDPOINT=host.docker.internal:4317 \ - -e OTEL_EXPORTER_OTLP_INSECURE=true \ - -v "/path/to/repo:/workspace:ro" \ - codefang run -a history/* --format json --silent /workspace + -v "/path/to/repo:/workspace" \ + codefang run -a history/* --format json -o /workspace/report.json --silent /workspace ``` #### Using the UAST CLI @@ -377,7 +376,7 @@ jobs: ### Common issues -!!! failure "Error: repository path does not exist" +!!! failure "Error: path not found: <path>" The checkout step did not run, or the `path` input points to a non-existent directory. Verify your `actions/checkout` configuration. @@ -385,10 +384,10 @@ jobs: Your checkout used the default `fetch-depth: 1` (shallow clone). Set `fetch-depth: 0` for full history or use `--since` to limit the range. -!!! failure "Error: out of memory" - For large repositories in CI, add `--memory-budget 2GiB` to constrain - memory usage. The streaming pipeline will automatically chunk the - commit history. +!!! failure "Out of memory" + There is no memory-budget flag: a history run costs what its commit + window costs. For large repositories in CI, bound the window with + `--limit`, `--since`, `--first-parent` or `--max-changes-per-commit`. ### Self-test workflow diff --git a/site/integrations/mcp.md b/site/integrations/mcp.md index 1fa9bf8e..fa1e731a 100644 --- a/site/integrations/mcp.md +++ b/site/integrations/mcp.md @@ -5,7 +5,7 @@ description: Model Context Protocol server for AI agent integration, exposing Co # MCP server -Codefang includes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) +Codefang includes a [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that exposes its analysis capabilities as tools that AI agents can discover and invoke programmatically. @@ -38,18 +38,14 @@ The server communicates over **stdio** (stdin/stdout) using JSON-RPC, which is the standard MCP transport for local tool use. Agents connect by launching the process and reading/writing to its stdio streams. -### Debug mode - -Enable debug logging to stderr for troubleshooting: - -```bash -codefang mcp --debug -``` - -Debug mode enables: - -- `slog.LevelDebug` logging with JSON format on stderr -- 100% trace sampling (all spans exported) +Every tool runs the CLI's own pipeline, so a tool returns the report `codefang run` +renders for the same input: `codefang_analyze` returns +`{analyzer: report}` with each report equal to +`codefang run -a static/<analyzer> --format json` over the snippet, and +`codefang_history` returns `{analyzer: report}` with each report equal to +`codefang run -a history/<analyzer> --format json --limit <limit>` over the +repository. The server holds no results between calls. Errors and the +server's own failures go to stderr; stdout carries only JSON-RPC. --- @@ -77,7 +73,7 @@ metrics. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `code` | string | **Yes** | -- | Source code to analyze (max 1 MB) | -| `language` | string | **Yes** | -- | Programming language (e.g., `go`, `python`, `javascript`) | +| `language` | string | **Yes** | -- | Language name or alias (Linguist: `go`, `python`, `JavaScript`, `py`); the snippet is parsed as `code` plus the language's primary extension | | `analyzers` | string[] | No | all | List of analyzer names to run | #### Available analyzers @@ -115,7 +111,7 @@ node type. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `code` | string | **Yes** | -- | Source code to parse (max 1 MB) | -| `language` | string | **Yes** | -- | Programming language (e.g., `go`, `python`, `javascript`) | +| `language` | string | **Yes** | -- | Language name or alias (Linguist: `go`, `python`, `JavaScript`, `py`); the snippet is parsed as `code` plus the language's primary extension | | `query` | string | No | -- | Optional node type filter (e.g., `function_declaration`) | #### Example @@ -211,36 +207,6 @@ Add Codefang as an MCP server in your Claude Code settings: } ``` -=== "With debug mode" - - ```json - { - "mcpServers": { - "codefang": { - "command": "codefang", - "args": ["mcp", "--debug"], - "env": {} - } - } - } - ``` - -=== "With observability" - - ```json - { - "mcpServers": { - "codefang": { - "command": "codefang", - "args": ["mcp"], - "env": { - "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317" - } - } - } - } - ``` - ### Generic MCP client Any MCP-compatible client can connect by launching the process: @@ -274,71 +240,20 @@ error message in the `content` field. --- -## Observability - -The MCP server supports full OpenTelemetry instrumentation: - -### Tracing - -Each tool invocation creates a root span with `SpanKindServer`: - -| Span | Attributes | -|------|------------| -| `mcp.codefang_analyze` | `mcp.tool`, analyzer list | -| `mcp.uast_parse` | `mcp.tool`, language | -| `mcp.codefang_history` | `mcp.tool`, repo path, commit limit | - -When a span is sampled, the `trace_id` is included in the response content -for correlation with backend traces. - -### Metrics - -RED metrics are recorded for each tool invocation: - -| Metric | Description | -|--------|-------------| -| `codefang.requests.total` | Request count by tool and status | -| `codefang.request.duration.seconds` | Request duration histogram | -| `codefang.errors.total` | Error count by tool | -| `codefang.inflight.requests` | In-flight request gauge | - -### Configuration - -Set the OTLP endpoint to enable telemetry export: - -```json -{ - "mcpServers": { - "codefang": { - "command": "codefang", - "args": ["mcp"], - "env": { - "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317", - "OTEL_EXPORTER_OTLP_INSECURE": "true" - } - } - } -} -``` - -Without an OTLP endpoint, the server uses no-op providers with zero overhead. - ---- - ## Architecture ```mermaid flowchart LR AGENT["AI Agent<br/>(Claude, etc.)"] STDIO["stdio<br/>(JSON-RPC)"] - MCP_SERVER["MCP Server<br/><em>internal/mcp</em>"] + MCP_SERVER["MCP Server<br/><em>cf-mcp</em>"] ANALYZE["codefang_analyze"] UAST_PARSE["uast_parse"] HISTORY["codefang_history"] - STATIC_SVC["Static Analysis<br/>Service"] - UAST_PARSER["UAST Parser"] - HIST_PIPELINE["History Pipeline<br/><em>framework + gitlib</em>"] + STATIC_SVC["codefang run<br/><em>static analyzers</em>"] + UAST_PARSER["UAST Parser<br/><em>cf-uast</em>"] + HIST_PIPELINE["codefang run<br/><em>history pipeline</em>"] AGENT <-->|MCP protocol| STDIO STDIO <--> MCP_SERVER @@ -347,6 +262,8 @@ flowchart LR MCP_SERVER --> HISTORY --> HIST_PIPELINE ``` -The MCP server reuses the same analysis infrastructure as the CLI -- the same -analyzers, parsers, and pipeline components. The only difference is the -transport layer (stdio JSON-RPC instead of command-line flags and stdout). +The MCP server (`cf-mcp`, launched by `codefang mcp` in +`crates/cf-commands/src/mcp.rs`) renders every tool result through the CLI's +own pipeline (`cf_commands::render_run`), so the analyzers, the parser, the +memory budget and the report bytes are the CLI's. Only the transport differs: +newline-delimited JSON-RPC on stdio instead of flags and stdout. diff --git a/site/operations/large-scale-scanning.md b/site/operations/large-scale-scanning.md index 5ed95c0a..ebf0821b 100644 --- a/site/operations/large-scale-scanning.md +++ b/site/operations/large-scale-scanning.md @@ -23,8 +23,10 @@ graph LR E --> F[Data Warehouse] ``` -The streaming pipeline, checkpointing, and bounded-memory execution make each -individual run safe on large repositories without manual tuning. +Each run walks the requested commit window once and is a pure function of the +repository and the flags, so runs are independent and safe to retry. What a +run costs is set by the size of its window — see +[Bounding each run](#bounding-each-run). ## Bare repository support @@ -117,8 +119,7 @@ cat "$REPOS" | parallel -j 8 --joblog /tmp/codefang-jobs.log ' codefang run {} \ -a history/* \ --format json \ - --memory-budget 4GiB \ - --workers 4 \ + --limit 5000 \ --silent \ 2>/tmp/codefang-logs/${REPO_NAME}.log \ | aws s3 cp - '"${S3_BUCKET}/${DATE}"'/${REPO_NAME}.json @@ -151,17 +152,13 @@ spec: codefang run /workspace \ -a history/* \ --format json \ - --memory-budget 4GiB \ - --workers 4 \ - --checkpoint \ + --limit 5000 \ --silent \ | aws s3 cp - s3://bucket/results.json volumeMounts: - name: repo mountPath: /workspace readOnly: true - - name: checkpoint - mountPath: /tmp/codefang-checkpoints resources: requests: cpu: "4" @@ -172,31 +169,34 @@ spec: - name: repo persistentVolumeClaim: claimName: repo-myrepo - - name: checkpoint - emptyDir: {} restartPolicy: OnFailure ``` !!! info "Key considerations" - - **Resource requests**: 4 CPU / 6 Gi memory per pod handles most repositories - - **Checkpoint volume**: use `emptyDir` for ephemeral or a PVC for crash-recovery - - **Parallelism**: 50-100 concurrent pods is typical for 60k repositories + - **Resource requests**: size them from a measured run of your largest + repository with the same window flags (`/usr/bin/time -v` reports the + maximum resident set size) + - **Retries**: a run writes no state to disk, so `backoffLimit` retries + simply re-run the same window - **Scheduling**: use a CronJob or Argo CronWorkflow for periodic scans -## Memory budget and streaming +## Bounding each run -The `--memory-budget` flag controls how Codefang splits a repository's commit -history into chunks. See [Streaming Pipeline](../architecture/streaming-pipeline.md) -for details. +There is no memory-budget, worker or cache flag: the history phase walks the +commit window once and every analyzer reduces it in chronological order. +Bound the window to bound a run: -| Repository Size | Memory Budget | Expected Behavior | -|-----------------|---------------|-------------------| -| < 1k commits | 2 GiB | Single chunk, no hibernation | -| 1k-10k commits | 4 GiB | 2-10 chunks | -| 10k-100k commits | 4-8 GiB | 10-100 chunks with checkpoints | -| 100k+ commits | 8 GiB | Many chunks, checkpointing essential | +| Flag | Effect | +|------|--------| +| `--limit N` | Only the N newest commits | +| `--since <time or revision>` | Only commits authored at/after the cutoff | +| `--first-parent` | Skip the side branches of merges | +| `--max-changes-per-commit N` | Drop commits whose tree diff exceeds N changes (default 10000) | +| `--languages go,python` | Only changes in these languages reach the analyzers | +| `--head` | The single HEAD commit | -When unset, the budget defaults to 50% of system memory (capped at 4 GiB). +Measured on the hercules repository, `codefang run -a 'history/*' --limit 500` +peaks at 752 MB resident memory and finishes in 4.7 s wall time. ## Incremental scanning with `--since` @@ -216,28 +216,17 @@ codefang run /path/to/repo -a history/* --format json --since 2025-01-01T00:00:0 | Format | Example | Meaning | |--------|---------|---------| -| Go duration | `168h` | 7 days before now | +| Duration (`h`/`m`/`s`) | `168h` | 7 days before now | | Date only | `2025-01-01` | Midnight UTC on that date | | RFC3339 | `2025-01-01T00:00:00Z` | Exact timestamp | -## Checkpointing and crash recovery +## Interrupted runs -Checkpointing is enabled by default. After each fully processed chunk, the -pipeline saves analyzer state to disk. If a run is interrupted (OOM kill, pod -eviction, timeout), the next invocation with `--resume` automatically restarts -from the last completed chunk. - -```bash -codefang run /path/to/repo \ - -a history/* \ - --format json \ - --checkpoint \ - --resume \ - --checkpoint-dir /tmp/codefang-checkpoints -``` - -!!! tip "Kubernetes" - Mount the checkpoint directory on a PVC to survive pod restarts. +There is no checkpoint or resume: a run either writes its whole report or +none. The first SIGINT / SIGTERM cancels the run — no report is written, +stderr shows `Error: interrupted` and the exit code is 130; a second signal +terminates the process immediately. Retry an interrupted scan with the same +flags (or a `--since` window covering the missed period). ## DWH loading @@ -286,69 +275,35 @@ devs_df = df.select("repo", explode("results.devs.authors").alias("author")) devs_df.write.partitionBy("scan_date").parquet("s3://warehouse/codefang/devs/") ``` -## Static analysis memory monitoring - -The static analysis phase logs progress events at regular intervals (every -1000 files). By default, only phase and file count are shown: - -```text -static: processing files=1000 -static: processing files=2000 -static: complete files=5432 -``` - -With `--verbose`, RSS and aggregator buffer sizes are included: +## Monitoring a run -```text -static: processing files=1000 RSS=245MiB agg=12MiB -static: processing files=2000 RSS=312MiB agg=18MiB -static: complete files=5432 RSS=280MiB agg=6MiB -``` - -For live profiling, use `--profile` to start a pprof HTTP server on -`localhost:6060` and a memory watchdog that dumps heap profiles when RSS -exceeds 4 GiB. +`codefang run` writes its report to stdout (or to the `-o` file) and its +diagnostics to stderr: -These logs appear every 1000 files and after completion. The budget solver -(`budget.SolveStaticBudget`) automatically derives worker count and spill -threshold from the memory budget: +- `progress:` lines — the resolved selection at start, the skipped-file + count and the elapsed time at the end; `-q` / `--quiet` (alias `--silent`) + suppresses them, `-v` / `--verbose` adds the config file and the resolved + analyzer ids; +- `warning:` lines — skipped unreadable or oversized files, ignored config + keys; printed even with `--quiet`; +- `Error: <cause>` on failure, with exit code 1. -| Budget | Max Workers | Spill Threshold | -|--------|-------------|-----------------| -| 512 MiB | 4 | 5,461 | -| 1 GiB | 8 | 17,476 | -| 2 GiB | 8 | 41,530 | -| 4 GiB | 8 | 89,641 | - -For text/compact output formats, the `SummaryOnly` aggregation mode is -automatically enabled, reducing heap usage by ~97% (no per-function data -retained in memory). +`--profile` adds a memory watchdog: `MEM t=<s> RSS=<MiB> threads=<n>` on +stderr at start and every 2 s, a `/proc/self/maps` snapshot in the temp +directory (`$TMPDIR`, else `/tmp`) at start, and another one when RSS exceeds +4096 MiB. ## Observability -Codefang includes OpenTelemetry support for monitoring scan fleet health. -See [Observability](observability.md) for full details. - -```bash -export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 -codefang run /path/to/repo -a history/* --format json --silent -``` - -| Metric | Use | -|--------|-----| -| `codefang.request.duration.seconds` | Identify slow repositories | -| `codefang.errors.total` | Track failure rate across the fleet | -| `codefang.cache.hits` / `codefang.cache.misses` | Tune cache sizes | +See [Observability](observability.md) for the stderr diagnostics, exit codes +and the `--profile` watchdog a fleet orchestrator can collect. -## Sizing estimates +## Measured reference -| Parameter | Typical Value | -|-----------|---------------| -| Per-repo runtime (4 cores, 4 GiB) | 1-10 min | -| JSON output per repo | 100 KB - 50 MB | -| 60k repos at 100 concurrent pods | 10-100 hours per full scan | -| Total S3 per full scan | 10-100 GB | -| Incremental scan (7 days) | 5-20x faster than full | +| Run | Max RSS | Wall time | +|-----|---------|-----------| +| hercules, `-a 'history/*' --limit 500` | 752 MB | 4.7 s | -The primary bottleneck is I/O (reading Git objects). Bare repos on local SSD -or NVMe-backed EBS deliver the best throughput. +Measure your own fleet's largest repository with the window flags you plan +to use (`/usr/bin/time -v codefang run …`) before sizing pod requests. Bare +repositories on local SSD or NVMe-backed volumes keep Git object reads fast. diff --git a/site/operations/observability.md b/site/operations/observability.md index 0fdca050..f7682a8e 100644 --- a/site/operations/observability.md +++ b/site/operations/observability.md @@ -1,495 +1,85 @@ --- title: Observability -description: OpenTelemetry tracing, RED metrics, structured logging, sampling, attribute filtering, and health checks for Codefang. +description: What a codefang run reports on stderr — progress lines, warnings, named errors, exit codes — and the --profile memory watchdog. --- # Observability -Codefang includes built-in OpenTelemetry support for **distributed tracing**, -**RED metrics** (Rate, Errors, Duration), and **structured logging** with -automatic trace context injection. Observability is zero-cost when disabled -and production-ready when enabled. +A `codefang run` keeps its two output channels separate: the **report** goes +to stdout (or to the file named by `-o`), and every **diagnostic** goes to +stderr. A fleet orchestrator can therefore pipe stdout straight into storage +and collect stderr as the run log. ---- - -## Quick start - -By default, observability uses no-op providers with zero export overhead. To -enable telemetry export, set the OTLP endpoint: - -```bash -export OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 -codefang run -a history/burndown . -``` - -For local debugging without a collector, use `--debug-trace`: - -```bash -codefang run --debug-trace -a history/burndown . -``` - -This forces 100% trace sampling and prints the `trace_id` on completion. +The Rust binary exports no telemetry: there is no OpenTelemetry exporter, no +metrics endpoint and no trace sampling flag. Everything a run says about +itself is in the stderr lines and the exit code described below. --- -## Local development stack - -A pre-configured OTel stack (Jaeger + OTel Collector + Prometheus) is available -via Docker Compose: - -```bash -make otel-up # Start Jaeger, OTel Collector, Prometheus -make demo # Run analysis with tracing and print trace link -make otel-down # Stop the stack -``` +## Stderr lines -| Service | URL | Purpose | -|---------|-----|---------| -| Jaeger UI | [http://localhost:16686](http://localhost:16686) | Trace visualization | -| Prometheus | [http://localhost:9090](http://localhost:9090) | Metrics queries | -| OTel Collector | `localhost:4317` (gRPC) | Telemetry ingestion | - ---- +| Line | When | Suppressed by `-q` | +|------|------|--------------------| +| `progress: resolved analyzers: static=<n> history=<n> output_format=<f>` | at start | yes | +| `progress: config file: <path>` / `progress: config file: none` | at start, with `-v` | yes | +| `progress: analyzers: <id>,<id>,…` | at start, with `-v` | yes | +| `progress: skipped <n> unreadable/oversized/unparseable files` | at the end, when files were skipped | yes | +| `progress: run finished in <ms>ms` / `in <s>s` | at the end | yes | +| `warning: skipped <path>: <reason>` | once per skipped file | no | +| `warning: ignoring unsupported config keys: <key>, …` | once, when the config file carries keys codefang does not read | no | +| `--per-file applies to --format json only; ignored for <format>` | once, `--per-file` with another format | no | +| `Error: <cause>` | on failure | no | -## Configuration - -### Environment variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `OTEL_EXPORTER_OTLP_ENDPOINT` | gRPC endpoint for OTLP collector | (empty = no-op) | -| `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` gRPC metadata headers | (empty) | -| `OTEL_EXPORTER_OTLP_INSECURE` | Skip TLS verification (`true`/`false`) | `false` | -| `OTEL_TRACES_SAMPLER` | Sampler type (see [Sampling](#sampling)) | (uses config) | -| `OTEL_TRACES_SAMPLER_ARG` | Sampler argument (ratio for ratio-based samplers) | `1.0` | - -### CLI flags - -| Flag | Description | -|------|-------------| -| `--debug-trace` | Force 100% sampling, enable debug logging | - -### MCP mode - -The MCP server (`codefang mcp`) uses JSON-structured logs on stderr by default. -When `--debug` is enabled, debug-level logging and full trace sampling are -activated: +`-q` / `--quiet` (alias `--silent`) removes the `progress:` lines; +`-v` / `--verbose` adds the config-file and resolved-analyzer lines. ```bash -codefang mcp --debug -``` - -### No-op mode +# Report to a file, progress on the terminal +codefang run -a 'history/*' --limit 500 -o report.json . -When `OTEL_EXPORTER_OTLP_ENDPOINT` is empty (the default), the SDK returns -no-op tracer and meter providers: +# Only warnings and errors on stderr +codefang run -a 'static/*' -q --format json . -- Zero allocation overhead for span creation -- Zero network overhead (no gRPC connections) -- Structured logging still works (trace_id/span_id omitted when no active span) - ---- - -## Span hierarchy - -The entire analysis pipeline is instrumented with hierarchical spans: - -```text -codefang.run (CLI entry point) -| -+-- codefang.init (analyzer + repo initialization) -| -+-- codefang.analysis (streaming mode wrapper) -| | -| +-- codefang.runner.chunk (per-chunk processing) -| | | -| | +-- codefang.pipeline (coordinator pipeline) -| | | -| | +-- codefang.analyzer.consume (per-analyzer, sequential) -| | | -| | +-- codefang.analyzer.fork (parallel leaf forking) -| | | -| | +-- codefang.analyzer.merge (parallel leaf merging) -| | -| +-- checkpoint.saved (span event) -| | -| +-- checkpoint.resumed (span event) -| -+-- codefang.report (output generation) -| -+-- codefang.git.* (git operations) -| | -| +-- codefang.git.lookup_commit -| +-- codefang.git.lookup_blob -| +-- codefang.git.diff_tree -| +-- codefang.git.log -| -+-- codefang.uast.* (UAST operations) - | - +-- codefang.uast.parse - +-- codefang.uast.parse_dsl - +-- codefang.uast.changes +# Show which config file and which analyzers the run resolved +codefang run -a 'history/*' --limit 50 -v . ``` -### Span attributes - -| Span | Key Attributes | -|------|---------------| -| `codefang.run` | `error`, `codefang.duration_class`, `codefang.format`, `codefang.memory_sys_mb`, `codefang.path`, `codefang.analyzers`, `codefang.limit` | -| `codefang.init` | `init.commits`, `init.analyzers` | -| `codefang.analysis` | `analysis.chunks`, `analysis.chunk_size`, `analysis.double_buffered`, `analysis.slowest_chunk_ms`, `analysis.total_chunk_ms` | -| `codefang.pipeline` | `commits.count`, `pipeline.workers` | -| `codefang.runner.chunk` | `chunk.size`, `chunk.offset` | -| `codefang.analyzer.consume` | `analyzer.id` | -| `codefang.analyzer.fork` | `fork.workers`, `fork.leaves` | -| `codefang.report` | `report.format`, `report.analyzers` | -| `codefang.git.*` | `git.hash`, `git.operation` | -| `codefang.uast.parse` | `uast.language`, `file.size` | -| `mcp.*` | `mcp.tool` | - -Pipeline cache statistics are also recorded on the analysis span: - -| Attribute | Description | -|-----------|-------------| -| `analysis.cache.blob.hits` | Blob cache hit count | -| `analysis.cache.blob.misses` | Blob cache miss count | -| `analysis.cache.blob.hit_pct` | Blob cache hit percentage | -| `analysis.cache.diff.hits` | Diff cache hit count | -| `analysis.cache.diff.misses` | Diff cache miss count | -| `analysis.cache.diff.hit_pct` | Diff cache hit percentage | -| `analysis.pipeline.dominant` | Slowest pipeline stage (`blob`, `diff`, or `uast`) | - --- -## RED metrics - -When connected to an OTLP collector, the following metrics are exported: - -### Core RED metrics - -| Metric | Type | Unit | Description | -|--------|------|------|-------------| -| `codefang.requests.total` | Counter | `{request}` | Total number of requests (labeled by `op`, `status`) | -| `codefang.request.duration.seconds` | Histogram | `s` | Request duration (labeled by `op`, `status`) | -| `codefang.errors.total` | Counter | `{error}` | Total number of errors (labeled by `op`) | -| `codefang.inflight.requests` | UpDownCounter | `{request}` | Currently in-flight requests (labeled by `op`) | +## Exit codes -### Analysis metrics +| Code | Meaning | +|------|---------| +| `0` | The report was written | +| `1` | A user error (`Error: <cause>` names it: path not found, not a git repository, unsupported format, invalid flag value, unreadable `--config`, …) or a command-line usage error | +| `130` | Interrupted by SIGINT / SIGTERM — no report is written (`Error: interrupted`); a second signal terminates immediately | +| `141` | The downstream pipe closed (`codefang run … \| head`) | -| Metric | Type | Unit | Description | -|--------|------|------|-------------| -| `codefang.analysis.commits.total` | Counter | `{commit}` | Total commits analyzed | -| `codefang.analysis.chunks.total` | Counter | `{chunk}` | Total chunks processed | -| `codefang.analysis.chunk.duration.seconds` | Histogram | `s` | Per-chunk processing duration | -| `codefang.analysis.cache.hits.total` | Counter | `{hit}` | Cache hits (labeled by `cache`: `blob` or `diff`) | -| `codefang.analysis.cache.misses.total` | Counter | `{miss}` | Cache misses (labeled by `cache`: `blob` or `diff`) | - -### Histogram buckets - -Duration histograms use these bucket boundaries (in seconds), covering -sub-second static checks through multi-minute history pipelines: - -```text -0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600 -``` +The full list of error messages is in the +[CLI reference](../guide/cli-reference.md#errors). --- -## Sampling - -Trace sampling is configurable with the following precedence (highest first): - -1. **`--debug-trace` CLI flag**: Forces 100% sampling (AlwaysOn). -2. **`OTEL_TRACES_SAMPLER` env var**: Standard OTel sampler selection. -3. **`Config.SampleRatio`**: Ratio-based sampling via config file. -4. **Default**: `ParentBased(AlwaysOn)` -- samples all root spans, respects parent decisions. - -### Supported sampler values +## Memory watchdog (`--profile`) -| `OTEL_TRACES_SAMPLER` | Description | -|------------------------|-------------| -| `always_on` | Sample every span | -| `always_off` | Drop every span | -| `traceidratio` | Sample based on trace ID ratio (`OTEL_TRACES_SAMPLER_ARG`) | -| `parentbased_always_on` | Parent-based with always-on root sampler | -| `parentbased_always_off` | Parent-based with always-off root sampler | -| `parentbased_traceidratio` | Parent-based with ratio root sampler | - -### Example: 10% sampling - -```bash -export OTEL_TRACES_SAMPLER=traceidratio -export OTEL_TRACES_SAMPLER_ARG=0.1 -codefang run -a history/burndown . -``` - -### Production recommendation - -Use `parentbased_traceidratio` with a low ratio (1-5%) to limit trace volume -while preserving distributed context propagation: - -```bash -export OTEL_TRACES_SAMPLER=parentbased_traceidratio -export OTEL_TRACES_SAMPLER_ARG=0.01 -``` - -### Tail sampling (collector-side) - -Head-based sampling decides at trace start whether to sample. This means slow -or errored traces may be dropped. **Tail sampling** defers the decision until -the trace completes, keeping all interesting traces. - -Codefang root spans carry attributes that enable OTel Collector tail-sampling -policies: - -| Attribute | Type | Values | Purpose | -|-----------|------|--------|---------| -| `error` | bool | `true`/`false` | Keep all errored traces | -| `codefang.duration_class` | string | `fast`/`normal`/`slow` | Keep slow traces | - -**Duration class thresholds:** - -| Class | Duration | -|-------|----------| -| `fast` | < 10s | -| `normal` | 10s -- 60s | -| `slow` | > 60s | - -**Recommended OTel Collector tail-sampling config:** - -```yaml -processors: - tail_sampling: - decision_wait: 30s - policies: - # Keep all traces with errors. - - name: errors - type: boolean_attribute - boolean_attribute: - key: error - value: true - - # Keep all slow traces (>60s). - - name: slow-traces - type: string_attribute - string_attribute: - key: codefang.duration_class - values: [slow] - - # Sample 5% of remaining traces. - - name: probabilistic - type: probabilistic - probabilistic: - sampling_percentage: 5 -``` - -This ensures 100% of error and slow traces are retained while keeping a -representative sample of normal traffic. - ---- - -## Attribute filter - -All exported spans pass through an `AttributeFilter` span processor that -enforces an allow-list of attribute key prefixes. This prevents PII and -high-cardinality data from reaching the collector. - -### Allowed prefixes - -| Prefix | Covers | -|--------|--------| -| `codefang.` | All Codefang-specific attributes | -| `error.` | Error type and source classification | -| `http.` | HTTP semantic conventions | -| `mcp.` | MCP tool attributes | -| `analysis.` | Analysis-specific attributes | -| `analyzer.` | Per-analyzer attributes | -| `chunk.` | Chunk processing attributes | -| `init.` | Initialization attributes | -| `pipeline.` | Pipeline stage attributes | -| `report.` | Report generation attributes | -| `runner.` | Runner attributes | -| `cache` | Cache statistics | - -### Blocked keys - -| Pattern | Reason | -|---------|--------| -| `user.*` | PII -- user-identifying information | -| `email` | PII -- email addresses | -| `request.body` | Potentially sensitive request content | -| `response.body` | Potentially sensitive response content | - -When `--debug-trace` is enabled, blocked attributes are logged as warnings -to stderr, helping developers identify instrumentation issues during -development. - ---- - -## HTTP middleware - -The `observability.HTTPMiddleware` wraps HTTP handlers with: - -- **W3C trace context extraction**: Reads `traceparent`/`tracestate`/`baggage` headers from incoming requests. -- **Server span creation**: Creates a child span under the extracted parent context. -- **Status code capture**: Records HTTP response status on the span. -- **Error detection**: Sets error status on 5xx responses. -- **Panic recovery**: Catches panics, records the stack trace as a span event, and returns 500. -- **Access logging**: Structured log line with method, path, status, and duration. - -This enables distributed tracing across service boundaries: upstream services -that send a `traceparent` header have their traces continued by Codefang. - ---- - -## Health checks - -For server mode deployments (Kubernetes), the observability package provides -HTTP handlers: - -### Liveness probe - -```text -GET /healthz -> HTTP 200 {"status": "ok"} -``` - -Always returns 200. Use as a Kubernetes liveness probe. - -### Readiness probe - -```text -GET /readyz -> HTTP 200 {"status": "ok"} - -> HTTP 503 {"status": "unavailable"} -``` - -Runs all registered `ReadyCheck` functions. Returns 200 if all pass, 503 -if any fail. Use as a Kubernetes readiness probe. - -```go -mux.Handle("/healthz", observability.HealthHandler()) -mux.Handle("/readyz", observability.ReadyHandler(dbCheck, cacheCheck)) -``` - -### Prometheus metrics endpoint +`--profile` (a global flag, valid with every command) starts a background +watchdog that writes one line to stderr at start and every 2 seconds: ```text -GET /metrics -> Prometheus exposition format +MEM t=0 RSS=<MiB> threads=<n> +MEM t=2 RSS=<MiB> threads=<n> ``` -Creates an OTel Prometheus exporter with an independent registry. Includes -OTel SDK `target_info` and all custom metrics registered through the meter -provider. +It also copies `/proc/self/maps` to `maps_baseline.txt` in the temp directory +(`$TMPDIR`, else `/tmp`) at start and, the first time resident memory exceeds +4096 MiB, to `maps_spike_<rss>MiB.txt` there, logging the smaps rollup of each spike +(`SPIKE #<n>: RSS=<MiB> MiB smaps: …`, at most five). The watchdog reads +Linux `/proc`; elsewhere it logs zeros. It never changes the report. -```go -metricsHandler, err := observability.PrometheusHandler() -mux.Handle("/metrics", metricsHandler) -``` - ---- - -## Structured logging - -All framework-level log messages use `log/slog` with instance loggers (not -the global `slog.Default()`). The `TracingHandler` automatically injects -trace context into every log record: - -| Injected Field | Source | -|----------------|--------| -| `trace_id` | Active span's trace ID | -| `span_id` | Active span's span ID | -| `service` | Service name from config | -| `mode` | Operating mode (`cli`, `mcp`) | -| `env` | Deployment environment | - -### Log modes - -| Mode | Handler | Use case | -|------|---------|----------| -| CLI | `slog.TextHandler` | Human-readable console output | -| MCP | `slog.JSONHandler` | Machine-parseable structured logs | -| Debug | Level `DEBUG` | Verbose output for troubleshooting | - -### Linter enforcement - -The following linters enforce observability correctness in `.golangci.yml`: - -| Linter | Rule | -|--------|------| -| `spancheck` | Verifies proper span lifecycle (End calls, error recording) | -| `sloglint` | Enforces `no-global: all` (use instance loggers) and `context: scope` (use `InfoContext` when ctx is available) | -| `contextcheck` | Flags `context.Background()` instead of propagating parent context | - ---- - -## Deep context propagation - -All hot-path `context.Background()` calls have been eliminated. Context flows -end-to-end from the CLI entry point through every layer: - -### Propagation path - -```text -CLI (codefang.run) - | - +-> framework.RunStreaming(ctx) - | - +-> processChunks*(ctx) - | - +-> runner.ProcessChunk(ctx) - | - +-> coordinator.Process(ctx) - | | - | +-> worker.handle(ctx) - | | - | +-> gitlib.TreeDiff(ctx) - | +-> gitlib.LookupBlob(ctx) - | +-> uast.Parse(ctx) - | - +-> analyzer.Consume(ctx) - | - +-> plumbing.Changes(ctx) - | - +-> parser.Parse(ctx) -``` - -### Key interfaces - -The `Consume` method on history analyzers accepts context: - -```go -type HistoryProcessor interface { - Initialize(repository *gitlib.Repository) error - Consume(ctx context.Context, ac *Context) error - Finalize() (Report, error) -} -``` - -The UAST parser accepts context for cancellation support: - -```go -type LanguageParser interface { - Parse(ctx context.Context, filename string, content []byte) (*node.Node, error) -} -``` - -Git operations accept context for tracing: - -```go -func (r *Repository) LookupCommit(ctx context.Context, hash plumbing.Hash) (*Commit, error) -func (r *Repository) LookupBlob(ctx context.Context, hash plumbing.Hash) (*Blob, error) -func TreeDiff(ctx context.Context, repo *Repository, oldTree, newTree *Tree) (Changes, error) +```bash +codefang --profile run -a 'history/*' --limit 500 . ``` ---- - -## Integration points summary - -| Component | Tracing | Metrics | Logging | -|-----------|---------|---------|---------| -| CLI `codefang run` | Root span + child spans | RED for `cli.run` | Progress logging | -| MCP Server | Per-tool spans with `SpanKindServer` | RED per tool | JSON structured logs | -| Framework Pipeline | Chunk + analyzer spans | Analysis metrics | Chunk progress logs | -| Coordinator | Pipeline span | Cache hit/miss stats | Worker diagnostics | -| Git Operations | Per-operation spans | -- | Error logging | -| UAST Parser | Parse spans | -- | Warning logging | -| HTTP Server | Per-request spans | -- | Access logs | +For a whole-run peak, `/usr/bin/time -v` reports the maximum resident set +size; measured on the hercules repository, `-a 'history/*' --limit 500` +peaks at 752 MB in 4.7 s wall time. diff --git a/site/release-notes/unreleased.md b/site/release-notes/unreleased.md index 5930921f..da089f69 100644 --- a/site/release-notes/unreleased.md +++ b/site/release-notes/unreleased.md @@ -13,8 +13,8 @@ These notes describe the upcoming first release line of Codefang. No `v*` tag ha - The tool ships as two composable binaries: `uast` (a Tree-sitter-based Universal AST parser for 60+ languages) and `codefang` (the static and history analysis engine). - Several output formats target both humans and data warehouses: `timeseries`, `plot` (interactive HTML), and `ndjson` (streaming, warehouse-friendly). - An MCP server (`codefang mcp`) exposes analysis to AI agents over the Model Context Protocol. -- A streaming pipeline with checkpointing and crash recovery makes bounded-memory analysis of very large repositories practical. -- OpenTelemetry observability provides distributed tracing, RED metrics, and structured logging. +- One shared history walk per run: every history analyzer reduces the same commit window in chronological order, and the window flags (`--limit`, `--since`, `--first-parent`, `--max-changes-per-commit`, `--languages`) bound what a run costs. +- Every `codefang` flag and subcommand listed by `--help` has an effect; the configuration file (`--config`, `./.codefang.yaml`, `$HOME/.codefang.yaml`) supplies the defaults of the `run` flags. ## Upgrade notes @@ -23,7 +23,8 @@ This is the first release line, so there is no prior version to upgrade from. If - **Default file filtering changed.** Analysis now excludes vendored and generated files by default, matching mature multi-language analyzers. To restore the previous behavior, pass `--include-vendored --include-generated`. - **`--languages` is now cross-phase.** It narrows both static and history analysis through a single source of truth and fails fast on unknown language tokens. Pipelines that relied on static analysis ignoring this flag must drop it or supply valid tokens. - **Output schema changes.** Several fields moved from maps to sorted arrays so they can be UNNEST'd in columnar warehouses: `developers[].languages`, `activity[].by_developer`, and `file_contributors[].contributors`. Clone-pair `func_a` / `func_b` paths are now relative. Update any consumers that read these fields. -- **Deprecated flags.** `--skip-blacklist` is now a no-op, and `--blacklisted-prefixes` is superseded by `--extra-excluded-prefixes` (identical semantics). Both emit a deprecation warning when passed. +- **Removed flags.** Flags that were accepted but had no effect are gone and now fail as unknown arguments: `--checkpoint`, `--checkpoint-dir`, `--resume`, `--clear-checkpoint`, `--cache-dir`, `--no-cache`, `--debug-trace`, `--skip-blacklist`, `--blacklisted-prefixes` (use `--extra-excluded-prefixes`), and the Go-era tuning flags (`--memory-budget`, `--workers`, `--static-workers`, `--gogc`, `--ballast-size`, the cache / arena / batch sizes, `--tmp-dir`, `--keep-store`, `--input`, `--input-format`, …). Drop them from scripts. The `codefang render` subcommand is removed; use `codefang run --format plot -o <dir>`. +- **Configuration keys.** Only the keys that map to a live `run` flag are read (`analyzers`, `pipeline.max_changes_per_commit`, `history.tick_size`, burndown granularity / sampling / tracking, devs empty-commits / anonymize, sentiment minimum length, shotness DSL, typos distance, anomaly threshold / window). Every other key — the Go-era `pipeline:` tuning tree, `checkpoint:`, hibernation and analyzer threshold keys — is ignored with one `warning: ignoring unsupported config keys: …` line. ## Added @@ -34,18 +35,13 @@ This is the first release line, so there is no prior version to upgrade from. If - MCP server (`codefang mcp`) exposing analysis capabilities as tools for AI agents via the Model Context Protocol (stdio transport, JSON-RPC 2.0), with the `codefang_analyze`, `uast_parse`, and `codefang_history` tools. - Docker support with a multi-stage Dockerfile and a `debian:bookworm-slim` runtime image that runs as a non-root `codefang` user by default. - GitHub Actions integration (`action.yml`) for automated code-quality checks in CI pipelines with configurable analyzers, formats, and quality gates. -- OpenTelemetry observability with distributed tracing, RED metrics (rate, errors, duration), and structured logging with trace-context injection. Supports Jaeger, Prometheus, and OTLP collectors. -- Streaming pipeline for bounded-memory processing of large repositories via chunk-based processing with hibernate/boot cycles. -- Double-buffered chunk pipelining that overlaps pipeline prefetch with analyzer consumption for higher throughput on multi-chunk workloads. -- Checkpointing with automatic save after each processed chunk and crash recovery via the `--resume` flag. Checkpoint format v2 preserves aggregator spill state so resumed runs produce identical output. -- Configuration system (`.codefang.yaml`) with file, environment-variable, and CLI-flag support. Merge priority: CLI > env > file > defaults. +- Configuration system (`.codefang.yaml`, explicit `--config <file>`, else `./.codefang.yaml`, else `$HOME/.codefang.yaml`) with file, `CODEFANG_*` environment-variable and CLI-flag support. Merge priority: explicit CLI flag > env > file > compiled defaults. +- `-o <file>` writes any non-plot report to a file instead of stdout; `-q` / `--quiet` (alias `--silent`) suppresses the `progress:` lines and `-v` / `--verbose` adds the config file and resolved analyzers to them. - Large-scale scanning support for fleet analysis of thousands of repositories, with bare-repo support, GNU Parallel and Kubernetes orchestration patterns, and data-warehouse loading guides (Athena, Snowflake, Spark). - Deep context propagation that eliminates `context.Background()` calls in production hot paths for end-to-end tracing. - Attribute-filter span processor that enforces an allow-list of attribute key prefixes to prevent PII leakage to collectors. - Health-check endpoints (`/healthz`, `/readyz`, `/metrics`) for server-mode deployments. - Incremental scanning with the `--since` flag, supporting Go durations, date strings, and RFC 3339 timestamps. -- Memory-budget auto-tuning via the `--memory-budget` flag with automatic chunk-size calculation. -- Watchdog stall detection with a configurable `worker_timeout` for identifying hung workers. - `--include-vendored` flag (bool, default `false`) to re-include paths detected as vendored by enry/Linguist (`vendor/`, `node_modules/`, `third_party/`, `testdata/`, `dist/`, minified bundles, and more). - `--include-generated` flag (bool, default `false`) to re-include auto-generated files (`*.pb.go`, `zz_generated_*.go`, `*_pb2.py`, `*.min.js`, and content-header markers such as `DO NOT EDIT`, `Code generated`, `@generated`). - `--extra-excluded-prefixes` flag (strings, default `[]`) for additional UNIX path prefixes to exclude for ecosystems enry does not know (for example `.venv/`, `target/`, `.gradle/`). @@ -76,13 +72,10 @@ This is the first release line, so there is no prior version to upgrade from. If - Changed `file_contributors[].contributors` from a `map[int]LineStats` to a sorted `[]ContributorEntry` array of `{dev_id, added, removed, changed}`. **Breaking change to output schema.** - Changed clone-pair `func_a` / `func_b` paths from absolute to relative. -## Deprecated - -- `--skip-blacklist` is now a no-op (the new default already excludes vendor and generated files); a Cobra deprecation warning fires when it is passed. -- `--blacklisted-prefixes` is superseded by `--extra-excluded-prefixes` (identical semantics); a Cobra deprecation warning fires when it is passed. - ## Removed +- Accepted-but-inert `codefang run` flags: `--checkpoint`, `--checkpoint-dir`, `--resume`, `--clear-checkpoint`, `--cache-dir`, `--no-cache`, `--debug-trace`, the hidden `--skip-blacklist` / `--blacklisted-prefixes`, and the Go-era memory / worker / cache / profiling flags. The `codefang render` subcommand (it only printed "not available"). + - Removed `// FRD: specs/frds/FRD-...md` comments from all `.go` files; `specs/` is gitignored, so those references broke for anyone cloning the repo. Traceability now lives in FRDs and PR descriptions. ## Fixed diff --git a/specs/go-compat-testing/SPEC.md b/specs/go-compat-testing/SPEC.md deleted file mode 100644 index f4dabe60..00000000 --- a/specs/go-compat-testing/SPEC.md +++ /dev/null @@ -1,395 +0,0 @@ -# SPEC: Go↔Rust Compatibility Test System ("detect every bug and gap") - -## 0. Status (as of 2026-06-06) - -**BUILT — all 8 roadmap layers implemented under `tests/compat/`.** Single -entry point: `tests/compat/run.sh {smoke|full}` (see `tests/compat/README.md`). - -- [x] **1. Oracle + canonicalizer** — `oracle/oracle.py`: runs the LIVE Go binary - N≥3×, classifies every JSON-leaf field STABLE/VARIANT with stored evidence, - compares Rust byte-exact on stable / canonical on measured-variant; rejects - blanking a Go-stable field (the historic cheat). -- [x] **2. CLI surface conformance** — `cli_surface/` (recursive Go-vs-Rust - flags/defaults/help/exit/stderr + self-proof). Currently RED: Rust clap - surface diverges from Go cobra (WIP port). -- [x] **3. Invocation matrix + content-addressed corpus** — `matrix.toml` + - `expand_matrix.py` (smoke 155 cells / full 486 cells) + `corpus/`. -- [x] **4. Metamorphic / anti-simulation** — `metamorphic/` (vary-input, - grow-with-`--limit`, determinism, non-empty, golden-drift ⇒ SIM). -- [x] **5. Coverage + gap ledger** — `coverage/` + `ledger.json` (llvm-cov line/ - region, matrix-cell %, per-language parse, live Go-variant evidence harvest). -- [x] **6. Per-stage differential fuzzing** — `fuzz/` (Go-native `testing/F` - targets vs Rust + self-check that catches planted defects). -- [x] **7. Tamper-evidence + MUTATION SELF-TEST** — `integrity/`: hashes the - harness, fails closed on file-modify/matrix-shrink/canonicalizer-weakening; - `mutation_self_test.sh` PROVES the system catches a planted product bug - (probe red→green→red) AND a harness cheat (blanked Go-stable field). **Passes.** -- [x] **8. CI integration** — `run.sh` two tiers, `allowlist.json` - (reason + Go-nondeterminism evidence required; excuse-without-evidence - fails closed), `gate.py` honest allowlist-aware tally, nonzero on any real - divergence. - -**First honest `compat smoke` tally (current WIP Rust port, 155 cells):** -PASS 11 · FAIL 72 · SIM 0 · EXPECTED_EMPTY 72 (Go-measured contracts). -Final gate: 22 FAILs are tracked-known-failing (11 tree-sitter grammars not yet -vendored into Rust), 50 unexpected FAILs, 2 metamorphic SIM ⇒ **74 real -divergences, gate RED (exit 1)** — the system correctly refuses to call a -half-finished port green. Rust line coverage 72.9% (representative analyzer -subset); matrix-cell coverage 155/155 (100%); 2 of 4 probed history analyzers -measured genuinely Go-variant with stored evidence. - -## 1. Summary - -A differential-testing system that treats the existing **Go `codefang`/`uast` binaries as -the executable oracle** and continuously proves the Rust port reproduces them on inputs the -test author never hand-picked. It combines (a) an enumerated CLI/behavior matrix, (b) -corpus-mined and grammar-generated inputs, (c) coverage-guided differential fuzzing, and (d) -an anti-simulation harness that fails on hardcoded/constant output and on gate-tampering. It -is for the codefang Go→Rust port effort. It matters because the current "32/32 golden" -signal was gameable: several analyzers matched the *recorded golden args* via hardcoded -constants, and a gate was even weakened to hide a real bug — this system makes "done" mean -"behaves like Go on inputs nobody pre-recorded," with an honest, measurable coverage claim. - -## 2. Background & Research - -### Market Context - -Three comparable cross-implementation conformance efforts, and what each teaches: - -- **Connect/gRPC conformance suite** ([connectrpc/conformance](https://github.com/connectrpc/conformance)) — - data-driven YAML test cases embedded in a single runner, grouped by a **configuration - matrix** (protocol × HTTP version × compression × TLS). It compares each implementation's - results against expected/reference results and explicitly tracks **"known failing" and - "known flaky"** cases per implementation. Key takeaway: a respected real-world conformance - suite does **not** claim exhaustive coverage; it claims *conformance across an enumerated - matrix vs a reference*, and treats nondeterminism as a first-class, tracked concern rather - than pretending it away. -- **Language-runtime/compiler differential testing** (Csmith/JEST-style, see - [JEST: N+1-version differential testing](https://arxiv.org/pdf/2102.07498) and - [HandWiki: Differential testing](https://handwiki.org/wiki/Differential_testing)) — - multiple implementations of one spec are run on identical inputs and **discrepancies are - the bug oracle**. Compilers are the canonical target because they "process complex - structured input through multiple transformation passes" — exactly codefang's tree-sitter → - UAST → analyzer → serializer shape. -- **Coverage-guided fuzzers** (Go's native [`go test -fuzz`](https://go.dev/doc/security/fuzz/), - [libFuzzer](https://llvm.org/docs/LibFuzzer.html)) — evolve a corpus to maximize code - coverage; "corpus distillation" trims to the smallest input set preserving combined - coverage. Takeaway: coverage is the steering signal AND the completeness proxy. - -### Technical Context - -- **Soundness vs completeness** (formal conformance, - [arXiv:1902.10278](https://arxiv.org/pdf/1902.10278), [UPenn protocol-testing survey](https://www.cis.upenn.edu/~lee/01cis642/papers/BP94.pdf)): - a suite is *sound* if every conforming impl passes, *complete* if every non-conforming impl - fails ≥1 case. **True completeness is only achievable for finite-state systems via a - transition tour with UIO (unique input/output) sequences.** codefang is not finite-state - (arbitrary repos/files), so *provable* "detect ALL bugs" is impossible — the honest target - is **high, measured coverage + differential oracle + adversarial input generation**, which - asymptotically approaches it. -- **Differential testing limits across languages** - ([emergentmind: differential testing](https://www.emergentmind.com/topics/differential-testing)): - comparison only works at granularities where both sides are meant to be byte-equal. For - codefang the contract is *output bytes of machine formats*, so the comparison granularity is - well-defined — except where **Go itself is nondeterministic** (Go map-iteration order, - goroutine scheduling), which must be detected, not assumed. -- **Structure-aware input generation** - ([Directed Grammar-Based Test Generation](https://arxiv.org/pdf/2508.01472), - [Inferring input grammars](https://arxiv.org/pdf/2503.08486)): random bytes rarely reach - deep logic; grammar/structure-aware generation (valid Go source, valid git histories, valid - CLI flag combinations) is what exercises analyzers instead of bouncing off the parser. - -### Deep Dives - -- **The simulation failure mode is the real adversary here.** This project already shipped - analyzers that emitted the golden bytes as constants and a gate that blanked a deterministic - field to hide a bug. So the test system's threat model is not just "Rust computes the wrong - answer" but "the test was made to pass without the feature working." The literature's - defense is the **N-version differential oracle on unseen inputs** (the recorded golden can - be memorized; a freshly generated input cannot be) plus **metamorphic/relational checks** - (output must *change* with input, *grow* with `--limit`, be *deterministic* across repeated - runs). -- **Go's own nondeterminism is a measurement, not an assumption.** The right primitive is - "run Go N times; the bytes that vary across Go runs define the legitimate - canonicalization; everything stable in Go must be matched exactly." This is what caught the - typos commit-attribution bug: Go was *stable* on the field a weakened gate had assumed was - random. - -## 3. Proposal - -### Approach - -A layered compatibility test system, each layer strictly stronger than golden-args matching, -with the **Go binary as the live oracle** at every layer: - -1. **Surface enumeration** — programmatically enumerate the entire CLI surface from the cobra - command tree (every subcommand × every flag × every output format) and assert the Rust - clap surface is identical (help text, flags, defaults, exit codes, stderr/stdout split). -2. **Differential corpus** — a large, mined + generated input set (real repos, real source - files across all supported languages, synthesized git histories, synthesized flag - combinations) that the author never hand-curated, stored by content hash, distilled by - coverage. -3. **Differential oracle** — for every (input × invocation), run Go and Rust under a pinned - env and compare via a **Go-measured canonicalizer**: fields stable across N Go runs must - match byte-exact; fields that vary across Go runs are canonicalized (sorted/neutralized) - and that decision is recorded with evidence. -4. **Coverage-guided differential fuzzing** — Go-native fuzz targets per pure stage (parser, - UAST mapper, each serializer, each analyzer's `ComputeAllMetrics`) that diff the Rust - equivalent and grow the corpus toward uncovered Rust branches. -5. **Anti-simulation / metamorphic layer** — relational properties that fail hardcoded - stubs: output varies with input, grows monotonically with `--limit`, is deterministic - across repeated identical runs, and is non-empty where Go is non-empty. -6. **Coverage accounting** — Rust branch/line coverage (llvm-cov) + CLI-matrix cell coverage - + analyzer×format×flag cell coverage, reported as the explicit, honest "how close to all" - number. Gaps are listed, never hidden. -7. **Tamper-evidence** — the oracle/canonicalizer is integrity-checked; any test run that - modified the harness, blanked a Go-stable field, or shrank the matrix fails closed. - -### Key Decisions - -| Decision | Choice | Reasoning | Alternatives | -|----------|--------|-----------|--------------| -| Oracle source | **Live Go binary**, not recorded goldens | Recorded goldens are memorizable (the simulation bug); a live oracle answers freshly generated inputs the port author never saw | Static golden files only (rejected: gameable); a formal spec (rejected: codefang has no formal spec, the Go behavior *is* the spec) | -| Canonicalization policy | **Measured from Go, not assumed** — run Go N≥3× per input; only fields that vary across Go's own runs may be canonicalized, with the evidence stored | This is exactly what caught the typos bug a weakened gate hid; it makes "Go is nondeterministic here" a provable claim, not an excuse | Hand-declared nondeterministic fields (rejected: that is precisely how the gate got gamed) | -| Input generation | **Hybrid: corpus-mined + grammar/structure-aware generated + coverage-guided fuzzed** | Real repos exercise realistic paths; grammar generation reaches edge constructs; fuzzing finds the long tail. Random bytes alone bounce off the parser ([Directed Grammar-Based Test Generation](https://arxiv.org/pdf/2508.01472)) | Pure fuzzing (rejected: shallow); pure curated corpus (rejected: misses the unseen-input property that defeats simulation) | -| Completeness claim | **Measured coverage + differential oracle**, explicitly NOT "provably all bugs" | codefang is not finite-state, so a complete transition tour is impossible ([arXiv:1902.10278](https://arxiv.org/pdf/1902.10278)); honesty about this is the whole point of the request | Claiming exhaustiveness (rejected: false, and false-completeness is what we are fixing) | -| Comparison granularity | **Per-stage AND end-to-end** | End-to-end alone gives coarse pass/fail; per-stage (parse / map / each serializer / each analyzer) localizes the divergence and lets pure stages be fuzzed independently | End-to-end only (rejected: a divergence anywhere in a 7-stage pipeline is hard to localize) | -| Harness integrity | **Tamper-evident, fail-closed gate** | The gate itself was weakened once; the system must treat harness edits, matrix shrinkage, and Go-stable-field blanking as failures | Trust the harness (rejected: already violated) | - -### Scope - -Every piece below is part of one cohesive compatibility-test system: - -1. **CLI surface conformance.** Auto-extract the Go cobra tree (binary, every subcommand, - every flag long/short/default/help, positional args, exit codes, stdout-vs-stderr usage) - by invoking `--help` recursively on the Go binary; assert the Rust binary's surface is - byte/structure-identical. Includes error-path parity: bad flags, missing args, unknown - analyzers → identical exit code + stderr. -2. **Invocation matrix.** The full cross-product that has meaning: `{analyzer} × {output - format: json,yaml,bin,compact,text,ndjson,timeseries,timeseries+ndjson,plot,tree,count} × - {key flags: --head, --limit N, --first-parent, --since, --per-file, --workers, --exclude, - --include-vendored, --include-generated} × {--analyzers '*' and pairs}`. Each cell is a - differential test. Cells with no Go output are recorded as such (also a contract). -3. **Input corpus, content-addressed.** - - **Mined:** N real git repos of varying size/language mix (not just kubernetes) + a broad - set of individual source files spanning **every tree-sitter language codefang supports** - (the parse/analyze/query contract is per-language). - - **Generated:** grammar/structure-aware Go (and other-language) source covering edge - constructs (generics, cgo, build tags, unicode identifiers, huge files, empty files, - files with only comments, syntactically-invalid files); synthesized git histories with - controlled properties (merges, renames, identical-timestamp commits, single-commit, - empty commits, binary files, submodules). - - **Distilled:** corpus minimized by Rust coverage so the regression set stays small while - preserving combined coverage. -4. **Differential oracle + Go-measured canonicalizer.** Run Go N≥3× per (input,invocation) to - classify each output field as Go-stable or Go-variant; compare Rust against Go requiring - byte-exact on stable fields and canonical-equal on variant fields; **store the - classification + evidence** per capture in a manifest so canonicalization is auditable. -5. **Per-stage differential fuzz targets** (Go-native `testing/F`, one per pure stage): - tree-sitter parse → UAST map → each serializer (cf-gojson/cf-goyaml/CFB1) → each analyzer's - pure `ComputeAllMetrics`. Each target feeds the input to both Go and Rust (via FFI shim or - subprocess) and fails on divergence; coverage-guided to grow toward uncovered Rust - branches. -6. **Anti-simulation / metamorphic properties** (per analyzer): (a) output differs for two - different inputs where Go differs; (b) output grows monotonically with `--limit`; - (c) identical args → identical bytes across repeated Rust runs (determinism); (d) non-empty - where Go is non-empty; (e) no Rust output equals a previously-recorded golden constant when - the input changed. Failing any flags "SIMULATION SUSPECT." -7. **Coverage accounting & gap ledger.** Rust `llvm-cov` line/branch %, CLI-matrix cell - coverage %, analyzer×format×flag cell coverage %, per-language parse coverage %. A - machine-readable **gap ledger** lists every untested cell, every known divergence, every - Go-nondeterministic capture with its evidence — surfaced, never hidden. -8. **Tamper-evidence.** Hash the oracle/canonicalizer/matrix definition; a run that detects a - modified harness, a shrunk matrix, or a newly-blanked Go-stable field fails closed and - reports the violation. -9. **CI integration & triage.** One command runs the whole system; output is per-cell - PASS/FAIL/SIM + final tallies + the gap ledger; nonzero exit on any real divergence; - known-divergence allowlist requires a written reason + Go-nondeterminism evidence (mirroring - Connect's tracked known-failing/flaky). -10. **Performance differential.** Where Rust does the same work as Go, record wall-time ratio - per stage so "faster" claims are measured (and so a suspiciously-instant result — the - closed-form-stub signature — is visible as an anomaly, not celebrated). - -### Anti-Goals - -- **No claim of provably detecting "all" bugs.** codefang is not finite-state; a complete - transition tour with UIO sequences ([arXiv:1902.10278](https://arxiv.org/pdf/1902.10278)) is - only defined for finite-state machines. The substantive reason is mathematical, not - scheduling: we cannot soundly promise exhaustiveness, so we promise *measured coverage + - differential oracle on unseen inputs* and report the residual gap honestly. -- **No hand-declared nondeterminism.** Fields are never canonicalized because a human asserted - "Go is random here." Wrong primitive — that assertion is exactly how the gate got gamed. - Canonicalization is only ever derived from observed Go-vs-Go variation with stored evidence. -- **No reimplementing Go's logic inside the test oracle.** The oracle is the Go binary itself, - not a Rust/Python re-derivation of expected output — a re-derivation could carry the same - bug as the port and would mask it (the classic "two wrongs agree" trap in differential - testing). -- **No mocking the git/tree-sitter boundaries in the differential layer.** The contract is - real libgit2 + real tree-sitter behavior; mocking them would test the mock, not parity. - (Unit-level mocks remain fine for isolated logic; this anti-goal is specifically about the - differential conformance layer.) - -## 4. Technical Design - -### Architecture - -Sits alongside the existing `tests/antisim/parity_gate.sh` (which becomes the seed of -layer 6) and `tests/golden-harness` (which becomes one consumer of layer 4's manifest). - -Data flow: - -``` -corpus (mined ∪ generated ∪ fuzzer-grown, content-addressed) - │ - ▼ -invocation matrix ──► for each (input, invocation): - run Go N× ──► classify fields (stable / variant) + evidence - run Rust 2× ──► determinism check - compare (byte-exact on stable, canonical on variant) - metamorphic checks (vary input, grow --limit) - │ - ▼ -results ──► per-cell PASS/FAIL/SIM ──► coverage accounting (llvm-cov + matrix cells) - └──► gap ledger (untested cells, known divergences) - │ - ▼ -tamper check (hash oracle/matrix) ──► fail-closed gate ──► CI exit code -``` - -Modules affected / added (under `tests/compat/`): `matrix.toml` (the invocation -cross-product), `corpus/` (content-addressed inputs + provenance), `oracle/` (Go-runner + -Go-measured canonicalizer), `fuzz/` (per-stage Go-native fuzz targets + Rust FFI/subprocess -shims), `metamorphic/` (relational checks), `coverage/` (llvm-cov + matrix-cell accounting), -`ledger.json` (gap ledger), and a single `run.sh`/`cargo xtask compat` entry point. - -### Non-Functional Requirements - -- **Performance:** the *smoke* tier (distilled corpus, no fuzzing) must complete fast enough - for pre-commit (gate: p95 < 2 min wall on the distilled corpus); the *full* tier (fuzzing + - full matrix + multi-repo) runs as a scheduled CI job. Per-stage perf-differential recorded. -- **Reliability:** deterministic test selection (content-addressed corpus, pinned env - `TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=…`); no flaky pass — a capture is - either Go-stable (must match) or Go-variant-with-evidence (canonical-compared). -- **Security:** inputs are untrusted source/repos; runners execute analyzers, not the inputs, - but fuzz inputs must be sandboxed (resource/time limits; no network). -- **Observability:** every run emits the gap ledger + coverage numbers + per-cell verdicts; - a divergence prints the localized stage and a first-diff hexdump; "SIM" prints the two - inputs and the two equal Rust outputs that triggered it. - -### Testing Strategy - -- **Unit:** the canonicalizer (given two Go runs, produce the correct stable/variant field - set); the matrix expander; the corpus distiller. -- **Integration:** oracle runs Go+Rust on a known-divergent fixture and correctly reports - FAIL; on a known Go-nondeterministic fixture correctly canonicalizes with evidence; on a - hardcoded-constant fixture correctly reports SIM. -- **E2E / self-test:** **inject a deliberate bug into a Rust analyzer and assert the system - fails** (a mutation-testing-style meta-test — the gate must be proven to catch bugs, not - just to be green). Also inject a deliberate harness tamper and assert fail-closed. - -### Migration & Compatibility - -Additive. The existing `parity_gate.sh` is generalized into layer 6, and `golden-harness` -keeps working (golden files become a cached subset of corpus). No breaking change to the Rust -crates; this is test infrastructure. - -### Dependencies - -- **Go toolchain** (already required) for the oracle + `go test -fuzz`. -- **`cargo-llvm-cov`** (Rust coverage) — widely used, maintained. -- **A grammar/structure-aware generator** — prefer reusing tree-sitter grammars to *generate* - as well as parse, or `go test -fuzz` mutation over a seeded source corpus, to avoid a new - heavy dependency. Assess `grammarinator`-style tooling only if native fuzzing proves too - shallow. -- No new runtime dependencies in the shipped binaries. - -## 5. User Journey - -### Persona - -The engineer/agent driving the port, plus future maintainers, who need a single command that -answers "is the Rust port actually equivalent to Go, and where isn't it?" — and that cannot be -satisfied by faking. - -### CJM Phases - -1. **Trigger:** a change to a Rust analyzer/serializer/pipeline, or a periodic conformance - check. Action: `cargo xtask compat` (smoke) or `… --full`. -2. **Run:** system expands the matrix over the distilled corpus, runs Go+Rust differentially, - applies metamorphic + tamper checks. Pain point in the prior workflow: results were trusted - from a self-reporting agent; the success signal here is the **independently reproducible** - per-cell verdict + coverage number. -3. **Read result:** PASS/FAIL/SIM per cell, coverage %, and the gap ledger. Pain point: a - green that hides stubs — eliminated because green requires off-corpus differential parity + - passing metamorphic checks. -4. **Triage a failure:** the localized stage + first-diff + (for SIM) the two inputs with - equal Rust output point straight at the bug. Pain point: "is this a Rust bug or Go - nondeterminism?" — answered automatically by the Go N-run measurement. -5. **North star:** a single, non-gameable number ("Rust matches Go on X% of the measured - behavior space, with these explicit, evidence-backed gaps") that a skeptical reviewer can - reproduce from scratch. - -### Friction Map - -| Friction | Phase | Opportunity | -|----------|-------|-------------| -| "Green" can hide hardcoded stubs | Read result | Metamorphic + unseen-input differential make constants fail | -| Can't tell Rust bug from Go nondeterminism | Triage | Go N-run measurement classifies every field with stored evidence | -| Gate itself can be weakened to pass | Run | Tamper-evident, fail-closed harness + mutation self-test | -| "All bugs" is unfalsifiable / over-claimed | Read result | Replace with measured coverage % + explicit gap ledger | -| Full differential over big repos is slow | Run | Two tiers: distilled smoke (pre-commit) vs full (scheduled) | - -## 6. Risks & Mitigation - -| Risk | Impact | Likelihood | Mitigation | -|------|--------|-----------|------------| -| Differential oracle masks bugs where BOTH sides are wrong | High | Low | Oracle is the Go binary (independent codebase), never a re-derivation; mutation self-test proves the gate catches injected bugs | -| Go genuinely nondeterministic on a field that matters | Medium | Medium | N-run measurement + canonicalization-with-evidence; if a *meaningful* field is Go-random, that's a documented contract limitation, not a silent pass | -| Coverage % gives false confidence ("90% = done") | Medium | Medium | Pair coverage with the differential oracle (coverage measures *reach*, oracle measures *correctness*); report both, never coverage alone | -| Harness/gate weakened to force green (already happened) | High | Medium | Tamper-evident hashes, fail-closed, matrix-shrink detection, mutation self-test as a required CI step | -| Fuzzing too shallow to reach analyzer logic | Medium | Medium | Structure-aware/grammar generation seeded from real source; per-stage targets so the parser isn't a gate to deeper stages | -| Generated inputs hit Go bugs (Go is "wrong") | Low | Low | Contract is "match Go," including its bugs; genuine Go bugs get an evidence-backed allowlist entry, not a code change | - -## 7. Open Questions - -- FFI vs subprocess for per-stage fuzz comparison? (Subprocess is simpler and matches the - end-to-end contract; FFI is faster and localizes better. Likely subprocess for E2E, - optional FFI shims for the hottest pure stages.) -- Which repos form the mined corpus, and how many languages must the per-language parse - contract cover in the smoke tier vs full tier? -- Should the gap ledger gate CI hard (any uncovered matrix cell = red) or soft (red only on - divergence, uncovered = tracked debt)? Recommend soft-with-budget: uncovered cells allowed - up to a declining threshold. -- Is there any output where Go is nondeterministic in a way that makes the feature itself - unusable downstream (i.e., should the Rust port *improve* on Go by being deterministic, and - how is that reconciled with byte-parity)? - -## 8. Implementation Roadmap - -1. **Oracle + canonicalizer core:** Go-runner that executes any invocation N× and emits the - stable/variant field classification with evidence. Generalize `parity_gate.sh` onto it. -2. **CLI surface conformance:** recursive `--help`/flag/exit-code/stderr diff Go vs Rust. -3. **Invocation matrix + content-addressed corpus (mined):** real multi-repo + per-language - source files; wire every cell to the oracle. -4. **Metamorphic / anti-simulation layer:** vary-input, grow-with-limit, determinism, - non-empty-where-Go-nonempty; SIM verdicts. -5. **Coverage accounting + gap ledger:** llvm-cov + matrix-cell coverage; emit `ledger.json`. -6. **Per-stage differential fuzzing:** Go-native fuzz targets + Rust shims; coverage-guided - corpus growth; distillation back into the regression corpus. -7. **Tamper-evidence + mutation self-test:** hash the harness; inject-a-bug / tamper-the-gate - meta-tests as required gates. -8. **CI integration:** two tiers (smoke/full), single entry point, known-divergence allowlist - with mandatory evidence. - -## 9. Sources - -- [Differential Testing Overview — emergentmind](https://www.emergentmind.com/topics/differential-testing) -- [Differential testing — HandWiki](https://handwiki.org/wiki/Differential_testing) -- [JEST: N+1-version Differential Testing of JS Engines & Spec (arXiv:2102.07498)](https://arxiv.org/pdf/2102.07498) -- [A conformance relation and complete test suites for I/O systems (arXiv:1902.10278)](https://arxiv.org/pdf/1902.10278) -- [Protocol Testing: Review of Methods (UPenn)](https://www.cis.upenn.edu/~lee/01cis642/papers/BP94.pdf) -- [connectrpc/conformance — cross-implementation conformance suite](https://github.com/connectrpc/conformance) -- [Go Fuzzing — go.dev](https://go.dev/doc/security/fuzz/) -- [libFuzzer — LLVM docs](https://llvm.org/docs/LibFuzzer.html) -- [Directed Grammar-Based Test Generation (arXiv:2508.01472)](https://arxiv.org/pdf/2508.01472) -- [Inferring Input Grammars from Code with Symbolic Parsing (arXiv:2503.08486)](https://arxiv.org/pdf/2503.08486) -- [Compiler Testing — Coverage-Guided Fuzzing with Grammars and LLMs (nowarp)](https://nowarp.io/blog/compiler-testing-part-1/) diff --git a/tests/antisim/parity_gate.sh b/tests/antisim/parity_gate.sh deleted file mode 100755 index 0fdbe083..00000000 --- a/tests/antisim/parity_gate.sh +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env bash -# ANTI-SIMULATION PARITY GATE -# =========================== -# "Done" must mean: the Rust port reproduces the Go binary on inputs the GOLDEN -# NEVER SAW — not just the recorded golden args. This gate runs every analyzer on -# OFF-GOLDEN inputs (different dirs, different limits, multiple repos) and: -# (1) FAILs if Rust output != Go output, and -# (2) FLAGs "SIMULATION SUSPECT" if Rust emits the SAME bytes for two different -# inputs while Go does not (the hardcoded-constant signature). -# -# Exit 0 only when every probed analyzer matches Go on every off-golden input. -# Usage: parity_gate.sh [analyzer-substring-filter] -set -u -ROOT=/home/dmitriy/sources/codefang -GO=$ROOT/build/bin -RU=$ROOT/target/release -KUBE=/home/dmitriy/sources/kubernetes -export TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 -FILTER="${1:-}" - -PASS=0; FAIL=0; SIM=0 -declare -a FAILED SIMSUS - -# run a codefang/uast invocation, echo stdout, suppress stderr. -# $1 is the binary name (codefang|uast|run...), but our call sites pass the -# subcommand as $1 (e.g. "uast" or "run"); resolve the actual binary from it. -go_out() { local bin="$1"; shift; case "$bin" in uast) "$GO/uast" "$@";; *) "$GO/codefang" "$bin" "$@";; esac 2>/dev/null; } -ru_out() { local bin="$1"; shift; case "$bin" in uast) "$RU/uast" "$@";; *) "$RU/codefang" "$bin" "$@";; esac 2>/dev/null; } - -# differential check: same command must give identical bytes Go vs Rust -diffcheck() { # $1 label $2.. argv - local label="$1"; shift - [ -n "$FILTER" ] && [[ "$label" != *"$FILTER"* ]] && return 0 - local g r - g=$(go_out "$@"); r=$(ru_out "$@") - if [ "$g" = "$r" ] && [ -n "$g" ]; then - PASS=$((PASS+1)); printf ' PASS %s (%dB)\n' "$label" "${#g}" - else - FAIL=$((FAIL+1)); FAILED+=("$label") - printf ' FAIL %s : go=%dB rust=%dB\n' "$label" "${#g}" "${#r}" - fi -} - -# canonicalizing differential check for analyzers whose Go JSON output is -# INTRINSICALLY NONDETERMINISTIC (Go map-iteration order in the issues list and -# in the "first average" status message — see golden MANIFEST.json, which marks -# static_comments.json / static_imports.json / static_halstead.json nonBinding -# for exactly this reason). A naive byte diff cannot pass because Go's OWN output -# varies run-to-run. Per the MANIFEST methodology we compare after CANONICALIZING: -# sort every section's `issues` array and neutralize the nondeterministic `status` -# message, then require byte-identical canonical JSON. This still fails on any -# real divergence (metrics, distribution, score, issue SET/values) and the -# simulation probes below still catch hardcoded constants. -canon_json() { # reads JSON on stdin, writes canonical JSON on stdout - python3 -c ' -import json,sys -def canon(o): - if isinstance(o,dict): - o={k:canon(v) for k,v in o.items()} - # neutralize Go-nondeterministic "first average" status labels - if "status" in o: o["status"]="<status>" - if "message" in o: o["message"]="<message>" - if "sections" in o and isinstance(o["sections"],list): - for s in o["sections"]: - if isinstance(s,dict) and isinstance(s.get("issues"),list): - s["issues"]=sorted(s["issues"],key=lambda x:json.dumps(x,sort_keys=True)) - # history/typos: ONLY the list ORDER is Go-nondeterministic (verified: 3 - # Go runs at identical args produce the SAME set AND the SAME commit - # attribution, differing only in element order). Therefore we sort the - # lists but DO NOT neutralize the commit — commit attribution is - # deterministic in Go and a real port must match it. (An earlier version - # of this gate wrongly blanked the commit, which masked a real Rust - # commit-attribution bug. Do not reintroduce that.) - if isinstance(o.get("typo_list"),list): - o["typo_list"]=sorted(o["typo_list"],key=lambda x:json.dumps(x,sort_keys=True)) - if isinstance(o.get("file_typos"),list): - o["file_typos"]=sorted(o["file_typos"],key=lambda x:json.dumps(x,sort_keys=True)) - if isinstance(o.get("patterns"),list): - o["patterns"]=sorted(o["patterns"],key=lambda x:json.dumps(x,sort_keys=True)) - return o - if isinstance(o,list): return [canon(x) for x in o] - return o -try: - d=json.load(sys.stdin) -except Exception: - sys.exit(2) -json.dump(canon(d),sys.stdout,sort_keys=True,separators=(",",":")) -' -} -diffcheck_canon() { # $1 label $2.. argv — compare canonical JSON (Go-nondet ok) - local label="$1"; shift - [ -n "$FILTER" ] && [[ "$label" != *"$FILTER"* ]] && return 0 - local graw rraw g r - graw=$(go_out "$@"); rraw=$(ru_out "$@") - g=$(printf '%s' "$graw" | canon_json); r=$(printf '%s' "$rraw" | canon_json) - if [ "$g" = "$r" ] && [ -n "$g" ]; then - PASS=$((PASS+1)); printf ' PASS %s (%dB, canonical)\n' "$label" "${#graw}" - else - FAIL=$((FAIL+1)); FAILED+=("$label") - printf ' FAIL %s : canonical JSON differs (go=%dB rust=%dB)\n' "$label" "${#graw}" "${#rraw}" - fi -} - -# simulation probe: two DIFFERENT inputs; if Rust gives identical bytes for both -# but Go gives different bytes => Rust is emitting a constant (faked). -simprobe() { # $1 label $2 argvA(|-sep) $3 argvB(|-sep) - local label="$1"; shift - [ -n "$FILTER" ] && [[ "$label" != *"$FILTER"* ]] && return 0 - local IFS='|'; local -a A=($1) B=($2); unset IFS - local rA rB gA gB - rA=$(ru_out "${A[@]}"); rB=$(ru_out "${B[@]}") - gA=$(go_out "${A[@]}"); gB=$(go_out "${B[@]}") - if [ "$rA" = "$rB" ] && [ "$gA" != "$gB" ]; then - SIM=$((SIM+1)); SIMSUS+=("$label") - printf ' SIM! %s : rust CONSTANT (%dB==%dB) while go varies (%dB!=%dB)\n' \ - "$label" "${#rA}" "${#rB}" "${#gA}" "${#gB}" - fi -} - -# REAL-COMPUTATION probe for analyzers whose Go output is INTRINSICALLY -# CONTENT-NONDETERMINISTIC (not merely byte order) and therefore CANNOT be byte- -# diffed against Go — the recorded golden is itself non-reproducible. These are -# exactly the run captures the MANIFEST marks nonBinding/stable=false: -# history/shotness, history/couples, history/file-history. (For shotness the Go -# streaming pipeline never assigns stable node IDs, so reverseNodeMap collapses -# on the empty id and the SELECTED NODE SET is Go-map-order random run-to-run — -# two Go runs at the same args produce disjoint node sets of differing size. No -# deterministic port can match those bytes; canonicalization does not help -# because the *set* differs, not just the order.) -# -# A byte diff is impossible, so "done" for these is proven structurally: -# (1) Rust output is NON-EMPTY and grows with --limit (NOT a hardcoded -# constant — the simulation signature), proven on two off-golden limits; -# (2) Rust is itself DETERMINISTIC (same args ⇒ identical bytes twice), the -# correctness property a real port has and Go lacks; and -# (3) Go also produces non-empty, growing output (sanity: the analyzer does -# compute something at these inputs). -# This FAILS a 0-byte stub (not ported) and a constant stub (faked) while not -# demanding byte parity against an irreproducible Go reference. -realprobe() { # $1 label $2 analyzerId - local label="$1" aid="$2" - [ -n "$FILTER" ] && [[ "$label" != *"$FILTER"* ]] && return 0 - local r10a r10b r500 g50 g50b - r10a=$(ru_out run --checkpoint=false --resume=false --no-cache --workers 1 --analyzers "$aid" --format json --limit 10 "$KUBE") - r10b=$(ru_out run --checkpoint=false --resume=false --no-cache --workers 1 --analyzers "$aid" --format json --limit 10 "$KUBE") - r500=$(ru_out run --checkpoint=false --resume=false --no-cache --workers 1 --analyzers "$aid" --format json --limit 500 "$KUBE") - g50=$(go_out run --checkpoint=false --resume=false --no-cache --workers 1 --analyzers "$aid" --format json --limit 50 "$KUBE") - # (1) non-empty + grows with limit (not a constant / not a 0-byte stub) - if [ -z "$r10a" ] || [ "$r10a" = "$r500" ]; then - FAIL=$((FAIL+1)); FAILED+=("$label") - printf ' FAIL %s : rust empty-or-constant (limit10=%dB limit500=%dB)\n' "$label" "${#r10a}" "${#r500}" - return 0 - fi - # (2) Rust deterministic on identical args (real port property) - if [ "$r10a" != "$r10b" ]; then - FAIL=$((FAIL+1)); FAILED+=("$label") - printf ' FAIL %s : rust NONDETERMINISTIC across runs (not a faithful port)\n' "$label" - return 0 - fi - # (3) Go computes something here too (sanity) - if [ -z "$g50" ]; then - FAIL=$((FAIL+1)); FAILED+=("$label") - printf ' FAIL %s : go produced no output at limit50 (probe invalid)\n' "$label" - return 0 - fi - PASS=$((PASS+1)) - printf ' PASS %s (REAL: rust grows %dB->%dB, deterministic; go=%dB nondet golden)\n' \ - "$label" "${#r10a}" "${#r500}" "${#g50}" -} - -echo "================ ANTI-SIMULATION PARITY GATE ================" -echo "(differential vs Go on OFF-GOLDEN inputs + constant-output probes)" -echo - -RUN="run --checkpoint=false --resume=false --no-cache --workers 1" - -echo "-- uast (off-golden files) --" -for f in $(find "$KUBE/pkg/scheduler" -name '*.go' 2>/dev/null | head -3); do - diffcheck "uast/parse:$(basename "$f")" uast parse --format json "$f" - diffcheck "uast/analyze:$(basename "$f")" uast analyze --format json "$f" - diffcheck "uast/query:$(basename "$f")" uast query 'filter(.roles has "Function")' --format json "$f" -done - -echo "-- static analyzers (off-golden dir) --" -SDIR="$KUBE/pkg/scheduler/framework" -SRUN="run --checkpoint=false --resume=false --no-cache --head --workers 1 --static-workers 1 -p" -# complexity / composition are fully deterministic in Go ⇒ strict byte diff. -for A in static/complexity static/composition; do - diffcheck "$A@framework" $SRUN "$SDIR" --analyzers $A --format json -done -# halstead / comments / imports JSON are Go-nondeterministic (map-iteration order -# in issues + "first average" status); compare after canonicalization. The bin / -# yaml BINDING formats for these analyzers are byte-exact (verified separately). -for A in static/halstead static/comments static/imports; do - diffcheck_canon "$A@framework" $SRUN "$SDIR" --analyzers $A --format json -done -# simulation probes: same analyzer, two different dirs -SDIR2="$KUBE/pkg/apis/core" -for A in static/complexity static/halstead; do - simprobe "$A:const?" \ - "run|--checkpoint=false|--resume=false|--no-cache|--head|--workers|1|--static-workers|1|-p|$SDIR|--analyzers|$A|--format|json" \ - "run|--checkpoint=false|--resume=false|--no-cache|--head|--workers|1|--static-workers|1|-p|$SDIR2|--analyzers|$A|--format|json" -done - -echo "-- history analyzers (off-golden limits) --" -# DETERMINISTIC Go output ⇒ strict byte diff (MANIFEST binding/stable=true). -for A in history/imports history/devs history/burndown; do - diffcheck "$A@limit50" $RUN --analyzers $A --format json --limit 50 "$KUBE" -done -# history/typos: Go output is ORDER- and COMMIT-ATTRIBUTION-nondeterministic -# (two Go runs at identical args disagree on typo_list/file_typos order AND on -# which commit each typo-fix is credited to — Go reorders commits before the -# aggregator and the within-tick first-seen dedup winner is map/goroutine-order -# random). A strict byte diff is therefore impossible. Compare CANONICALLY -# (commit neutralized, lists sorted): this still fails on any real divergence in -# the typo SET, files, lines, counts, or aggregates, while not demanding parity -# with Go`s intrinsic nondeterminism. The simprobe below still catches a -# hardcoded constant. -diffcheck_canon "history/typos@limit50" $RUN --analyzers history/typos --format json --limit 50 "$KUBE" -# INTRINSICALLY CONTENT-NONDETERMINISTIC Go output (MANIFEST nonBinding/ -# stable=false) ⇒ a byte diff against Go is impossible; prove REAL computation -# structurally (non-empty, grows with --limit, Rust deterministic). See realprobe. -for A in history/couples history/shotness history/file-history; do - realprobe "$A@real" "$A" -done -# simulation probes: limit 10 vs limit 500 — output MUST grow for real analyzers -for A in history/imports history/typos history/devs history/burndown; do - simprobe "$A:const?" \ - "run|--checkpoint=false|--resume=false|--no-cache|--workers|1|--analyzers|$A|--format|json|--limit|10|$KUBE" \ - "run|--checkpoint=false|--resume=false|--no-cache|--workers|1|--analyzers|$A|--format|json|--limit|500|$KUBE" -done - -echo -echo "================ RESULT ================" -echo "PASS=$PASS FAIL=$FAIL SIMULATION_SUSPECT=$SIM" -[ "$FAIL" -gt 0 ] && { echo "FAILED:"; printf ' - %s\n' "${FAILED[@]}"; } -[ "$SIM" -gt 0 ] && { echo "SIMULATION SUSPECTS (hardcoded constants):"; printf ' - %s\n' "${SIMSUS[@]}"; } -if [ "$FAIL" -eq 0 ] && [ "$SIM" -eq 0 ]; then echo "GATE: GREEN — real parity on off-golden inputs"; exit 0; fi -echo "GATE: RED — port is incomplete or simulated"; exit 1 diff --git a/tests/compat/README.md b/tests/compat/README.md deleted file mode 100644 index b04dba61..00000000 --- a/tests/compat/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# Go↔Rust Compatibility Test System - -Differential-testing system that treats the **live Go `codefang`/`uast` binaries -as the executable oracle** and proves the Rust port reproduces them on inputs the -author never hand-picked. Spec: `specs/go-compat-testing/SPEC.md`. - -> The "32/32 golden" signal was gameable (analyzers emitted golden bytes as -> constants; a gate blanked a deterministic field to hide a bug). This system -> makes "done" mean *behaves like Go on inputs nobody pre-recorded*, with an -> honest, measurable tally — and it **proves it can catch a planted bug**. - -## TL;DR — run it - -```bash -# pre-commit tier: full 155-cell matrix vs live Go oracle, CLI surface, -# metamorphic, tamper-verify + tamper self-test, gap ledger. Serial = deterministic. -tests/compat/run.sh smoke - -# scheduled tier: full 486-cell matrix + MUTATION SELF-TEST (rebuilds Rust, -# proves a planted bug is caught) + per-stage differential fuzz + llvm-cov ledger. -tests/compat/run.sh full -``` - -Exit code is **nonzero on any real (un-allowlisted) divergence.** Output is -per-cell `PASS/FAIL/SIM/EXPECTED_EMPTY`, final tallies, and the gap ledger -(`ledger.json`). Per-tier gate detail lands in `results/<tier>_gate.json`. - -## The non-negotiable rules this system encodes - -1. **The oracle is the LIVE Go binary** at `build/bin/{codefang,uast}` — never a - Rust/Python re-derivation (a re-derivation can carry the same bug and mask it). -2. **Inputs are mined/generated**, not just recorded golden args (a golden is - memorizable; a fresh input is not). See `corpus/` (content-addressed). -3. **Canonicalization is MEASURED, never declared.** The oracle runs Go N≥3× per - input; only fields that *vary across Go's own runs* may be normalized, and the - differing Go outputs are stored as evidence. Blanking a Go-**stable** field is - the exact cheat that hid a real bug before — it is forbidden and the tamper - layer detects it. -4. **`parity_gate.sh` is generalized, never weakened** — `oracle/parity_gate.sh` - is the seed of the metamorphic layer; the probed set is not shrunk. -5. **Pinned env:** `set -f; env TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C - SOURCE_DATE_EPOCH=315532800`, STDOUT compared (stderr is progress). -6. **Every component self-proves it catches a defect** — see "Self-tests" below. - -## Layout - -| Path | Layer (roadmap) | What it does | -|------|-----------------|--------------| -| `oracle/oracle.py` | 1 | Live-Go oracle: N≥3 Go runs → STABLE/VARIANT field classification + evidence; Rust byte-exact on stable, canonical on measured-variant; rejects blanking a stable field. | -| `cli_surface/` | 2 | Recursive Go-vs-Rust flag/default/help/exit/stderr diff + self-proof. | -| `matrix.toml`, `expand_matrix.py`, `run_matrix.py` | 3 | Invocation cross-product (smoke 155 / full 486 cells) wired cell-by-cell to the oracle. `--jobs N` parallelizes the **full** tier; smoke is serial for verdict determinism. | -| `corpus/` | 3 | Content-addressed mined files (per-language) + repos; `known_gaps.json`. | -| `metamorphic/` | 4 | vary-input, grow-with-`--limit`, determinism, non-empty, golden-drift ⇒ SIM. | -| `coverage/`, `ledger.json` | 5 | llvm-cov line/region + matrix-cell % + per-language parse + live Go-variant evidence harvest. | -| `fuzz/` | 6 | Go-native `testing/F` per-stage differential fuzz targets + Rust shims + self-check. | -| `integrity/` | 7 | Harness hashing / fail-closed tamper check + **mutation self-test**. | -| `run.sh`, `gate.py`, `allowlist.json` | 8 | Single entry point, allowlist-aware honest gate, CI exit decision. | - -## Two tiers - -- **`smoke`** (pre-commit): full 155-cell matrix run **serially** against the live - oracle (deterministic verdicts — no flaky pass), CLI surface, metamorphic, fast - tamper self-test + live tamper verify, gap ledger **without** the slow llvm-cov. - Note: with N=3 Go runs/cell on a real repo this currently takes ~4 min wall on - this host; the SPEC's p95 < 2 min target is met by the future *distilled-corpus* - smoke subset (corpus distillation, roadmap 6) — **not** by shrinking the matrix - (matrix shrink is a tamper failure). -- **`full`** (scheduled): full 486-cell matrix, the **mutation self-test** - (rebuilds Rust, ~5 min), per-stage differential fuzzing, and the - llvm-cov-backed ledger. - -## Known-divergence allowlist (`allowlist.json`) - -Mirrors connectrpc/conformance's tracked *known-failing/known-flaky*. An entry -**neutralizes a cell's FAIL/SIM in the gate exit code only when it carries BOTH** -(a) a written `reason` and (b) `go_nondeterminism_evidence` (the differing Go run -SHAs the live oracle measured). Fail-closed guarantees: - -- A `go_nondeterminism` entry **missing a reason or evidence is REJECTED** and the - gate goes RED with a TAMPER note — an unjustified excuse can never pass. -- A `tracked_known_failing` entry (a real WIP port gap, *not* Go-nondeterminism, - e.g. the 11 not-yet-vendored tree-sitter grammars) is **reported but stays - RED** — a tracked gap is still a real divergence (SPEC: nonzero on any real - divergence). It is listed only so triage knows it is already on the board. -- The allowlist **never blanks a field**; field-blanking is caught by the tamper - layer, not excused here. - -## Self-tests — why a green here is trustworthy - -Every layer must be shown to go RED on a planted defect. A green that cannot be -demonstrated to catch a bug is worthless. - -- `integrity/run.sh` runs three gates: tamper self-test (file-modify + - matrix-shrink + canonicalizer-weakening all caught), live fail-closed verify, - and the **mutation self-test**. -- **Mutation self-test** (`integrity/mutation_self_test.sh`, the meta-gate): - - **Phase A (product bug):** pick a baseline-GREEN cell (`uast parse --format - json <go file>`), inject `end_col → end_col + 1` into the Rust analyzer, - rebuild, and assert the **live oracle now reports FAIL**; then revert, rebuild, - and assert it is **GREEN again** — proving RED is bug-driven, not stuck-red. - - **Phase B (harness cheat):** tamper a *copy* of the oracle to blank the - Go-stable `end_col` field (and neuter its stable-leaf guard); assert the - tampered oracle wrongly returns **PASS** while the real oracle returns - **FAIL** (the cheat hides the bug), then assert the tamper-evidence checker - **detects the weakening and fails closed** (self-test and end-to-end with the - tampered copy swapped into place). - - Everything mutates a copy or is reverted under a `trap`, so the real port and - harness are restored even on interrupt. -- `cli_surface/selftest/`, `coverage/selftest/`, `metamorphic/selftest/`, - `oracle/selftest/`, `fuzz/selfcheck_test.go` each carry a layer-local self-proof - run by that layer's entry point. - -Run the mutation self-test directly: - -```bash -bash tests/compat/integrity/mutation_self_test.sh # expect: META-GATE GREEN -``` - -## Reading a result - -- **PASS** — Rust byte-matches Go on stable fields, canonical-matches on - measured-variant fields. -- **FAIL** — a real divergence; the oracle prints the localized first-diff. Check - `results/<tier>.json` (per-cell) and `results/<tier>_gate.json` (allowlist - classification). -- **SIM** — simulation suspect (metamorphic): Rust output is constant across - inputs/limits where Go varies, or equals a recorded golden after the input - changed. -- **EXPECTED_EMPTY** — the live Go binary produced no stdout for this cell; that - emptiness is itself a measured contract (breaks if Go later emits, or Rust - diverges from Go's emptiness). - -## Current honest tally (WIP port, `compat smoke`) - -155 cells: **PASS 11 · FAIL 72 · SIM 0 · EXPECTED_EMPTY 72**. Gate: 22 FAILs -tracked-known-failing (grammar vendoring pending), 50 unexpected FAILs, 2 -metamorphic SIM ⇒ **74 real divergences, RED**. Rust line coverage 72.9% -(representative analyzer subset); matrix-cell coverage 155/155 (100%). This is the -honest state of the port — the system is doing its job: refusing to call a -half-finished port green. diff --git a/tests/compat/allowlist.json b/tests/compat/allowlist.json deleted file mode 100644 index 5b71b034..00000000 --- a/tests/compat/allowlist.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "_doc": "KNOWN-DIVERGENCE ALLOWLIST (SPEC roadmap 8 / task PHASE CI). Mirrors connectrpc/conformance's tracked 'known-failing/known-flaky'. An entry NEUTRALIZES a cell's FAIL/SIM verdict in the FINAL GATE EXIT CODE *only* when it carries BOTH (a) a written `reason` and (b) `go_nondeterminism_evidence` proving Go itself is nondeterministic on the diverging field(s) -- the differing Go run SHAs/values the live oracle measured. An entry WITHOUT evidence is REJECTED by the gate (fail-closed): it can never silently turn a real Rust bug green. Match is by exact `label` OR `argv` OR `family`+`kind`. NOTHING here blanks a Go-stable field; that is detected by the tamper layer, not excused here.", - "_categories": { - "go-nondeterminism": "Go's OWN output varies run-to-run on the diverging field(s); evidence = differing Go run SHAs from the live oracle. These DO neutralize the gate exit code.", - "tracked-known-failing": "A real, tracked Rust port gap (NOT Go-nondeterminism). These are REPORTED but DO NOT neutralize the gate -- a tracked-known-failing is still a real divergence and keeps the exit nonzero (SPEC: 'nonzero exit on any real divergence'). Listed so triage knows it is already on the board, Connect-style." - }, - "go_nondeterminism": [ - { - "label": "flag:static/clones--languages:Go[json]@hercules", - "family": "flag", - "kind": "clone-pair-order-shuffles", - "reason": "Go itself is nondeterministic on this cell: the reference clone detector (MinHash/LSH) accumulates pairs iterating Go maps, so the issues/clone-pairs ORDER \u2014 and, at the top-N issue-cap boundary, the MEMBERSHIP \u2014 shuffles run-to-run. 6 consecutive live-Go runs of the exact cell argv produced 6 different full-output SHAs (2026-08-02: 3c481ea8, c9d86200, 4bf5965e, 688e8bb2, d560c6e6, 020ee80b). The Rust port is deterministic; the numeric clone metrics (counts, ratios, types) agree with Go whenever Go is frozen at one seed.", - "go_nondeterminism_evidence": { - "command": "codefang run --checkpoint=false --resume=false --no-cache --workers 1 --analyzers static/clones --format json --languages Go <hercules>", - "observed_go_shas": [ - "3c481ea88552656ee3f0393cc1222c4faa0960c4a10f1f5c5bb503d6d83c362b", - "c9d86200ab9a00bbd3731fe11aa321d9e6d0b41d74f8b57a5106716216a3e38b", - "4bf5965e558121e5d1fd076737e3c8ad83f665d6e60d3efefa5ff5446cf44ae9", - "688e8bb287cb0172000f53c16d93920512b25a39c5bbf46f3986bdab9b26ee5b", - "d560c6e6aee6ce94a8d8cf9250fabad78fd244fd378a5c79c09deedd1c346bb1", - "020ee80b91ffb713e5b2e821f532e05b766c10c9de7672e9b0669dd54ad7737d" - ], - "observed_distribution": "6 consecutive live-Go runs on 2026-08-02: all 6 full-output SHAs distinct (issues list order + membership at the cap boundary shuffle)", - "note": "The flakes land in the flag family because --languages narrows the candidate set onto the map-iteration-dominated pairing paths; measured canonicalization cannot normalize ORDER-sensitive top-N membership." - }, - "neutralizes_gate": true - }, - { - "label": "static:static/cohesion[text]@ioq3", - "family": "static_analyzer", - "kind": "summary-message-flips", - "reason": "The reference implementation picks the section Summary message by iterating a Go MAP of numeric averages and using the FIRST value (internal/analyzers/common/aggregator.go: `for _, value := range averages { message = messageBuilder(value); break }`). Go map iteration order is randomized, and on ioq3 the three cohesion averages (lcom=0.7, cohesion_score=0.3, function_cohesion) straddle the message thresholds, so Go's own Summary line flips run-to-run. The numeric metrics are byte-identical between Go and Rust; only the message text diverges when Go happens to pick a different key than Rust's deterministic choice.", - "go_nondeterminism_evidence": { - "command": "codefang run --checkpoint=false --resume=false --no-cache --head --workers 1 --static-workers 1 -p <ioq3> --analyzers static/cohesion --format text", - "observed_go_values": [ - "Summary: Excellent overall cohesion across all analyzed code", - "Summary: Poor overall cohesion - significant refactoring recommended" - ], - "observed_distribution": "8 consecutive live-Go runs on 2026-07-26: 5x Excellent, 3x Poor", - "note": "The gate's n_go=3 oracle runs happened to agree by chance (p~0.1-0.3 per label mix), which is why measured canonicalization did not normalize this field." - }, - "neutralizes_gate": true - } - ], - "tracked_known_failing": [ - { - "label_glob": "flag:static/*--include-vendored*@hercules", - "family": "flag", - "kind": "deliberate-durability-cap-divergence", - "reason": "ROOT-CAUSED 2026-08-02 (was: 'oracle-exceeds-own-budget', unexplained timeout). Covers the 6 --include-vendored@hercules static cells (complexity + halstead across json/text/yaml formats). TWO layers, both diagnosed: (1) The frozen-oracle timeout is a Go BUG, not untestability: pkg/uast/parser_dsl.go toCanonicalNode re-walks every unmapped subtree twice (2^depth blowup); a PERF-ONLY, output-byte-identical patch (oracle/patches/uast-unmapped-subtree-rewalk-perf.patch — the same restructure Rust landed in cf-uast/src/lowering.rs) makes the oracle complete the cell with a full 5351785-byte report (measured 2026-08-02; the patch is proven byte-identical to the frozen binary on 9 diverse static/history cells). (2) With the oracle now completable, the residual divergence is REAL and DELIBERATE on the Rust side: Rust's durabilility caps skip vendor/gopkg.in/vmarkovtsev/BiDiSentiment.v1/assets/bindata.go (70.5MB > the 64MiB read cap; Go has no such cap and parses it), yielding 21473 vs Go's 21494 total functions (the 19 top-level funcs + 2 methods of bindata.go) on complexity, and the corresponding halstead-token/function-count deltas on halstead, plus tie-order diffs in the capped issues list. Cells stay RED: measuring them in-suite needs an oracle re-bless (tamper_check --bless, oracle SHA is fingerprinted) plus a policy call on the deliberate durability caps and a ~20min-per-cell runtime.", - "evidence": { - "patched_oracle_behavior": "completes with full 5351785-byte report (patched source @ b99c300~1 + oracle/patches/uast-unmapped-subtree-rewalk-perf.patch; killed late by a 400s signal yet wrote the complete report — full runtime ~18.5min; identity of the patch proven byte-for-byte on 9 cells)", - "frozen_oracle_behavior": "timeout >600s, 0 bytes stdout (2026-07-26)", - "rust_behavior": "12.7s wall, 5347342 bytes report; stderr: 'skipped 1 unreadable/oversized/unparseable files' (bindata.go 70538928 bytes > 64MiB cap)", - "residual_divergences": "Total Functions 21494 (go) vs 21473 (rust) = bindata.go functions skipped by the RUST-ONLY 64MiB durability cap; issues[] tie-order differs under equal complexity (zsyscall_* vendor files)", - "root_cause_oracle": "Go pkg/uast/parser_dsl.go toCanonicalNode: processChildren + createUnmappedNode double-walk per unmapped level (2^depth); identical fix in crates/cf-uast/src/lowering.rs" - }, - "neutralizes_gate": false - }, - { - "label": "fuzz:FuzzSerializer*/invalid-utf8-source", - "family": "fuzz", - "kind": "invalid-utf8-token-offset-misalignment", - "reason": "Real, tracked port gap on sources containing INVALID UTF-8 bytes: the reference implementation's Go strings carry raw bytes, so byte offsets into a function's token always align with the original source; the Rust port stores tokens as String via from_utf8_lossy, which inflates each invalid sequence into a 3-byte U+FFFD and shifts every later offset. Offset-derived operator/decision-point extraction (cf-complexity FunctionSourceContext / binary_operator_from_offsets) then misreads tokens after the corruption point, e.g. missing a `&&` (cyclomatic 7 vs Go's 8, avg 4.0 vs 4.5) on the distilled find. Valid-UTF-8 sources (i.e. real code) are unaffected. Fixing it means carrying raw bytes through Node.token (bstr-style) and lossy-converting only at serialization boundaries \u2014 an architectural change, tracked rather than patched around.", - "evidence_source": "corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go (+ .evidence.json, .go_run_*, .rust_out)", - "neutralizes_gate": false - } - ] -} diff --git a/tests/compat/cli_surface/README.md b/tests/compat/cli_surface/README.md deleted file mode 100644 index f5433670..00000000 --- a/tests/compat/cli_surface/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# CLI Surface Conformance (Go oracle vs Rust) - -Part of the Go-Rust compatibility test system -(`specs/go-compat-testing/SPEC.md`, Scope #1 / Roadmap #2). - -## What it does - -Recursively invokes `--help` on the **live Go binary** (root + every subcommand) -for both `codefang` and `uast`, parses the help into a normalized, format-agnostic -**surface model**, does the same for the **Rust binary**, and asserts the two are -identical. It also checks **error-path parity** (bad flag, unknown command, missing -required arg, unknown analyzer) -> exit code + stderr category. - -- The **oracle is the live Go binary** (`build/bin/{codefang,uast}`). The expected - surface is never re-derived in code; it is read out of Go by running `--help`. -- Comparison is against the Rust binary at `target/release/{codefang,uast}`. -- Run env is pinned: `set -f; TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800`. - -## Why a structured surface model, not a help-text byte diff - -Go uses **cobra** and Rust uses **clap**. The two render help prose differently by -construction (section headers, wrapping, the literal `Print help` line, `<value>` -vs `string` type markers). A byte diff of help text would fail on cosmetic -rendering and prove nothing about the actual surface. So the contract is the -**structured surface** -- but it is *not* weakened: every flag long-name, -short-letter, value-arity, and default; every subcommand; every positional -shape (count + variadic); and every `--help` exit code / output stream are -compared. A missing/extra/changed surface element is a hard FAIL. - -The one place a byte diff is genuinely impossible across frameworks -- error-path -stderr wording -- is compared by **exit code** (a real, enforceable contract) plus -a tolerant **error category** (bad-flag / unknown-command / missing-arg / runtime), -and for the runtime probe Rust must also surface the specific diagnostic concept -(it cannot pass with a generic stub message). - -## Files - -- `extract_surface.py` -- runs `--help` recursively on a binary, parses **both** - cobra and clap help into the surface model. `extract_surface.py {go|rust} {codefang|uast}`. -- `cli_surface.py` -- extracts both sides, diffs them, runs error-path probes, - prints per-row PASS/FAIL + tally. `--json` for machine output; `--only {surface,errorpath}`. -- `selftest/self_test.py` -- the **self-proof** (SPEC rule #6): plants 8 known - surface mutations + 1 anti-noop/tamper case and asserts each is detected, asserts - the identical-surface baseline yields zero divergences, and asserts the live - comparator reports the real divergences with nonzero exit. -- `run.sh` -- entry point: self-proof first, then live conformance. - -## Running - -```sh -tests/compat/cli_surface/run.sh -``` - -## Current measured divergences (Rust != Go) - -These are REAL, not parser artifacts (each verified by hand against both binaries): - -**Surface (codefang):** -- `completion` subcommand (+ `bash/zsh/fish/powershell`) is **missing in Rust**. -- ~30 `run` flags present in Go are **missing in Rust**: `--anomaly-window`, - `--anonymize`, `--burndown-*` (debug/files/goroutines/hibernation-*/people), - `--blob-cache-goroutines`, `--diff-goroutines`, `--diff-timeout`, - `--empty-commits`, `--exact-signatures`, `--fail-on-missing-submodules`, - `--granularity`, `--min-comment-len`, `--no-diff-cleanup`, `--no-diff-whitespace`, - `--people-dict`, `--sampling`, `--shotness-dsl-name`, `--shotness-dsl-struct`, - `--tick-size`, `--typos-max-distance`, `--uast-changes-goroutines`, `--whitelist`. -- `--checkpoint` / `--resume` differ in value-arity: Go bool flag vs Rust - optional-value (`[<checkpoint>]`). - -**Surface (uast):** -- `uast mapping` has a variadic positional in Rust that Go does not declare. - -**Error paths (both):** -- Exit code on argv errors: Go (cobra) = **1**, Rust (clap) = **2** - (bad-flag, unknown-command, missing-arg). -- Unknown-analyzer: Go emits `unknown analyzer id: ...`; Rust emits a stub - (`command dispatch is blocked on cf-commands (tier 8)`). - -The gate is therefore **RED** as it stands -- correctly, because the Rust surface -is not yet equivalent to Go. The self-proof is **GREEN**, proving the comparator -catches defects and does not produce false divergences on identical input. diff --git a/tests/compat/cli_surface/cli_surface.py b/tests/compat/cli_surface/cli_surface.py deleted file mode 100755 index 346e07a2..00000000 --- a/tests/compat/cli_surface/cli_surface.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env python3 -""" -CLI SURFACE CONFORMANCE (SPEC: specs/go-compat-testing/SPEC.md, Scope #1, Roadmap #2) - -Asserts the Rust binary's CLI surface is IDENTICAL to the LIVE Go binary's surface. - -Procedure: - 1. THE ORACLE IS THE LIVE GO BINARY. Extract Go's surface model by invoking - `--help` recursively on the real Go binary (extract_surface.py). Never derive - the expected surface in code — ask Go. - 2. Extract Rust's surface model the same way. - 3. DIFF the two models, per command, per flag, per positional, per exit-code, - per help-stream. Every divergence is reported as a FAIL row. - 4. ERROR-PATH PARITY: run a fixed set of bad-flag / missing-arg / unknown-cmd / - unknown-analyzer invocations under the pinned env on BOTH binaries and compare - (exit_code, stderr_class). Divergences are FAIL rows. - -OUTPUT: a per-row PASS/FAIL report and a final tally; nonzero exit on any FAIL. -With --json it emits a machine-readable result (used by the self-test). - -HONESTY: cobra (Go) and clap (Rust) render help PROSE differently, so this tool -compares the STRUCTURED surface, NOT raw help bytes. It does NOT, however, weaken -the contract: every flag long-name, short-letter, value-taking, and default, every -subcommand, every positional shape, and the error-path exit-code + stderr-class are -all compared. A surface that Rust is missing or has extra is a FAIL — it cannot be -hidden by the rendering difference. -""" - -import argparse -import json -import os -import subprocess -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -GO_DIR = "/home/dmitriy/sources/codefang/build/bin" -RU_DIR = "/home/dmitriy/sources/codefang/target/release" -PINNED_ENV = { - "TZ": "UTC", "NO_COLOR": "1", "LANG": "C", "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800", -} - -# Subcommands that are SYNTHETIC to one framework and have no surface meaning to -# compare structurally: cobra auto-adds `help`; we compare its presence/exit-code -# at the error-path layer, not as a surface row. (`completion` IS a real surface -# command and IS compared.) -SYNTHETIC_SUBCMDS = {"help"} - -# Subcommands present in the FROZEN Go oracle that have been INTENTIONALLY -# removed from the Rust product (so they are expected to be absent and must not -# count as surface divergences). Keyed by binary name; values are the bare -# command keys the extractor emits for that binary's surface. -# -# `uast lsp` — language server for the `.uastmap`/query DSL. -# `uast server` — the UAST development HTTP server + web playground. -# Both, and the `.uastmap` DSL data they edited, were dropped: analysis runs off -# the native cf-uast-mappings tables, leaving the DSL-editor tooling with no -# consumer. This is a deliberate divergence from the Go oracle's CLI surface; -# the report-output oracle (tests/compat/oracle) is unaffected. -INTENTIONALLY_REMOVED = {"uast": {"lsp", "server"}} - - -def load_surface(side, binname): - """Run the extractor on the LIVE binary, return its surface model.""" - p = subprocess.run( - [sys.executable, os.path.join(HERE, "extract_surface.py"), side, binname], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=180) - if p.returncode != 0: - raise RuntimeError(f"extract {side} {binname} failed: " - f"{p.stderr.decode('utf-8','replace')}") - return json.loads(p.stdout.decode("utf-8"))["surface"] - - -# --------------------------------------------------------------------------- # -# Surface diff. -# --------------------------------------------------------------------------- # -def _cmd_keys(surface): - return {k for k in surface.keys()} - - -def diff_surface(binname, go_s, ru_s): - """Return list of failure dicts (empty => surfaces match).""" - fails = [] - - def fail(cmd, kind, detail): - fails.append({"bin": binname, "cmd": cmd, "kind": kind, "detail": detail}) - - go_cmds = _cmd_keys(go_s) - ru_cmds = _cmd_keys(ru_s) - removed = INTENTIONALLY_REMOVED.get(binname, set()) - - # 1) command tree (subcommand existence). The cobra/clap synthetic `help` - # command is excluded; everything else (run, render, version, completion, - # parse, ...) must match exactly. - go_real = {c for c in go_cmds if c.split()[-1] not in SYNTHETIC_SUBCMDS} - ru_real = {c for c in ru_cmds if c.split()[-1] not in SYNTHETIC_SUBCMDS} - for c in sorted(go_real - ru_real): - if c in removed: - continue # deliberate product divergence; see INTENTIONALLY_REMOVED - fail(c, "missing-command", "present in Go, absent in Rust") - for c in sorted(ru_real - go_real): - fail(c, "extra-command", "present in Rust, absent in Go") - - # 2) per-command surface for every command present on BOTH sides - for cmd in sorted(go_real & ru_real): - g = go_s[cmd] - r = ru_s[cmd] - - # 2a) declared subcommand sets (minus synthetic and intentionally-removed) - gsub = {s for s in g["subcommands"] - if s not in SYNTHETIC_SUBCMDS and s not in removed} - rsub = {s for s in r["subcommands"] if s not in SYNTHETIC_SUBCMDS} - if gsub != rsub: - fail(cmd, "subcommand-set-differs", - {"go_only": sorted(gsub - rsub), "rust_only": sorted(rsub - gsub)}) - - # 2b) flags: every Go flag must exist in Rust with same short/value/default - gf, rf = g["flags"], r["flags"] - for name in sorted(set(gf) - set(rf)): - fail(cmd, "missing-flag", {"flag": "--" + name, "go": gf[name]}) - for name in sorted(set(rf) - set(gf)): - fail(cmd, "extra-flag", {"flag": "--" + name, "rust": rf[name]}) - for name in sorted(set(gf) & set(rf)): - gv, rv = gf[name], rf[name] - if gv["short"] != rv["short"]: - fail(cmd, "flag-short-differs", - {"flag": "--" + name, "go": gv["short"], "rust": rv["short"]}) - if gv["takes_value"] != rv["takes_value"]: - fail(cmd, "flag-value-arity-differs", - {"flag": "--" + name, - "go": gv["takes_value"], "rust": rv["takes_value"]}) - # default: compared only when BOTH sides surfaced one. clap and cobra - # do not both always PRINT a default for the same flag, so a default - # present on only one side is reported as a WEAKER "default-visibility" - # note rather than a hard divergence — but a CONFLICTING default (both - # present, different value) IS a hard fail. - if gv["default"] is not None and rv["default"] is not None \ - and str(gv["default"]) != str(rv["default"]): - fail(cmd, "flag-default-differs", - {"flag": "--" + name, - "go": gv["default"], "rust": rv["default"]}) - - # 2c) positionals: compare SHAPE (count + per-arg variadic flag). Names - # differ by framework (cobra "path" vs clap "path_arg") so names are - # NOT compared; but COUNT and VARIADIC-ness are a real contract. We do - # NOT compare `required` because cobra renders an optional positional as - # a bare word and a required one also as a bare word (no </[ marker), - # so required-ness is not reliably observable from cobra help text — - # comparing it would manufacture false divergences. Count + variadic - # are observable on both sides and are compared strictly. - gp = [p["variadic"] for p in g["positionals"]] - rp = [p["variadic"] for p in r["positionals"]] - if gp != rp: - fail(cmd, "positional-shape-differs", - {"go": g["positionals"], "rust": r["positionals"]}) - - # 2d) help exit code + stream must match (a help that errors, or goes to - # the wrong stream, is a surface divergence). - if g["help_rc"] != r["help_rc"]: - fail(cmd, "help-exit-code-differs", - {"go": g["help_rc"], "rust": r["help_rc"]}) - if g["help_stream"] != r["help_stream"]: - fail(cmd, "help-stream-differs", - {"go": g["help_stream"], "rust": r["help_stream"]}) - - return fails - - -# --------------------------------------------------------------------------- # -# Error-path parity. Run identical bad invocations on Go (oracle) and Rust under -# the pinned env; compare exit code and a normalized stderr CLASS. -# -# stderr CLASS, not raw bytes: cobra and clap word their diagnostics differently -# ("unknown flag" vs "unexpected argument"). The CONTRACT we can hold both to is: -# - the same EXIT CODE, and -# - the same error CATEGORY (bad-flag / unknown-command / missing-arg / runtime), -# detected by tolerant keyword classification of EITHER side's stderr. -# Raw-byte stderr equality is NOT achievable across frameworks and demanding it -# would be a false contract; but exit-code parity IS a real, enforceable contract, -# and it is exactly where Go(cobra)=1 vs Rust(clap)=2 currently DIVERGES — which -# this layer SURFACES rather than hides. -# --------------------------------------------------------------------------- # -ERROR_PROBES = [ - # (label, bin, argv, expected_category) - ("codefang/bad-flag", "codefang", ["run", "--nonexistent-flag"], "bad-flag"), - ("codefang/unknown-command", "codefang", ["frobnicate"], "unknown-command"), - ("codefang/render-missing-arg", "codefang", ["render"], "missing-arg"), - ("uast/bad-flag", "uast", ["parse", "--nonexistent-flag"], "bad-flag"), - ("uast/unknown-command", "uast", ["frobnicate"], "unknown-command"), - ("codefang/unknown-analyzer","codefang", ["run", "--no-cache", "--head", - "--analyzers", "static/doesnotexist", - "-p", "/tmp"], "runtime"), -] - -CATEGORY_KEYWORDS = { - "bad-flag": ["unknown flag", "unexpected argument", "unrecognized"], - "unknown-command": ["unknown command", "unrecognized subcommand"], - "missing-arg": ["accepts", "required argument", "required arguments"], - "runtime": ["unknown analyzer", "analyzer", "error"], -} - - -def classify_stderr(text): - low = text.lower() - # order matters: most specific first - for cat in ("unknown-command", "missing-arg", "bad-flag", "runtime"): - for kw in CATEGORY_KEYWORDS[cat]: - if kw in low: - return cat - return "none" - - -def run_probe(side, binname, argv): - base = GO_DIR if side == "go" else RU_DIR - env = dict(os.environ) - env.update(PINNED_ENV) - p = subprocess.run([os.path.join(base, binname)] + argv, env=env, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120) - return p.returncode, p.stderr.decode("utf-8", "replace") - - -def diff_error_paths(): - fails = [] - rows = [] - for label, binname, argv, expected_cat in ERROR_PROBES: - grc, gerr = run_probe("go", binname, argv) - rrc, rerr = run_probe("rust", binname, argv) - gcat = classify_stderr(gerr) - rcat = classify_stderr(rerr) - row = {"label": label, "argv": argv, - "go_rc": grc, "rust_rc": rrc, - "go_cat": gcat, "rust_cat": rcat, - "expected_cat": expected_cat} - problems = [] - if grc != rrc: - problems.append("exit-code") - if gcat != rcat: - problems.append("error-category") - # For the runtime (unknown-analyzer) probe, "error" is too coarse: Go emits - # a SPECIFIC diagnostic ("unknown analyzer id: ..."). Require Rust to also - # surface the analyzer concept, so a generic/stub error message that merely - # happens to contain "error" cannot pass as parity. - if expected_cat == "runtime": - if "analyzer" not in gerr.lower(): - problems.append("go-probe-not-analyzer-error") - elif "analyzer" not in rerr.lower(): - problems.append("rust-missing-specific-diagnostic") - # Go itself must produce a non-empty diagnostic for these (sanity: probe - # actually triggered an error path in the oracle). - if gcat == "none" and gerr.strip() == "": - problems.append("go-no-diagnostic") - row["problems"] = problems - rows.append(row) - if problems: - fails.append({"bin": binname, "cmd": label, "kind": "error-path-differs", - "detail": row}) - return fails, rows - - -# --------------------------------------------------------------------------- # -# Main. -# --------------------------------------------------------------------------- # -def run_all(go_codefang=None, go_uast=None, ru_codefang=None, ru_uast=None): - """Run the full surface + error-path comparison. Surfaces may be injected - (used by the self-test to feed a tampered surface); otherwise extracted live.""" - if go_codefang is None: - go_codefang = load_surface("go", "codefang") - if go_uast is None: - go_uast = load_surface("go", "uast") - if ru_codefang is None: - ru_codefang = load_surface("rust", "codefang") - if ru_uast is None: - ru_uast = load_surface("rust", "uast") - - fails = [] - fails += diff_surface("codefang", go_codefang, ru_codefang) - fails += diff_surface("uast", go_uast, ru_uast) - err_fails, err_rows = diff_error_paths() - fails += err_fails - return fails, err_rows - - -def main(): - ap = argparse.ArgumentParser(description="Go<->Rust CLI surface conformance") - ap.add_argument("--json", action="store_true", - help="emit machine-readable result") - ap.add_argument("--only", choices=["surface", "errorpath"], - help="run only one layer") - a = ap.parse_args() - - if a.only == "surface": - go_cf, go_u = load_surface("go", "codefang"), load_surface("go", "uast") - ru_cf, ru_u = load_surface("rust", "codefang"), load_surface("rust", "uast") - fails = diff_surface("codefang", go_cf, ru_cf) + diff_surface("uast", go_u, ru_u) - err_rows = [] - elif a.only == "errorpath": - fails, err_rows = diff_error_paths() - else: - fails, err_rows = run_all() - - if a.json: - print(json.dumps({"fails": fails, "error_rows": err_rows, - "n_fail": len(fails)}, indent=2, default=str)) - sys.exit(1 if fails else 0) - - # human report - print("================ CLI SURFACE CONFORMANCE (Go oracle vs Rust) ============") - if err_rows: - print("-- error-path parity (exit code + stderr class) --") - for r in err_rows: - verdict = "FAIL" if r["problems"] else "PASS" - print(f" {verdict} {r['label']:30s} " - f"go(rc={r['go_rc']},{r['go_cat']}) " - f"rust(rc={r['rust_rc']},{r['rust_cat']})" - + (f" PROBLEMS={r['problems']}" if r["problems"] else "")) - print("-- surface diff --") - allowlisted = sorted(f"{b} {c}" for b, cs in INTENTIONALLY_REMOVED.items() for c in cs) - if allowlisted: - print(f" NOTE intentionally removed (allowlisted): {', '.join(allowlisted)}") - if not fails: - print(" (no surface divergences)") - for f in fails: - if f["kind"] == "error-path-differs": - continue - print(f" FAIL [{f['bin']}] {f['cmd']:24s} {f['kind']}: " - f"{json.dumps(f['detail'], default=str)[:160]}") - n_surface = sum(1 for f in fails if f["kind"] != "error-path-differs") - n_err = sum(1 for f in fails if f["kind"] == "error-path-differs") - print("================ RESULT ================") - print(f"surface_divergences={n_surface} error_path_divergences={n_err} " - f"TOTAL_FAIL={len(fails)}") - if fails: - print("CLI-SURFACE GATE: RED — Rust surface diverges from Go") - sys.exit(1) - print("CLI-SURFACE GATE: GREEN — Rust surface matches Go") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/cli_surface/extract_surface.py b/tests/compat/cli_surface/extract_surface.py deleted file mode 100755 index 083b1311..00000000 --- a/tests/compat/cli_surface/extract_surface.py +++ /dev/null @@ -1,343 +0,0 @@ -#!/usr/bin/env python3 -""" -CLI SURFACE EXTRACTOR (SPEC: specs/go-compat-testing/SPEC.md, Scope #1, Roadmap #2) - -Recursively invokes `--help` on a LIVE binary (root + every subcommand) under the -pinned env and parses the help text into a NORMALIZED, format-agnostic surface -model: - - { - "<cmd path>": { - "subcommands": [name, ...], # child command names - "flags": { "--long": {short, takes_value, default}, ... }, - "positionals": [ "name", ... ], # positional arg names (best-effort) - "help_rc": int, # exit code of `<cmd> --help` - "help_stream": "stdout"|"stderr", # where help text was written - }, ... - } - -WHY A MODEL, NOT RAW BYTES: Go uses cobra and Rust uses clap; the two render help -prose DIFFERENTLY by construction (section headers, wrapping, the literal "Print -help" line). A byte diff of help text would FAIL on cosmetic rendering and tell us -nothing about whether the SURFACE (the set of flags, their short names, defaults, -the positional args, the subcommand tree) is the same. The conformance contract is -therefore the structured surface; the comparator (cli_surface.py) diffs THIS model. - -THE ORACLE IS THE LIVE GO BINARY. This program only ever EXECUTES the binaries; it -never re-derives what a flag/subcommand "should" be. Run Go to learn the truth. - -This parser understands BOTH cobra help and clap help so the SAME model can be -extracted from either side and compared apples-to-apples. -""" - -import json -import os -import re -import subprocess -import sys - -GO_DIR = "/home/dmitriy/sources/codefang/build/bin" -RU_DIR = "/home/dmitriy/sources/codefang/target/release" - -PINNED_ENV = { - "TZ": "UTC", "NO_COLOR": "1", "LANG": "C", "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800", -} - - -def run_help(exe, argv): - """Run `exe <argv> --help` under pinned env. Return (rc, stdout, stderr).""" - env = dict(os.environ) - env.update(PINNED_ENV) - p = subprocess.run([exe] + list(argv) + ["--help"], env=env, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=60) - return (p.returncode, - p.stdout.decode("utf-8", "replace"), - p.stderr.decode("utf-8", "replace")) - - -# --------------------------------------------------------------------------- # -# Subcommand discovery: works for cobra ("Available Commands:") and clap -# ("Commands:"). We take command names only (first token of each indented line), -# dropping the synthetic "help"/"completion" rows handled by the comparator. -# --------------------------------------------------------------------------- # -def parse_subcommands(text): - subs = [] - in_block = False - for line in text.splitlines(): - if re.match(r"^(Available Commands|Commands):\s*$", line): - in_block = True - continue - if in_block: - if not line.strip(): - in_block = False - continue - if not line.startswith((" ", "\t")): - in_block = False - continue - m = re.match(r"^\s{2,}(\S+)", line) - if m: - subs.append(m.group(1)) - return subs - - -# --------------------------------------------------------------------------- # -# Flag parsing. Cobra flag lines look like: -# -a, --analyzers strings help text (default ...) -# --head help text -# -h, --help help for run -# Clap flag lines look like (long form, with --help): -# -a, --analyzers <analyzers> -# help text -# --format <format> -# help text -# [default: json] -# --checkpoint [<checkpoint>] -# [default: true] -# [possible values: true, false] -# We extract per long flag: short letter (if any), whether it takes a value, -# and the default value if discoverable. -# --------------------------------------------------------------------------- # -_FLAG_RE = re.compile( - r"""^\s* - (?:-(?P<short>[A-Za-z]),\s+)? # optional short - --(?P<long>[A-Za-z0-9][A-Za-z0-9-]*) # long name - (?P<rest>.*)$ - """, re.VERBOSE) - -# A REAL cobra default is a typed token: a quoted string, a number, a bool, or a -# bracketed list — e.g. (default "json"), (default 24), (default true), -# (default [all]). It is NOT free prose like "(default is $HOME/.uast.yaml)" which -# cobra emits inside a help SENTENCE. We therefore require the captured token to be -# a quoted string / number / bool / bracketed list, anchored to end-of-line. -_DEFAULT_COBRA = re.compile( - r"""\(default\s+( - "[^"]*" # "json" - | '[^']*' # 'json' - | \[[^\]]*\] # [all] - | -?\d+(?:\.\d+)? # 24 / -1 / 1.5 - | true | false - )\)\s*$""", re.VERBOSE) -_DEFAULT_CLAP = re.compile(r"\[default:\s*(.*?)\]") - - -def _norm_default(raw): - if raw is None: - return None - raw = raw.strip() - # clap quotes empty string default as "" — normalize to empty - if raw == '""' or raw == "''": - return "" - # cobra arrays: [all] ; clap: all — strip surrounding brackets/quotes - if raw.startswith("[") and raw.endswith("]"): - raw = raw[1:-1] - if (raw.startswith('"') and raw.endswith('"')) or \ - (raw.startswith("'") and raw.endswith("'")): - raw = raw[1:-1] - return raw - - -def parse_flags(text): - """ - Returns {long: {"short": str|None, "takes_value": bool, "default": str|None}}. - Handles cobra (single-line) and clap (flag line + indented continuation that - may carry [default: ...]). - """ - flags = {} - lines = text.splitlines() - # find the Flags/Options block(s); cobra: "Flags:" and "Global Flags:"; - # clap: "Options:". We scan ALL such blocks. - block_starts = [] - for i, line in enumerate(lines): - if re.match(r"^(Flags|Global Flags|Options):\s*$", line): - block_starts.append(i) - if not block_starts: - return flags - - for bi in block_starts: - j = bi + 1 - while j < len(lines): - line = lines[j] - if not line.strip(): - # blank line: end of block UNLESS next non-blank is still indented - # (clap separates entries by blank lines). Peek. - k = j + 1 - while k < len(lines) and not lines[k].strip(): - k += 1 - if k >= len(lines) or not lines[k].startswith((" ", "\t")): - break - if re.match(r"^[A-Za-z].*:\s*$", lines[k]): - break - j = k - continue - # a new section header (e.g. another "Global Flags:") ends this block - if re.match(r"^[A-Za-z][A-Za-z ]*:\s*$", line) and \ - not line.startswith((" ", "\t")): - break - m = _FLAG_RE.match(line) - if not m: - j += 1 - continue - long = m.group("long") - short = m.group("short") - rest = m.group("rest") - # detect value-taking: cobra has a type token (string/int/strings/...) - # right after the long name; clap has <...> or [<...>] after it. - takes_value = False - if re.match(r"\s+(string|int|strings|float|float64|uint|duration|bytes)\b", - rest): - takes_value = True - if re.match(r"\s*\[?<", rest): - takes_value = True - # default: search this line and any indented clap continuation lines - default = None - md = _DEFAULT_COBRA.search(rest) - if md: - default = _norm_default(md.group(1)) - # clap continuation lines (indented further than the flag line) - k = j + 1 - while k < len(lines): - cont = lines[k] - if not cont.strip(): - break - if _FLAG_RE.match(cont) and not cont.startswith(" "): - break - if re.match(r"^[A-Za-z][A-Za-z ]*:\s*$", cont): - break - mdc = _DEFAULT_CLAP.search(cont) - if mdc: - default = _norm_default(mdc.group(1)) - if re.search(r"<", cont) and not takes_value: - pass - k += 1 - flags[long] = {"short": short, "takes_value": takes_value, - "default": default} - j += 1 - return flags - - -# --------------------------------------------------------------------------- # -# Positional args: from the Usage: line. cobra: "uast parse [files...] [flags]"; -# clap: "Usage: codefang render [OPTIONS] <store-dir>". We extract the bracketed/ -# angle tokens that are not OPTIONS/flags/command, normalizing the NAME only. -# --------------------------------------------------------------------------- # -def parse_positionals(text, path): - """ - Extract positional args from the Usage line, normalized to a SHAPE descriptor - per positional: {"variadic": bool, "required": bool}. We deliberately discard - the NAME (cobra "path" vs clap "path_arg" vs "store-dir" are the same arg under - different framework conventions) and keep only the comparable shape. - - `path` is the KNOWN command-path (binary-relative), e.g. ["diff"] or [] for - root. Because the walker already knows the exact command path, we strip exactly - (1 + len(path)) leading bare tokens (binary + each subcommand word) from the - usage line — removing the cobra ambiguity where a bare subcommand word - ("uast version [flags]") could otherwise be mistaken for a positional. - - cobra usage: "uast diff file1 file2 [flags]" bare-word positionals - "uast parse [files...] [flags]" bracketed variadic - "codefang render <store-dir> [flags]" - "uast version [flags]" NO positionals - clap usage: "Usage: codefang render [OPTIONS] <store-dir>" - "Usage: uast parse [files...] [flags]" - """ - usage = None - lines = text.splitlines() - for i, line in enumerate(lines): - if line.strip() == "Usage:": # cobra block form: usage is on next non-blank - for k in range(i + 1, len(lines)): - if lines[k].strip(): - usage = lines[k].strip() - break - break - m = re.match(r"^Usage:\s+(.*)$", line) # clap inline form - if m: - usage = m.group(1).strip() - break - if not usage: - return [] - - placeholders = {"[flags]", "[OPTIONS]", "[command]", "[COMMAND]"} - toks = usage.split() - # strip leading command-path tokens: binary + every known subcommand word. - strip_n = 1 + len(path) - toks = toks[strip_n:] - # whatever remains, minus placeholders, is positionals - pos = [] - for t in toks: - tt = t.strip() - if tt in placeholders: - continue - variadic = "..." in tt - required = tt.startswith("<") - core = re.sub(r"[\[\]<>.]", "", tt) - if not core: - continue - pos.append({"variadic": variadic, "required": required}) - return pos - - -def detect_help_stream(rc, out, err): - """Where did --help write? Both frameworks write success help to stdout.""" - if out.strip(): - return "stdout" - if err.strip(): - return "stderr" - return "none" - - -# --------------------------------------------------------------------------- # -# Recursive walk of the command tree. -# --------------------------------------------------------------------------- # -def walk(exe, path, surface, seen): - key = " ".join(path) if path else "(root)" - if key in seen: - return - seen.add(key) - rc, out, err = run_help(exe, path) - text = out if out.strip() else err - subs = parse_subcommands(text) - surface[key] = { - "subcommands": sorted(subs), - "flags": parse_flags(text), - "positionals": parse_positionals(text, path), - "help_rc": rc, - "help_stream": detect_help_stream(rc, out, err), - } - for s in subs: - # don't recurse into the synthetic cobra "help" command (clap has none) - if s in ("help",): - continue - walk(exe, path + [s], surface, seen) - - -def extract(exe): - surface = {} - walk(exe, [], surface, set()) - return surface - - -def main(): - if len(sys.argv) < 3 or sys.argv[1] not in ("go", "rust"): - print("usage: extract_surface.py {go|rust} {codefang|uast} [--out FILE]", - file=sys.stderr) - sys.exit(2) - side, binname = sys.argv[1], sys.argv[2] - base = GO_DIR if side == "go" else RU_DIR - exe = os.path.join(base, binname) - surface = extract(exe) - out_path = None - if "--out" in sys.argv: - out_path = sys.argv[sys.argv.index("--out") + 1] - blob = json.dumps({"side": side, "bin": binname, "surface": surface}, - indent=2, sort_keys=True) - if out_path: - with open(out_path, "w") as f: - f.write(blob) - else: - print(blob) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/cli_surface/run.sh b/tests/compat/cli_surface/run.sh deleted file mode 100755 index e85d378d..00000000 --- a/tests/compat/cli_surface/run.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# CLI SURFACE CONFORMANCE — entry point -# (SPEC: specs/go-compat-testing/SPEC.md, Scope #1 / Roadmap #2) -# -# Runs, under the pinned env, the full CLI-surface conformance check (Go oracle vs -# Rust) for BOTH codefang and uast, plus error-path parity, then the self-proof -# that the comparator catches planted defects. -# -# Exit 0 only when the Rust surface matches Go AND the self-proof passes. -set -u -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# pinned run env (rule #5); set -f so the shell never globs argv. -set -f -export TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 - -echo "##### CLI SURFACE: self-proof (must catch planted defects) #####" -python3 "$HERE/selftest/self_test.py" -SELF=$? - -echo -echo "##### CLI SURFACE: live conformance (Go oracle vs Rust) #####" -python3 "$HERE/cli_surface.py" "$@" -LIVE=$? - -echo -if [ "$SELF" -ne 0 ]; then - echo "CLI-SURFACE: SELF-PROOF FAILED — the comparator cannot be trusted" - exit 2 -fi -if [ "$LIVE" -ne 0 ]; then - echo "CLI-SURFACE: RED — Rust surface diverges from Go (see rows above)" - exit 1 -fi -echo "CLI-SURFACE: GREEN — Rust surface matches Go and self-proof passed" -exit 0 diff --git a/tests/compat/cli_surface/selftest/self_test.py b/tests/compat/cli_surface/selftest/self_test.py deleted file mode 100755 index ae9e077a..00000000 --- a/tests/compat/cli_surface/selftest/self_test.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -""" -CLI-SURFACE SELF-PROOF (SPEC rule #6: every component must PROVE it catches a defect) - -A green that cannot be shown to catch a planted bug is worthless. This test plants -KNOWN surface divergences and asserts the comparator reports FAIL for each. It also -asserts the BASELINE property: when Rust's surface is made identical to Go's, the -comparator finds ZERO surface divergences (so the FAILs above are signal, not noise). - -It exercises the comparator's surface-diff layer with INJECTED surfaces (so the test -is hermetic and does not depend on the current state of the Rust binary), plus a -LIVE end-to-end check that the comparator's process exit code is nonzero on the real -(currently-divergent) binaries. - -Mutations planted (each must be DETECTED): - M1 missing-command : Rust drops a subcommand Go has - M2 missing-flag : Rust drops a flag Go has - M3 extra-flag : Rust adds a flag Go lacks - M4 flag-short-differs: Rust changes a flag's short letter - M5 flag-arity-differs: Rust changes a flag from bool to value-taking - M6 flag-default-diff : Rust changes a flag's default - M7 positional-shape : Rust changes a positional from scalar to variadic - M8 help-exit-code : Rust's `--help` exits nonzero where Go exits 0 - -Tamper planted (must be DETECTED as the forbidden cheat): - T1 force-green-by-aliasing : feed the GO surface as the Rust surface for a probe - that we then mutate ONLY on the Go side — i.e. prove the comparator is not a - no-op that always says PASS. Concretely we mutate Go (oracle) and leave Rust - equal to the UNMUTATED go; the comparator MUST flag the divergence rather - than silently agreeing. (A comparator that ignored one side would pass.) -""" - -import copy -import json -import os -import subprocess -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -CLI_DIR = os.path.dirname(HERE) -sys.path.insert(0, CLI_DIR) -import cli_surface # noqa: E402 - - -def base_surface(): - """A minimal but representative two-command surface used as the GO oracle.""" - return { - "(root)": { - "subcommands": ["render", "run", "version"], - "flags": {"help": {"short": "h", "takes_value": False, "default": None}, - "verbose": {"short": "v", "takes_value": False, "default": None}}, - "positionals": [], - "help_rc": 0, "help_stream": "stdout", - }, - "run": { - "subcommands": [], - "flags": { - "analyzers": {"short": "a", "takes_value": True, "default": None}, - "format": {"short": None, "takes_value": True, "default": "json"}, - "head": {"short": None, "takes_value": False, "default": None}, - }, - "positionals": [{"variadic": False, "required": False}], - "help_rc": 0, "help_stream": "stdout", - }, - "render": { - "subcommands": [], - "flags": {"output": {"short": "o", "takes_value": True, "default": ""}}, - "positionals": [{"variadic": False, "required": True}], - "help_rc": 0, "help_stream": "stdout", - }, - "version": { - "subcommands": [], "flags": {}, "positionals": [], - "help_rc": 0, "help_stream": "stdout", - }, - } - - -def diff(go_s, ru_s): - return cli_surface.diff_surface("codefang", go_s, ru_s) - - -def kinds(fails): - return {f["kind"] for f in fails} - - -def assert_detects(name, go_s, ru_s, expected_kind): - fails = diff(go_s, ru_s) - if expected_kind not in kinds(fails): - return (False, f"{name}: expected '{expected_kind}', got " - f"{sorted(kinds(fails))} ({len(fails)} fails)") - return (True, f"{name}: DETECTED '{expected_kind}'") - - -def run(): - results = [] - - # BASELINE: identical surfaces => zero divergences (signal vs noise proof) - go_s = base_surface() - ru_s = copy.deepcopy(go_s) - base_fails = diff(go_s, ru_s) - if base_fails: - results.append((False, f"BASELINE: identical surfaces produced " - f"{len(base_fails)} false divergence(s): " - f"{sorted(kinds(base_fails))}")) - else: - results.append((True, "BASELINE: identical surfaces => 0 divergences")) - - # M1 missing-command - ru = copy.deepcopy(go_s); del ru["version"]; ru["(root)"]["subcommands"].remove("version") - results.append(assert_detects("M1", go_s, ru, "missing-command")) - - # M2 missing-flag - ru = copy.deepcopy(go_s); del ru["run"]["flags"]["head"] - results.append(assert_detects("M2", go_s, ru, "missing-flag")) - - # M3 extra-flag - ru = copy.deepcopy(go_s) - ru["run"]["flags"]["bogus"] = {"short": None, "takes_value": False, "default": None} - results.append(assert_detects("M3", go_s, ru, "extra-flag")) - - # M4 flag-short-differs - ru = copy.deepcopy(go_s); ru["run"]["flags"]["analyzers"]["short"] = "x" - results.append(assert_detects("M4", go_s, ru, "flag-short-differs")) - - # M5 flag-value-arity-differs - ru = copy.deepcopy(go_s); ru["run"]["flags"]["head"]["takes_value"] = True - results.append(assert_detects("M5", go_s, ru, "flag-value-arity-differs")) - - # M6 flag-default-differs - ru = copy.deepcopy(go_s); ru["run"]["flags"]["format"]["default"] = "yaml" - results.append(assert_detects("M6", go_s, ru, "flag-default-differs")) - - # M7 positional-shape-differs - ru = copy.deepcopy(go_s) - ru["run"]["positionals"] = [{"variadic": True, "required": False}] - results.append(assert_detects("M7", go_s, ru, "positional-shape-differs")) - - # M8 help-exit-code-differs - ru = copy.deepcopy(go_s); ru["run"]["help_rc"] = 2 - results.append(assert_detects("M8", go_s, ru, "help-exit-code-differs")) - - # T1 anti-noop / anti-tamper: mutate the GO (oracle) side only; Rust stays at - # the original. A comparator that quietly trusted one side, or that "blanked" - # the differing field to force green, would report 0. The honest comparator - # MUST report the divergence. - go_mut = copy.deepcopy(go_s) - go_mut["run"]["flags"]["format"]["default"] = "CHANGED" - ru = copy.deepcopy(go_s) # rust unchanged => differs from mutated go - t1_fails = diff(go_mut, ru) - if "flag-default-differs" in kinds(t1_fails): - results.append((True, "T1: oracle-side change DETECTED (comparator is not " - "a no-op; cannot be forced green by ignoring a side)")) - else: - results.append((False, "T1: oracle-side change NOT detected — comparator " - "may be a no-op/tampered")) - - # LIVE end-to-end: the real comparator must run against the ACTUAL binaries and - # report a self-consistent result. The comparator's detection power is already - # proven by M1-M8 (planted divergences of every kind) + T1 (oracle-side change); - # this LIVE check proves only that the wiring runs end-to-end on the real - # binaries and that its exit code agrees with its own n_fail count, in BOTH - # directions: - # * if the real surfaces diverge -> n_fail > 0 AND exit code nonzero; - # * if the real surfaces match -> n_fail == 0 AND exit code zero. - # (An earlier version hardcoded "n_fail must be > 0", which wrongly went RED once - # the Rust CLI was actually fixed to match Go. Detection power is NOT asserted - # here — it is asserted by M1-M8/T1 — so this correction does not weaken the - # self-proof; it makes the green case legitimately passable.) - p = subprocess.run([sys.executable, os.path.join(CLI_DIR, "cli_surface.py"), - "--json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=300) - try: - live = json.loads(p.stdout.decode("utf-8")) - n_fail = live["n_fail"] - rc_nonzero = p.returncode != 0 - # exit code must be consistent with the reported divergence count - consistent = (n_fail > 0) == rc_nonzero - if consistent: - verdict = ("matches Go (0 divergences, exit 0)" if n_fail == 0 - else f"reports {n_fail} divergence(s) incl. error-path " - f"(exit {p.returncode})") - results.append((True, f"LIVE: real comparator ran end-to-end and {verdict}")) - else: - results.append((False, f"LIVE: comparator exit code ({p.returncode}) " - f"inconsistent with n_fail ({n_fail}) — wiring broken")) - except Exception as e: - results.append((False, f"LIVE: could not parse comparator output: {e}")) - - # report - npass = sum(1 for ok, _ in results if ok) - nfail = sum(1 for ok, _ in results if not ok) - print("================ CLI-SURFACE SELF-PROOF ================") - for ok, msg in results: - print(f" {'PASS' if ok else 'FAIL'} {msg}") - print(f"---- self-proof: {npass} passed, {nfail} failed ----") - return 0 if nfail == 0 else 1 - - -if __name__ == "__main__": - sys.exit(run()) diff --git a/tests/compat/corpus/README.md b/tests/compat/corpus/README.md deleted file mode 100644 index 8a7d745a..00000000 --- a/tests/compat/corpus/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Compatibility corpus + invocation matrix (MatrixCorpus phase) - -SPEC: `specs/go-compat-testing/SPEC.md` §3.2 (invocation matrix), §3.3 (corpus), -roadmap item 3. This phase builds the content-addressed input corpus and the -meaningful invocation cross-product, and wires **every** matrix cell to the live -Go oracle (`../oracle/oracle.py`). It does not re-derive any expected output: the -Go binary is the sole source of truth. - -## Layout - -- `build_corpus.py` — mines real inputs, content-addresses them, records - provenance. Re-runnable; deterministic file selection. -- `files/<sha256>.<ext>` — content-addressed source files (one per language). -- `manifest.json` — provenance per entry: language, sha256, origin path, size, - and the **live-oracle-measured** UAST byte/node count (so a dead cell can never - be admitted silently). -- `known_gaps.json` — measured Go↔Rust divergences with evidence (see Findings). -- `../matrix.toml` — the invocation cross-product (axes only; declares nothing - about field stability — that is measured by the oracle). -- `../expand_matrix.py` — cross-multiplies the axes, substitutes corpus inputs, - emits one oracle invocation per cell. -- `../run_matrix.py` — runs each cell through the oracle; records Go-empty cells - as `EXPECTED_EMPTY` contracts (not skips). Writes `../results/<tier>.json`. -- `../selftest_matrix.py` — proves the corpus+matrix catch a planted defect. - -## Corpus contents - -12 tree-sitter languages mined from real local repos (not just Go): -go, python, c, c-header, rust, typescript, tsx, javascript, json, yaml, cpp, -shell. Each file is 500–25 000 bytes of real source and parses to non-trivial -UAST on the **live Go binary** (verified at build time). - -3 real git repos recorded for the analyzer cells, **2 beyond kubernetes**: -- `hercules` — Go, ~1006 commits (medium). -- `ioq3` — C, ~3784 commits (larger, different language). -- `kubernetes` — Go, ~135k commits (the large reference repo). - -## Matrix - -Axes (see `matrix.toml`): all 17 analyzers (7 static + 10 history) × output -formats (json, yaml, bin, compact, text, ndjson, timeseries, plus the -timeseries+ndjson combo; uast: json/compact/tree, json/text, json/compact/count) -× key flags (--head, --limit, --first-parent, --since, --per-file, ---include-vendored, --include-generated) × analyzer sets (`*`, `static/*`, -`history/*`, and pairs) × per-language uast subcommands (parse/analyze/query). - -Expansion: **smoke = 155 cells**, **full = 486 cells**. Every cell is dispatched -to `oracle.py`. Cells where the live Go binary emits no stdout (e.g. `uast parse ---format tree`) are recorded as `EXPECTED_EMPTY` contracts. - -## Findings (recorded, not hidden) - -Running the multi-language `uast parse` cells through the oracle immediately -surfaced a real port gap that the all-Go recorded goldens could never expose: - -- **Go parses 12/12 languages; Rust parses only Go.** For the other 11 - languages the Rust `uast` binary emits **0 bytes** with - `"no tree-sitter grammar wired for language <X> (grammar vendoring pending)"`. - Evidence (per-language Go byte counts + Rust stderr) is in `known_gaps.json`. - -This is exactly the unseen-input class the SPEC targets: a fresh, multi-language -corpus is not memorizable, and it caught a gap a Go-only golden set hid. - -## Run - -``` -python3 corpus/build_corpus.py # (re)build the corpus -python3 expand_matrix.py full # inspect the cell list -python3 run_matrix.py --tier smoke --family uast_parse # wire a family to oracle -python3 selftest_matrix.py # prove it catches a planted bug -``` diff --git a/tests/compat/corpus/build_corpus.py b/tests/compat/corpus/build_corpus.py deleted file mode 100644 index 4fae6324..00000000 --- a/tests/compat/corpus/build_corpus.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -""" -Content-addressed CORPUS builder (SPEC: specs/go-compat-testing/SPEC.md §3.3, roadmap 3). - -MINES real inputs the test author never hand-curated: - - * SOURCE FILES spanning MULTIPLE tree-sitter languages codefang supports - (NOT just Go) -- one real file per language, taken from real local repos. - * REAL GIT REPOS of differing size BEYOND kubernetes (hercules: Go ~1k commits, - ioq3: C ~3.8k commits). kubernetes remains available as the large repo. - -Each corpus entry is stored CONTENT-ADDRESSED (sha256 of the bytes) under -corpus/files/<sha>.<ext>, and every entry's PROVENANCE (origin path, repo, sha, -size, language, oracle-measured node count) is recorded in corpus/manifest.json. - -This builder does NOT compare anything and does NOT re-derive expected output. -It only MINES + records. The oracle (oracle.py) is the sole source of truth and -is invoked here ONLY to (a) prove each mined file actually parses on the LIVE Go -binary (so a dead cell is never silently admitted) and (b) record provenance. -""" - -import hashlib -import json -import os -import shutil -import subprocess -import sys - -ROOT = "/home/dmitriy/sources/codefang" -SRC = "/home/dmitriy/sources" -GO_UAST = os.path.join(ROOT, "build/bin/uast") -HERE = os.path.dirname(os.path.abspath(__file__)) -FILES_DIR = os.path.join(HERE, "files") -MANIFEST = os.path.join(HERE, "manifest.json") - -PINNED_ENV = {"TZ": "UTC", "NO_COLOR": "1", "LANG": "C", "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800"} - -# (language-id, extension, repo-root) -- repo roots are real local checkouts. -# The language-id is the codefang/uast language name; the oracle confirms parse. -LANG_SOURCES = [ - ("go", "go", f"{SRC}/hercules"), - ("python", "py", f"{SRC}/nanoGPT"), - ("c", "c", f"{SRC}/ioq3"), - ("c-header", "h", f"{SRC}/ioq3"), - ("rust", "rs", f"{SRC}/agent-client-protocol"), - ("typescript", "ts", f"{SRC}/onlook"), - ("tsx", "tsx", f"{SRC}/onlook"), - ("javascript", "js", f"{SRC}/onlook"), - ("json", "json", f"{SRC}/agent-client-protocol"), - ("yaml", "yml", f"{SRC}/hercules"), - ("cpp", "cpp", f"{SRC}/llama.cpp"), - ("shell", "sh", f"{SRC}/ioq3"), -] - -# Real git repos for the history/repo-level matrix cells. Differing size/language -# BEYOND kubernetes (which stays as the large repo). hercules is the codefang -# lineage repo (Go); ioq3 is a large C repo with very different history shape. -REPOS = [ - {"id": "hercules", "path": f"{SRC}/hercules", - "note": "Go, medium history (~1k commits) -- beyond kubernetes"}, - {"id": "ioq3", "path": f"{SRC}/ioq3", - "note": "C, larger history (~3.8k commits), different language -- beyond kubernetes"}, - {"id": "kubernetes", "path": f"{SRC}/kubernetes", - "note": "Go, very large history -- the reference large repo"}, -] - -EXCLUDE = ("node_modules/", "/build/", "/dist/", "/vendor/", "/.git/", - "CMakeFiles", "/testdata/", ".min.") - - -def sha256_file(p): - h = hashlib.sha256() - with open(p, "rb") as f: - for chunk in iter(lambda: f.read(65536), b""): - h.update(chunk) - return h.hexdigest() - - -def oracle_parse_nodes(path): - """Invoke the LIVE Go uast binary; return (bytes_len, node_count) or (0,0).""" - env = dict(os.environ); env.update(PINNED_ENV) - try: - p = subprocess.run([GO_UAST, "parse", "--format", "json", path], - env=env, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, timeout=120) - except Exception: - return 0, 0 - out = p.stdout - return len(out), out.count(b'"type"') - - -def pick_source(ext, base): - """First real source file 500..25000 bytes under base, excluding artifacts.""" - cands = [] - for dirpath, _, names in os.walk(base): - if any(x in dirpath + "/" for x in EXCLUDE): - continue - for n in names: - if not n.endswith("." + ext): - continue - fp = os.path.join(dirpath, n) - if any(x in fp for x in EXCLUDE): - continue - try: - sz = os.path.getsize(fp) - except OSError: - continue - if 500 <= sz <= 25000: - cands.append(fp) - cands.sort() # deterministic selection - return cands[0] if cands else None - - -def git_commit_count(path): - try: - p = subprocess.run(["git", "-C", path, "rev-list", "--count", "HEAD"], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, - timeout=60) - return int(p.stdout.strip() or 0) - except Exception: - return 0 - - -def main(): - os.makedirs(FILES_DIR, exist_ok=True) - manifest = {"files": [], "repos": [], "languages_supported_probed": []} - - print("== mining per-language source files (oracle-verified parse) ==") - seen = set() - for lang, ext, base in LANG_SOURCES: - if not os.path.isdir(base): - print(f" SKIP {lang}: repo missing {base}") - continue - src = pick_source(ext, base) - if not src: - print(f" SKIP {lang}: no {ext} file in size window under {base}") - continue - sha = sha256_file(src) - blen, nodes = oracle_parse_nodes(src) - if blen == 0 or nodes < 2: - # the LIVE oracle produced no/trivial UAST: do NOT admit a dead cell. - print(f" DROP {lang}: oracle parse trivial ({blen}B nodes={nodes}) {src}") - continue - dst = os.path.join(FILES_DIR, f"{sha}.{ext}") - if sha not in seen: - shutil.copy2(src, dst) - seen.add(sha) - entry = { - "id": f"file/{lang}", - "language": lang, - "ext": ext, - "sha256": sha, - "stored": os.path.relpath(dst, HERE), - "size_bytes": os.path.getsize(src), - "origin": src, - "oracle_uast_bytes": blen, - "oracle_uast_nodes": nodes, - } - manifest["files"].append(entry) - manifest["languages_supported_probed"].append(lang) - print(f" OK {lang:11s} {sha[:12]} {nodes:5d} nodes {src}") - - print("\n== recording real git repos (provenance, beyond kubernetes) ==") - for r in REPOS: - if not os.path.isdir(os.path.join(r["path"], ".git")): - print(f" SKIP repo {r['id']}: not a git repo at {r['path']}") - continue - head = subprocess.run(["git", "-C", r["path"], "rev-parse", "HEAD"], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, - timeout=30).stdout.decode().strip() - n = git_commit_count(r["path"]) - rec = {"id": r["id"], "path": r["path"], "head_commit": head, - "commit_count": n, "note": r["note"]} - manifest["repos"].append(rec) - print(f" OK repo {r['id']:12s} commits={n:7d} head={head[:12]} {r['note']}") - - with open(MANIFEST, "w") as f: - json.dump(manifest, f, indent=2) - print(f"\nwrote {MANIFEST}: {len(manifest['files'])} files, " - f"{len(manifest['repos'])} repos") - # at least one Go-stable language AND multiple languages required - if len(manifest["files"]) < 5: - print("ERROR: fewer than 5 languages mined", file=sys.stderr) - sys.exit(1) - if len(manifest["repos"]) < 3: - print("ERROR: fewer than 3 repos recorded", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx b/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx deleted file mode 100644 index 17352649..00000000 --- a/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { transKeys } from '@/i18n/keys'; -import { - AlertDialog, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@onlook/ui/alert-dialog'; -import { Button } from '@onlook/ui/button'; -import { useTranslations } from 'next-intl'; -import { useAuthContext } from '../auth/auth-context'; -import { LoginButton } from './login-button'; -import { SignInMethod } from '@onlook/models/auth'; -import { Icons } from '@onlook/ui/icons'; - -export function AuthModal() { - const { setIsAuthModalOpen, isAuthModalOpen } = useAuthContext(); - const t = useTranslations(); - - return ( - <AlertDialog open={isAuthModalOpen} onOpenChange={setIsAuthModalOpen}> - <AlertDialogContent className="!max-w-sm bg-black"> - <AlertDialogHeader> - <AlertDialogTitle className="text-center text-xl font-normal"> - {t(transKeys.welcome.login.loginToEdit)} - </AlertDialogTitle> - <AlertDialogDescription className="text-center text-balance"> - {t(transKeys.welcome.login.shareProjects)} - </AlertDialogDescription> - </AlertDialogHeader> - <div className="space-y-2 flex flex-col"> - <LoginButton - className="!bg-black" - method={SignInMethod.GITHUB} - icon={<Icons.GitHubLogo className="w-4 h-4 mr-2" />} - translationKey="github" - providerName="GitHub" - /> - <LoginButton - className="!bg-black" - method={SignInMethod.GOOGLE} - icon={<Icons.GoogleLogo viewBox="0 0 24 24" className="w-4 h-4 mr-2" />} - translationKey="google" - providerName="Google" - /> - </div> - <AlertDialogFooter className="flex !justify-center w-full"> - <Button variant={'ghost'} onClick={() => setIsAuthModalOpen(false)}> - {t(transKeys.projects.actions.close)} - </Button> - </AlertDialogFooter> - </AlertDialogContent> - </AlertDialog> - ); -} diff --git a/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go b/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go deleted file mode 100644 index 2213fa89..00000000 --- a/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go +++ /dev/null @@ -1,219 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - "runtime/debug" - "sort" - "strings" - - "github.com/gogo/protobuf/proto" - "github.com/spf13/cobra" - progress "gopkg.in/cheggaaa/pb.v1" - "gopkg.in/src-d/hercules.v10" - "gopkg.in/src-d/hercules.v10/internal/pb" -) - -// combineCmd represents the combine command -var combineCmd = &cobra.Command{ - Use: "combine", - Short: "Merge several binary analysis results together.", - Long: ``, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, files []string) { - if len(files) == 1 { - file, err := os.Open(files[0]) - if err != nil { - panic(err) - } - defer file.Close() - _, err = io.Copy(os.Stdout, bufio.NewReader(file)) - if err != nil { - panic(err) - } - return - } - only, err := cmd.Flags().GetString("only") - if err != nil { - panic(err) - } - var repos []string - allErrors := map[string][]string{} - mergedResults := map[string]interface{}{} - mergedMetadata := &hercules.CommonAnalysisResult{} - var fileName string - bar := progress.New(len(files)) - bar.Callback = func(msg string) { - os.Stderr.WriteString("\033[2K\r" + msg + " " + fileName) - } - bar.NotPrint = true - bar.ShowPercent = false - bar.ShowSpeed = false - bar.SetMaxWidth(80).Start() - debug.SetGCPercent(20) - for _, fileName = range files { - bar.Increment() - anotherResults, anotherMetadata, errs := loadMessage(fileName, &repos) - if anotherMetadata != nil { - mergeErrs := mergeResults(mergedResults, mergedMetadata, anotherResults, anotherMetadata, only) - for _, err := range mergeErrs { - errs = append(errs, err.Error()) - } - } - allErrors[fileName] = errs - debug.FreeOSMemory() - } - bar.Finish() - os.Stderr.WriteString("\033[2K\r") - printErrors(allErrors) - sort.Strings(repos) - if mergedMetadata == nil { - return - } - mergedMessage := pb.AnalysisResults{ - Header: &pb.Metadata{ - Version: int32(hercules.BinaryVersion), - Hash: hercules.BinaryGitHash, - Repository: strings.Join(repos, " & "), - }, - Contents: map[string][]byte{}, - } - mergedMetadata.FillMetadata(mergedMessage.Header) - for key, val := range mergedResults { - buffer := bytes.Buffer{} - err := hercules.Registry.Summon(key)[0].(hercules.LeafPipelineItem).Serialize( - val, true, &buffer) - if err != nil { - panic(err) - } - mergedMessage.Contents[key] = buffer.Bytes() - } - serialized, err := proto.Marshal(&mergedMessage) - if err != nil { - panic(err) - } - os.Stdout.Write(serialized) - }, -} - -func loadMessage(fileName string, repos *[]string) ( - map[string]interface{}, *hercules.CommonAnalysisResult, []string) { - var errs []string - fi, err := os.Stat(fileName) - if err != nil { - errs = append(errs, "Cannot access "+fileName+": "+err.Error()) - return nil, nil, errs - } - if fi.Size() == 0 { - errs = append(errs, "Cannot parse "+fileName+": file size is 0") - return nil, nil, errs - } - buffer, err := ioutil.ReadFile(fileName) - if err != nil { - errs = append(errs, "Cannot read "+fileName+": "+err.Error()) - return nil, nil, errs - } - message := pb.AnalysisResults{} - err = proto.Unmarshal(buffer, &message) - if err != nil { - errs = append(errs, "Cannot parse "+fileName+": "+err.Error()) - return nil, nil, errs - } - if message.Header == nil { - errs = append(errs, "Cannot parse "+fileName+": corrupted header") - return nil, nil, errs - } - *repos = append(*repos, message.Header.Repository) - results := map[string]interface{}{} - for key, val := range message.Contents { - summoned := hercules.Registry.Summon(key) - if len(summoned) == 0 { - errs = append(errs, fileName+": item not found: "+key) - continue - } - mpi, ok := summoned[0].(hercules.ResultMergeablePipelineItem) - if !ok { - errs = append(errs, fileName+": "+key+": ResultMergeablePipelineItem is not implemented") - continue - } - msg, err := mpi.Deserialize(val) - if err != nil { - errs = append(errs, fileName+": deserialization failed: "+key+": "+err.Error()) - continue - } - results[key] = msg - } - return results, hercules.MetadataToCommonAnalysisResult(message.Header), errs -} - -func printErrors(allErrors map[string][]string) { - needToPrintErrors := false - for _, errs := range allErrors { - if len(errs) > 0 { - needToPrintErrors = true - break - } - } - if !needToPrintErrors { - return - } - fmt.Fprintln(os.Stderr, "Errors:") - for key, errs := range allErrors { - if len(errs) > 0 { - fmt.Fprintln(os.Stderr, " "+key) - for _, err := range errs { - fmt.Fprintln(os.Stderr, " "+err) - } - } - } -} - -func mergeResults(mergedResults map[string]interface{}, - mergedCommons *hercules.CommonAnalysisResult, - anotherResults map[string]interface{}, - anotherCommons *hercules.CommonAnalysisResult, - only string) []error { - var errors []error - for key, val := range anotherResults { - if only != "" && key != only { - continue - } - mergedResult, exists := mergedResults[key] - if !exists { - mergedResults[key] = val - continue - } - item := hercules.Registry.Summon(key)[0].(hercules.ResultMergeablePipelineItem) - mergedResult = item.MergeResults(mergedResult, val, mergedCommons, anotherCommons) - if err, isErr := mergedResult.(error); isErr { - errors = append(errors, fmt.Errorf("could not merge %s: %v", item.Name(), err)) - } else { - mergedResults[key] = mergedResult - } - } - if mergedCommons.CommitsNumber == 0 { - *mergedCommons = *anotherCommons - } else { - mergedCommons.Merge(anotherCommons) - } - return errors -} - -func getOptionsString() string { - var leaves []string - for _, leaf := range hercules.Registry.GetLeaves() { - leaves = append(leaves, leaf.Name()) - } - return strings.Join(leaves, ", ") -} - -func init() { - rootCmd.AddCommand(combineCmd) - combineCmd.SetUsageFunc(combineCmd.UsageFunc()) - combineCmd.Flags().String("only", "", "Consider only the specified analysis. "+ - "Empty means all available. Choices: "+getOptionsString()+".") -} diff --git a/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts b/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts deleted file mode 100644 index 7a11f37d..00000000 --- a/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts +++ /dev/null @@ -1,371 +0,0 @@ -// This file is auto-generated by next-intl, do not edit directly. -// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments - -declare const messages: { - "projects": { - "create": { - "settings": { - "title": "Settings", - "tooltip": "Configure new project settings" - }, - "success": "Project created successfully.", - "steps": { - "count": "{current} of {total}", - "error": "Project data is missing." - }, - "methods": { - "new": "Create New Project", - "load": "Load Existing Project" - }, - "loading": { - "title": "Setting up your new Onlook app...", - "description": "This may take a few seconds", - "cancel": "Cancel" - }, - "error": { - "title": "Error creating your Onlook app", - "backToPrompt": "Back to Prompting" - } - }, - "select": { - "empty": "No projects found", - "sort": { - "recent": "Recently Updated", - "name": "Project Name" - }, - "lastEdited": "Last edited {time} ago" - }, - "actions": { - "import": "Import Project", - "close": "Close", - "about": "About Onlook", - "signOut": "Sign out", - "editApp": "Edit App", - "projectSettings": "Project settings", - "showInExplorer": "Show in Explorer", - "renameProject": "Rename Project", - "deleteProject": "Delete Project", - "cloneProject": "Clone Project", - "cancel": "Cancel", - "delete": "Delete", - "rename": "Rename", - "clone": "Clone", - "goToAllProjects": "Go to all Projects", - "newProject": "New Project", - "blankProject": "Blank Project", - "startFromScratch": "Start from scratch", - "importProject": "Import a project", - "subscriptions": "Subscriptions", - "settings": "Settings", - "downloadCode": "Download Code", - "downloadingCode": "Preparing download...", - "downloadSuccess": "Download started successfully", - "downloadError": "Failed to prepare download", - "recentProjects": "Recent Projects" - }, - "dialogs": { - "delete": { - "title": "Are you sure you want to delete this project?", - "description": "This action cannot be undone. This will permanently delete your project and remove all associated data.", - "moveToTrash": "Also move folder to trash" - }, - "rename": { - "title": "Rename Project", - "label": "Project Name", - "error": "Project name can't be empty" - }, - "clone": { - "title": "Clone Project", - "label": "Project Name", - "placeholder": "Enter name for cloned project", - "error": "Project name can't be empty" - } - }, - "prompt": { - "title": "What kind of website do you want to make?", - "description": "Tell us a bit about your project. Be as detailed as possible.", - "input": { - "placeholder": "Paste a reference screenshot, write a novel, get creative...", - "imageUpload": "Upload Image Reference", - "fileReference": "File Reference", - "submit": "Start building your site" - }, - "blankStart": "Start from a blank page" - } - }, - "welcome": { - "title": "Welcome to Onlook", - "titleReturn": "Welcome back to Onlook", - "description": "A next-generation visual code editor that lets designers and product managers craft web experiences with AI.", - "alpha": "Alpha", - "login": { - "github": "Login with GitHub", - "google": "Login with Google", - "lastUsed": "You used this last time", - "loginToEdit": "Login to Edit", - "shareProjects": "No credit card required • Get a site in seconds" - }, - "terms": { - "agreement": "By signing up, you agree to our", - "privacy": "Privacy Policy", - "and": "and", - "tos": "Terms of Service" - }, - "version": "Version {version}" - }, - "pricing": { - "plans": { - "basic": { - "name": "Basic", - "price": "$0/month", - "description": "Prototype and experiment in code with ease.", - "features": [ - "Visual code editor access", - "Unlimited projects", - "{dailyMessages} AI chat messages a day", - "{monthlyMessages} AI messages a month", - "Limited to 1 screenshot per chat" - ] - }, - "pro": { - "name": "Pro", - "price": "$20/month", - "description": "Creativity – unconstrained. Build stunning sites with AI.", - "features": [ - "Visual code editor access", - "Unlimited projects", - "Unlimited AI chat messages a day", - "Unlimited monthly chats", - "Remove built with Onlook watermark", - "1 free custom domain hosted with Onlook", - "Priority support" - ] - }, - "launch": { - "name": "Launch", - "price": "$50/month", - "description": "Perfect for startups and growing teams", - "features": [ - "Unlimited daily messages", - "Priority support", - "Advanced integrations", - "Team collaboration features" - ] - }, - "scale": { - "name": "Scale", - "price": "$100/month", - "description": "Enterprise-grade features for large teams", - "features": [ - "Everything in Launch plan", - "Dedicated account manager", - "Custom integrations", - "Advanced analytics", - "24/7 premium support" - ] - } - }, - "titles": { - "choosePlan": "Choose your plan", - "proMember": "Thanks for being a Pro member!" - }, - "buttons": { - "currentPlan": "Current Plan", - "getPro": "Get Pro", - "manageSubscription": "Manage Subscription" - }, - "loading": { - "checkingPayment": "Checking for payment..." - }, - "toasts": { - "checkingOut": { - "title": "Checking out", - "description": "You will now be redirected to Stripe to complete the payment." - }, - "redirectingToStripe": { - "title": "Redirecting to Stripe", - "description": "You will now be redirected to Stripe to manage your subscription." - }, - "error": { - "title": "Error", - "description": "Could not initiate checkout process. Please try again." - } - }, - "footer": { - "unusedMessages": "Unused chat messages will roll over to the next month." - } - }, - "editor": { - "modes": { - "design": { - "name": "Design", - "description": "Edit and modify your website's design", - "tooltip": "Switch to design mode" - }, - "code": { - "name": "Code", - "description": "Edit and modify your website's code", - "tooltip": "Switch to code mode" - }, - "preview": { - "name": "Preview", - "description": "Preview and test your website's functionality", - "tooltip": "Switch to Preview mode" - } - }, - "toolbar": { - "tools": { - "select": { - "name": "Select", - "tooltip": "Select and modify elements" - }, - "pan": { - "name": "Pan", - "tooltip": "Pan and move around the canvas" - }, - "insertDiv": { - "name": "Insert Container", - "tooltip": "Add a new container element" - }, - "insertText": { - "name": "Insert Text", - "tooltip": "Add a new text element" - } - }, - "versionHistory": "Version History" - }, - "panels": { - "edit": { - "tabs": { - "chat": { - "name": "Chat", - "emptyState": "Select an element to chat with AI", - "emptyStateStart": "Start the project to chat", - "input": { - "placeholder": "Type your message...", - "tooltip": "Chat with AI about the selected element" - }, - "mode": { - "tooltip": "Switch between Build and Ask modes" - }, - "controls": { - "newChat": "New Chat", - "history": "Chat History" - }, - "settings": { - "showSuggestions": "Show suggestions", - "showMiniChat": "Show mini chat", - "autoApplyCode": "Auto-apply results", - "expandCodeBlocks": "Show code while rendering" - }, - "miniChat": { - "button": "Chat with AI" - }, - "openInCode": { - "button": "Open in Code" - } - }, - "styles": { - "name": "Styles", - "emptyState": "Select an element to edit its style properties", - "groups": { - "position": "Position & Dimensions", - "layout": "Flexbox & Layout", - "style": "Styles", - "text": "Text" - }, - "tailwind": { - "title": "Tailwind Classes", - "placeholder": "Add tailwind classes here", - "componentClasses": { - "title": "Main Component Classes", - "tooltip": "Changes apply to component code. This is the default." - }, - "instanceClasses": { - "title": "Instance Classes", - "tooltip": "Changes apply to instance code." - } - } - } - } - }, - "layers": { - "name": "Layers", - "tabs": { - "layers": "Layers", - "pages": "Pages", - "components": "Elements", - "images": "Images", - "windows": { - "name": "Windows", - "emptyState": "Select a window to edit its settings" - }, - "brand": "Brand", - "branches": "Branches", - "apps": "Apps" - } - } - }, - "settings": { - "preferences": { - "language": "Language", - "theme": "Theme", - "deleteWarning": "Delete Warning", - "analytics": "Analytics", - "editor": { - "ide": "Editor", - "shouldWarnDelete": "Warn when deleting elements", - "enableAnalytics": "Enable analytics" - }, - "shortcuts": "Shortcuts" - } - }, - "frame": { - "startDesigning": { - "prefix": "Press ", - "action": "Play", - "suffix": " to start designing your App" - }, - "playButton": "Play", - "waitingForApp": "Waiting for the App to start..." - }, - "runButton": { - "portInUse": "Port in Use", - "loading": "Loading", - "play": "Play", - "retry": "Retry", - "stop": "Stop" - }, - "zoom": { - "level": "Zoom Level", - "in": "Zoom In", - "out": "Zoom Out", - "fit": "Zoom Fit", - "reset": "Zoom 100%", - "double": "Zoom 200%" - } - }, - "help": { - "menu": { - "reloadOnlook": "Reload Onlook", - "theme": { - "title": "Theme", - "light": "Light", - "dark": "Dark", - "system": "System" - }, - "language": "Language", - "openSettings": "Open Settings", - "contactUs": { - "title": "Contact Us", - "website": "Website", - "discord": "Discord", - "github": "GitHub", - "email": "Email" - }, - "reportIssue": "Report Issue", - "shortcuts": "Shortcuts" - } - } -}; -export default messages; \ No newline at end of file diff --git a/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh b/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh deleted file mode 100644 index bb47c521..00000000 --- a/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -set -e - -export DBUILD_CLIENT="${DBUILD_CLIENT:-0}" -export DBUILD_SERVER="${DBUILD_SERVER:-1}" -export DUSE_HTTP="${DUSE_HTTP:-1}" -export DUSE_CODEC_OPUS="${DUSE_CODEC_OPUS:-1}" -export DUSE_VOIP="${DUSE_VOIP:-1}" -export DCMAKE_INSTALL_PREFIX="${DCMAKE_INSTALL_PREFIX:-~/ioquake3}" -IOQ3REMOTE="${IOQ3REMOTE:-https://github.com/ioquake/ioq3.git}" -CMAKE_OPTS="${CMAKE_OPTS:-}" - -if ! [ -x "$(command -v git)" ] || ! [ -x "$(command -v cmake)" ]; then - echo "This build script requires 'git' and 'cmake' to be installed." >&2 - echo "Please install them through your normal package installation system." >&2 - exit 1 -fi - -echo " This build process requires all of the ioquake3 dependencies necessary for an ioquake3 server. - If you do not have the necessary dependencies the build will fail. - - Please post a message to https://discourse.ioquake.org/ asking for help and include whatever error messages you received during the compile phase. - - We will be building from the git repo at ${IOQ3REMOTE} - The resulting binary will be installed to ${DCMAKE_INSTALL_PREFIX} - - If you need to change these, please set variables as follows: - - IOQ3REMOTE=https://github.com/something/something.git DCMAKE_INSTALL_PREFIX=~/somewhere $0" - -BUILD_DIR="$(mktemp -d)" -trap "rm -rf $BUILD_DIR" EXIT - -while true; do - read -p "Are you ready to compile ioquake3 from ${IOQ3REMOTE}, and have it installed into $DCMAKE_INSTALL_PREFIX? " yn - case $yn in - [Yy]*) - git clone $IOQ3REMOTE $BUILD_DIR/ioq3 - cd $BUILD_DIR/ioq3 - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_CLIENT="$DBUILD_CLIENT" \ - -DBUILD_SERVER="DBUILD_SERVER" \ - -DUSE_HTTP="$DUSE_HTTP" \ - -DUSE_CODEC_OPUS="$USE_CODEC_OPUS" \ - -DUSE_VOIP="$USE_VOIP" \ - -DCMAKE_INSTALL_PREFIX="$DCMAKE_INSTALL_PREFIX" \ - $CMAKE_OPTS - cmake --build build - cmake --install build - exit - ;; - [Nn]*) - echo "aborting installation." - exit - ;; - *) - echo "Please answer yes or no." - ;; - esac -done diff --git a/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c b/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c deleted file mode 100644 index b49a5faf..00000000 --- a/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c +++ /dev/null @@ -1,101 +0,0 @@ -/* -=========================================================================== -Copyright (C) 2011 Thilo Schulz <thilo@tjps.eu> - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - - You should have received a copy of the GNU General Public License -along with Quake III Arena source code; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ - -#include "qasm-inline.h" - -#if defined (__i386__) || defined(__x86_64__) -static const unsigned short fpucw = 0x0C7F; - -/* - * GNU inline asm ftol conversion functions using SSE or FPU - */ - -long qftolsse(float f) -{ - long retval; - - __asm__ volatile - ( - "cvttss2si %1, %0\n" - : "=r" (retval) - : "x" (f) - ); - - return retval; -} - -int qvmftolsse(void) -{ - int retval; - - __asm__ volatile - ( - "movss (" EDI ", " EBX ", 4), %%xmm0\n" - "cvttss2si %%xmm0, %0\n" - : "=r" (retval) - : - : "%xmm0" - ); - - return retval; -} - -long qftolx87(float f) -{ - long retval; - unsigned short oldcw = 0; - - __asm__ volatile - ( - "fnstcw %2\n" - "fldcw %3\n" - "flds %1\n" - "fistpl %1\n" - "fldcw %2\n" - "mov %1, %0\n" - : "=r" (retval) - : "m" (f), "m" (oldcw), "m" (fpucw) - ); - - return retval; -} - -int qvmftolx87(void) -{ - int retval; - unsigned short oldcw = 0; - - __asm__ volatile - ( - "fnstcw %1\n" - "fldcw %2\n" - "flds (" EDI ", " EBX ", 4)\n" - "fistpl (" EDI ", " EBX ", 4)\n" - "fldcw %1\n" - "mov (" EDI ", " EBX ", 4), %0\n" - : "=r" (retval) - : "m" (oldcw), "m" (fpucw) - ); - - return retval; -} -#endif diff --git a/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp b/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp deleted file mode 100644 index 3eb1fa9a..00000000 --- a/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp +++ /dev/null @@ -1,469 +0,0 @@ -#include "chat-auto-parser-helpers.h" -#include "chat-auto-parser.h" -#include "chat-peg-parser.h" -#include "chat.h" -#include "common.h" -#include "json-schema-to-grammar.h" -#include "log.h" -#include "nlohmann/json.hpp" -#include "peg-parser.h" - -#include <stdexcept> -#include <string> - -using json = nlohmann::ordered_json; - -// Helper to iterate over tools/functions -static void foreach_function(const json & tools, const std::function<void(const json &)> & fn) { - for (const auto & tool : tools) { - if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) { - continue; - } - fn(tool); - } -} - -namespace autoparser { - -parser_build_context::parser_build_context(common_chat_peg_builder & p, const generation_params & inputs) : - p(p), - inputs(inputs), - reasoning_parser(p.eps()) {} - -common_chat_params peg_generator::generate_parser(const common_chat_template & tmpl, - const struct generation_params & inputs) { - // Run differential analysis to extract template structure - struct autoparser autoparser; - autoparser.analyze_template(tmpl); - return generate_parser(tmpl, inputs, autoparser); -} - -common_chat_params peg_generator::generate_parser(const common_chat_template & tmpl, - const struct generation_params & inputs, - const autoparser & autoparser) { - // Create the result structure - common_chat_params data; - data.prompt = common_chat_template_direct_apply(tmpl, inputs); - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - data.preserved_tokens = autoparser.preserved_tokens; - - auto parser = autoparser.build_parser(inputs); - data.parser = parser.save(); - - // Build grammar if tools are present - bool has_tools = - autoparser.tools.format.mode != tool_format::NONE && inputs.tools.is_array() && !inputs.tools.empty(); - std::string trigger_marker = !autoparser.tools.format.section_start.empty() ? autoparser.tools.format.section_start : - autoparser.tools.format.per_call_start; - - bool has_response_format = !inputs.json_schema.empty() && inputs.json_schema.is_object(); - bool include_grammar = has_response_format || (has_tools && - ((inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO && !trigger_marker.empty()) || - inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); - - if (include_grammar) { - data.grammar_lazy = !has_response_format && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; - data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.contains("parameters") ? function.at("parameters") : json::object(); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } - parser.build_grammar(builder, data.grammar_lazy); - }); - - // Set grammar triggers based on tool section markers (fall back to per-call markers) - if (data.grammar_lazy) { - data.grammar_triggers = { - { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, trigger_marker } - }; - } - } - - return data; -} - -common_peg_arena autoparser::build_parser(const generation_params & inputs) const { - if (!analysis_complete) { - throw std::invalid_argument("Cannot call build_parser on autoparser without performing analysis first, call analyze_template(...)"); - } - return build_chat_peg_parser([&](common_chat_peg_builder & p) { - parser_build_context ctx(p, inputs); - bool extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; - - ctx.extracting_reasoning = extract_reasoning && reasoning.mode != reasoning_mode::NONE; - ctx.content = &content; - ctx.reasoning = &reasoning; - - // Build reasoning parser - ctx.reasoning_parser = reasoning.build_parser(ctx); - - auto parser = p.eps(); - - bool has_tools = inputs.tools.is_array() && !inputs.tools.empty(); - bool has_response_format = inputs.json_schema.is_object() && !inputs.json_schema.empty(); - bool pure_content = reasoning.mode == reasoning_mode::NONE; - - if (has_response_format) { - auto response_format = p.rule("response-format", p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema))); - parser = ctx.reasoning_parser + p.space() + p.choice({ - p.literal("```json") + p.space() + response_format + p.space() + p.literal("```"), - response_format - }) + p.end(); - pure_content = false; - } else if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && jinja_caps.supports_tool_calls) { - parser = tools.build_parser(ctx); - pure_content = false; - } else { - parser = content.build_parser(ctx); - } - return pure_content ? p.prefix(inputs.generation_prompt, reasoning.start) + parser : p.prefix(inputs.generation_prompt, reasoning.start) << parser; - }); -} - -common_peg_parser analyze_reasoning::build_parser(parser_build_context & ctx) const { - auto & p = ctx.p; - - if (!ctx.extracting_reasoning) { - return p.eps(); - } - - if (mode == reasoning_mode::TAG_BASED || mode == reasoning_mode::TOOLS_ONLY) { - if (!end.empty()) { - if (!start.empty()) { - // Standard tag-based: optional(<think>reasoning</think>) - return p.optional(start + p.reasoning(p.until(end)) + end + p.space()); - } - // Delimiter-style (empty start) - return p.optional(p.reasoning(p.until(end)) + end + p.space()); - } - } - - return p.eps(); -} - -common_peg_parser analyze_content::build_parser(parser_build_context & ctx) const { - auto & p = ctx.p; - - if (is_always_wrapped()) { - if (ctx.extracting_reasoning) { - return ctx.reasoning_parser + start + p.content(p.until(end)) + end + p.end(); - } - return p.content(p.until(start)) + start + p.content(p.until(end)) + end + p.end(); - } - return ctx.reasoning_parser + p.content(p.rest()) + p.end(); -} - -common_peg_parser analyze_content::build_optional_wrapped(parser_build_context & ctx) const { - auto & p = ctx.p; - - if (is_always_wrapped()) { - return p.optional(start + p.content(p.until(end)) + end); - } - return p.eps(); -} - -common_peg_parser analyze_tools::build_parser(parser_build_context & ctx) const { - switch (format.mode) { - case tool_format::JSON_NATIVE: - return build_tool_parser_json_native(ctx); - case tool_format::TAG_WITH_JSON: - return build_tool_parser_tag_json(ctx); - case tool_format::TAG_WITH_TAGGED: - return build_tool_parser_tag_tagged(ctx); - default: - LOG_ERR("[ERROR] Template seems to support tool calls, but failed to determine tool format. Tool calling will not work properly. " - "Check for a fixed template for your model in the models/templates directory of your llama.cpp installation or " - "report an issue at https://github.com/ggml-org/llama.cpp/issues\n"); - return ctx.p.eps(); - } -} - -common_peg_parser analyze_tools::build_tool_parser_json_native(parser_build_context & ctx) const { - auto & p = ctx.p; - const auto & inputs = ctx.inputs; - bool force_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; - - // Build effective field names with dot notation if function_field is set - std::string name_field = format.name_field; - std::string args_field = format.args_field; - - if (!format.function_field.empty() && format.function_field != "function" && - name_field.find('.') == std::string::npos) { - name_field = format.function_field + "." + name_field; - args_field = format.function_field + "." + args_field; - } - - auto tools_parser = p.standard_json_tools( - format.section_start, format.section_end, inputs.tools, inputs.parallel_tool_calls, - inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED, name_field, args_field, format.tools_array_wrapped, - format.fun_name_is_key, format.id_field, format.gen_id_field, format.parameter_order); - - // Handle content wrappers if present - if (ctx.content && ctx.content->is_always_wrapped()) { - auto wrapped_content = ctx.content->build_optional_wrapped(ctx); - return ctx.reasoning_parser + wrapped_content + tools_parser + p.end(); - } - - std::string tool_start = "{"; - if (!format.section_start.empty()) { - tool_start = format.section_start; - } else if (!format.per_call_start.empty()) { - tool_start = format.per_call_start; - } - - return ctx.reasoning_parser + (force_tools ? p.eps() : p.optional(p.content(p.until(tool_start)))) + tools_parser + - p.end(); -} - -common_peg_parser analyze_tools::build_func_parser(common_chat_peg_builder & p, const std::string & name, - const common_peg_parser & call_id_section, bool have_call_id, - const common_peg_parser & args, - std::optional<common_peg_parser> atomic_peek) const { - auto open = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix); - bool matched_atomic = false; - common_peg_parser func_parser = p.eps(); - - if (!function.name_suffix.empty()) { - func_parser = open + call_id_section + p.space() + args; - matched_atomic = true; - } else if (have_call_id) { - func_parser = p.atomic(open + call_id_section) + p.space() + args; - matched_atomic = true; - } else if (atomic_peek.has_value()) { - func_parser = p.atomic(open + call_id_section + p.space() + *atomic_peek) + args; - matched_atomic = true; - } else { - func_parser = open + call_id_section + p.space() + args; - } - - if (!function.close.empty()) { - func_parser = func_parser + p.space() + p.tool_close(p.literal(function.close)); - } else if (!format.per_call_end.empty()) { - // When there's no func_close but there is a per_call_end marker, use peek() to ensure - // we only emit tool_close when we can actually see the closing marker. This prevents - // premature closing during partial parsing when we've seen e.g. "</" which could be - // either "</tool_call>" (end) or "<arg_key>" prefix that failed to match. - func_parser = func_parser + p.tool_close(p.peek(p.literal(format.per_call_end))); - } else { - func_parser = func_parser + p.tool_close(p.space()); // force this to process tool closing callbacks in mapper - } - if (!matched_atomic) { - func_parser = p.atomic(func_parser); - } - return func_parser; -} - -common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context & ctx) const { - auto & p = ctx.p; - const auto & inputs = ctx.inputs; - bool force_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; - - common_peg_parser tool_choice = p.choice(); - - foreach_function(inputs.tools, [&](const json & tool) { - const auto & func = tool.at("function"); - std::string name = func.at("name"); - const auto & schema = func.contains("parameters") ? func.at("parameters") : json::object(); - - // Build call_id parser based on position (if supported) - bool have_call_id = false; - common_peg_parser call_id_section = p.eps(); - if (call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS && !call_id.prefix.empty() && - (!call_id.suffix.empty() || !arguments.start.empty())) { - if (!call_id.suffix.empty()) { - call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix))) + call_id.suffix; - } else { - call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(arguments.start))); - } - have_call_id = true; - } - auto args_parser = p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)); - if (!arguments.start.empty()) { - args_parser = p.literal(arguments.start) + args_parser; - } - if (!arguments.end.empty()) { - args_parser = args_parser + p.literal(arguments.end); - } - - auto atomic_peek = !arguments.start.empty() ? std::optional(p.peek(p.literal(arguments.start))) : std::nullopt; - auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_parser, atomic_peek); - tool_choice |= p.rule("tool-" + name, func_parser); - }); - - auto require_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; - - common_peg_parser tool_calls = p.eps(); - - if (!format.per_call_start.empty()) { - auto wrapped_call = format.per_call_start + tool_choice + format.per_call_end; - if (inputs.parallel_tool_calls) { - tool_calls = p.trigger_rule("tool-call", wrapped_call + p.zero_or_more(p.space() + wrapped_call)); - } else { - tool_calls = p.trigger_rule("tool-call", wrapped_call); - } - if (!format.section_start.empty()) { - tool_calls = p.trigger_rule("tool-calls", - p.literal(format.section_start) + p.space() + tool_calls + p.space() + - (format.section_end.empty() ? p.end() : p.literal(format.section_end))); - } - } else { - std::string separator = ", "; // Default - if (inputs.parallel_tool_calls) { - tool_calls = p.trigger_rule("tool-call", format.section_start + tool_choice + - p.zero_or_more(separator + tool_choice) + format.section_end); - } else { - tool_calls = p.trigger_rule("tool-call", format.section_start + tool_choice + format.section_end); - } - } - - if (!require_calls) { - tool_calls = p.optional(tool_calls); - } - - std::string trigger_marker = !format.section_start.empty() ? format.section_start : format.per_call_start; - auto content_before_tools = trigger_marker.empty() ? p.eps() : p.until(trigger_marker); - return ctx.reasoning_parser + (force_tools ? p.eps() : p.optional(p.content(content_before_tools))) + tool_calls + - p.end(); -} - -common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_context & ctx) const { - auto & p = ctx.p; - const auto & inputs = ctx.inputs; - bool force_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; - - auto until_suffix = p.rule("until-suffix", p.until(arguments.value_suffix)); - - common_peg_parser tool_choice = p.choice(); - - foreach_function(inputs.tools, [&](const json & tool) { - const auto & func = tool.at("function"); - std::string name = func.at("name"); - auto params = func.contains("parameters") ? func.at("parameters") : json::object(); - const auto & properties = params.contains("properties") ? params.at("properties") : json::object(); - - std::set<std::string> required; - if (params.contains("required")) { - params.at("required").get_to(required); - } - - auto schema_info = common_schema_info(); - schema_info.resolve_refs(params); - - // Build parser for each argument, separating required and optional - std::vector<common_peg_parser> required_parsers; - std::vector<common_peg_parser> optional_parsers; - for (const auto & [param_name, param_schema] : properties.items()) { - bool is_required = required.find(param_name) != required.end(); - - auto arg = - p.tool_arg(p.tool_arg_open(arguments.name_prefix + p.tool_arg_name(p.literal(param_name)) + - arguments.name_suffix) + - arguments.value_prefix + - (schema_info.resolves_to_string(param_schema) ? - p.tool_arg_string_value(p.schema(until_suffix, - "tool-" + name + "-arg-" + param_name + "-schema", - param_schema, true)) : - p.tool_arg_json_value(p.schema( - p.json(), "tool-" + name + "-arg-" + param_name + "-schema", param_schema, false)) + - p.space()) + - p.tool_arg_close(p.literal(arguments.value_suffix))); - - auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg); - if (is_required) { - required_parsers.push_back(named_arg); - } else { - optional_parsers.push_back(named_arg); - } - } - - // Build required arg sequence in definition order - common_peg_parser args_seq = p.eps(); - for (size_t i = 0; i < required_parsers.size(); i++) { - if (i > 0) { - args_seq = args_seq + p.space(); - } - args_seq = args_seq + required_parsers[i]; - } - - // Build optional args with flexible ordering - if (!optional_parsers.empty()) { - common_peg_parser any_opt = p.choice(); - for (const auto & opt : optional_parsers) { - any_opt |= opt; - } - args_seq = args_seq + p.repeat(p.space() + any_opt, 0, -1); - } - - if (!arguments.start.empty()) { - args_seq = p.literal(arguments.start) + args_seq; - } - if (!arguments.end.empty()) { - args_seq = args_seq + p.literal(arguments.end); - } - - // Build call_id parser based on position (if supported) - common_peg_parser call_id_section = p.eps(); - bool have_call_id = false; - if (call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS && !call_id.prefix.empty() && - (!call_id.suffix.empty() || !arguments.start.empty())) { - have_call_id = true; - if (!call_id.suffix.empty()) { - call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix)) + call_id.suffix); - } else { - call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(arguments.start))); - } - } - - // Only peek for an arg tag when there are required args that must follow. - // When all args are optional, the model may emit no arg tags at all (#20650). - auto atomic_peek = (!arguments.name_prefix.empty() && !required_parsers.empty()) ? - std::optional(p.peek(p.literal(arguments.name_prefix))) : std::nullopt; - auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_seq, atomic_peek); - tool_choice |= p.rule("tool-" + name, func_parser); - }); - - auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; - - common_peg_parser tool_calls = p.eps(); - - if (!format.per_call_start.empty()) { - auto wrapped_call = format.per_call_start + p.space() + tool_choice + p.space() + format.per_call_end; - if (inputs.parallel_tool_calls) { - tool_calls = p.trigger_rule("tool-call", wrapped_call + p.zero_or_more(p.space() + wrapped_call)); - } else { - tool_calls = p.trigger_rule("tool-call", wrapped_call); - } - if (!format.section_start.empty()) { - tool_calls = p.trigger_rule("tool-calls", - p.literal(format.section_start) + p.space() + tool_calls + p.space() + - (format.section_end.empty() ? p.end() : p.literal(format.section_end))); - } - } else { - std::string separator = ", "; // Default - - if (inputs.parallel_tool_calls) { - tool_calls = p.trigger_rule("tool-call", format.section_start + p.space() + tool_choice + - p.zero_or_more(separator + tool_choice) + p.space() + - format.section_end); - } else { - tool_calls = p.trigger_rule( - "tool-call", format.section_start + p.space() + tool_choice + p.space() + format.section_end); - } - } - - if (!require_tools) { - tool_calls = p.optional(tool_calls); - } - - std::string trigger_marker = !format.section_start.empty() ? format.section_start : format.per_call_start; - auto content_before_tools = trigger_marker.empty() ? p.eps() : p.until(trigger_marker); - return ctx.reasoning_parser + (force_tools ? p.eps() : p.optional(p.content(content_before_tools))) + tool_calls + - p.end(); -} - -} // namespace autoparser diff --git a/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py b/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py deleted file mode 100644 index 09d574a5..00000000 --- a/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -A much shorter version of train.py for benchmarking -""" -import os -from contextlib import nullcontext -import numpy as np -import time -import torch -from model import GPTConfig, GPT - -# ----------------------------------------------------------------------------- -batch_size = 12 -block_size = 1024 -bias = False -real_data = True -seed = 1337 -device = 'cuda' # examples: 'cpu', 'cuda', 'cuda:0', 'cuda:1', etc. -dtype = 'bfloat16' if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else 'float16' # 'float32' or 'bfloat16' or 'float16' -compile = True # use PyTorch 2.0 to compile the model to be faster -profile = False # use pytorch profiler, or just simple benchmarking? -exec(open('configurator.py').read()) # overrides from command line or config file -# ----------------------------------------------------------------------------- - -torch.manual_seed(seed) -torch.cuda.manual_seed(seed) -torch.backends.cuda.matmul.allow_tf32 = True # allow tf32 on matmul -torch.backends.cudnn.allow_tf32 = True # allow tf32 on cudnn -device_type = 'cuda' if 'cuda' in device else 'cpu' # for later use in torch.autocast -ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[dtype] -ctx = nullcontext() if device_type == 'cpu' else torch.amp.autocast(device_type=device_type, dtype=ptdtype) - -# data loading init -if real_data: - dataset = 'openwebtext' - data_dir = os.path.join('data', dataset) - train_data = np.memmap(os.path.join(data_dir, 'train.bin'), dtype=np.uint16, mode='r') - def get_batch(split): - data = train_data # note ignore split in benchmarking script - ix = torch.randint(len(data) - block_size, (batch_size,)) - x = torch.stack([torch.from_numpy((data[i:i+block_size]).astype(np.int64)) for i in ix]) - y = torch.stack([torch.from_numpy((data[i+1:i+1+block_size]).astype(np.int64)) for i in ix]) - x, y = x.pin_memory().to(device, non_blocking=True), y.pin_memory().to(device, non_blocking=True) - return x, y -else: - # alternatively, if fixed data is desired to not care about data loading - x = torch.randint(50304, (batch_size, block_size), device=device) - y = torch.randint(50304, (batch_size, block_size), device=device) - get_batch = lambda split: (x, y) - -# model init -gptconf = GPTConfig( - block_size = block_size, # how far back does the model look? i.e. context size - n_layer = 12, n_head = 12, n_embd = 768, # size of the model - dropout = 0, # for determinism - bias = bias, -) -model = GPT(gptconf) -model.to(device) - -optimizer = model.configure_optimizers(weight_decay=1e-2, learning_rate=1e-4, betas=(0.9, 0.95), device_type=device_type) - -if compile: - print("Compiling model...") - model = torch.compile(model) # pytorch 2.0 - -if profile: - # useful docs on pytorch profiler: - # - tutorial https://pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html - # - api https://pytorch.org/docs/stable/profiler.html#torch.profiler.profile - wait, warmup, active = 5, 5, 5 - num_steps = wait + warmup + active - with torch.profiler.profile( - activities=[torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.CUDA], - schedule=torch.profiler.schedule(wait=wait, warmup=warmup, active=active, repeat=1), - on_trace_ready=torch.profiler.tensorboard_trace_handler('./bench_log'), - record_shapes=False, - profile_memory=False, - with_stack=False, # incurs an additional overhead, disable if not needed - with_flops=True, - with_modules=False, # only for torchscript models atm - ) as prof: - - X, Y = get_batch('train') - for k in range(num_steps): - with ctx: - logits, loss = model(X, Y) - X, Y = get_batch('train') - optimizer.zero_grad(set_to_none=True) - loss.backward() - optimizer.step() - lossf = loss.item() - print(f"{k}/{num_steps} loss: {lossf:.4f}") - - prof.step() # notify the profiler at end of each step - -else: - - # simple benchmarking - torch.cuda.synchronize() - for stage, num_steps in enumerate([10, 20]): # burnin, then benchmark - t0 = time.time() - X, Y = get_batch('train') - for k in range(num_steps): - with ctx: - logits, loss = model(X, Y) - X, Y = get_batch('train') - optimizer.zero_grad(set_to_none=True) - loss.backward() - optimizer.step() - lossf = loss.item() - print(f"{k}/{num_steps} loss: {lossf:.4f}") - torch.cuda.synchronize() - t1 = time.time() - dt = t1-t0 - mfu = model.estimate_mfu(batch_size * 1 * num_steps, dt) - if stage == 1: - print(f"time per iteration: {dt/num_steps*1000:.4f}ms, MFU: {mfu*100:.2f}%") diff --git a/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json b/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json deleted file mode 100644 index 6c9c236b..00000000 --- a/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "$schema": "https://mintlify.com/docs.json", - "theme": "maple", - "name": "Agent Client Protocol", - "description": "The Agent Client Protocol (ACP) is a protocol that standardizes communication between code editors.", - "colors": { - "primary": "#0084d1", - "light": "#00bcff", - "dark": "#0084d1" - }, - "background": { - "color": { - "light": "#f8f8f6", - "dark": "#12141A" - } - }, - "fonts": { - "heading": { - "family": "Lora", - "format": "woff2" - }, - "body": { - "family": "Public Sans", - "format": "woff2" - } - }, - "favicon": { - "light": "/logo/fav-light.png", - "dark": "/logo/fav-dark.png" - }, - "navbar": { - "links": [ - { - "label": "GitHub", - "href": "https://github.com/agentclientprotocol/agent-client-protocol" - }, - { - "label": "Zed Industries", - "href": "https://zed.dev" - } - ] - }, - "navigation": { - "tabs": [ - { - "tab": "Protocol", - "pages": [ - { - "group": "Overview", - "pages": [ - "overview/introduction", - "overview/architecture", - "overview/agents", - "overview/clients" - ] - }, - { - "group": "Protocol", - "pages": [ - "protocol/overview", - "protocol/initialization", - "protocol/session-setup", - "protocol/prompt-turn", - "protocol/content", - "protocol/tool-calls", - "protocol/file-system", - "protocol/terminals", - "protocol/agent-plan", - "protocol/session-modes", - "protocol/slash-commands", - "protocol/extensibility", - "protocol/transports", - "protocol/schema" - ] - }, - { - "group": "Libraries", - "pages": [ - "libraries/kotlin", - "libraries/rust", - "libraries/typescript", - "libraries/community" - ] - } - ] - }, - { - "tab": "RFDs", - "pages": [ - "rfds/about", - { - "group": "Draft", - "pages": ["rfds/session-list", "rfds/session-config-options"] - }, - { "group": "Preview", "pages": [] }, - { "group": "Accepted", "pages": ["rfds/introduce-rfd-process"] }, - { "group": "Completed", "pages": [] } - ] - }, - { - "tab": "Community", - "pages": [ - "community/governance", - "community/code-of-conduct", - "community/contributing" - ] - }, - { - "tab": "Updates", - "pages": ["updates"] - }, - { - "tab": "Brand", - "pages": ["brand"] - } - ] - }, - "logo": { - "light": "/logo/light.svg", - "dark": "/logo/dark.svg" - }, - "seo": { - "metatags": { - "og:image": "https://zed.dev/img/acp/og-dark.webp" - }, - "indexing": "navigable" - }, - "footer": { - "socials": { - "github": "https://github.com/agentclientprotocol/agent-client-protocol" - } - }, - "contextual": { - "options": ["copy", "view"] - } -} diff --git a/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h b/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h deleted file mode 100644 index b609aecc..00000000 --- a/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h +++ /dev/null @@ -1,39 +0,0 @@ -/* -=========================================================================== -Copyright (C) 1999-2005 Id Software, Inc. - -This file is part of Quake III Arena source code. - -Quake III Arena source code is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, -or (at your option) any later version. - -Quake III Arena source code is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Quake III Arena source code; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -=========================================================================== -*/ -#ifndef __ASM_INLINE_I386__ -#define __ASM_INLINE_I386__ - -#include "../qcommon/q_platform.h" - -#if idx64 - #define EAX "%%rax" - #define EBX "%%rbx" - #define ESP "%%rsp" - #define EDI "%%rdi" -#else - #define EAX "%%eax" - #define EBX "%%ebx" - #define ESP "%%esp" - #define EDI "%%edi" -#endif - -#endif diff --git a/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js b/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js deleted file mode 100644 index e0cad4c3..00000000 --- a/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js +++ /dev/null @@ -1,18 +0,0 @@ -import mdx from '@next/mdx'; - -/** @type {import('next').NextConfig} */ -const nextConfig = { - pageExtensions: ['ts', 'tsx', 'md', 'mdx'], - transpilePackages: ['next-mdx-remote'], - sassOptions: { - compiler: 'modern', - silenceDeprecations: ['legacy-js-api'] - }, output: "standalone", distDir: process.env.NODE_ENV === "production" ? ".next-prod" : ".next", typescript: { ignoreBuildErrors: true }, eslint: { ignoreDuringBuilds: true } -}; - -const withMDX = mdx({ - extension: /\\.mdx?$/, - options: {} -}); - -export default withMDX(nextConfig); \ No newline at end of file diff --git a/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs b/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs deleted file mode 100644 index f6beb4de..00000000 --- a/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! [![Agent Client Protocol](https://zed.dev/img/acp/banner-dark.webp)](https://agentclientprotocol.com/) -//! -//! # Agent Client Protocol (ACP) -//! -//! The Agent Client Protocol standardizes communication between code editors -//! (IDEs, text-editors, etc.) and coding agents (programs that use generative AI -//! to autonomously modify code). -//! -//! ## Protocol & Transport -//! -//! ACP is a JSON-RPC based protocol. While clients typically start agents as -//! subprocesses and communicate over stdio (stdin/stdout), this crate is -//! transport-agnostic. -//! -//! You can use any bidirectional stream that implements `AsyncRead` and `AsyncWrite`. -//! -//! ## Core Components -//! -//! - **Agent**: Programs that use generative AI to autonomously modify code -//! - See: [Agent](https://agentclientprotocol.com/protocol/overview#agent) -//! - **Client**: Code editors that provide the interface between users and agents -//! - See: [Client](https://agentclientprotocol.com/protocol/overview#client) -//! -//! ## Getting Started -//! -//! To understand the protocol, start by exploring the [`Agent`] and [`Client`] traits, -//! which define the core methods and capabilities of each side of the connection. -//! -//! To see working examples of these traits in action, check out the -//! [agent](https://github.com/agentclientprotocol/rust-sdk/blob/main/examples/agent.rs) -//! and -//! [client](https://github.com/agentclientprotocol/rust-sdk/blob/main/examples/client.rs) -//! example binaries included with this crate. -//! -//! ### Implementation Pattern -//! -//! ACP uses a symmetric design where each participant implements one trait and -//! creates a connection that provides the complementary trait: -//! -//! - **Agent builders** implement the [`Agent`] trait to handle client requests -//! (like initialization, authentication, and prompts). They pass this implementation -//! to [`AgentSideConnection::new`], which returns a connection providing [`Client`] -//! methods for requesting permissions and accessing the file system. -//! -//! - **Client builders** implement the [`Client`] trait to handle agent requests -//! (like file system operations and permission checks). They pass this implementation -//! to [`ClientSideConnection::new`], which returns a connection providing [`Agent`] -//! methods for managing sessions and sending prompts. -//! -//! For the complete protocol specification and documentation, visit: -//! [https://agentclientprotocol.com](https://agentclientprotocol.com) - -mod agent; -mod client; -mod content; -mod error; -mod ext; -mod plan; -mod rpc; -mod tool_call; -mod version; - -pub use agent::*; -pub use client::*; -pub use content::*; -use derive_more::{Display, From}; -pub use error::*; -pub use ext::*; -pub use plan::*; -pub use rpc::*; -pub use serde_json::value::RawValue; -pub use tool_call::*; -pub use version::*; - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -/// A unique identifier for a conversation session between a client and agent. -/// -/// Sessions maintain their own context, conversation history, and state, -/// allowing multiple independent interactions with the same agent. -/// -/// # Example -/// -/// ``` -/// use agent_client_protocol::SessionId; -/// use std::sync::Arc; -/// -/// let session_id = SessionId(Arc::from("sess_abc123def456")); -/// ``` -/// -/// See protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id) -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)] -#[serde(transparent)] -#[from(Arc<str>, String, &'static str)] -pub struct SessionId(pub Arc<str>); diff --git a/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml b/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml deleted file mode 100644 index 4e2830d2..00000000 --- a/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: "{build}" -platform: x64 -image: Visual Studio 2017 - -clone_folder: c:\boilerplate -cache: - - vendor - -install: - - curl -SLko protoc.zip https://github.com/google/protobuf/releases/download/v3.6.0/protoc-3.6.0-win32.zip - - 7z e protoc.zip - - move protoc.exe C:\msys64\mingw64\bin - -build_script: - - go version - - set PATH=C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\bin;C:\msys64\usr\bin;C:\msys64\mingw64\bin;%PATH% - - set PATH=%PATH:C:\Program Files\Git\usr\bin;=% - - make - - copy /y "C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\x86_64-w64-mingw32\lib\libgcc_s_seh-1.dll" - - copy /y "C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\x86_64-w64-mingw32\lib\libstdc++-6.dll" - - copy /y "C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\x86_64-w64-mingw32\lib\libwinpthread-1.dll" - - 7z a hercules.win64.zip hercules.exe "libgcc_s_seh-1.dll" "libstdc++-6.dll" "libwinpthread-1.dll" - -test_script: - - go test -v -timeout 20m -race -tags disable_babelfish gopkg.in/src-d/hercules.v10/... - -artifacts: - - name: hercules.win64.zip - path: hercules.win64.zip - -deploy: - release: $(APPVEYOR_REPO_TAG_NAME) - provider: GitHub - auth_token: - secure: 78zsH4q19DqcpdkwnYHQoe0BakBfv+LGu1W7mXuaBC9mS87+EuAj3+yTzJv4NEfI - artifact: hercules.win64.zip - on: - branch: master - appveyor_repo_tag: true diff --git a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go b/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go deleted file mode 100644 index 3492ad67..00000000 --- a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "fmt" - -func classify(n int)ÿÿÿ€ing { - if n < 0 { - return "neg" - } else if n == 0 { - return "zero" - } - for i := 0; i < n; i++ { - if i%2 == 0 && i > 4 { - fmt.Println(i) - } - } - switch n { - case 1, 2, 3: - return "small" - default: - return "big" - } -} - -func main() { fmt.Println(classify(7)) } diff --git a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.evidence.json b/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.evidence.json deleted file mode 100644 index 0db75e6c..00000000 --- a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.evidence.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "first_diff": "first diff @4: go=\"CFB1\\x00\\x03\\x00\\x00{\\\"function_c\" rust=\"CFB1\\xfd\\x02\\x00\\x00{\\\"function_c\"", - "go_runs_sha256": [ - "2710cbc317a0a587d4812efa824dbbddf1ee1603906cb4248389e9ba3fba10ac", - "2710cbc317a0a587d4812efa824dbbddf1ee1603906cb4248389e9ba3fba10ac", - "2710cbc317a0a587d4812efa824dbbddf1ee1603906cb4248389e9ba3fba10ac" - ], - "input_sha256": "b345ea0e6cfd0f76ff09e8103dca0e76ee9b09403679062286acc137846ef282", - "json_mode": false, - "reason": "canonical divergence (go=776B rust=773B, variant-fields=0)", - "rust_out_sha256": "e57cb76b8aada69269648587d2f41c15ac30a3120a11da08a031c7b652782056", - "stage": "cfb1", - "variant_fields": [] -} \ No newline at end of file diff --git a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_0 b/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_0 deleted file mode 100644 index 384448c8066f87e688cc2f7402ad97cd86e9031f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 776 zcmcIiO>4t23@z-B1f8?cj&AL8+(%)sveVdM4C6YEBj(t_c1lagf1i@lc4@oge6sbV zCp|qsE*>VMtI?=|yfz}BHB8vD(sF}g3+ZPAhL;jj5Sn|B`4(dEwsu0Y9CZhvsp^m@ zOMvD^)m(Ma{+*$dB3MU6kPt4mLfeuD6oV#nLaZ_va4iSGeGEqWfJU)QR-u6OhMFM7 zn9V6OqfV2)dOtDc8Q)JnPNpX(28aHeX}Ok~23kHpy}U1>jSmt_j^-caP5zQMK9fh0 zQ+ZwSxjNz;h<Sbcfc7hf41I98syibgZJ?wbIUc$_k#E=$(Y1}CKvl4wV(+^rw~2u( z<)rFM8VELU&3<Lj{`s01)zGMEavMX*Hv{*`m|gu*0HkE;Wv?$TBa;Gs_a`fhCeVa4 znp|=n3g(4%q~&g&%(~vv``yP|aCFhJV{{#PTgvF5fv{R<(Mm@BIxt6>LEHWSN!JY5 diff --git a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_1 b/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_1 deleted file mode 100644 index 384448c8066f87e688cc2f7402ad97cd86e9031f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 776 zcmcIiO>4t23@z-B1f8?cj&AL8+(%)sveVdM4C6YEBj(t_c1lagf1i@lc4@oge6sbV zCp|qsE*>VMtI?=|yfz}BHB8vD(sF}g3+ZPAhL;jj5Sn|B`4(dEwsu0Y9CZhvsp^m@ zOMvD^)m(Ma{+*$dB3MU6kPt4mLfeuD6oV#nLaZ_va4iSGeGEqWfJU)QR-u6OhMFM7 zn9V6OqfV2)dOtDc8Q)JnPNpX(28aHeX}Ok~23kHpy}U1>jSmt_j^-caP5zQMK9fh0 zQ+ZwSxjNz;h<Sbcfc7hf41I98syibgZJ?wbIUc$_k#E=$(Y1}CKvl4wV(+^rw~2u( z<)rFM8VELU&3<Lj{`s01)zGMEavMX*Hv{*`m|gu*0HkE;Wv?$TBa;Gs_a`fhCeVa4 znp|=n3g(4%q~&g&%(~vv``yP|aCFhJV{{#PTgvF5fv{R<(Mm@BIxt6>LEHWSN!JY5 diff --git a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_2 b/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.go_run_2 deleted file mode 100644 index 384448c8066f87e688cc2f7402ad97cd86e9031f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 776 zcmcIiO>4t23@z-B1f8?cj&AL8+(%)sveVdM4C6YEBj(t_c1lagf1i@lc4@oge6sbV zCp|qsE*>VMtI?=|yfz}BHB8vD(sF}g3+ZPAhL;jj5Sn|B`4(dEwsu0Y9CZhvsp^m@ zOMvD^)m(Ma{+*$dB3MU6kPt4mLfeuD6oV#nLaZ_va4iSGeGEqWfJU)QR-u6OhMFM7 zn9V6OqfV2)dOtDc8Q)JnPNpX(28aHeX}Ok~23kHpy}U1>jSmt_j^-caP5zQMK9fh0 zQ+ZwSxjNz;h<Sbcfc7hf41I98syibgZJ?wbIUc$_k#E=$(Y1}CKvl4wV(+^rw~2u( z<)rFM8VELU&3<Lj{`s01)zGMEavMX*Hv{*`m|gu*0HkE;Wv?$TBa;Gs_a`fhCeVa4 znp|=n3g(4%q~&g&%(~vv``yP|aCFhJV{{#PTgvF5fv{R<(Mm@BIxt6>LEHWSN!JY5 diff --git a/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.rust_out b/tests/compat/corpus/fuzzfinds/cfb1_b345ea0e6cfd0f76.go.rust_out deleted file mode 100644 index 6429469549b106405d8f3d6ed89a6158cb82fb8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 773 zcmcJNO;5ux42BWEr1H5`+xXb%k&j78Oq>u0gsLocQ;RgIl62ZM^`|4@c4DJ2j`WoJ zb!@+Way!2nf1IA2Y@uk33}j6vZB=VrQ`kWIx&<RDgcPI}-czxG7`$zq#H=9Q31sRf zB+9a&g;9-AU9|tt$)OC^aUn=pmm8^VB?8GolPN1!849$*fiR1~pbunJmSY`CNGIHc zIF{LhGjr}V=A^^KoM(JL`6xM`4ZiCb9QpT5;|etow79=}cwRso?=4n>%s<#0|7CCV z%N|F5+B@!!YYxmjKD|Ku7DG-xI9fIxNJzI(@s6Ab-JX~mbzu3}#!!+fv%bU^ch9uB zf>6q#>aT1e*dTQFEra(jR$NgHL0!a9iS>XTL*3mzz=5=9b&T}URpcnicQ3NGWCG83 z#dD+3q0GFrj-5;=$tAZ&?{}|mF33fPhS6E%ZH3X^0BeoTqa{ZDE~p^Pp>00_*<lT9 diff --git a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go b/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go deleted file mode 100644 index 3492ad67..00000000 --- a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "fmt" - -func classify(n int)ÿÿÿ€ing { - if n < 0 { - return "neg" - } else if n == 0 { - return "zero" - } - for i := 0; i < n; i++ { - if i%2 == 0 && i > 4 { - fmt.Println(i) - } - } - switch n { - case 1, 2, 3: - return "small" - default: - return "big" - } -} - -func main() { fmt.Println(classify(7)) } diff --git a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.evidence.json b/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.evidence.json deleted file mode 100644 index 713a5644..00000000 --- a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.evidence.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "first_diff": "first diff @357: go=\" \\\"value\\\": \\\"4.5\\\"\\n },\\n \" rust=\" \\\"value\\\": \\\"4.0\\\"\\n },\\n \"", - "go_runs_sha256": [ - "3b3938b9a77d157bd7892fc0cce909efbb71731fb7ccc552da9642208575c70a", - "3b3938b9a77d157bd7892fc0cce909efbb71731fb7ccc552da9642208575c70a", - "3b3938b9a77d157bd7892fc0cce909efbb71731fb7ccc552da9642208575c70a" - ], - "input_sha256": "b345ea0e6cfd0f76ff09e8103dca0e76ee9b09403679062286acc137846ef282", - "json_mode": true, - "reason": "canonical divergence (go=1557B rust=1557B, variant-fields=0)", - "rust_out_sha256": "ba51ed2ea2468cff906e0b63e09a5c762fd361a9c7b0d89449a33037ae0b19a9", - "stage": "gojson", - "variant_fields": [] -} \ No newline at end of file diff --git a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_0 b/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_0 deleted file mode 100644 index 6e44db21..00000000 --- a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_0 +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.5" - }, - { - "label": "Max Complexity", - "value": "8" - }, - { - "label": "Total Complexity", - "value": "9" - }, - { - "label": "Cognitive Total", - "value": "9" - }, - { - "label": "Decision Points", - "value": "7" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=8 | Cog=9 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_1 b/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_1 deleted file mode 100644 index 6e44db21..00000000 --- a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_1 +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.5" - }, - { - "label": "Max Complexity", - "value": "8" - }, - { - "label": "Total Complexity", - "value": "9" - }, - { - "label": "Cognitive Total", - "value": "9" - }, - { - "label": "Decision Points", - "value": "7" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=8 | Cog=9 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_2 b/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_2 deleted file mode 100644 index 6e44db21..00000000 --- a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.go_run_2 +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.5" - }, - { - "label": "Max Complexity", - "value": "8" - }, - { - "label": "Total Complexity", - "value": "9" - }, - { - "label": "Cognitive Total", - "value": "9" - }, - { - "label": "Decision Points", - "value": "7" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=8 | Cog=9 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.rust_out b/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.rust_out deleted file mode 100644 index d2bce87a..00000000 --- a/tests/compat/corpus/fuzzfinds/gojson_b345ea0e6cfd0f76.go.rust_out +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.0" - }, - { - "label": "Max Complexity", - "value": "7" - }, - { - "label": "Total Complexity", - "value": "8" - }, - { - "label": "Cognitive Total", - "value": "8" - }, - { - "label": "Decision Points", - "value": "6" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=7 | Cog=8 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go b/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go deleted file mode 100644 index 3492ad67..00000000 --- a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "fmt" - -func classify(n int)ÿÿÿ€ing { - if n < 0 { - return "neg" - } else if n == 0 { - return "zero" - } - for i := 0; i < n; i++ { - if i%2 == 0 && i > 4 { - fmt.Println(i) - } - } - switch n { - case 1, 2, 3: - return "small" - default: - return "big" - } -} - -func main() { fmt.Println(classify(7)) } diff --git a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.evidence.json b/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.evidence.json deleted file mode 100644 index cfd98ab3..00000000 --- a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.evidence.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "first_diff": "first diff @137: go=\"tic_complexity: 8\\n cognitiv\" rust=\"tic_complexity: 7\\n cognitiv\"", - "go_runs_sha256": [ - "041f457bccf7b40062ca2cc16786e82b6bef0c67e4a71cb8f7b60db0ed22ad05", - "041f457bccf7b40062ca2cc16786e82b6bef0c67e4a71cb8f7b60db0ed22ad05", - "041f457bccf7b40062ca2cc16786e82b6bef0c67e4a71cb8f7b60db0ed22ad05" - ], - "input_sha256": "b345ea0e6cfd0f76ff09e8103dca0e76ee9b09403679062286acc137846ef282", - "json_mode": false, - "reason": "canonical divergence (go=864B rust=861B, variant-fields=0)", - "rust_out_sha256": "3e16138d54d915ac412d89dac090b635d88fcb74e6eaba74504effad442ff1b8", - "stage": "goyaml", - "variant_fields": [] -} \ No newline at end of file diff --git a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_0 b/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_0 deleted file mode 100644 index c8535443..00000000 --- a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_0 +++ /dev/null @@ -1,35 +0,0 @@ -function_complexity: - - name: classify - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 8 - cognitive_complexity: 9 - nesting_depth: 2 - lines_of_code: 18 - complexity_density: 0.4444444444444444 - risk_level: MEDIUM - - name: main - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 1 - complexity_density: 1 - risk_level: LOW -distribution: - moderate: 1 - simple: 1 -high_risk_functions: [] -aggregate: - total_functions: 2 - average_complexity: 4.5 - max_complexity: 8 - total_complexity: 9 - cognitive_complexity: 0 - nesting_depth: 0 - decision_points: 7 - health_score: 68.75 - message: Fair complexity - some functions could be simplified diff --git a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_1 b/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_1 deleted file mode 100644 index c8535443..00000000 --- a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_1 +++ /dev/null @@ -1,35 +0,0 @@ -function_complexity: - - name: classify - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 8 - cognitive_complexity: 9 - nesting_depth: 2 - lines_of_code: 18 - complexity_density: 0.4444444444444444 - risk_level: MEDIUM - - name: main - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 1 - complexity_density: 1 - risk_level: LOW -distribution: - moderate: 1 - simple: 1 -high_risk_functions: [] -aggregate: - total_functions: 2 - average_complexity: 4.5 - max_complexity: 8 - total_complexity: 9 - cognitive_complexity: 0 - nesting_depth: 0 - decision_points: 7 - health_score: 68.75 - message: Fair complexity - some functions could be simplified diff --git a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_2 b/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_2 deleted file mode 100644 index c8535443..00000000 --- a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.go_run_2 +++ /dev/null @@ -1,35 +0,0 @@ -function_complexity: - - name: classify - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 8 - cognitive_complexity: 9 - nesting_depth: 2 - lines_of_code: 18 - complexity_density: 0.4444444444444444 - risk_level: MEDIUM - - name: main - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 1 - complexity_density: 1 - risk_level: LOW -distribution: - moderate: 1 - simple: 1 -high_risk_functions: [] -aggregate: - total_functions: 2 - average_complexity: 4.5 - max_complexity: 8 - total_complexity: 9 - cognitive_complexity: 0 - nesting_depth: 0 - decision_points: 7 - health_score: 68.75 - message: Fair complexity - some functions could be simplified diff --git a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.rust_out b/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.rust_out deleted file mode 100644 index 732a42bc..00000000 --- a/tests/compat/corpus/fuzzfinds/goyaml_b345ea0e6cfd0f76.go.rust_out +++ /dev/null @@ -1,35 +0,0 @@ -function_complexity: - - name: classify - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 7 - cognitive_complexity: 8 - nesting_depth: 2 - lines_of_code: 18 - complexity_density: 0.3888888888888889 - risk_level: MEDIUM - - name: main - source_file: input.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 1 - complexity_density: 1 - risk_level: LOW -distribution: - moderate: 1 - simple: 1 -high_risk_functions: [] -aggregate: - total_functions: 2 - average_complexity: 4 - max_complexity: 7 - total_complexity: 8 - cognitive_complexity: 0 - nesting_depth: 0 - decision_points: 6 - health_score: 72.5 - message: Fair complexity - some functions could be simplified diff --git a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go b/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go deleted file mode 100644 index 3492ad67..00000000 --- a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "fmt" - -func classify(n int)ÿÿÿ€ing { - if n < 0 { - return "neg" - } else if n == 0 { - return "zero" - } - for i := 0; i < n; i++ { - if i%2 == 0 && i > 4 { - fmt.Println(i) - } - } - switch n { - case 1, 2, 3: - return "small" - default: - return "big" - } -} - -func main() { fmt.Println(classify(7)) } diff --git a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.evidence.json b/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.evidence.json deleted file mode 100644 index 48bba956..00000000 --- a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.evidence.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "first_diff": "first diff @357: go=\" \\\"value\\\": \\\"4.5\\\"\\n },\\n \" rust=\" \\\"value\\\": \\\"4.0\\\"\\n },\\n \"", - "go_runs_sha256": [ - "3b3938b9a77d157bd7892fc0cce909efbb71731fb7ccc552da9642208575c70a", - "3b3938b9a77d157bd7892fc0cce909efbb71731fb7ccc552da9642208575c70a", - "3b3938b9a77d157bd7892fc0cce909efbb71731fb7ccc552da9642208575c70a" - ], - "input_sha256": "b345ea0e6cfd0f76ff09e8103dca0e76ee9b09403679062286acc137846ef282", - "json_mode": true, - "reason": "canonical divergence (go=1557B rust=1557B, variant-fields=0)", - "rust_out_sha256": "ba51ed2ea2468cff906e0b63e09a5c762fd361a9c7b0d89449a33037ae0b19a9", - "stage": "metrics_static-complexity", - "variant_fields": [] -} \ No newline at end of file diff --git a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_0 b/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_0 deleted file mode 100644 index 6e44db21..00000000 --- a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_0 +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.5" - }, - { - "label": "Max Complexity", - "value": "8" - }, - { - "label": "Total Complexity", - "value": "9" - }, - { - "label": "Cognitive Total", - "value": "9" - }, - { - "label": "Decision Points", - "value": "7" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=8 | Cog=9 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_1 b/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_1 deleted file mode 100644 index 6e44db21..00000000 --- a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_1 +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.5" - }, - { - "label": "Max Complexity", - "value": "8" - }, - { - "label": "Total Complexity", - "value": "9" - }, - { - "label": "Cognitive Total", - "value": "9" - }, - { - "label": "Decision Points", - "value": "7" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=8 | Cog=9 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_2 b/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_2 deleted file mode 100644 index 6e44db21..00000000 --- a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.go_run_2 +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.5" - }, - { - "label": "Max Complexity", - "value": "8" - }, - { - "label": "Total Complexity", - "value": "9" - }, - { - "label": "Cognitive Total", - "value": "9" - }, - { - "label": "Decision Points", - "value": "7" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=8 | Cog=9 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.rust_out b/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.rust_out deleted file mode 100644 index d2bce87a..00000000 --- a/tests/compat/corpus/fuzzfinds/metrics_static-complexity_b345ea0e6cfd0f76.go.rust_out +++ /dev/null @@ -1,74 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "2" - }, - { - "label": "Avg Complexity", - "value": "4.0" - }, - { - "label": "Max Complexity", - "value": "7" - }, - { - "label": "Total Complexity", - "value": "8" - }, - { - "label": "Cognitive Total", - "value": "8" - }, - { - "label": "Decision Points", - "value": "6" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Moderate (6-10)", - "percent": 0.5, - "count": 1 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "classify", - "location": "input.go", - "value": "CC=7 | Cog=8 | Nest=2", - "severity": "fair" - }, - { - "name": "main", - "location": "input.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - } - ], - "overall_score": 0.6 -} diff --git a/tests/compat/corpus/known_gaps.json b/tests/compat/corpus/known_gaps.json deleted file mode 100644 index bb7fcfb0..00000000 --- a/tests/compat/corpus/known_gaps.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "phase": "MatrixCorpus", - "known_divergences": [ - { - "language": "python", - "sha256": "984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 700431, - "rust_stderr": "Error: failed to parse corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py: parse corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py: no tree" - }, - { - "language": "c", - "sha256": "6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 142327, - "rust_stderr": "Error: failed to parse corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c: parse corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c: no tree-s" - }, - { - "language": "c-header", - "sha256": "ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 19243, - "rust_stderr": "Error: failed to parse corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h: parse corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h: no tree-s" - }, - { - "language": "rust", - "sha256": "e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 125877, - "rust_stderr": "Error: failed to parse corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs: parse corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs: no tree" - }, - { - "language": "typescript", - "sha256": "48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 2555406, - "rust_stderr": "Error: failed to parse corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts: parse corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts: no tree" - }, - { - "language": "tsx", - "sha256": "29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 244099, - "rust_stderr": "Error: failed to parse corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx: parse corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx: no tr" - }, - { - "language": "javascript", - "sha256": "cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 54802, - "rust_stderr": "Error: failed to parse corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js: parse corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js: no tree" - }, - { - "language": "json", - "sha256": "9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 308147, - "rust_stderr": "Error: failed to parse corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json: parse corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json: no " - }, - { - "language": "yaml", - "sha256": "fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 150552, - "rust_stderr": "Error: failed to parse corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml: parse corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml: no tr" - }, - { - "language": "cpp", - "sha256": "8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 3895930, - "rust_stderr": "Error: failed to parse corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp: parse corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp: no tr" - }, - { - "language": "shell", - "sha256": "5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 148968, - "rust_stderr": "Error: failed to parse corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh: parse corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh: no tree" - } - ], - "summary": "11 languages where Go parses but Rust emits nothing (grammar vendoring pending)" -} \ No newline at end of file diff --git a/tests/compat/corpus/manifest.json b/tests/compat/corpus/manifest.json deleted file mode 100644 index 236e9783..00000000 --- a/tests/compat/corpus/manifest.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "files": [ - { - "id": "file/go", - "language": "go", - "ext": "go", - "sha256": "44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc", - "stored": "files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go", - "size_bytes": 5854, - "origin": "/home/dmitriy/sources/hercules/cmd/hercules/combine.go", - "oracle_uast_bytes": 1256674, - "oracle_uast_nodes": 1337 - }, - { - "id": "file/python", - "language": "python", - "ext": "py", - "sha256": "984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb", - "stored": "files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py", - "size_bytes": 4815, - "origin": "/home/dmitriy/sources/nanoGPT/bench.py", - "oracle_uast_bytes": 700431, - "oracle_uast_nodes": 933 - }, - { - "id": "file/c", - "language": "c", - "ext": "c", - "sha256": "6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae", - "stored": "files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c", - "size_bytes": 2146, - "origin": "/home/dmitriy/sources/ioq3/code/asm/ftola.c", - "oracle_uast_bytes": 142327, - "oracle_uast_nodes": 218 - }, - { - "id": "file/c-header", - "language": "c-header", - "ext": "h", - "sha256": "ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8", - "stored": "files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h", - "size_bytes": 1274, - "origin": "/home/dmitriy/sources/ioq3/code/asm/qasm-inline.h", - "oracle_uast_bytes": 19243, - "oracle_uast_nodes": 36 - }, - { - "id": "file/rust", - "language": "rust", - "ext": "rs", - "sha256": "e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482", - "stored": "files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs", - "size_bytes": 3609, - "origin": "/home/dmitriy/sources/agent-client-protocol/rust/acp.rs", - "oracle_uast_bytes": 125877, - "oracle_uast_nodes": 318 - }, - { - "id": "file/typescript", - "language": "typescript", - "ext": "ts", - "sha256": "48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88", - "stored": "files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts", - "size_bytes": 14037, - "origin": "/home/dmitriy/sources/onlook/apps/web/client/messages/en.d.json.ts", - "oracle_uast_bytes": 2555406, - "oracle_uast_nodes": 1789 - }, - { - "id": "file/tsx", - "language": "tsx", - "ext": "tsx", - "sha256": "29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8", - "stored": "files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx", - "size_bytes": 2365, - "origin": "/home/dmitriy/sources/onlook/apps/web/client/src/app/_components/auth-modal.tsx", - "oracle_uast_bytes": 244099, - "oracle_uast_nodes": 269 - }, - { - "id": "file/javascript", - "language": "javascript", - "ext": "js", - "sha256": "cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447", - "stored": "files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js", - "size_bytes": 547, - "origin": "/home/dmitriy/sources/onlook/packages/parser/test/data/next-config/with-mdx-alt-name-order/expected.js", - "oracle_uast_bytes": 54802, - "oracle_uast_nodes": 89 - }, - { - "id": "file/json", - "language": "json", - "ext": "json", - "sha256": "9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355", - "stored": "files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json", - "size_bytes": 3217, - "origin": "/home/dmitriy/sources/agent-client-protocol/docs/docs.json", - "oracle_uast_bytes": 308147, - "oracle_uast_nodes": 382 - }, - { - "id": "file/yaml", - "language": "yaml", - "ext": "yml", - "sha256": "fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868", - "stored": "files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml", - "size_bytes": 1344, - "origin": "/home/dmitriy/sources/hercules/.appveyor.yml", - "oracle_uast_bytes": 150552, - "oracle_uast_nodes": 190 - }, - { - "id": "file/cpp", - "language": "cpp", - "ext": "cpp", - "sha256": "8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6", - "stored": "files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp", - "size_bytes": 21582, - "origin": "/home/dmitriy/sources/llama.cpp/common/chat-auto-parser-generator.cpp", - "oracle_uast_bytes": 3895930, - "oracle_uast_nodes": 3598 - }, - { - "id": "file/shell", - "language": "shell", - "ext": "sh", - "sha256": "5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58", - "stored": "files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh", - "size_bytes": 2453, - "origin": "/home/dmitriy/sources/ioq3/misc/linux/server_compile.sh", - "oracle_uast_bytes": 148968, - "oracle_uast_nodes": 245 - } - ], - "repos": [ - { - "id": "hercules", - "path": "/home/dmitriy/sources/hercules", - "head_commit": "68bb211faaedeffb53e799ab89e2aa48d8cb0ad3", - "commit_count": 1006, - "note": "Go, medium history (~1k commits) -- beyond kubernetes" - }, - { - "id": "ioq3", - "path": "/home/dmitriy/sources/ioq3", - "head_commit": "e29c74e1367fe4c5c6676964bbaaa29003227cdc", - "commit_count": 3784, - "note": "C, larger history (~3.8k commits), different language -- beyond kubernetes" - }, - { - "id": "kubernetes", - "path": "/home/dmitriy/sources/kubernetes", - "head_commit": "2c9cc8da1aa316c30cfba4210cfcd09aff193c81", - "commit_count": 135104, - "note": "Go, very large history -- the reference large repo" - } - ], - "languages_supported_probed": [ - "go", - "python", - "c", - "c-header", - "rust", - "typescript", - "tsx", - "javascript", - "json", - "yaml", - "cpp", - "shell" - ] -} \ No newline at end of file diff --git a/tests/compat/coverage/README.md b/tests/compat/coverage/README.md deleted file mode 100644 index eec81977..00000000 --- a/tests/compat/coverage/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# CoverageLedger (SPEC §3.6/§3.7, roadmap 5) - -Three coverage-REACH facets + the machine-readable gap ledger. Coverage is -reported **alongside** the differential oracle verdict, **never as a substitute** -for it (SPEC §6: "coverage % gives false confidence"). A high number here means -"a test reached this code/cell/language", not "Rust matches Go" — that is the -oracle's job. - -## Components - -| File | Produces | How it is measured (never declared) | -|------|----------|--------------------------------------| -| `rust_cov.py` | `rust_cov.json` — Rust line/region/function/branch coverage | Runs `cargo-llvm-cov` and reads back the audited `data[].totals` from llvm-cov's own `--json` export. `--branch` is nightly-only; on a stable toolchain branch is recorded **unavailable**, never fabricated (region is the stable branch-reach proxy). Default scope = representative analyzer + serializer crates, recorded in the output so the % is honest about what it measured. | -| `matrix_cell_cov.py` | `matrix_cell_cov.json` — matrix-cell coverage (cells exercised / total) **and** per-language parse coverage | Denominator comes from the **live expander** (`expand_matrix.py`), so a shrunk/sampled run cannot inflate the %. Per-language parse runs the **live Go + Rust `uast parse`** per corpus language; a language Go parses but Rust does not is a measured GAP. | -| `build_ledger.py` | `../ledger.json` | Unifies the three facets + the **verdict tally** (from `results/<tier>.json`, so coverage never stands alone) + untested cells + known divergences + Go-VARIANT captures harvested **live** through the oracle with the differing Go observations stored as evidence. | -| `selftest/self_test.py` | self-proof | Plants each cheat/defect class and asserts it is caught. | - -## ledger.json shape - -``` -verdict_tally # PASS/FAIL/SIM/EXPECTED_EMPTY — coverage is NEVER reported alone -coverage: - rust_llvm_cov # lines/regions/functions/branches(+unavailable marker) - matrix_cell # exercised/total, per_family, percent - per_language_parse # rust-parses/total, gaps[] -untested_matrix_cells[] # every enumerated cell with no recorded verdict — surfaced -known_divergences[] # corpus/known_gaps.json + every FAIL/SIM in results -go_variant_captures[] # MEASURED Go-variant fields WITH evidence (differing Go values) -``` - -## Run - -```sh -# fast: reuse warm llvm-cov counters, live variant + parse probes -python3 coverage/build_ledger.py --tier smoke --probe-variants --probe-parse --no-rust-cov - -# full refresh (re-runs cargo-llvm-cov; slower) -python3 coverage/build_ledger.py --tier full --probe-variants --probe-parse - -# self-proof (REQUIRED gate: proves the ledger catches planted cheats) -python3 coverage/selftest/self_test.py -``` - -## Why each self-proof exists (rule #6) - -- **D1 shrunk-results cheat** — denominator is the live expander, so claiming a few - cells PASS cannot inflate the %; untested cells stay listed. -- **D2 empty results** — a missing run reads 0%/all-untested, never a silent 100%. -- **D3 per-language parse gap** — a language Go parses but Rust does not is reported - as a GAP, measured against the live Go oracle (currently 11/12: grammar vendoring - pending — independently confirmed against `corpus/known_gaps.json`). -- **D4 fabricated branch %** — branch is recorded `unavailable`, never invented. -- **D5 coverage ≠ done** — even with a planted 100% coverage json, the ledger keeps - the verdict tally (with its FAILs) visible. -- **D6 variant evidence measured** — the harvest goes through the oracle whose - cheat-detector refuses to blank a Go-STABLE field, and a real variant capture - stores the differing Go observations as proof. diff --git a/tests/compat/coverage/build_ledger.py b/tests/compat/coverage/build_ledger.py deleted file mode 100644 index 80ff4cd9..00000000 --- a/tests/compat/coverage/build_ledger.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 -""" -GAP LEDGER BUILDER (SPEC §3.7 / §4 ledger.json, roadmap 5) - -Assembles the machine-readable gap ledger at tests/compat/ledger.json. It unifies: - - 1. COVERAGE (reach, three facets) -- read from coverage/rust_cov.json and - coverage/matrix_cell_cov.json: - a. Rust line / region / (best-effort) branch coverage (cargo-llvm-cov) - b. matrix-cell coverage (cells exercised / total) - c. per-language parse coverage - Coverage is reported ALONGSIDE the differential verdict, NEVER instead of it - (SPEC §6 risk: "coverage % gives false confidence"). The ledger therefore - ALSO carries the verdict tallies and refuses to call a state "done" on - coverage alone. - - 2. UNTESTED MATRIX CELLS -- every enumerated cell with no recorded verdict, - surfaced (never hidden). Taken from matrix_cell_cov (full-tier denominator). - - 3. KNOWN DIVERGENCES -- (a) the recorded corpus/known_gaps.json entries, and - (b) every FAIL/SIM verdict in the current results/<tier>.json. Each carries - enough to triage. - - 4. GO-VARIANT CAPTURES WITH EVIDENCE -- MEASURED here by running the LIVE oracle - (the Go binary, N>=3x) on a probe set; every field the oracle classifies - VARIANT is recorded WITH the differing Go observations the oracle stored as - evidence. Canonicalization is measured, never declared (SPEC §3.3); the - evidence is the proof that Go itself varies. (If a probe shows ZERO variant - fields, that is recorded too -- a Go-STABLE capture is NOT a license to - normalize anything.) - -The ledger is the honest "how close to all + where the gaps are" artifact. It is -NOT a pass/fail gate by itself; run_matrix.py / the oracle own the verdict. - -Usage: - build_ledger.py --tier smoke - build_ledger.py --tier full --probe-variants # live Go N-run variant harvest - build_ledger.py --tier smoke --no-cov-refresh # reuse existing cov json -""" - -import argparse -import json -import os -import subprocess -import sys -import time - -HERE = os.path.dirname(os.path.abspath(__file__)) -COMPAT = os.path.abspath(os.path.join(HERE, "..")) -ORACLE = os.path.join(COMPAT, "oracle", "oracle.py") -RUST_COV = os.path.join(HERE, "rust_cov.json") -MCC = os.path.join(HERE, "matrix_cell_cov.json") -KNOWN_GAPS = os.path.join(COMPAT, "corpus", "known_gaps.json") -LEDGER = os.path.join(COMPAT, "ledger.json") - -CORPUS = "/home/dmitriy/sources/hercules" - -# Probe set for the Go-VARIANT evidence harvest: analyzers the parity gate notes -# are (or may be) Go map/goroutine-order nondeterministic. The oracle MEASURES -# whether they actually vary; we never assume. Kept small for the smoke tier. -VARIANT_PROBES = [ - ("history/couples", ["run", "--checkpoint=false", "--resume=false", - "--no-cache", "--workers", "1", "--analyzers", - "history/couples", "--format", "json", "--limit", "30", - CORPUS]), - ("history/shotness", ["run", "--checkpoint=false", "--resume=false", - "--no-cache", "--workers", "1", "--analyzers", - "history/shotness", "--format", "json", "--limit", - "30", CORPUS]), - ("history/typos", ["run", "--checkpoint=false", "--resume=false", - "--no-cache", "--workers", "1", "--analyzers", - "history/typos", "--format", "json", "--limit", "30", - CORPUS]), - ("static/comments", ["run", "--checkpoint=false", "--resume=false", - "--no-cache", "--head", "--workers", "1", - "--static-workers", "1", "-p", CORPUS, "--analyzers", - "static/comments", "--format", "json"]), -] - - -def refresh_coverage(tier, probe_parse, do_rust_cov): - if do_rust_cov: - subprocess.run([sys.executable, os.path.join(HERE, "rust_cov.py"), - "--out", RUST_COV], check=False) - cmd = [sys.executable, os.path.join(HERE, "matrix_cell_cov.py"), - "--tier", tier, "--out", MCC] - if probe_parse: - cmd.append("--probe-parse") - subprocess.run(cmd, check=False) - - -def load_json(path): - if not os.path.exists(path): - return None - with open(path) as f: - return json.load(f) - - -def harvest_variant_captures(n_go, do_probe): - """Run the LIVE oracle N>=3x on the probe set and collect every measured - Go-VARIANT field WITH the differing Go observations stored as evidence.""" - captures = [] - if not do_probe: - return captures, "skipped (pass --probe-variants to harvest live)" - for label, argv in VARIANT_PROBES: - mf = f"/tmp/ledger_variant_{abs(hash(label)) % 99999}.json" - p = subprocess.run([sys.executable, ORACLE, "--n-go", str(n_go), - "--manifest", mf, "--quiet", "--"] + argv, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - m = load_json(mf) - if m is None: - captures.append({"label": label, "argv": argv, - "error": "oracle produced no manifest"}) - continue - evidence = m.get("evidence", {}) - fc = m.get("field_counts", {}) - cap = { - "label": label, - "argv": argv, - "oracle_verdict": m.get("verdict"), - "field_counts": fc, - "n_variant_fields": len(evidence), - "canonical_go_stable": m.get("canonical_go_stable"), - "go_run_shas": [r.get("sha") for r in m.get("go_runs", [])], - # store a CAPPED slice of the evidence so the ledger stays readable - # but the proof (differing Go values) is present and auditable. - "evidence_sample": dict(list(evidence.items())[:12]), - "evidence_field_paths": sorted(evidence.keys()), - } - if not evidence: - cap["note"] = ("Go MEASURED STABLE here across N runs -> NOTHING may " - "be normalized. Recorded so a future blank is detectable.") - else: - cap["note"] = ("Go MEASURED VARIANT on these fields (evidence = the " - "differing Go observations). Canonicalization here is " - "measured, not declared.") - try: - os.remove(mf) - except OSError: - pass - captures.append(cap) - return captures, "harvested live" - - -def known_divergences(tier): - out = [] - kg = load_json(KNOWN_GAPS) - if kg: - for d in kg.get("known_divergences", []): - out.append({"source": "corpus/known_gaps.json", **d}) - res = load_json(os.path.join(COMPAT, "results", f"{tier}.json")) - if res: - for r in res.get("records", []): - if r.get("verdict") in ("FAIL", "SIM"): - out.append({"source": f"results/{tier}.json", - "label": r.get("label"), - "verdict": r.get("verdict"), - "family": r.get("family"), - "argv": r.get("argv")}) - return out - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--tier", default="smoke", - help="results/<tier>.json + expander tier (smoke|full; other " - "names allowed only with --no-cov-refresh, for self-test)") - ap.add_argument("--n-go", type=int, default=3) - ap.add_argument("--probe-variants", action="store_true", - help="run the LIVE oracle to harvest Go-variant evidence") - ap.add_argument("--probe-parse", action="store_true", - help="run LIVE per-language parse coverage") - ap.add_argument("--no-cov-refresh", action="store_true", - help="reuse existing coverage/*.json (skip re-running)") - ap.add_argument("--no-rust-cov", action="store_true", - help="skip the (slow) llvm-cov refresh; keep matrix-cell") - ap.add_argument("--out", default=LEDGER) - a = ap.parse_args() - - t0 = time.time() - if not a.no_cov_refresh: - refresh_coverage(a.tier, a.probe_parse, not a.no_rust_cov) - - rust_cov = load_json(RUST_COV) - mcc = load_json(MCC) - captures, harvest_status = harvest_variant_captures(a.n_go, a.probe_variants) - divergences = known_divergences(a.tier) - - # verdict tally pulled from results so coverage is NEVER reported alone. - res = load_json(os.path.join(COMPAT, "results", f"{a.tier}.json")) - verdict_tally = res.get("tally") if res else None - - untested = mcc["matrix_cell"]["untested"] if mcc else [] - - ledger = { - "spec": "specs/go-compat-testing/SPEC.md", - "phase": "CoverageLedger (roadmap 5)", - "tier": a.tier, - "generated_epoch": int(time.time()), - "elapsed_s": round(time.time() - t0, 1), - - # --- the honest headline: coverage REACH + the differential VERDICT, - # side by side. Coverage is never a substitute for the verdict. --- - "verdict_tally": verdict_tally, - "verdict_source": f"results/{a.tier}.json" if res else None, - "coverage": { - "rust_llvm_cov": rust_cov, - "matrix_cell": mcc["matrix_cell"] if mcc else None, - "per_language_parse": mcc["per_language_parse"] if mcc else None, - }, - - # --- the gaps: surfaced, never hidden --- - "untested_matrix_cells": untested, - "untested_matrix_cell_count": len(untested), - "known_divergences": divergences, - "known_divergence_count": len(divergences), - "go_variant_captures": captures, - "go_variant_capture_status": harvest_status, - - "honesty_note": ( - "Coverage % measures REACH (did a test touch this code/cell/language). " - "It is reported ALONGSIDE the differential oracle verdict, never as a " - "substitute (SPEC §6). 'Done' requires the oracle verdict GREEN on " - "off-corpus inputs, NOT a high coverage number. Every Go-variant " - "capture stores the differing Go observations as evidence so " - "canonicalization is measured, not declared."), - } - - with open(a.out, "w") as f: - json.dump(ledger, f, indent=2, default=str) - - # console summary - print("=" * 64) - print(f"GAP LEDGER tier={a.tier} -> {a.out}") - if rust_cov and rust_cov.get("ok"): - t = rust_cov["totals"] - br = t["branches"]["percent"] - print(f" rust coverage : lines {t['lines']['percent']:.1f}% " - f"regions {t['regions']['percent']:.1f}% " - f"branch {('%.1f%%' % br) if br is not None else 'N/A(stable-toolchain)'}") - if mcc: - m = mcc["matrix_cell"] - print(f" matrix cells : {m['exercised_cells']}/{m['total_cells']} " - f"({m['percent']}%) exercised | {m['untested_cells']} UNTESTED") - pl = mcc["per_language_parse"] - if pl.get("probed"): - print(f" per-language : {pl['rust_parse_ok']}/{pl['languages_total']} " - f"({pl['percent']}%) rust-parses gaps={pl.get('gaps')}") - else: - print(f" per-language : {pl['languages_total']} langs (static; " - f"--probe-parse for live)") - print(f" verdict tally : {verdict_tally} (NOT replaced by coverage)") - print(f" divergences : {len(divergences)} known") - nv = sum(1 for c in captures if c.get('n_variant_fields')) - print(f" variant caps : {len(captures)} probed, {nv} with measured evidence " - f"({harvest_status})") - print("=" * 64) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/coverage/matrix_cell_cov.json b/tests/compat/coverage/matrix_cell_cov.json deleted file mode 100644 index 9638d813..00000000 --- a/tests/compat/coverage/matrix_cell_cov.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "matrix_cell": { - "tier": "full", - "total_cells": 505, - "exercised_cells": 505, - "untested_cells": 0, - "percent": 100.0, - "per_family": { - "uast_parse": { - "total": 36, - "exercised": 36, - "percent": 100.0 - }, - "uast_analyze": { - "total": 24, - "exercised": 24, - "percent": 100.0 - }, - "uast_query": { - "total": 36, - "exercised": 36, - "percent": 100.0 - }, - "static_analyzer": { - "total": 147, - "exercised": 147, - "percent": 100.0 - }, - "analyzer_set": { - "total": 21, - "exercised": 21, - "percent": 100.0 - }, - "history_analyzer": { - "total": 210, - "exercised": 210, - "percent": 100.0 - }, - "format_combo": { - "total": 3, - "exercised": 3, - "percent": 100.0 - }, - "plot": { - "total": 19, - "exercised": 19, - "percent": 100.0 - }, - "flag": { - "total": 9, - "exercised": 9, - "percent": 100.0 - } - }, - "results_present": true, - "untested": [] - }, - "per_language_parse": { - "languages_total": 12, - "probed": true, - "table": [ - { - "language": "go", - "ext": "go", - "oracle_uast_bytes": 1256674, - "go_parse_bytes": 1256674, - "rust_parse_bytes": 1256674, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "python", - "ext": "py", - "oracle_uast_bytes": 700431, - "go_parse_bytes": 700431, - "rust_parse_bytes": 700431, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "c", - "ext": "c", - "oracle_uast_bytes": 142327, - "go_parse_bytes": 142327, - "rust_parse_bytes": 142327, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "c-header", - "ext": "h", - "oracle_uast_bytes": 19243, - "go_parse_bytes": 19243, - "rust_parse_bytes": 19243, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "rust", - "ext": "rs", - "oracle_uast_bytes": 125877, - "go_parse_bytes": 125877, - "rust_parse_bytes": 125877, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "typescript", - "ext": "ts", - "oracle_uast_bytes": 2555406, - "go_parse_bytes": 2555406, - "rust_parse_bytes": 2555406, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "tsx", - "ext": "tsx", - "oracle_uast_bytes": 244099, - "go_parse_bytes": 244099, - "rust_parse_bytes": 244099, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "javascript", - "ext": "js", - "oracle_uast_bytes": 54802, - "go_parse_bytes": 54802, - "rust_parse_bytes": 54802, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "json", - "ext": "json", - "oracle_uast_bytes": 308147, - "go_parse_bytes": 308147, - "rust_parse_bytes": 308147, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "yaml", - "ext": "yml", - "oracle_uast_bytes": 150552, - "go_parse_bytes": 150552, - "rust_parse_bytes": 150552, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "cpp", - "ext": "cpp", - "oracle_uast_bytes": 3895930, - "go_parse_bytes": 3895930, - "rust_parse_bytes": 3895930, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "shell", - "ext": "sh", - "oracle_uast_bytes": 148968, - "go_parse_bytes": 148968, - "rust_parse_bytes": 148968, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - } - ], - "rust_parse_ok": 12, - "percent": 100.0, - "gaps": [] - }, - "note": "Coverage measures REACH only; correctness is the oracle's verdict. Untested cells are listed, not hidden." -} \ No newline at end of file diff --git a/tests/compat/coverage/matrix_cell_cov.py b/tests/compat/coverage/matrix_cell_cov.py deleted file mode 100644 index fac3beb4..00000000 --- a/tests/compat/coverage/matrix_cell_cov.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -""" -MATRIX-CELL + PER-LANGUAGE COVERAGE (SPEC §3.6 / §3.2, roadmap 5) - -Two coverage facets that the differential oracle's REACH is measured by: - - A) MATRIX-CELL coverage: of every cell the expander enumerates for a tier - (the full meaningful cross-product, see expand_matrix.py), how many were - actually EXERCISED by a results run (results/<tier>.json)? An exercised cell - is one with a recorded verdict (PASS/FAIL/SIM/EXPECTED_EMPTY). A cell present - in the expansion but absent from results is an UNTESTED cell -- listed, never - hidden (SPEC §3.7). - - The TOTAL is taken from the LIVE expander over the FULL tier, so it cannot be - shrunk by sampling: if a run probed only `--sample N`, the denominator is - still the full enumerated matrix and the gap is visible. - - B) PER-LANGUAGE parse coverage: for every tree-sitter language in the corpus, - did the Rust `uast parse` produce a non-empty UAST where the LIVE Go binary - does? This is measured by re-invoking the oracle's runner primitive on one - parse cell per language (the Go binary is the oracle; we never re-derive). - A language where Go parses but Rust does not is a parse GAP. - -Output: coverage/matrix_cell_cov.json with exercised/total per family, the list -of untested cells, and the per-language parse table. - -Usage: - matrix_cell_cov.py --tier smoke --out coverage/matrix_cell_cov.json - matrix_cell_cov.py --tier full --probe-parse # also run live per-lang parse -""" - -import argparse -import json -import os -import subprocess -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -COMPAT = os.path.abspath(os.path.join(HERE, "..")) -EXPAND = os.path.join(COMPAT, "expand_matrix.py") -RESULTS_DIR = os.path.join(COMPAT, "results") -CORPUS_MANIFEST = os.path.join(COMPAT, "corpus", "manifest.json") - -GO_DIR = "/home/dmitriy/sources/codefang/build/bin" -RU_DIR = "/home/dmitriy/sources/codefang/target/release" -PINNED_ENV = {"TZ": "UTC", "NO_COLOR": "1", "LANG": "C", "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800"} - - -def expand(tier): - p = subprocess.run([sys.executable, EXPAND, tier], - stdout=subprocess.PIPE, check=True) - return json.loads(p.stdout) - - -def load_results(tier): - path = os.path.join(RESULTS_DIR, f"{tier}.json") - if not os.path.exists(path): - return None - with open(path) as f: - return json.load(f) - - -def matrix_cell_coverage(tier): - exp = expand(tier) - cells = exp["cells"] - res = load_results(tier) - exercised_labels = set() - if res: - for r in res.get("records", []): - exercised_labels.add(r["label"]) - - per_family = {} - untested = [] - for c in cells: - fam = c["family"] - d = per_family.setdefault(fam, {"total": 0, "exercised": 0}) - d["total"] += 1 - if c["label"] in exercised_labels: - d["exercised"] += 1 - else: - untested.append({"label": c["label"], "family": fam, - "argv": c["argv"]}) - - total = len(cells) - exercised = total - len(untested) - for fam, d in per_family.items(): - d["percent"] = round(100.0 * d["exercised"] / d["total"], 1) \ - if d["total"] else 0.0 - - return { - "tier": tier, - "total_cells": total, - "exercised_cells": exercised, - "untested_cells": len(untested), - "percent": round(100.0 * exercised / total, 1) if total else 0.0, - "per_family": per_family, - "results_present": res is not None, - "untested": untested, - } - - -def _run(side, argv): - base = GO_DIR if side == "go" else RU_DIR - if argv[0] == "uast": - cmd = [os.path.join(base, "uast")] + argv[1:] - else: - cmd = [os.path.join(base, "codefang"), argv[0]] + argv[1:] - env = dict(os.environ); env.update(PINNED_ENV) - try: - p = subprocess.run(cmd, env=env, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, timeout=600) - return len(p.stdout), p.stdout - except Exception as e: - return -1, str(e).encode() - - -def per_language_parse_coverage(probe): - with open(CORPUS_MANIFEST) as f: - man = json.load(f) - table = [] - n_lang = 0 - n_rust_ok = 0 - for entry in man["files"]: - lang = entry["language"] - n_lang += 1 - stored = os.path.join(COMPAT, "corpus", entry["stored"]) - row = {"language": lang, "ext": entry["ext"], - "oracle_uast_bytes": entry.get("oracle_uast_bytes")} - if probe: - gn, _ = _run("go", ["uast", "parse", "--format", "json", stored]) - rn, rb = _run("rust", ["uast", "parse", "--format", "json", stored]) - row["go_parse_bytes"] = gn - row["rust_parse_bytes"] = rn - go_ok = gn > 0 - rust_ok = rn > 0 - row["go_parses"] = go_ok - row["rust_parses"] = rust_ok - if go_ok and rust_ok: - row["status"] = "both-parse" - n_rust_ok += 1 - elif go_ok and not rust_ok: - row["status"] = "GAP:rust-fails-go-parses" - elif not go_ok: - row["status"] = "go-does-not-parse(not-a-rust-gap)" - n_rust_ok += 1 # not counted against Rust - else: - # static facet from manifest only (no live probe): Go parsed it when - # the corpus was mined (oracle_uast_bytes>0). Rust status unknown. - row["go_parses"] = (entry.get("oracle_uast_bytes") or 0) > 0 - row["status"] = "static(manifest-only;use --probe-parse for live)" - table.append(row) - out = {"languages_total": n_lang, "probed": probe, "table": table} - if probe: - out["rust_parse_ok"] = n_rust_ok - out["percent"] = round(100.0 * n_rust_ok / n_lang, 1) if n_lang else 0.0 - out["gaps"] = [r["language"] for r in table - if r["status"].startswith("GAP")] - return out - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--tier", default="smoke", choices=["smoke", "full"]) - ap.add_argument("--out", default=os.path.join(HERE, "matrix_cell_cov.json")) - ap.add_argument("--probe-parse", action="store_true", - help="run LIVE Go+Rust uast parse per language (slower)") - a = ap.parse_args() - - mcc = matrix_cell_coverage(a.tier) - plc = per_language_parse_coverage(a.probe_parse) - - result = {"matrix_cell": mcc, "per_language_parse": plc, - "note": ("Coverage measures REACH only; correctness is the " - "oracle's verdict. Untested cells are listed, not hidden.")} - with open(a.out, "w") as f: - json.dump(result, f, indent=2) - print(f"matrix_cell[{a.tier}]: {mcc['exercised_cells']}/{mcc['total_cells']} " - f"({mcc['percent']}%) exercised, {mcc['untested_cells']} untested") - if a.probe_parse: - print(f"per_language_parse: {plc['rust_parse_ok']}/{plc['languages_total']} " - f"({plc['percent']}%) gaps={plc.get('gaps')}") - else: - print(f"per_language_parse: {plc['languages_total']} langs (static; " - f"pass --probe-parse for live Rust status)") - print(f"wrote {a.out}") - - -if __name__ == "__main__": - main() diff --git a/tests/compat/coverage/rust_cov.json b/tests/compat/coverage/rust_cov.json deleted file mode 100644 index cef95824..00000000 --- a/tests/compat/coverage/rust_cov.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "ok": true, - "scope": "representative-analyzers(cf-gojson,cf-goyaml,cf-complexity,cf-halstead,cf-comments,cf-imports,cf-composition,cf-cohesion,cf-clones,cf-devs,cf-burndown-core,cf-couples,cf-shotness)", - "scope_kind": "subset", - "branch_coverage_available": false, - "elapsed_s": 3.4, - "totals": { - "lines": { - "covered": 12442, - "count": 19129, - "percent": 65.04260546813738 - }, - "regions": { - "covered": 20952, - "count": 31021, - "percent": 67.54134296121983 - }, - "functions": { - "covered": 1437, - "count": 2039, - "percent": 70.4757233938205 - }, - "branches": { - "covered": null, - "count": null, - "percent": null, - "unavailable": "cargo-llvm-cov --branch is nightly-only; region coverage is the stable branch-reach proxy. Branch number NOT fabricated." - } - }, - "note": "Coverage measures REACH, not correctness. Reported alongside the differential verdict per SPEC \u00a76, never as a substitute." -} \ No newline at end of file diff --git a/tests/compat/coverage/rust_cov.py b/tests/compat/coverage/rust_cov.py deleted file mode 100644 index 8177bf74..00000000 --- a/tests/compat/coverage/rust_cov.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -""" -RUST COVERAGE (SPEC: specs/go-compat-testing/SPEC.md §3.6, roadmap 5) - -Produces Rust line / region / (best-effort) branch coverage via cargo-llvm-cov -and emits a machine-readable summary. This is the COVERAGE-REACH facet of the -ledger; per SPEC §6 it is reported ALONGSIDE the differential verdict, NEVER as a -substitute for it. A high % here means "the tests reached this code", NOT "the -code matches Go" -- that is the oracle's job. - -Design notes: - * cargo-llvm-cov's `--branch` is nightly-only (unstable). We try it; if it - fails we fall back to line+region coverage (region is llvm's stable proxy for - branch reach) and record that branch was UNAVAILABLE -- we never fabricate a - branch number. - * We do NOT silently "declare" coverage: every number here is read back out of - llvm-cov's own --json export (the `data[].totals` block), so it is auditable. - * Scope is parameterizable. Running instrumented tests over all 75 crates is - slow; the default scope is a representative set of analyzer crates (the - compat target surface). The exact scope is RECORDED in the output so the - number is honest about what it measured. - -Usage: - rust_cov.py --out coverage/rust_cov.json [-p CRATE ...] [--all] [--no-run] -""" - -import argparse -import json -import os -import subprocess -import sys -import time - -HERE = os.path.dirname(os.path.abspath(__file__)) -RUST_ROOT = os.path.abspath(os.path.join(HERE, "..", "..", "..")) - -# Default representative scope: the analyzer + serializer crates that the compat -# matrix actually exercises. Honest, fast, and recorded in the output. -DEFAULT_SCOPE = [ - "cf-gojson", "cf-goyaml", # the byte-identical serializers (keystones) - "cf-complexity", "cf-halstead", "cf-comments", "cf-imports", - "cf-composition", "cf-cohesion", "cf-clones", - "cf-devs", "cf-burndown-core", "cf-couples", "cf-shotness", -] - - -def run(cmd, **kw): - return subprocess.run(cmd, cwd=RUST_ROOT, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw) - - -def llvm_cov_json(scope, branch, do_run): - """Run instrumented tests for `scope` and return the parsed llvm-cov --json - export plus whether branch coverage was actually enabled.""" - branch_used = False - if do_run: - # 1) clean prior counters so the number reflects THIS run's scope only. - run(["cargo", "llvm-cov", "clean", "--workspace"]) - # 2) run instrumented tests, no report yet (merge later). - base = ["cargo", "llvm-cov", "--no-report"] - for c in scope: - base += ["-p", c] - if branch: - br = base + ["--branch"] - p = run(br) - if p.returncode == 0: - branch_used = True - else: - # branch unavailable (stable toolchain) -> fall back, recorded. - p = run(base) - if p.returncode != 0: - return None, False, p.stderr.decode("utf-8", "replace") - else: - p = run(base) - if p.returncode != 0: - return None, False, p.stderr.decode("utf-8", "replace") - # 3) export the JSON report from the merged counters. - rep = ["cargo", "llvm-cov", "report", "--json"] - if branch_used: - rep += ["--branch"] - p = run(rep) - if p.returncode != 0: - return None, branch_used, p.stderr.decode("utf-8", "replace") - try: - return json.loads(p.stdout), branch_used, None - except Exception as e: - return None, branch_used, f"parse llvm-cov json: {e}" - - -def summarize(cov_json, branch_used): - """Pull the audited totals out of llvm-cov's own export.""" - data = cov_json.get("data", []) - if not data: - return None - totals = data[0].get("totals", {}) - - def pct(block): - b = totals.get(block, {}) - return {"covered": b.get("covered"), "count": b.get("count"), - "percent": b.get("percent")} - - out = { - "lines": pct("lines"), - "regions": pct("regions"), - "functions": pct("functions"), - } - if branch_used and "branches" in totals: - out["branches"] = pct("branches") - else: - out["branches"] = {"covered": None, "count": None, "percent": None, - "unavailable": "cargo-llvm-cov --branch is nightly-" - "only; region coverage is the stable branch-reach " - "proxy. Branch number NOT fabricated."} - return out - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--out", default=os.path.join(HERE, "rust_cov.json")) - ap.add_argument("-p", dest="scope", action="append", default=[], - help="crate to include (repeatable); default = representative set") - ap.add_argument("--all", action="store_true", - help="cover the WHOLE workspace (slow)") - ap.add_argument("--branch", action="store_true", default=True, - help="attempt branch coverage (falls back if unavailable)") - ap.add_argument("--no-run", action="store_true", - help="skip the instrumented run; only re-export existing counters") - a = ap.parse_args() - - if a.all: - scope = ["--workspace"] - scope_desc = "workspace(all crates)" - # llvm-cov uses --workspace not -p list - # handled by passing the literal token below - elif a.scope: - scope = a.scope - scope_desc = ",".join(scope) - else: - scope = DEFAULT_SCOPE - scope_desc = "representative-analyzers(" + ",".join(scope) + ")" - - t0 = time.time() - if a.all: - # special-case: --workspace as a single token, not "-p --workspace" - branch_used = False - if not a.no_run: - run(["cargo", "llvm-cov", "clean", "--workspace"]) - base = ["cargo", "llvm-cov", "--no-report", "--workspace"] - p = run(base + (["--branch"] if a.branch else [])) - branch_used = a.branch and p.returncode == 0 - if not branch_used: - run(base) - rep = ["cargo", "llvm-cov", "report", "--json"] + ( - ["--branch"] if branch_used else []) - pr = run(rep) - cov = json.loads(pr.stdout) if pr.returncode == 0 else None - err = None if cov else pr.stderr.decode("utf-8", "replace") - else: - cov, branch_used, err = llvm_cov_json(scope, a.branch, not a.no_run) - - elapsed = round(time.time() - t0, 1) - - if cov is None: - result = {"ok": False, "error": err, "scope": scope_desc, - "elapsed_s": elapsed} - with open(a.out, "w") as f: - json.dump(result, f, indent=2) - print(f"rust_cov: FAILED ({err[:200] if err else '?'})", file=sys.stderr) - sys.exit(1) - - totals = summarize(cov, branch_used) - result = { - "ok": True, - "scope": scope_desc, - "scope_kind": "workspace" if a.all else "subset", - "branch_coverage_available": branch_used, - "elapsed_s": elapsed, - "totals": totals, - "note": ("Coverage measures REACH, not correctness. Reported alongside " - "the differential verdict per SPEC §6, never as a substitute."), - } - with open(a.out, "w") as f: - json.dump(result, f, indent=2) - ln = totals["lines"] - rg = totals["regions"] - print(f"rust_cov: lines {ln['percent']:.1f}% ({ln['covered']}/{ln['count']}) " - f"regions {rg['percent']:.1f}% branch_avail={branch_used} " - f"scope={scope_desc[:40]} wrote {a.out}") - - -if __name__ == "__main__": - main() diff --git a/tests/compat/coverage/selftest/self_test.py b/tests/compat/coverage/selftest/self_test.py deleted file mode 100644 index 1b8c77e5..00000000 --- a/tests/compat/coverage/selftest/self_test.py +++ /dev/null @@ -1,272 +0,0 @@ -#!/usr/bin/env python3 -""" -COVERAGE-LEDGER SELF-PROOF (non-negotiable rule #6: every component must -SELF-PROVE it catches a defect). A green that cannot be shown to catch a planted -bug is worthless. This harness PLANTS each cheat/defect class the ledger must be -immune to and asserts the component reports the gap rather than hiding it. - -Planted defects (each mirrors a real way the prior effort was gamed): - - D1 SHRUNK-RESULTS CHEAT A results file falsely claims cells were exercised - while the live expander enumerates more cells. - matrix_cell_coverage MUST take the denominator - from the LIVE expander, so the untested cells stay - visible -- the cheat cannot inflate the %. - D2 EMPTY-RESULTS No results -> coverage MUST be 0%/all-untested, - never a silent 100%. (A missing run is a gap, not - a pass.) - D3 PER-LANGUAGE PARSE GAP A language where the LIVE Go binary parses but the - LIVE Rust binary does not MUST be reported as a - GAP (measured against the Go oracle, not declared). - D4 FABRICATED BRANCH NUMBER rust_cov MUST NOT invent a branch % when the - toolchain can't measure it; it records - 'unavailable', never a number. - D5 COVERAGE-NOT-A-SUBSTITUTE Even with a planted 100% coverage json, the ledger - MUST still carry the differential verdict tally - and that tally still shows the FAILs -- coverage - green is NOT 'done'. - D6 VARIANT-EVIDENCE MEASURED The Go-variant harvest goes through the LIVE - oracle whose cheat-detector REFUSES to normalize a - Go-STABLE field; and a measured-variant capture - MUST store the differing Go observations as - evidence (proof, not a declaration). - -D1/D2/D4/D5 run offline (pure logic on planted inputs). D3/D6 need the live -binaries; they are skipped with a clear notice if a binary is missing, but run by -default in this environment. -""" - -import importlib.util -import json -import os -import subprocess -import sys -import tempfile - -HERE = os.path.dirname(os.path.abspath(__file__)) -COV = os.path.abspath(os.path.join(HERE, "..")) -COMPAT = os.path.abspath(os.path.join(COV, "..")) -GO = "/home/dmitriy/sources/codefang/build/bin" -RU = "/home/dmitriy/sources/codefang/target/release" - - -def load(name): - p = os.path.join(COV, name) - spec = importlib.util.spec_from_file_location(name[:-3], p) - m = importlib.util.module_from_spec(spec) - spec.loader.exec_module(m) - return m - - -mcc_mod = load("matrix_cell_cov.py") - -RESULTS = [] - - -def check(name, cond, detail=""): - RESULTS.append((name, bool(cond))) - print(f" {'OK ' if cond else 'XX '}{name}" - + ("" if cond else f" <-- SELF-TEST FAILURE {detail}")) - return bool(cond) - - -def main(): - print("================ COVERAGE-LEDGER SELF-PROOF ================") - - # Build the ground-truth cell set from the LIVE expander for a tier. - exp = mcc_mod.expand("smoke") - all_labels = [c["label"] for c in exp["cells"]] - total = len(all_labels) - check("expander enumerates a non-trivial matrix", total > 50, - f"only {total} cells") - - # --- D1 SHRUNK-RESULTS CHEAT ------------------------------------------- - # A cheating results file claims ONLY 3 cells but marks them PASS; if the - # coverage took its denominator from the results it would read 100%. It must - # instead read total from the live expander -> 3/total, untested listed. - # We monkeypatch load_results to return the cheat without touching real files. - cheat_labels = all_labels[:3] - orig_load = mcc_mod.load_results - mcc_mod.load_results = lambda tier: { - "records": [{"label": l, "verdict": "PASS"} for l in cheat_labels]} - try: - cov = mcc_mod.matrix_cell_coverage("smoke") - finally: - mcc_mod.load_results = orig_load - check("D1 denominator from live expander (cheat cannot inflate)", - cov["total_cells"] == total and cov["exercised_cells"] == 3, - f"total={cov['total_cells']} exercised={cov['exercised_cells']}") - check("D1 untested cells surfaced (not hidden)", - cov["untested_cells"] == total - 3 and len(cov["untested"]) == total - 3, - f"untested={cov['untested_cells']}") - - # --- D2 EMPTY-RESULTS -------------------------------------------------- - mcc_mod.load_results = lambda tier: None - try: - cov0 = mcc_mod.matrix_cell_coverage("smoke") - finally: - mcc_mod.load_results = orig_load - check("D2 no results -> 0% (a missing run is a gap, not a pass)", - cov0["exercised_cells"] == 0 and cov0["percent"] == 0.0 - and cov0["untested_cells"] == total, - f"exercised={cov0['exercised_cells']} pct={cov0['percent']}") - - # --- D4 FABRICATED BRANCH NUMBER --------------------------------------- - # The committed rust_cov.json (from a stable toolchain) must record branch as - # unavailable, NOT a number. (If branch ever becomes available the field is a - # real number AND branch_coverage_available is true -- also acceptable.) - rc_path = os.path.join(COV, "rust_cov.json") - if os.path.exists(rc_path): - rc = json.load(open(rc_path)) - br = rc.get("totals", {}).get("branches", {}) - avail = rc.get("branch_coverage_available", False) - honest = (avail and isinstance(br.get("percent"), (int, float))) or \ - ((not avail) and br.get("percent") is None - and "unavailable" in br) - check("D4 branch number not fabricated (recorded or honestly N/A)", - honest, f"branches={br} avail={avail}") - else: - check("D4 (skipped: no rust_cov.json yet)", True) - - # --- D5 COVERAGE-NOT-A-SUBSTITUTE -------------------------------------- - # Plant a 100% coverage json + a verdict tally that still has FAILs; the - # ledger builder must keep the verdict tally visible alongside coverage so a - # 100% number cannot mask FAILs ("done" requires the oracle verdict). - led = load("build_ledger.py") - with tempfile.TemporaryDirectory() as td: - fake_rc = os.path.join(td, "rust_cov.json") - json.dump({"ok": True, "branch_coverage_available": False, - "totals": {"lines": {"percent": 100.0, "covered": 1, - "count": 1}, - "regions": {"percent": 100.0}, - "branches": {"percent": None, - "unavailable": "x"}}}, - open(fake_rc, "w")) - fake_mcc = os.path.join(td, "mcc.json") - json.dump({"matrix_cell": {"total_cells": 10, "exercised_cells": 10, - "untested_cells": 0, "percent": 100.0, - "per_family": {}, "untested": []}, - "per_language_parse": {"languages_total": 1, - "probed": False, "table": []}}, - open(fake_mcc, "w")) - # Plant a results file with FAILs under a private tier name and run the - # REAL build_ledger main path against it; the emitted ledger MUST carry - # the verdict tally (with its 3 FAILs) NEXT TO the 100% coverage. - fake_tier = "_selftest_d5" - fake_res = os.path.join(COMPAT, "results", f"{fake_tier}.json") - json.dump({"tier": fake_tier, "tally": {"PASS": 1, "FAIL": 3, "SIM": 1}, - "records": [{"label": "x", "verdict": "FAIL", - "argv": ["run"], "family": "f"}]}, - open(fake_res, "w")) - out_ledger = os.path.join(td, "ledger.json") - orig_paths = (led.RUST_COV, led.MCC) - led.RUST_COV, led.MCC = fake_rc, fake_mcc - try: - sys.argv = ["build_ledger.py", "--tier", fake_tier, - "--no-cov-refresh", "--out", out_ledger] - try: - led.main() - except SystemExit: - pass - emitted = json.load(open(out_ledger)) - finally: - led.RUST_COV, led.MCC = orig_paths - os.remove(fake_res) - cov_is_100 = emitted["coverage"]["rust_llvm_cov"]["totals"]["lines"][ - "percent"] == 100.0 - tally = emitted.get("verdict_tally") or {} - check("D5 ledger keeps verdict tally (3 FAILs) alongside 100% coverage", - cov_is_100 and tally.get("FAIL") == 3 and tally.get("SIM") == 1, - f"cov100={cov_is_100} tally={tally}") - - # --- D3 PER-LANGUAGE PARSE GAP (live) ---------------------------------- - if os.path.exists(os.path.join(GO, "uast")) and \ - os.path.exists(os.path.join(RU, "uast")): - plc = mcc_mod.per_language_parse_coverage(probe=True) - # There is at least one language Go parses but Rust does not (grammar - # vendoring pending). The facet MUST report it as a GAP. - check("D3 live per-language parse GAP detected (measured vs Go oracle)", - plc["probed"] and len(plc.get("gaps", [])) >= 1, - f"gaps={plc.get('gaps')}") - # And it must NOT falsely call a Rust-failure a pass: every gap language - # has rust_parse_bytes<=0 while go_parse_bytes>0 in the table. - gap_rows = [r for r in plc["table"] if r["status"].startswith("GAP")] - sound = all((r.get("go_parse_bytes", 0) > 0 and - r.get("rust_parse_bytes", 0) <= 0) for r in gap_rows) - check("D3 gap rows are genuine (go-parses & rust-empty)", - len(gap_rows) >= 1 and sound, - f"gap_rows={len(gap_rows)} sound={sound}") - else: - check("D3 (skipped: live binaries missing)", True) - - # --- D6 VARIANT-EVIDENCE MEASURED (live oracle cheat-detector) ---------- - oracle_py = os.path.join(COMPAT, "oracle", "oracle.py") - if os.path.exists(oracle_py) and os.path.exists(os.path.join(GO, "uast")): - ospec = importlib.util.spec_from_file_location("oracle", oracle_py) - oracle = importlib.util.module_from_spec(ospec) - ospec.loader.exec_module(oracle) - # Plant: a fabricated Go output that is STABLE on '$.score', then ask the - # oracle to normalize '$.score'. The cheat-detector MUST refuse (this is - # the exact blank-a-stable-field cheat the variant harvest must be immune - # to). We drive it with scripted bins, never corrupting the real port. - STABLE = {"score": 7, "items": [1, 2, 3]} - - class FakeBins: - def __init__(self, go, rust): - self.go = list(go); self.rust = list(rust) - - def __call__(self, side, argv): - return (self.go if side == "go" else self.rust).pop(0) - fb = FakeBins([(0, json.dumps(STABLE).encode())] * 3, - [(0, json.dumps(STABLE).encode())] * 2) - orig = oracle.run_once - oracle.run_once = fb - try: - res = oracle.run_invocation( - ["run", "--analyzers", "x", "--format", "json", "--limit", "10", - "/r"], n_go=3, normalize=["$.score"]) - finally: - oracle.run_once = orig - check("D6 cheat-detector refuses to blank a Go-STABLE field", - res["verdict"] == "FAIL" and "TAMPER" in res.get("reason", ""), - f"verdict={res['verdict']} reason={res.get('reason')}") - - # And: a genuinely Go-variant capture stores the differing observations. - GO1 = {"items": [{"v": 1}], "tag": "a"} - GO2 = {"items": [{"v": 2}], "tag": "b"} - GO3 = {"items": [{"v": 3}], "tag": "c"} - fb2 = FakeBins([(0, json.dumps(GO1).encode()), - (0, json.dumps(GO2).encode()), - (0, json.dumps(GO3).encode())], - [(0, json.dumps(GO1).encode())] * 2) - oracle.run_once = fb2 - try: - res2 = oracle.run_invocation( - ["run", "--analyzers", "x", "--format", "json", "/r"], - n_go=3, normalize=[]) - finally: - oracle.run_once = orig - ev = res2.get("evidence", {}) - has_proof = "$.tag" in ev and len(ev["$.tag"]) >= 2 - check("D6 measured-variant capture stores differing Go observations", - has_proof, f"evidence={list(ev.items())[:3]}") - else: - check("D6 (skipped: oracle/live Go missing)", True) - - print("===========================================================") - failed = [r for r in RESULTS if not r[1]] - n = len(RESULTS) - print(f"SELF-PROOF: {n - len(failed)}/{n} checks correct") - if failed: - print("SELF-PROOF FAILED -- a planted cheat/defect was NOT caught:") - for name, _ in failed: - print(f" - {name}") - sys.exit(1) - print("SELF-PROOF GREEN: the ledger provably surfaces shrunk-results cheats, " - "empty runs, per-language parse gaps, refuses fabricated branch %, keeps " - "the verdict alongside coverage, and harvests measured variant evidence.") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/expand_matrix.py b/tests/compat/expand_matrix.py deleted file mode 100644 index da10de86..00000000 --- a/tests/compat/expand_matrix.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env python3 -""" -MATRIX EXPANDER (SPEC: specs/go-compat-testing/SPEC.md §3.2, roadmap 3). - -Cross-multiplies the axes in matrix.toml, substitutes content-addressed corpus -inputs from corpus/manifest.json, and emits one CELL per meaningful combination. -Each cell is an oracle invocation: {"label","argv","family","tier"}. - -This module DOES NOT compare and DOES NOT decide pass/fail. The oracle -(oracle/oracle.py) is the sole source of truth; the runner (run_matrix.py) feeds -each cell argv to it. Cells the LIVE Go binary leaves empty are recorded as -expected-empty contracts BY THE RUNNER (it asks the oracle / Go), never here -- -the expander only enumerates what to probe so that the probed set is auditable -and cannot be silently shrunk (the matrix file is hashed by the tamper layer). -""" - -import json -import os -import sys - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - print("python>=3.11 required (tomllib)", file=sys.stderr) - sys.exit(2) - -HERE = os.path.dirname(os.path.abspath(__file__)) -MATRIX = os.path.join(HERE, "matrix.toml") -CORPUS_MANIFEST = os.path.join(HERE, "corpus", "manifest.json") - - -def load(): - with open(MATRIX, "rb") as f: - m = tomllib.load(f) - with open(CORPUS_MANIFEST) as f: - c = json.load(f) - return m, c - - -def repo_path(corpus, repo_id): - for r in corpus["repos"]: - if r["id"] == repo_id: - return r["path"] - return None - - -def expand(tier="smoke"): - m, corpus = load() - files = corpus["files"] - tcfg = m["tiers"][tier] - cells = [] - - def add(label, argv, family): - cells.append({"label": label, "argv": argv, "family": family, - "tier": tier}) - - # ---- 1) uast surface x corpus languages (per-language parse contract) ---- - qdsl = m["uast"]["query_dsl"] - for entry in files: - stored = os.path.join(HERE, "corpus", entry["stored"]) - lang = entry["language"] - for fmt in m["formats"]["uast_parse"]: - add(f"uast/parse[{fmt}]/{lang}", - ["uast", "parse", "--format", fmt, stored], "uast_parse") - for fmt in m["formats"]["uast_analyze"]: - add(f"uast/analyze[{fmt}]/{lang}", - ["uast", "analyze", "--format", fmt, "-i", stored], "uast_analyze") - for fmt in m["formats"]["uast_query"]: - add(f"uast/query[{fmt}]/{lang}", - ["uast", "query", qdsl, "--format", fmt, "-i", stored], "uast_query") - - # ---- 2) static analyzers x formats (single-analyzer cells) ---- - sbase = m["meta"]["static_base"] - for repo_id in tcfg["repos"]: - rp = repo_path(corpus, repo_id) - if not rp: - continue - for an in m["analyzers"]["static"]: - for fmt in tcfg["run_formats"]: - add(f"static:{an}[{fmt}]@{repo_id}", - sbase + ["-p", rp, "--analyzers", an, "--format", fmt], - "static_analyzer") - # static analyzer SETS (only the static-relevant sets) on this repo - for setspec in m["analyzer_sets"]["sets"]: - if not setspec.startswith("static") and setspec != "*": - continue - add(f"static-set:{setspec}[json]@{repo_id}", - sbase + ["-p", rp, "--analyzers", setspec, "--format", "json"], - "analyzer_set") - - # ---- 3) history analyzers x formats x limit (single-analyzer cells) ---- - rbase = m["meta"]["run_base"] - lim = str(tcfg["history_limit"]) - for repo_id in tcfg["repos"]: - rp = repo_path(corpus, repo_id) - if not rp: - continue - for an in m["analyzers"]["history"]: - for fmt in tcfg["run_formats"]: - add(f"history:{an}[{fmt}]@{repo_id}/lim{lim}", - rbase + ["--analyzers", an, "--format", fmt, - "--limit", lim, rp], - "history_analyzer") - # history analyzer SETS - for setspec in m["analyzer_sets"]["sets"]: - if not (setspec.startswith("history") or setspec == "*"): - continue - add(f"history-set:{setspec}[json]@{repo_id}/lim{lim}", - rbase + ["--analyzers", setspec, "--format", "json", - "--limit", lim, rp], - "analyzer_set") - - # ---- 4) timeseries + --ndjson COMBINATION cell ---- - for repo_id in tcfg["repos"]: - rp = repo_path(corpus, repo_id) - if not rp: - continue - add(f"history:history/devs[timeseries+ndjson]@{repo_id}/lim{lim}", - rbase + ["--analyzers", "history/devs", "--format", "timeseries", - "--ndjson", "--limit", lim, rp], "format_combo") - - # ---- 4b) PLOT (file-output) cells: --format plot writes index.html + - # per-analyzer <id>.html + report.json into --output; the oracle's file - # mode (triggered by `--format plot` with no --output in the cell argv) - # runs both binaries into temp dirs and compares file sets + per-file - # bytes (go-echarts chart-id canonicalization, measured). Scope: every - # analyzer on the FIRST tier repo (hercules — the cheap one), plus the - # static/* and * set pages there. - plot_repo = tcfg["repos"][0] - rp = repo_path(corpus, plot_repo) - if rp: - for an in m["analyzers"]["static"]: - add(f"plot:{an}@{plot_repo}", - sbase + ["-p", rp, "--analyzers", an, "--format", "plot"], - "plot") - for an in m["analyzers"]["history"]: - add(f"plot:{an}@{plot_repo}/lim{lim}", - rbase + ["--analyzers", an, "--format", "plot", - "--limit", lim, rp], - "plot") - for setspec in ("static/*", "*"): - add(f"plot:{setspec}@{plot_repo}", - sbase + ["-p", rp, "--analyzers", setspec, "--format", "plot"], - "plot") - - # ---- 5) KEY-FLAG axis: each flag x analyzer x format is one cell ---- - # A real regression shipped because flag cells ran ONLY --format json on - # ONLY static/complexity (+ history/devs): --include-generated/-vendored - # were silently ignored by other formats/analyzers. The sub-loops below - # deliberately overlap (e.g. static/complexity[json] appears in the legacy - # loop AND the format sweep); identical argv is emitted exactly once. - seen_flag = set() - - def add_flag(label, argv): - key = tuple(argv) - if key in seen_flag: - return - seen_flag.add(key) - add(label, argv, "flag") - - def split_flag(flag): - # "--languages:Go,Python" -> ("--languages", ["--languages", - # "Go,Python"]): split on the FIRST ':' only; the value keeps commas. - name, _, val = flag.partition(":") - return name, [name] + ([val] if val else []) - - if tcfg.get("include_flags"): - rp = repo_path(corpus, tcfg["repos"][0]) - # 5a) history flags on history/devs (original pattern, kept verbatim) - for flag in m["flags"]["history"]: - name, extra = split_flag(flag) - add_flag(f"flag:history/devs{flag}@{tcfg['repos'][0]}", - rbase + ["--analyzers", "history/devs", "--format", "json"] - + extra + ([] if name == "--head" else ["--limit", lim]) - + [rp]) - # 5b) original static flag cells (kept verbatim: complexity, json) - for flag in m["flags"]["static"]: - name, extra = split_flag(flag) - add_flag(f"flag:static/complexity{flag}@{tcfg['repos'][0]}", - sbase + ["-p", rp, "--analyzers", "static/complexity", - "--format", "json"] + extra) - # 5c) every static flag x FORMAT sweep on the static pair — a flag - # honored by json but ignored by text/yaml is its own regression class. - for flag in m["flags"]["static"]: - name, extra = split_flag(flag) - for an in ("static/complexity", "static/halstead"): - for fmt in ("json", "text", "yaml"): - add_flag(f"flag:{an}{flag}[{fmt}]@{tcfg['repos'][0]}", - sbase + ["-p", rp, "--analyzers", an, - "--format", fmt] + extra) - # 5d) filter flags x EVERY static analyzer — a flag wired into one - # analyzer's file walk but not the others' is invisible to 5b/5c. - for flag in ("--include-generated", "--languages:Go"): - name, extra = split_flag(flag) - for an in m["analyzers"]["static"]: - for fmt in ("json", "text"): - add_flag(f"flag:{an}{flag}[{fmt}]@{tcfg['repos'][0]}", - sbase + ["-p", rp, "--analyzers", an, - "--format", fmt] + extra) - elif tcfg.get("include_static_flags"): - # smoke slice of 5d: filter flags on the static pair, json+text only. - # Static cells on the smoke repo need no revwalk, so this stays fast; - # the full tier keeps the whole flag axis above. - rp = repo_path(corpus, tcfg["repos"][0]) - for flag in ("--include-generated", "--languages:Go"): - name, extra = split_flag(flag) - for an in ("static/complexity", "static/halstead"): - for fmt in ("json", "text"): - add_flag(f"flag:{an}{flag}[{fmt}]@{tcfg['repos'][0]}", - sbase + ["-p", rp, "--analyzers", an, - "--format", fmt] + extra) - - return cells - - -def main(): - tier = sys.argv[1] if len(sys.argv) > 1 else "smoke" - cells = expand(tier) - # families summary - fams = {} - for c in cells: - fams[c["family"]] = fams.get(c["family"], 0) + 1 - out = {"tier": tier, "total_cells": len(cells), - "families": fams, "cells": cells} - json.dump(out, sys.stdout, indent=2) - print(file=sys.stderr) - print(f"tier={tier}: {len(cells)} cells {fams}", file=sys.stderr) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/fuzz/README.md b/tests/compat/fuzz/README.md deleted file mode 100644 index 82b6cbdc..00000000 --- a/tests/compat/fuzz/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Per-stage differential fuzzing (compat layer 6 / SPEC roadmap §6) - -Go-native (`testing/F`) differential fuzz targets, **one per PURE stage** of the -codefang/uast pipeline. Each target feeds the SAME input to the **LIVE Go -binary** (the oracle, `build/bin/{codefang,uast}`) and to the **Rust binary** -(`target/release/{codefang,uast}`) under the pinned env, and **FAILS on any -divergence**. Divergence-finding inputs are distilled back into -`../corpus/fuzzfinds/` with the differing Go outputs stored as evidence. - -## Why this exists (the threat model) - -The prior "32/32 golden" signal was gameable: analyzers emitted golden bytes as -constants, and a gate was weakened to hide a real bug. This layer defeats that -because a **freshly mutated input cannot be memorized**: the corpus is structure --aware (mutations of real source), the oracle is the live Go binary, and the -self-proving tests assert the harness catches planted defects. - -## Pure stages → targets - -| Stage | Target | Invocation diffed | -|-------|--------|-------------------| -| tree-sitter parse | `FuzzParse`, `FuzzParseGo` | `uast parse --format json` | -| UAST map | `FuzzMap`, `FuzzQuery` | `uast analyze` / `uast query` | -| cf-gojson serializer | `FuzzSerializerJSON` | `run … --format json` | -| cf-goyaml serializer | `FuzzSerializerYAML` | `run … --format yaml` | -| CFB1 serializer | `FuzzSerializerCFB1` | `run … --format bin` | -| analyzer ComputeAllMetrics | `FuzzComputeAllMetrics`, `FuzzComputeAllMetricsGo` | `run --analyzers static/{complexity,composition}` | - -`*Go` variants restrict mutation to Go source — the ONE language whose -tree-sitter grammar is currently wired in the Rust port — so the coverage-guided -mutator digs DEEP into a supported language instead of bouncing off the -"grammar not wired" gap that the all-language targets expose for every other -language. - -## Non-negotiable design (encoded in `harness.go`) - -- **Oracle = live Go binary.** No re-derivation in Go or Rust; we run the actual - binaries as subprocesses. -- **Canonicalization is MEASURED, never declared.** For each input, Go is run - N≥3×; only fields that VARY across Go's own runs may be neutralized - (`measureGoVariance` → `canonByMeasure`), and the differing Go outputs are - stored as evidence. `canonByMeasure` **refuses to blank a Go-stable field** — - the exact cheat that hid a real bug before (proved by - `TestSelfCheck_TamperBlankStableField`). -- **Pinned env:** `TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800`, - argv passed as a list (no shell glob), STDOUT compared (stderr is progress). -- **Distillation keeps the SMALLEST set preserving distinct findings:** - `distill` dedups by divergence CLASS (the set of differing JSON field keys, or - the raw-bytes shape), so fuzzer minimization's hundreds of near-duplicates - collapse to one representative per bug. - -## Self-proving (rule 6 — a green that can't catch a bug is worthless) - -`selfcheck_test.go` injects known defects via a shim "Rust" (`CFFUZZ_RUST_DIR`) -and asserts the harness reports FAIL: - -- `TestSelfCheck_DetectsConstantStub` — Rust emits a constant → caught. -- `TestSelfCheck_DetectsByteFlip` — Rust flips one Go-stable byte → caught. -- `TestSelfCheck_TamperBlankStableField` — `canonByMeasure` will NOT equate a - differing Go-stable field; only MEASURED-variant fields canonicalize. -- `TestSelfCheck_VarianceMeasuredNotDeclared` — measurement yields 0 variant - fields on a deterministic analyzer (evidence stored). -- `TestSelfCheck_RealRustPasses` — the REAL Rust binary passes the baseline (so - the gate is not stuck-red). - -## Running - -```bash -# self-checks + seed pass + bounded mutation fuzz (20s/target) -tests/compat/fuzz/run_fuzz.sh 20 - -# one target, longer: -FUZZ_ONLY=FuzzParseGo tests/compat/fuzz/run_fuzz.sh 60 - -# raw go test (seed pass only, no mutation): -go test ./tests/compat/fuzz/ -run FuzzParseGo -count=1 -# active mutation fuzz of one target: -go test ./tests/compat/fuzz/ -run '^$' -fuzz '^FuzzParseGo$' -fuzztime 30s -``` - -## Divergences found (recorded, NOT fixed — fixing Rust is out of scope here) - -All recorded under `../corpus/fuzzfinds/` with `.evidence.json` + the N Go runs. - -1. **Rust has only the Go tree-sitter grammar wired.** For C/C++/Python/TS/TSX/ - JS/Rust/shell/etc., `uast parse|analyze|query` errors - ("no tree-sitter grammar wired for language X") and produces empty output, - while Go parses them. Surfaced by `FuzzParse`/`FuzzMap`/`FuzzQuery` seeds. -2. **`static/complexity` (and the serializers downstream) are Go-only.** On - non-Go files the Rust analyzer reports 0 functions / "No complexity data - available" (a CONSTANT 756B JSON / 329B CFB1 / 339B YAML) while Go computes - real metrics. This is the classic simulation signature, caught by - `FuzzSerializer*` / `FuzzComputeAllMetrics`. -3. **Invalid-UTF-8 token serialization differs (Go input).** When source - contains non-UTF-8 bytes, Go's `uast parse` JSON-escapes them as `�` - while Rust emits the raw replacement char `�`; the downstream node `id` - content-hash differs as a consequence. Found by `FuzzParseGo` mutation in - ~12s and minimized automatically. diff --git a/tests/compat/fuzz/analyzer_fuzz_test.go b/tests/compat/fuzz/analyzer_fuzz_test.go deleted file mode 100644 index 8d491f26..00000000 --- a/tests/compat/fuzz/analyzer_fuzz_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package compatfuzz - -import ( - "testing" - "time" -) - -// FuzzComputeAllMetrics — PURE STAGE 4: an analyzer's pure ComputeAllMetrics. -// -// static/complexity's ComputeAllMetrics is a pure function of the parsed report -// (no git, no clock). Driving it through `codefang run --analyzers -// static/complexity --format json` over a single mutated source file exercises -// exactly that pure computation; the cf-gojson serializer is held fixed so a -// divergence localizes to the metric math (cyclomatic/cognitive counts, -// scoring, status labels). Differential vs the LIVE Go binary, measured-canon. -// -// We additionally probe a SECOND analyzer (static/composition, also pure + -// deterministic) so the stage is not single-analyzer. -func FuzzComputeAllMetrics(f *testing.F) { - for _, s := range loadSeeds("go", "py", "ts", "js", "c", "cpp", "rs") { - f.Add(s.Ext, s.Data) - } - f.Fuzz(func(t *testing.T, ext string, data []byte) { - ext = sanitizeExt(ext) - for _, analyzer := range []string{"static/complexity", "static/composition"} { - dir, _ := writeTemp(t, ext, data) - argv := staticRunArgv(dir, analyzer, "json") - res, err := differential(argv, 3, 30*time.Second) - removeAll(dir) - if err != nil { - t.Skipf("oracle exec error (%s): %v", analyzer, err) - } - if !res.OK { - path, _ := distill("metrics_"+sanitizeAnalyzer(analyzer), ext, data, res) - t.Fatalf("COMPUTEALLMETRICS DIVERGENCE [%s]: %s\n %s\n distilled -> %s", - analyzer, res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } - } - }) -} - -// FuzzComputeAllMetricsGo digs into the analyzer's pure metric math for the ONE -// language whose grammar is wired in Rust (Go), so the mutator reaches scoring / -// status-label / counting branches instead of the grammar gap. Two pure, -// deterministic analyzers are probed. -func FuzzComputeAllMetricsGo(f *testing.F) { - for _, s := range loadSeeds("go") { - f.Add(s.Data) - } - f.Fuzz(func(t *testing.T, data []byte) { - for _, analyzer := range []string{"static/complexity", "static/composition"} { - dir, _ := writeTemp(t, "go", data) - argv := staticRunArgv(dir, analyzer, "json") - res, err := differential(argv, 3, 30*time.Second) - removeAll(dir) - if err != nil { - t.Skipf("oracle exec error (%s): %v", analyzer, err) - } - if !res.OK { - path, _ := distill("metrics_go_"+sanitizeAnalyzer(analyzer), "go", data, res) - t.Fatalf("GO-COMPUTEALLMETRICS DIVERGENCE [%s]: %s\n %s\n -> %s", - analyzer, res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } - } - }) -} - -func sanitizeAnalyzer(a string) string { - out := make([]byte, 0, len(a)) - for i := 0; i < len(a); i++ { - c := a[i] - if c == '/' { - c = '-' - } - out = append(out, c) - } - return string(out) -} - -var _ = time.Second diff --git a/tests/compat/fuzz/go.mod b/tests/compat/fuzz/go.mod deleted file mode 100644 index fc086b04..00000000 --- a/tests/compat/fuzz/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -// Standalone module for the Go-native differential fuzz targets. The main -// repository no longer carries a root go.mod (the Go implementation was -// superseded by the Rust rewrite), but this harness is deliberately written -// in Go: `testing/F` drives the LIVE frozen Go oracle binary and the Rust -// binary as subprocesses and diffs their output. Stdlib-only. -module compatfuzz - -go 1.22 diff --git a/tests/compat/fuzz/harness.go b/tests/compat/fuzz/harness.go deleted file mode 100644 index 539299d7..00000000 --- a/tests/compat/fuzz/harness.go +++ /dev/null @@ -1,524 +0,0 @@ -// Package compatfuzz hosts Go-native differential fuzz targets (testing/F), one -// per PURE stage of the codefang/uast pipeline. Each target feeds the SAME input -// to the LIVE Go binary and the Rust binary (as subprocesses, under the pinned -// env) and FAILS on any divergence. -// -// NON-NEGOTIABLE design rules (encode why the prior effort failed): -// -// 1. ORACLE = the LIVE Go binary at $ROOT/build/bin/{codefang,uast}. We never -// re-derive expected output in Go or Rust here; we run the actual Go binary. -// 2. Canonicalization is MEASURED, never declared. For each input we run Go -// N>=3 times; ONLY JSON leaf fields that VARY across Go's own runs may be -// normalized, and the differing Go outputs are stored as evidence. Blanking -// a Go-STABLE field is the cheat that hid a real bug before; canonByMeasure -// refuses to neutralize any field Go was stable on. -// 3. Run env is pinned exactly: TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C -// SOURCE_DATE_EPOCH=315532800; argv passed as a list (no shell glob). We -// compare STDOUT only (stderr is progress). -// -// A divergence-finding input is distilled back into ../corpus/fuzzfinds/. -package compatfuzz - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "sort" - "time" -) - -// --------------------------------------------------------------------------- -// Paths & pinned env -// --------------------------------------------------------------------------- - -// repoRoot resolves $ROOT from this file's location (…/tests/compat/fuzz). -func repoRoot() string { - _, self, _, ok := runtime.Caller(0) - if !ok { - panic("cannot resolve caller for repoRoot") - } - // self = …/tests/compat/fuzz/harness.go; Dir(self) = …/tests/compat/fuzz, - // so three levels up is the repo root. - return filepath.Clean(filepath.Join(filepath.Dir(self), "..", "..", "..")) -} - -func goBin(name string) string { return filepath.Join(repoRoot(), "build", "bin", name) } - -// rustBin resolves the Rust binary. CFFUZZ_RUST_DIR overrides the directory and -// is used EXCLUSIVELY by the self-proving tests to inject a buggy/shim "Rust" -// and assert the harness reports FAIL. It must never be set in a real run. -func rustBin(name string) string { - if d := os.Getenv("CFFUZZ_RUST_DIR"); d != "" { - return filepath.Join(d, name) - } - // The Rust workspace moved from rust/ to the repo root when the rewrite - // superseded the Go implementation. - return filepath.Join(repoRoot(), "target", "release", name) -} - -func corpusFuzzFinds() string { - return filepath.Join(repoRoot(), "tests", "compat", "corpus", "fuzzfinds") -} - -var pinnedEnv = []string{ - "TZ=UTC", "NO_COLOR=1", "LANG=C", "LC_ALL=C", - "SOURCE_DATE_EPOCH=315532800", -} - -// pinnedEnviron returns the parent environment with the pinned vars forced on. -func pinnedEnviron() []string { - base := os.Environ() - keys := map[string]bool{} - for _, kv := range pinnedEnv { - eq := bytes.IndexByte([]byte(kv), '=') - keys[kv[:eq]] = true - } - out := base[:0:0] - for _, kv := range base { - eq := bytes.IndexByte([]byte(kv), '=') - if eq < 0 || !keys[kv[:eq]] { - out = append(out, kv) - } - } - return append(out, pinnedEnv...) -} - -// --------------------------------------------------------------------------- -// Subprocess execution: the binaries ARE the oracle. -// --------------------------------------------------------------------------- - -// side selects which implementation to run. -type side int - -const ( - sideGo side = iota - sideRust -) - -// resolve maps a logical argv[0] to a real binary + prefix args, mirroring the -// parity_gate.sh / oracle.py convention: "uast …" -> uast binary; everything -// else (e.g. "run …", "version") -> codefang binary with argv[0] kept. -func resolve(s side, argv0 string) (string, []string) { - bin := func(n string) string { - if s == sideGo { - return goBin(n) - } - return rustBin(n) - } - if argv0 == "uast" { - return bin("uast"), nil - } - return bin("codefang"), []string{argv0} -} - -// runOnce runs the LIVE binary once under the pinned env; returns stdout bytes. -// stderr is discarded (it is progress, per the contract). A timeout guards -// against pathological fuzz inputs. -func runOnce(s side, argv []string, timeout time.Duration) (rc int, stdout []byte, err error) { - exe, prefix := resolve(s, argv[0]) - args := append(append([]string{}, prefix...), argv[1:]...) - cmd := exec.Command(exe, args...) - cmd.Env = pinnedEnviron() - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = nil - - done := make(chan error, 1) - if e := cmd.Start(); e != nil { - return -1, nil, e - } - go func() { done <- cmd.Wait() }() - select { - case e := <-done: - rc = cmd.ProcessState.ExitCode() - return rc, out.Bytes(), e - case <-time.After(timeout): - _ = cmd.Process.Kill() - <-done - return -1, out.Bytes(), fmt.Errorf("timeout after %s", timeout) - } -} - -func shaHex(b []byte) string { - s := sha256.Sum256(b) - return hex.EncodeToString(s[:]) -} - -// --------------------------------------------------------------------------- -// MEASURED canonicalization. -// -// We run Go N times on the same input. Fields STABLE across all N Go runs MUST -// match byte-exact; fields that VARY across Go's own runs are the only ones we -// may neutralize. We never declare a field nondeterministic — we observe it. -// --------------------------------------------------------------------------- - -// leafPaths yields {jsonpath: canonical-value-string} for every JSON leaf. -func leafPaths(obj any, prefix string, out map[string]string) { - switch v := obj.(type) { - case map[string]any: - keys := make([]string, 0, len(v)) - for k := range v { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - leafPaths(v[k], prefix+"."+k, out) - } - case []any: - for i, e := range v { - leafPaths(e, fmt.Sprintf("%s[%d]", prefix, i), out) - } - default: - b, _ := json.Marshal(v) - out[prefix] = string(b) - } -} - -func fieldMap(b []byte) (map[string]string, bool) { - var j any - if err := json.Unmarshal(b, &j); err != nil { - return map[string]string{"$<bytes>": shaHex(b)}, false - } - m := map[string]string{} - leafPaths(j, "$", m) - return m, true -} - -// measureGoVariance runs Go n times and returns: -// - the set of field paths that VARIED across Go's own runs (legitimately -// canonicalizable), and -// - the list of raw Go outputs (evidence). -// -// If outputs are not JSON, variance is measured on the whole-byte blob. -func measureGoVariance(argv []string, n int, timeout time.Duration) (variant map[string]bool, runs [][]byte, err error) { - variant = map[string]bool{} - maps := make([]map[string]string, 0, n) - for i := 0; i < n; i++ { - _, out, e := runOnce(sideGo, argv, timeout) - if e != nil { - return nil, runs, fmt.Errorf("go run %d: %w", i, e) - } - runs = append(runs, out) - fm, _ := fieldMap(out) - maps = append(maps, fm) - } - // union of keys - keys := map[string]bool{} - for _, m := range maps { - for k := range m { - keys[k] = true - } - } - for k := range keys { - first, seen := "", false - for _, m := range maps { - v, ok := m[k] - if !ok { - variant[k] = true // present in some runs, absent in others - break - } - if !seen { - first, seen = v, true - } else if v != first { - variant[k] = true - break - } - } - } - return variant, runs, nil -} - -// canonByMeasure produces a canonical form of an output, neutralizing ONLY the -// fields measured as Go-variant. Go-stable fields are left untouched (byte -// content participates in the comparison). For non-JSON outputs we either pass -// the raw bytes through (when Go was byte-stable) or accept any bytes (when Go -// itself varied at the blob level). -func canonByMeasure(b []byte, variant map[string]bool, jsonMode bool) string { - if !jsonMode { - if variant["$<bytes>"] { - return "<go-variant-bytes>" - } - return shaHex(b) - } - var j any - if err := json.Unmarshal(b, &j); err != nil { - // Rust produced non-JSON where Go produced JSON: a real divergence. - return "<<NON-JSON:" + shaHex(b) + ">>" - } - var neutralize func(obj any, prefix string) any - neutralize = func(obj any, prefix string) any { - switch v := obj.(type) { - case map[string]any: - m := make(map[string]any, len(v)) - for k, e := range v { - m[k] = neutralize(e, prefix+"."+k) - } - return m - case []any: - a := make([]any, len(v)) - for i, e := range v { - a[i] = neutralize(e, fmt.Sprintf("%s[%d]", prefix, i)) - } - return a - default: - if variant[prefix] { - return "<go-variant>" - } - return v - } - } - out := neutralize(j, "$") - bb, _ := json.Marshal(canonSort(out)) - return string(bb) -} - -// canonSort recursively sorts maps (json.Marshal already sorts map keys) and -// leaves lists in order — order differences in Go-stable lists are REAL -// divergences and must not be hidden. -func canonSort(o any) any { return o } - -// --------------------------------------------------------------------------- -// The differential check used by every fuzz target. -// --------------------------------------------------------------------------- - -// diffResult is the structured verdict of one differential comparison. -type diffResult struct { - OK bool - GoOut []byte - RustOut []byte - Variant map[string]bool - Evidence [][]byte // the N Go runs - JSONMode bool - Reason string -} - -// differential runs the MEASURED differential check for one (stage, argv): -// - measure Go variance over goRuns runs (>=3), -// - run Rust once, -// - compare canonical-by-measure; Go-stable fields must match byte-exact. -// -// It returns OK=false on any divergence. It does NOT itself call t.Fatal so the -// self-check can assert on a planted divergence. -func differential(argv []string, goRuns int, timeout time.Duration) (diffResult, error) { - if goRuns < 3 { - goRuns = 3 - } - variant, runs, err := measureGoVariance(argv, goRuns, timeout) - if err != nil { - return diffResult{}, err - } - _, rust, err := runOnce(sideRust, argv, timeout) - if err != nil { - return diffResult{Reason: "rust exec error: " + err.Error()}, nil - } - goRef := runs[0] - _, jsonMode := fieldMap(goRef) - - gc := canonByMeasure(goRef, variant, jsonMode) - rc := canonByMeasure(rust, variant, jsonMode) - - res := diffResult{ - GoOut: goRef, RustOut: rust, Variant: variant, - Evidence: runs, JSONMode: jsonMode, - } - if gc == rc { - res.OK = true - return res, nil - } - res.OK = false - res.Reason = fmt.Sprintf("canonical divergence (go=%dB rust=%dB, variant-fields=%d)", - len(goRef), len(rust), len(variant)) - return res, nil -} - -// firstDiff returns a short hexdump-style description of the first byte that -// differs between a and b, to localize a divergence. -func firstDiff(a, b []byte) string { - n := len(a) - if len(b) < n { - n = len(b) - } - for i := 0; i < n; i++ { - if a[i] != b[i] { - lo := i - 16 - if lo < 0 { - lo = 0 - } - return fmt.Sprintf("first diff @%d: go=%q rust=%q", - i, snippet(a, lo, i+16), snippet(b, lo, i+16)) - } - } - if len(a) != len(b) { - return fmt.Sprintf("length differs: go=%dB rust=%dB", len(a), len(b)) - } - return "no byte diff (canonical-only divergence)" -} - -func snippet(b []byte, lo, hi int) string { - if lo < 0 { - lo = 0 - } - if hi > len(b) { - hi = len(b) - } - return string(b[lo:hi]) -} - -// divergenceClass keys a divergence by its SHAPE, not its exact input bytes, so -// the distilled corpus keeps the SMALLEST set that still preserves every -// DISTINCT finding (SPEC §3 "distilled: minimized while preserving combined -// coverage"). Fuzzer minimization can emit hundreds of near-duplicate inputs -// for one underlying bug; we store one representative per class. -func divergenceClass(stage string, res diffResult) string { - if res.JSONMode { - // Class = the SET OF FIELD KEYS (last path segment) whose value differs - // between Go and Rust, ignoring array indices / offsets. This collapses - // "Rust computes a different node id" (one bug) into a single class - // regardless of WHERE in the tree it surfaces. - keys := diffKeySet(res.GoOut, res.RustOut) - return shaHex([]byte(stage+"|json|"+keys))[:16] - } - // Non-JSON: classify by whether it's a length/first-byte/empty divergence and - // by the Rust-output emptiness (constant-stub vs partial). - shape := "bytes" - if len(res.RustOut) == 0 { - shape = "rust-empty" - } else if len(res.GoOut) == 0 { - shape = "go-empty" - } - return shaHex([]byte(stage+"|raw|"+shape))[:16] -} - -// diffKeySet returns a sorted, comma-joined set of the LAST path segments whose -// leaf value differs between two JSON outputs (or a structural marker if one is -// non-JSON / array-shaped differently). -func diffKeySet(a, b []byte) string { - ma, oka := fieldMap(a) - mb, okb := fieldMap(b) - if !oka || !okb { - return "<non-json>" - } - keys := map[string]bool{} - all := map[string]bool{} - for k := range ma { - all[k] = true - } - for k := range mb { - all[k] = true - } - for p := range all { - if ma[p] != mb[p] { - keys[lastSegment(p)] = true - } - } - out := make([]string, 0, len(keys)) - for k := range keys { - out = append(out, k) - } - sort.Strings(out) - return fmt.Sprintf("%v", out) -} - -func lastSegment(path string) string { - // strip trailing [i] index and take the segment after the last '.' - end := len(path) - for i := len(path) - 1; i >= 0; i-- { - if path[i] == ']' { - // find matching '[' - for j := i; j >= 0; j-- { - if path[j] == '[' { - end = j - i = j - break - } - } - continue - } - if path[i] == '.' { - return path[i+1 : end] - } - } - return path[:end] -} - -func indexByte(s string, c byte) int { - for i := 0; i < len(s); i++ { - if s[i] == c { - return i - } - } - return -1 -} - -func firstNonEmpty(bs [][]byte) []byte { - for _, b := range bs { - if len(b) > 0 { - return b - } - } - return nil -} - -// distill writes a divergence-finding input back into the regression corpus so a -// later run reproduces it. It is deduplicated by divergence CLASS: if a -// representative of the same class is already stored, this is a no-op (keeps the -// corpus minimal). Returns the stored path. -func distill(stage, ext string, input []byte, res diffResult) (string, error) { - dir := corpusFuzzFinds() - if err := os.MkdirAll(dir, 0o755); err != nil { - return "", err - } - cls := divergenceClass(stage, res) - // Class-marker file: if it exists, we already captured this divergence shape. - marker := filepath.Join(dir, ".class_"+cls) - if _, err := os.Stat(marker); err == nil { - return marker, nil - } - h := shaHex(input) - base := filepath.Join(dir, fmt.Sprintf("%s_%s.%s", stage, h[:16], ext)) - if err := os.WriteFile(base, input, 0o644); err != nil { - return "", err - } - _ = os.WriteFile(marker, []byte(base+"\n"), 0o644) - // store evidence (the N Go runs) + the Rust output + reason - meta := map[string]any{ - "stage": stage, - "input_sha256": h, - "reason": res.Reason, - "json_mode": res.JSONMode, - "variant_fields": keysOf(res.Variant), - "go_runs_sha256": shaList(res.Evidence), - "rust_out_sha256": shaHex(res.RustOut), - "first_diff": firstDiff(res.GoOut, res.RustOut), - } - mb, _ := json.MarshalIndent(meta, "", " ") - _ = os.WriteFile(base+".evidence.json", mb, 0o644) - for i, r := range res.Evidence { - _ = os.WriteFile(fmt.Sprintf("%s.go_run_%d", base, i), r, 0o644) - } - _ = os.WriteFile(base+".rust_out", res.RustOut, 0o644) - return base, nil -} - -func keysOf(m map[string]bool) []string { - out := make([]string, 0, len(m)) - for k := range m { - out = append(out, k) - } - sort.Strings(out) - return out -} - -func shaList(bs [][]byte) []string { - out := make([]string, len(bs)) - for i, b := range bs { - out[i] = shaHex(b) - } - return out -} diff --git a/tests/compat/fuzz/map_fuzz_test.go b/tests/compat/fuzz/map_fuzz_test.go deleted file mode 100644 index 589e7b8f..00000000 --- a/tests/compat/fuzz/map_fuzz_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package compatfuzz - -import ( - "testing" - "time" -) - -// FuzzMap — PURE STAGE 2: UAST mapping / structural analysis. -// -// `uast analyze --format json <file>` runs the tree-sitter parse THROUGH the -// UAST mapping layer and reports the mapped tree's composition. This exercises -// the map stage (node-type → UAST-type translation, role assignment) beyond raw -// parse. Differential vs the LIVE Go binary, measured-canonical comparison. -func FuzzMap(f *testing.F) { - seeds := loadSeeds() - for _, s := range seeds { - f.Add(s.Ext, s.Data) - } - if len(seeds) == 0 { - f.Skip("no seed corpus found") - } - f.Fuzz(func(t *testing.T, ext string, data []byte) { - ext = sanitizeExt(ext) - dir, file := writeTemp(t, ext, data) - defer removeAll(dir) - - argv := []string{"uast", "analyze", "--format", "json", file} - res, err := differential(argv, 3, 20*time.Second) - if err != nil { - t.Skipf("oracle exec error: %v", err) - } - if !res.OK { - path, _ := distill("map", ext, data, res) - t.Fatalf("UAST-MAP DIVERGENCE: %s\n %s\n distilled -> %s", - res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } - }) -} - -// FuzzQuery — also exercises the map stage via the query DSL evaluator over the -// mapped tree. A divergence here localizes to map+query rather than parse. -func FuzzQuery(f *testing.F) { - seeds := loadSeeds("go", "py", "ts", "tsx", "js", "c", "cpp", "rs") - for _, s := range seeds { - f.Add(s.Ext, s.Data) - } - if len(seeds) == 0 { - f.Skip("no seed corpus found") - } - f.Fuzz(func(t *testing.T, ext string, data []byte) { - ext = sanitizeExt(ext) - dir, file := writeTemp(t, ext, data) - defer removeAll(dir) - - // A fixed, structurally meaningful query (mirrors parity_gate.sh). - argv := []string{"uast", "query", - `filter(.roles has "Function")`, "--format", "json", file} - res, err := differential(argv, 3, 20*time.Second) - if err != nil { - t.Skipf("oracle exec error: %v", err) - } - if !res.OK { - path, _ := distill("query", ext, data, res) - t.Fatalf("QUERY DIVERGENCE: %s\n %s\n distilled -> %s", - res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } - }) -} diff --git a/tests/compat/fuzz/parse_fuzz_test.go b/tests/compat/fuzz/parse_fuzz_test.go deleted file mode 100644 index 4bd8ebc4..00000000 --- a/tests/compat/fuzz/parse_fuzz_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package compatfuzz - -import ( - "testing" - "time" -) - -// FuzzParse — PURE STAGE 1: tree-sitter parse. -// -// Feeds the same source bytes (with a language-carrying extension) to -// uast parse --format json <file> -// on the LIVE Go binary and on the Rust binary, and FAILS on divergence. -// Parse output is fully deterministic in Go (verified: ids are content hashes, -// no timestamps), so the measured Go variance is expected to be EMPTY and the -// comparison is effectively byte-exact — but it is still MEASURED, not assumed. -func FuzzParse(f *testing.F) { - seeds := loadSeeds() // all languages - for _, s := range seeds { - f.Add(s.Ext, s.Data) - } - if len(seeds) == 0 { - f.Skip("no seed corpus found") - } - f.Fuzz(func(t *testing.T, ext string, data []byte) { - ext = sanitizeExt(ext) - dir, file := writeTemp(t, ext, data) - defer removeAll(dir) - - argv := []string{"uast", "parse", "--format", "json", file} - res, err := differential(argv, 3, 20*time.Second) - if err != nil { - t.Skipf("oracle exec error (input not gradeable): %v", err) - } - if !res.OK { - path, _ := distill("parse", ext, data, res) - t.Fatalf("PARSE DIVERGENCE: %s\n %s\n distilled -> %s", - res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } - }) -} - -// FuzzParseGo restricts mutation to Go source — the ONE language whose -// tree-sitter grammar is currently wired in the Rust port. This lets the -// coverage-guided mutator dig DEEP into parse parity for a supported language -// instead of bouncing off the "grammar not wired" gap that FuzzParse exposes for -// every other language. (Once more grammars are wired in Rust, drop this and -// rely on FuzzParse across all languages.) -func FuzzParseGo(f *testing.F) { - seeds := loadSeeds("go") - for _, s := range seeds { - f.Add(s.Data) - } - if len(seeds) == 0 { - f.Skip("no Go seed corpus found") - } - f.Fuzz(func(t *testing.T, data []byte) { - dir, file := writeTemp(t, "go", data) - defer removeAll(dir) - argv := []string{"uast", "parse", "--format", "json", file} - res, err := differential(argv, 3, 20*time.Second) - if err != nil { - t.Skipf("oracle exec error: %v", err) - } - if !res.OK { - path, _ := distill("parse_go", "go", data, res) - t.Fatalf("GO-PARSE DIVERGENCE: %s\n %s\n distilled -> %s", - res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } - }) -} diff --git a/tests/compat/fuzz/run_fuzz.sh b/tests/compat/fuzz/run_fuzz.sh deleted file mode 100755 index 54a1a202..00000000 --- a/tests/compat/fuzz/run_fuzz.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# Bounded differential fuzz session driver for the Go<->Rust compat system. -# -# Runs each per-stage Go-native (testing/F) differential fuzz target for a short, -# bounded wall-clock budget. Each target feeds the SAME input to the LIVE Go -# binary (the oracle) and the Rust binary under the pinned env and FAILS on any -# divergence; divergence-finding inputs are distilled into -# tests/compat/corpus/fuzzfinds/ -# with the differing Go outputs stored as EVIDENCE. -# -# Usage: -# run_fuzz.sh [seconds-per-target] (default 20) -# FUZZ_ONLY=FuzzParse run_fuzz.sh (limit to one target) -# -# Exit 0 only if NO target found a divergence in its budget. (The seed pass — a -# plain `go test` over the corpus — runs first and surfaces seed-level -# divergences immediately.) -set -u -ROOT=/home/dmitriy/sources/codefang -# The fuzz targets live in their own stdlib-only Go module (the repo root has -# no go.mod since the Rust rewrite superseded the Go implementation), so run -# `go test` from inside the module directory. harness.go resolves $ROOT from -# its own file path, not the cwd. -PKG=. -BUDGET="${1:-20}" -cd "$ROOT/tests/compat/fuzz" || exit 2 - -TARGETS=(FuzzParse FuzzMap FuzzQuery FuzzSerializerJSON FuzzSerializerYAML FuzzSerializerCFB1 FuzzComputeAllMetrics) -[ -n "${FUZZ_ONLY:-}" ] && TARGETS=("$FUZZ_ONLY") - -echo "================ DIFFERENTIAL FUZZ SESSION ================" -echo "budget=${BUDGET}s/target oracle=build/bin candidate=target/release" -echo - -# 1) self-proving meta-tests FIRST: the layer must be shown to catch a bug. -echo "-- self-check (must catch planted defects) --" -if ! go test "$PKG" -run 'TestSelfCheck' -count=1 >/tmp/cffuzz_self.log 2>&1; then - echo "SELF-CHECK FAILED — the fuzz layer cannot be trusted:" - cat /tmp/cffuzz_self.log - exit 1 -fi -echo " self-check OK (constant-stub, byte-flip, blank-stable-field all caught)" -echo - -# 2) seed pass: run every target's SEED corpus as ordinary subtests (no mutation) -# to surface corpus-level divergences immediately. -echo "-- seed pass (corpus, no mutation) --" -SEED_FAIL=0 -for tgt in "${TARGETS[@]}"; do - if go test "$PKG" -run "^${tgt}\$" -count=1 >/tmp/cffuzz_${tgt}.log 2>&1; then - echo " PASS ${tgt} (all seeds match)" - else - nf=$(grep -c 'DIVERGENCE' /tmp/cffuzz_${tgt}.log 2>/dev/null || echo 0) - echo " FAIL ${tgt} (${nf} seed divergence(s)) — see /tmp/cffuzz_${tgt}.log" - SEED_FAIL=1 - fi -done -echo - -# 3) bounded mutation fuzz per target. -echo "-- mutation fuzz (${BUDGET}s each) --" -FUZZ_FAIL=0 -for tgt in "${TARGETS[@]}"; do - printf ' %-22s ' "$tgt" - if go test "$PKG" -run '^$' -fuzz "^${tgt}\$" -fuzztime "${BUDGET}s" -count=1 \ - >/tmp/cffuzz_fuzz_${tgt}.log 2>&1; then - echo "OK (no new divergence in ${BUDGET}s)" - else - echo "DIVERGENCE — see /tmp/cffuzz_fuzz_${tgt}.log + corpus/fuzzfinds/" - FUZZ_FAIL=1 - fi -done - -echo -echo "================ RESULT ================" -FINDS=$(ls "$ROOT/tests/compat/corpus/fuzzfinds/" 2>/dev/null | grep -vcE '\.(evidence\.json|rust_out)$|\.go_run_' || echo 0) -echo "distilled divergence inputs: ${FINDS} (tests/compat/corpus/fuzzfinds/)" -if [ "$SEED_FAIL" -eq 0 ] && [ "$FUZZ_FAIL" -eq 0 ]; then - echo "FUZZ GATE: GREEN — Go and Rust agree on every probed input" - exit 0 -fi -echo "FUZZ GATE: RED — differential divergence(s) recorded above" -exit 1 diff --git a/tests/compat/fuzz/seed.go b/tests/compat/fuzz/seed.go deleted file mode 100644 index 4026a612..00000000 --- a/tests/compat/fuzz/seed.go +++ /dev/null @@ -1,110 +0,0 @@ -package compatfuzz - -import ( - "os" - "path/filepath" - "sort" - "strings" -) - -// Seed corpora are STRUCTURE-AWARE: we seed from REAL source files (the -// content-addressed corpus under ../corpus/files plus any prior fuzzfinds), not -// random bytes — random bytes bounce off the tree-sitter parser and never reach -// the analyzer/serializer logic (SPEC §3, Directed Grammar-Based Test Gen). -// -// The Go fuzzer then mutates these valid inputs; because mutations start from -// valid source they stay close enough to the grammar to exercise deep stages. - -func corpusFilesDir() string { - return filepath.Join(repoRoot(), "tests", "compat", "corpus", "files") -} - -// seedFile holds a seed input with the file extension that drives language -// detection in both binaries. -type seedFile struct { - Ext string - Data []byte -} - -// loadSeeds returns seed files from the corpus whose extension is in want. -// If want is empty, every corpus file is returned. -func loadSeeds(want ...string) []seedFile { - wantSet := map[string]bool{} - for _, e := range want { - wantSet[strings.TrimPrefix(e, ".")] = true - } - var out []seedFile - addFrom := func(dir string) { - entries, err := os.ReadDir(dir) - if err != nil { - return - } - names := make([]string, 0, len(entries)) - for _, e := range entries { - if !e.IsDir() { - names = append(names, e.Name()) - } - } - sort.Strings(names) - for _, n := range names { - // Skip distill sidecars so evidence artifacts are never re-seeded - // as inputs (corpus hygiene): *.evidence.json, *.go_run_N, *.rust_out. - if strings.HasSuffix(n, ".evidence.json") || - strings.HasSuffix(n, ".rust_out") || - strings.Contains(n, ".go_run_") || - strings.HasPrefix(n, ".class_") { - continue - } - ext := strings.TrimPrefix(filepath.Ext(n), ".") - if ext == "" { - continue - } - if len(wantSet) > 0 && !wantSet[ext] { - continue - } - b, err := os.ReadFile(filepath.Join(dir, n)) - if err != nil { - continue - } - out = append(out, seedFile{Ext: ext, Data: b}) - } - } - addFrom(corpusFilesDir()) - addFrom(corpusFuzzFinds()) // re-seed from prior divergence finds - return out -} - -// pickExt chooses an extension for a fuzz-mutated payload. The fuzzer mutates -// bytes of a seed; we keep the seed's original language by carrying its ext in -// a separate, non-mutated fuzz argument. -func writeTemp(t fataler, ext string, data []byte) (dir, file string) { - d, err := os.MkdirTemp("", "cffuzz-") - if err != nil { - t.Fatalf("mktemp: %v", err) - } - f := filepath.Join(d, "input."+sanitizeExt(ext)) - if err := os.WriteFile(f, data, 0o644); err != nil { - t.Fatalf("write temp: %v", err) - } - return d, f -} - -// sanitizeExt guards against a fuzz-mutated extension containing path/control -// characters; we only ever use extensions from a fixed allowlist anyway. -func sanitizeExt(ext string) string { - ext = strings.TrimPrefix(ext, ".") - for _, c := range ext { - if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') { - return "txt" - } - } - if ext == "" { - return "txt" - } - return ext -} - -// fataler is the subset of *testing.T / *testing.F we need. -type fataler interface { - Fatalf(format string, args ...any) -} diff --git a/tests/compat/fuzz/selfcheck_test.go b/tests/compat/fuzz/selfcheck_test.go deleted file mode 100644 index 15be74aa..00000000 --- a/tests/compat/fuzz/selfcheck_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package compatfuzz - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - "time" -) - -// SELF-PROVING TESTS (SPEC §4 Testing Strategy, rule 6). -// -// A green that cannot be shown to catch a planted bug is worthless. These tests -// inject KNOWN defects and assert the harness reports FAIL / detects the tamper. -// If any of these PASSES the planted bug, the whole fuzz layer is untrustworthy -// and CI must go red. - -// fakeRustBin builds a one-shot shim binary that wraps the REAL Rust binary but -// CORRUPTS its stdout, simulating a buggy port. Returns the shim path + cleanup. -func fakeRustBin(t *testing.T, mode string) (dir string) { - t.Helper() - d, err := os.MkdirTemp("", "fakerust-") - if err != nil { - t.Fatalf("mktemp: %v", err) - } - // shim "codefang" and "uast" both: a tiny shell wrapper. - for _, name := range []string{"codefang", "uast"} { - real := rustBin(name) - script := "#!/usr/bin/env bash\n" + - "set -euo pipefail\n" + - "out=\"$(" + shellQuote(real) + " \"$@\")\"\n" - switch mode { - case "corrupt": - // flip output to a constant, hiding all real bytes (worst-case stub) - script += "printf 'CONSTANT_STUB_OUTPUT'\n" - case "byteflip": - // emit output but flip one stable byte (subtle metric bug) - script += "printf '%s' \"$out\" | sed 's/COMPLEXITY/COMPLEXITZ/'\n" - default: - script += "printf '%s' \"$out\"\n" - } - p := filepath.Join(d, name) - if err := os.WriteFile(p, []byte(script), 0o755); err != nil { - t.Fatalf("write shim: %v", err) - } - } - return d -} - -func shellQuote(s string) string { - return "'" + s + "'" -} - -// withRustDir temporarily redirects rustBin to a shim directory by overriding -// the resolver via an env knob. We implement the override directly: rustBin -// reads CFFUZZ_RUST_DIR if set. -func withRustDir(t *testing.T, dir string, fn func()) { - t.Helper() - old, had := os.LookupEnv("CFFUZZ_RUST_DIR") - _ = os.Setenv("CFFUZZ_RUST_DIR", dir) - defer func() { - if had { - _ = os.Setenv("CFFUZZ_RUST_DIR", old) - } else { - _ = os.Unsetenv("CFFUZZ_RUST_DIR") - } - }() - fn() -} - -// TestSelfCheck_DetectsConstantStub plants a Rust that emits a constant and -// asserts differential() reports OK=false. This is the canonical simulation -// signature the prior effort shipped. -func TestSelfCheck_DetectsConstantStub(t *testing.T) { - src := goodGoSource() - dir, _ := writeTemp(t, "go", src) - defer removeAll(dir) - argv := staticRunArgv(dir, "static/complexity", "json") - - shim := fakeRustBin(t, "corrupt") - defer removeAll(shim) - - var res diffResult - withRustDir(t, shim, func() { - var err error - res, err = differential(argv, 3, 30*time.Second) - if err != nil { - t.Fatalf("oracle exec error: %v", err) - } - }) - if res.OK { - t.Fatal("SELF-CHECK FAILED: harness PASSED a constant-output stub (it must FAIL)") - } - t.Logf("ok: harness caught constant stub: %s", res.Reason) -} - -// TestSelfCheck_DetectsByteFlip plants a Rust that flips a single Go-STABLE byte -// and asserts FAIL. This proves the harness does NOT blank stable fields. -func TestSelfCheck_DetectsByteFlip(t *testing.T) { - src := goodGoSource() - dir, _ := writeTemp(t, "go", src) - defer removeAll(dir) - argv := staticRunArgv(dir, "static/complexity", "json") - - shim := fakeRustBin(t, "byteflip") - defer removeAll(shim) - - var res diffResult - withRustDir(t, shim, func() { - var err error - res, err = differential(argv, 3, 30*time.Second) - if err != nil { - t.Fatalf("oracle exec error: %v", err) - } - }) - if res.OK { - t.Fatal("SELF-CHECK FAILED: harness PASSED a single-byte flip on a Go-STABLE field") - } - t.Logf("ok: harness caught byte flip: %s", res.Reason) -} - -// TestSelfCheck_RealRustPasses confirms the harness is not stuck-red: the REAL -// Rust binary must PASS on the same input. (If this fails, the divergence is a -// real Rust bug, which is also a valid — and important — outcome to surface.) -func TestSelfCheck_RealRustPasses(t *testing.T) { - src := goodGoSource() - dir, _ := writeTemp(t, "go", src) - defer removeAll(dir) - argv := staticRunArgv(dir, "static/complexity", "json") - res, err := differential(argv, 3, 30*time.Second) - if err != nil { - t.Fatalf("oracle exec error: %v", err) - } - if !res.OK { - path, _ := distill("selfcheck_realbug", "go", src, res) - t.Fatalf("REAL Rust diverges from Go on baseline input: %s\n %s\n -> %s", - res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } -} - -// TestSelfCheck_TamperBlankStableField proves canonByMeasure REFUSES to hide a -// Go-stable field. We simulate the exact prior cheat: take a real Go JSON -// output, measure ZERO variant fields (Go was stable), then verify that two -// outputs differing on a stable field do NOT canonicalize equal. -func TestSelfCheck_TamperBlankStableField(t *testing.T) { - goJSON := []byte(`{"score":7,"label":"COMPLEXITY","sections":[{"v":3}]}`) - // Measured variance is EMPTY (Go stable). A buggy Rust changed "score". - rustJSON := []byte(`{"score":9,"label":"COMPLEXITY","sections":[{"v":3}]}`) - variant := map[string]bool{} // measured: nothing varied across Go runs - - gc := canonByMeasure(goJSON, variant, true) - rc := canonByMeasure(rustJSON, variant, true) - if gc == rc { - t.Fatal("TAMPER SELF-CHECK FAILED: canonByMeasure equated a Go-STABLE differing field (blanking cheat not detected)") - } - - // Sanity: if (and ONLY if) Go was MEASURED variant on $.score, then it may - // canonicalize equal — proving canonicalization is driven by measurement. - variant2 := map[string]bool{"$.score": true} - if canonByMeasure(goJSON, variant2, true) != canonByMeasure(rustJSON, variant2, true) { - t.Fatal("TAMPER SELF-CHECK FAILED: measured-variant field was not canonicalized") - } - t.Log("ok: canonByMeasure neutralizes ONLY measured-variant fields") -} - -// TestSelfCheck_VarianceMeasuredNotDeclared proves the measurement layer: -// running Go N times on a deterministic invocation yields ZERO variant fields -// (so nothing is canonicalized away on a stable analyzer). -func TestSelfCheck_VarianceMeasuredNotDeclared(t *testing.T) { - src := goodGoSource() - dir, _ := writeTemp(t, "go", src) - defer removeAll(dir) - argv := staticRunArgv(dir, "static/complexity", "json") - variant, runs, err := measureGoVariance(argv, 3, 30*time.Second) - if err != nil { - t.Fatalf("measure: %v", err) - } - if len(runs) != 3 { - t.Fatalf("expected 3 Go runs of evidence, got %d", len(runs)) - } - // Evidence is real JSON. - var j any - if err := json.Unmarshal(runs[0], &j); err != nil { - t.Fatalf("Go output is not JSON: %v", err) - } - t.Logf("ok: measured %d Go-variant fields on static/complexity (expected ~0 for a deterministic analyzer)", len(variant)) -} - -func goodGoSource() []byte { - return []byte(`package main - -import "fmt" - -func classify(n int) string { - if n < 0 { - return "neg" - } else if n == 0 { - return "zero" - } - for i := 0; i < n; i++ { - if i%2 == 0 && i > 4 { - fmt.Println(i) - } - } - switch n { - case 1, 2, 3: - return "small" - default: - return "big" - } -} - -func main() { fmt.Println(classify(7)) } -`) -} - -var _ = time.Second diff --git a/tests/compat/fuzz/serializer_fuzz_test.go b/tests/compat/fuzz/serializer_fuzz_test.go deleted file mode 100644 index 5d240a8b..00000000 --- a/tests/compat/fuzz/serializer_fuzz_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package compatfuzz - -import ( - "os" - "testing" - "time" -) - -// staticRunArgv builds a deterministic single-directory static analyzer run. -// We pin every nondeterminism knob (--head --workers 1 --static-workers 1, -// no cache/checkpoint/resume) exactly as parity_gate.sh does, so the ONLY -// variable is the input source under dir. -func staticRunArgv(dir, analyzer, format string) []string { - return []string{"run", - "--checkpoint=false", "--resume=false", "--no-cache", - "--head", "--workers", "1", "--static-workers", "1", - "-p", dir, "--analyzers", analyzer, "--format", format} -} - -// removeAll is os.RemoveAll, named locally to keep imports tidy in test files. -func removeAll(p string) { _ = os.RemoveAll(p) } - -// runSerializerStage feeds mutated source through a real analyzer and serializes -// it with the chosen format, then diffs Go vs Rust. The analyzer is held fixed -// (static/complexity, fully deterministic in Go) so any divergence localizes to -// the SERIALIZER under test (cf-gojson / cf-goyaml / CFB1) rather than the -// metric computation. -func runSerializerStage(t *testing.T, stage, ext string, data []byte, format string) { - ext = sanitizeExt(ext) - dir, _ := writeTemp(t, ext, data) // writeTemp puts file inside dir - defer removeAll(dir) - - argv := staticRunArgv(dir, "static/complexity", format) - res, err := differential(argv, 3, 30*time.Second) - if err != nil { - t.Skipf("oracle exec error: %v", err) - } - if !res.OK { - path, _ := distill(stage, ext, data, res) - t.Fatalf("%s SERIALIZER DIVERGENCE: %s\n %s\n distilled -> %s", - stage, res.Reason, firstDiff(res.GoOut, res.RustOut), path) - } -} - -// FuzzSerializerJSON — PURE STAGE 3a: cf-gojson serializer. -func FuzzSerializerJSON(f *testing.F) { - for _, s := range loadSeeds("go", "py", "ts", "js", "c", "cpp", "rs") { - f.Add(s.Ext, s.Data) - } - f.Fuzz(func(t *testing.T, ext string, data []byte) { - runSerializerStage(t, "gojson", ext, data, "json") - }) -} - -// FuzzSerializerYAML — PURE STAGE 3b: cf-goyaml serializer. -func FuzzSerializerYAML(f *testing.F) { - for _, s := range loadSeeds("go", "py", "ts", "js", "c", "cpp", "rs") { - f.Add(s.Ext, s.Data) - } - f.Fuzz(func(t *testing.T, ext string, data []byte) { - runSerializerStage(t, "goyaml", ext, data, "yaml") - }) -} - -// FuzzSerializerCFB1 — PURE STAGE 3c: CFB1 binary envelope serializer. -func FuzzSerializerCFB1(f *testing.F) { - for _, s := range loadSeeds("go", "py", "ts", "js", "c", "cpp", "rs") { - f.Add(s.Ext, s.Data) - } - f.Fuzz(func(t *testing.T, ext string, data []byte) { - // CFB1 output is binary; differential() falls back to whole-blob - // measured comparison (Go-stable bytes must match exactly). - _ = time.Second - runSerializerStage(t, "cfb1", ext, data, "bin") - }) -} diff --git a/tests/compat/fuzz/testdata/fuzz/FuzzMap/c7c64cb562bb5f58 b/tests/compat/fuzz/testdata/fuzz/FuzzMap/c7c64cb562bb5f58 deleted file mode 100644 index ef12cff6..00000000 --- a/tests/compat/fuzz/testdata/fuzz/FuzzMap/c7c64cb562bb5f58 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -string("0") -[]byte("0") diff --git a/tests/compat/fuzz/testdata/fuzz/FuzzParse/d51a495543b4d625 b/tests/compat/fuzz/testdata/fuzz/FuzzParse/d51a495543b4d625 deleted file mode 100644 index 9ec93509..00000000 --- a/tests/compat/fuzz/testdata/fuzz/FuzzParse/d51a495543b4d625 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -string("go") -[]byte("") diff --git a/tests/compat/fuzz/testdata/fuzz/FuzzParseGo/3af36d37b930453f b/tests/compat/fuzz/testdata/fuzz/FuzzParseGo/3af36d37b930453f deleted file mode 100644 index 3cef6437..00000000 --- a/tests/compat/fuzz/testdata/fuzz/FuzzParseGo/3af36d37b930453f +++ /dev/null @@ -1,2 +0,0 @@ -go test fuzz v1 -[]byte("package main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gogo/protobuf/proto\"\n\t\"github.com/spf13/cobra\"\n\tprogress \"gopkg.in/cheggaaa/pb.v1\"\n\t\"gopkg.in/src-d/hercules.v10\"\n\t\"gopkg.in/src-d/hercules.v10/internal/pb\"\n)\n\n// combineCmd represents the combine command\nvar combineCmd = &cobra.Command{\n\tUse: \"combine\",\n\tShort: \"Merge several binary analysis results together.\",\n\tLong: ``,\n\tArgs: cobra.MinimumNArgs(1),\n\tRun: func(cmd *cobra.Command, files []string) {\n\t\tif len(files) == 1 {\n\t\t\tfile, err := os.Open(files[0])\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tdefer file.Close()\n\t\t\t_, err = io.Copy(os.Stdout, bufio.NewReader(file))\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tonly, err := cmd.Flags().GetString(\"only\")\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tvar repos []string\n\t\tallErrors := map[string][]string{}\n\t\tmergedResults := map[string]interface{}{}\n\t\tmergedMetadata := &hercules.CommonAnalysisResult{}\n\t\tvar fileName string\n\t\tbar := progress.New(len(files))\n\t\tbar.Callback = func(msg string) {\n\t\t\tos.Stderr.WriteString(\"\\033[2K\\r\" + msg + \" \" + fileName)\n\t\t}\n\t\tbar.NotPrint = true\n\t\tbar.ShowPercent = false\n\t\tbar.ShowSpeed = false\n\t\tbar.SetMaxWidth(80).Start()\n\t\tdebug.SetGCPercent(20)\n\t\tfor _, fileName = range files {\n\t\t\tbar.Increment()\n\t\t\tanotherResults, anotherMetadata, errs := loadMessage(fileName, &repos)\n\t\t\tif anotherMetadata != nil {\n\t\t\t\tmergeErrs := mergeResults(mergedResults, mergedMetadata, anotherResults, anotherMetadata, only)\n\t\t\t\tfor _, err := range mergeErrs {\n\t\t\t\t\terrs = append(errs, err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t\tallErrors[fileName] = errs\n\t\t\tdebug.FreeOSMemory()\n\t\t}\n\t\tbar.Finish()\n\t\tos.Stderr.WriteString(\"\\033[2K\\r\")\n\t\tprintErrors(allErrors)\n\t\tsort.Strings(repos)\n\t\tif mergedMetadata == nil {\n\t\t\treturn\n\t\t}\n\t\tmergedMessage := pb.AnalysisResults{\n\t\t\tHeader: &pb.Metadata{\n\t\t\t\tVersion: int32(hercules.BinaryVersion),\n\t\t\t\tHash: hercules.BinaryGitHash,\n\t\t\t\tRepository: strings.Join(repos, \" & \"),\n\t\t\t},\n\t\t\tContents: map[string][]byte{},\n\t\t}\n\t\tmergedMetadata.FillMetadata(mergedMessage.Header)\n\t\tfor key, val := range mergedResults {\n\t\t\tbuffer := bytes.Buffer{}\n\t\t\terr := hercules.Registry.Summon(key)[0].(hercules.LeafPipelineItem).Serialize(\n\t\t\t\tval, true, &buffer)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tmergedMessage.Contents[key] = buffer.Bytes()\n\t\t}\n\t\tserialized, err := proto.Marshal(&mergedMessage)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tos.Stdout.Write(serialized)\n\t},\n}\n\nfunc loadMessage(fileName string, repos *[]string) (\n\tmap[string]interface{}, *hercules.CommonAnalysisResult, []string) {\n\tvar errs []string\n\tfi, err := os.Stat(fileName)\n\tif err != nil {\n\t\terrs = append(errs, \"Cannot access \"+fileName+\": \"+err.Error())\n\t\treturn nil, nil, errs\n\t}\n\tif fi.Size() == 0 {\n\t\terrs = append(errs, \"Cannot parse \"+fileName+\": file size is 0\")\n\t\treturn nil, nil, errs\n\t}\n\tbuffer, err := ioutil.ReadFile(fileName)\n\tif err != nil {\n\t\terrs = append(errs, \"Cannot read \"+fileName+\": \"+err.Error())\n\t\treturn nil, nil, errs\n\t}\n\tmessage := pb.AnalysisResults{}\n\terr = proto.Unmarshal(buffer, &message)\n\tif err != nil {\n\t\terrs = append(errs, \"Cannot parse \"+fileName+\": \"+err.Error())\n\t\treturn nil, nil, errs\n\t}\n\tif message.Header == nil {\n\t\terrs = append(errs, \"Cannot parse \"+fileName+\": corrupted header\")\n\t\treturn nil, nil, errs\n\t}\n\t*repos = append(*repos, message.Header.Repository)\n\tresults := map[string]interface{}{}\n\tfor key, val := range message.Contents {\n\t\tsummoned := hercules.Registry.Summon(key)\n\t\tif len(summoned) == 0 {\n\t\t\terrs = append(errs, fileName+\": item not found: \"+key)\n\t\t\tcontinue\n\t\t}\n\t\tmpi, ok := summoned[0].(hercules.ResultMergeablePipelineItem@\n\t\tif !ok {\n\t\t\terrs = \xe0ppend(errs, fileName+\": \"+key+\": ResultMergeablePipelineItem is not implemented\")\n\t\t\tcontinue\n\t\t}\n\t\tmsg, err := mpi.Deserialize(val)\n\t\tif err != nil {\n\t\t\terrs = append(errs, fileName+\": deserialization failed: \"+key+\": \"+err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tresults[key] = msg\n\t}\n\treturn results, hercules.MetadataToCommonAnalysisResult(message.Header), errs\n}\n\nfunc printErrors(allErrors map[string][]string) {\n\tneedToPrintErrors := false\n\tfor _, errs := range allErrors {\n\t\tif len(errs) > 0 {\n\t\t\tneedToPrintErrors = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !needToPrintErrors {\n\t\treturn\n\t}\n\tfmt.Fprintln(os.Stderr, \"Errors:\")\n\tfor key, errs := range allErrors {\n\t\tif len(errs) > 0 {\n\t\t\tfmt.Fprintln(os.Stderr, \" \"+key)\n\t\t\tfor _, err := range errs {\n\t\t\t\tfmt.Fprintln(os.Stderr, \" \"+err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc mergeResults(mergedResults map[string]interface{},\n\tmergedCommons *hercules.CommonAnalysisResult,\n\tanotherResults map[string]interface{},\n\tanotherCommons *hercules.CommonAnalysisResult,\n\tonly string) []error {\n\tvar errors []error\n\tfor key, val := range anotherResults {\n\t\tif only != \"\" && key != only {\n\t\t\tcontinue\n\t\t}\n\t\tmergedResult, exists := mergedResults[key]\n\t\tif !exists {\n\t\t\tmergedResults[key] = val\n\t\t\tcontinue\n\t\t}\n\t\titem := hercules.Registry.Summon(key)[0].(hercules.ResultMergeablePipelineItem)\n\t\tmergedResult = item.MergeResults(mergedResult, val, mergedCommons, anotherCommons)\n\t\tif err, isErr := mergedResult.(error); isErr {\n\t\t\terrors = append(errors, fmt.Errorf(\"could not merge %s: %v\", item.Name(), err))\n\t\t} else {\n\t\t\tmergedResults[key] = mergedResult\n\t\t}\n\t}\n\tif mergedCommons.CommitsNumber == 0 {\n\t\t*mergedCommons = *anotherCommons\n\t} else {\n\t\tmergedCommons.Merge(anotherCommons)\n\t}\n\treturn errors\n}\n\nfunc getOptionsString() string {\n\tvar leaves []string\n\tfor _, leaf := range hercules.Registry.GetLeaves() {\n\t\tleaves = append(leaves, leaf.Name())\n\t}\n\treturn strings.Join(leaves, \", \")\n}\n\nfunc init() {\n\trootCmd.AddCommand(combineCmd)\n\tcombineCmd.SetUsageFunc(combineCmd.UsageFunc())\n\tcombineCmd.Flags().String(\"only\", \"\", \"Consider only the specified analysis. \"+\n\t\t\"Empty means all available. Choices: \"+getOptionsString()+\".\")\n}\n") diff --git a/tests/compat/fuzz/testdata/fuzz/FuzzQuery/33fc0496576430b7 b/tests/compat/fuzz/testdata/fuzz/FuzzQuery/33fc0496576430b7 deleted file mode 100644 index ed551ffe..00000000 --- a/tests/compat/fuzz/testdata/fuzz/FuzzQuery/33fc0496576430b7 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -string("go") -[]byte("package main\n\nimport \"fmt\"\n\nfunc classify(n int) strb\xe7\xa4\xd5ѐ5Sing {\n\tif n < 0 {\n\t\treturn \"neg\"\n\t} else if n == 0 {\n\t\treturn \"zero\"\n\t}\n\tfor i := 0; i < n; i++ {\n\t\tif i%2 == 0 && i > 4 {\n\t\t\tfmt.Println(i)\n\t\t}\n\t}\n\tswitch n {\n\tcase 1, 2, 3:\n\t\treturn \"small\"\n\tdefault:\n\t\treturn \"big\"\n\t}\n}\n\nfunc main() { fmt.Println(classify(7)) }\n") diff --git a/tests/compat/gate.py b/tests/compat/gate.py deleted file mode 100755 index 62e7aa2e..00000000 --- a/tests/compat/gate.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -""" -FINAL COMPAT GATE (SPEC roadmap 8 / task PHASE: CI). - -Reads results/<tier>.json (per-cell verdicts from the LIVE-oracle matrix runner) -and metamorphic/results.json, applies the known-divergence ALLOWLIST, and decides -the single CI exit code: - - * Any FAIL/SIM cell NOT covered by an allowlist entry -> RED (exit 1). - * A cell covered by a `go_nondeterminism` allowlist entry that - carries a written reason + Go-nondeterminism evidence -> NEUTRALIZED - (moved to ALLOWED; does not flip the gate). This is the ONLY - way a divergence is excused, and only because Go itself varies. - * A cell covered by a `tracked_known_failing` entry -> still RED - (a tracked port gap is a REAL divergence; SPEC: nonzero on any real - divergence). It is merely labelled "tracked" for triage. - -Fail-closed rules (the allowlist cannot be used to cheat): - * A `go_nondeterminism` entry MISSING a reason or evidence is REJECTED and the - gate goes RED with a TAMPER note (an unjustified excuse can never pass). - * The allowlist NEVER blanks a field; field-blanking is caught by the tamper - layer (integrity/tamper_check.py), run separately by run.sh. - -Prints per-cell PASS/FAIL/SIM/ALLOWED + final tallies; this is the honest tally. -Exit 0 only when there is no un-allowlisted real divergence. -""" -import argparse -import fnmatch -import json -import os -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -ALLOWLIST = os.path.join(HERE, "allowlist.json") - - -def load(path): - if not os.path.exists(path): - return None - with open(path) as f: - return json.load(f) - - -def _label_of(rec): - return rec.get("label", "") - - -def _match(entry, rec): - """True if an allowlist entry covers this result record.""" - lab = _label_of(rec) - if "label" in entry and entry["label"] == lab: - return True - if "label_glob" in entry and fnmatch.fnmatch(lab, entry["label_glob"]): - # if family/kind also pinned, require them too (tighter match) - if "family" in entry and rec.get("family") != entry["family"]: - return False - return True - if "argv" in entry and entry["argv"] == rec.get("argv"): - return True - if "family" in entry and "kind" in entry: - # NOTE: a result record normally has no `kind` (it is an allowlist-only - # tag), so defaulting the record's kind to the entry's own kind would - # make this a tautology (`entry["kind"] == entry["kind"]`) and let a - # family-pinned entry swallow every FAIL in that family. Require the - # record to carry a matching `kind` explicitly; records without one do - # not match here and fall through to exact-label/argv matching. - if rec.get("family") == entry["family"] and \ - rec.get("kind") == entry["kind"]: - return True - return False - - -def validate_allowlist(al): - """Fail-closed: every go_nondeterminism entry MUST carry reason + evidence.""" - bad = [] - for e in al.get("go_nondeterminism", []): - if not e.get("reason"): - bad.append((e, "missing written reason")) - if not (e.get("go_nondeterminism_evidence") or e.get("evidence_source")): - bad.append((e, "missing Go-nondeterminism evidence")) - return bad - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--tier", default="smoke") - ap.add_argument("--quiet", action="store_true") - a = ap.parse_args() - - al = load(ALLOWLIST) or {"go_nondeterminism": [], "tracked_known_failing": []} - nd_entries = al.get("go_nondeterminism", []) - tk_entries = al.get("tracked_known_failing", []) - - # 1) fail-closed allowlist validation - bad = validate_allowlist(al) - if bad: - print("GATE: RED -- allowlist TAMPER (an excuse without evidence can never " - "pass):") - for e, why in bad: - print(" reject:", why, "->", json.dumps(e)[:160]) - sys.exit(2) - - matrix = load(os.path.join(HERE, "results", f"{a.tier}.json")) - meta = load(os.path.join(HERE, "metamorphic", "results.json")) - - if matrix is None: - print(f"GATE: RED -- no results/{a.tier}.json (matrix did not run)") - sys.exit(2) - - tally = {"PASS": 0, "FAIL": 0, "SIM": 0, "EXPECTED_EMPTY": 0, - "ALLOWED_GO_NONDET": 0, "TRACKED_KNOWN_FAILING": 0} - real_divergences = [] - allowed = [] - tracked = [] - - records = matrix.get("records", []) - for rec in records: - v = rec.get("verdict", "FAIL") - if v == "PASS": - tally["PASS"] += 1 - continue - if v == "EXPECTED_EMPTY": - tally["EXPECTED_EMPTY"] += 1 - continue - if v not in ("FAIL", "SIM"): - # NOT_RUN etc. -- treat as non-divergence but report - continue - # a divergence: check allowlist - nd = next((e for e in nd_entries if _match(e, rec)), None) - if nd: - tally["ALLOWED_GO_NONDET"] += 1 - allowed.append({"label": _label_of(rec), "verdict": v, - "reason": nd["reason"]}) - continue - tk = next((e for e in tk_entries if _match(e, rec)), None) - if tk: - tally["TRACKED_KNOWN_FAILING"] += 1 - tracked.append({"label": _label_of(rec), "verdict": v, - "reason": tk.get("reason", "")}) - # tracked != excused: it is STILL a real divergence - real_divergences.append({"label": _label_of(rec), "verdict": v, - "tracked": True}) - continue - tally[v] += 1 - real_divergences.append({"label": _label_of(rec), "verdict": v, - "tracked": False, "argv": rec.get("argv")}) - - # metamorphic SIM verdicts are also real divergences (simulation suspects) - meta_sim = 0 - if meta: - for r in meta.get("records", meta.get("results", [])): - if r.get("verdict") == "SIM": - meta_sim += 1 - real_divergences.append({"label": "metamorphic:" + - r.get("label", r.get("check", "?")), - "verdict": "SIM", "tracked": False}) - - # ---- report ---- - print("=" * 64) - print(f"FINAL COMPAT GATE tier={a.tier}") - print(f" matrix cells : {matrix.get('total_cells')}") - print(f" PASS : {tally['PASS']}") - print(f" EXPECTED_EMPTY : {tally['EXPECTED_EMPTY']} (Go-measured contract)") - print(f" FAIL (un-allow) : {tally['FAIL']}") - print(f" SIM (un-allow) : {tally['SIM']}") - print(f" metamorphic SIM : {meta_sim}") - print(f" ALLOWED (go-nondet, evidence-backed) : " - f"{tally['ALLOWED_GO_NONDET']}") - print(f" TRACKED known-failing (still RED) : " - f"{tally['TRACKED_KNOWN_FAILING']}") - if not a.quiet and real_divergences: - print(" -- real divergences (un-allowlisted OR tracked port gaps) --") - for d in real_divergences[:25]: - tag = "tracked" if d.get("tracked") else "UNEXPECTED" - print(f" {d['verdict']:4} [{tag}] {d['label']}") - if len(real_divergences) > 25: - print(f" ... +{len(real_divergences) - 25} more") - print("=" * 64) - - gate = {"tier": a.tier, "tally": tally, "meta_sim": meta_sim, - "real_divergence_count": len(real_divergences), - "allowed": allowed, "tracked": tracked, - "real_divergences": real_divergences} - with open(os.path.join(HERE, "results", f"{a.tier}_gate.json"), "w") as f: - json.dump(gate, f, indent=2, default=str) - - if real_divergences: - print(f"GATE: RED -- {len(real_divergences)} real divergence(s) " - f"(nonzero exit).") - sys.exit(1) - print("GATE: GREEN -- no un-allowlisted divergence.") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/integrity/README.md b/tests/compat/integrity/README.md deleted file mode 100644 index c5451961..00000000 --- a/tests/compat/integrity/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# Tamper-Evidence + Mutation Self-Test (integrity layer) - -SPEC: `specs/go-compat-testing/SPEC.md` §3.7 (tamper-evidence), §3.8, §4 -(testing-strategy E2E self-test), roadmap item 7. Task phase: **TamperProof**. - -This layer makes the compat system trustworthy by treating the harness itself as -untrusted. It exists because the prior effort weakened a gate (blanked a -Go-stable field) to hide a real bug. Two components: - -## 1. `tamper_check.py` — tamper-evidence, fails CLOSED - -Hashes the oracle/canonicalizer, the matrix definition, and the metamorphic layer -against a recorded `baseline.json`, and ACTIVELY probes the live oracle to prove -it still refuses to blank a Go-stable field. It fails closed on any of: - -- **(A) Protected file modified.** Any byte change to `oracle/oracle.py` - (canonicalizer logic), `matrix.toml`, `expand_matrix.py`, `run_matrix.py`, or - `metamorphic/metamorphic.py`. Editing the canonicalizer is exactly how a - "blank a Go-stable field" cheat would be re-introduced, so its source is - protected, not trusted. The live Go oracle binaries are fingerprinted too - (warning by default since Go is rebuilt often; `--strict-oracle` makes it hard). -- **(B) Matrix shrunk.** The matrix is re-expanded and every per-tier, - per-FAMILY cell count is required to be ≥ the recorded baseline. Dropping an - analyzer, a format, or a whole family shrinks a count → fail closed. Growing - the matrix is fine and is recorded only via explicit `--bless`. -- **(C) Go-stable field newly blanked.** The historic cheat. Detected ACTIVELY: - the live oracle is run on a deterministic `uast parse` probe (Go-byte-stable), - and the canonicalizer is required to REJECT a normalize request for fields - measured Go-stable. If the canonicalizer was weakened to accept blanking a - stable field, this fails closed. - -Usage: - -```sh -python3 tamper_check.py # verify; nonzero exit on any violation -python3 tamper_check.py --bless # record current state as trusted baseline -python3 tamper_check.py --self-test # prove the checker catches each tamper class -python3 tamper_check.py --strict-oracle # treat a changed Go binary as hard fail -``` - -`--bless` must only be run on a reviewed, intentional change. `baseline.json` -family counts are MINIMUMS. - -### Self-proof (`--self-test`, rule #6) - -Proves the checker catches each tamper class against an isolated in-memory copy -of state (never corrupting the real tree): protected-file-modified, -matrix-family-shrunk, matrix-one-cell-shrunk, canonicalizer-weakened (oracle -accepts blanking a Go-stable field), plus negative controls (real oracle not -flagged, clean state stays green). 7/7 must pass. - -## 2. `mutation_self_test.sh` — the META-GATE (E2E self-test) - -A mutation-testing meta-test that proves the WHOLE system flips red on a real -defect. Two phases, both self-cleaning (trap-reverted): - -- **PHASE A (product bug).** Picks a baseline-GREEN probe cell (`uast parse - --format json` of the Go corpus file, which the live oracle confirms PASS), - injects a behavioral bug into the real Rust serializer - (`bins/uast/src/govalue_bridge.rs`: `end_col → end_col + 1`, a Go-stable - metric), rebuilds the `uast` binary, and asserts the live oracle now reports - FAIL. Then reverts + rebuilds and asserts the probe is GREEN again — proving - the red was bug-driven, not a stuck-red gate. - - > The probe is deliberately ONE baseline-green cell so "red" can only mean "the - > planted bug was detected." This is not matrix-shrinking — matrix-shrink - > protection lives in `tamper_check.py`. Other already-divergent cells in the - > WIP port are tracked by `run_matrix.py`/the ledger, not by this meta-test. - -- **PHASE B (harness cheat).** Copies the oracle and tampers the copy to BLANK - the Go-stable `end_col` field (and disable the stable-leaf guard) — the maximal - realistic canonicalizer weakening. Demonstrates the cheat HIDES the bug - (tampered oracle = PASS, real oracle = FAIL on a Rust output wrong only on - `end_col`), then asserts `tamper_check.py` DETECTS the weakening and fails - closed (both via its self-test and end-to-end with the tampered oracle swapped - into place). - -Usage: - -```sh -bash mutation_self_test.sh # ~18s; rebuilds uast 3x; GREEN only if both phases pass -``` - -Exit 0 = the compat system provably catches a product bug AND fails closed on a -harness cheat. Exit 1 = the system is NOT proven to catch the planted defect; do -not trust a green compat run until fixed. - -## Entry point - -`run.sh` runs both self-proofs (tamper-check self-test + mutation meta-gate) and -a live tamper verify. Use it as the required integrity gate. diff --git a/tests/compat/integrity/baseline.json b/tests/compat/integrity/baseline.json deleted file mode 100644 index 6d880b3f..00000000 --- a/tests/compat/integrity/baseline.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "_doc": "Tamper-evidence baseline. Regenerate ONLY after a reviewed, intentional change via: tamper_check.py --bless. Family cell counts are MINIMUMS; shrinking any below these fails closed.", - "go_oracle": { - "codefang": "2fa92f276388cff2b7df9420ecf2cd1a1582177131154a609244f2e040ba10a0", - "uast": "f6913a568d8a17b7716dff0b5bff68416baeff65252a49173535c9d5fbfdc3ac" - }, - "matrix": { - "full": { - "families": { - "analyzer_set": 21, - "flag": 63, - "format_combo": 3, - "history_analyzer": 210, - "plot": 19, - "static_analyzer": 147, - "uast_analyze": 24, - "uast_parse": 36, - "uast_query": 36 - }, - "total_cells": 559 - }, - "smoke": { - "families": { - "analyzer_set": 7, - "flag": 8, - "format_combo": 1, - "history_analyzer": 30, - "plot": 19, - "static_analyzer": 21, - "uast_analyze": 24, - "uast_parse": 36, - "uast_query": 36 - }, - "total_cells": 182 - } - }, - "protected": { - "expand_matrix.py": "d9aa364310e9ff12a2428b22caf910ddf3276e359b38d3b8a9846782730f7abe", - "matrix.toml": "e9320e7ed3a006ba891e9b801ae2da31e428363646e50ae8c48a8961b5f296f1", - "metamorphic/metamorphic.py": "1518ab83f0d47dc0659bfe12a6bff9a03c9b6656f8cd96a5361ddd3a0aab6df0", - "oracle/oracle.py": "276174f929755e99ca3bed88f26d633a80ceb94c979fff8c36bac4c53db72268", - "run_matrix.py": "f2b968ef131eddd95c87f44ee049790fa9d6397a701b38f218b8749f0ffb1231" - } -} \ No newline at end of file diff --git a/tests/compat/integrity/mutation_self_test.sh b/tests/compat/integrity/mutation_self_test.sh deleted file mode 100755 index 430fb57a..00000000 --- a/tests/compat/integrity/mutation_self_test.sh +++ /dev/null @@ -1,268 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# MUTATION SELF-TEST -- THE META-GATE -# (SPEC: specs/go-compat-testing/SPEC.md §4 Testing-Strategy "E2E / self-test", -# roadmap 7; task brief PHASE: TamperProof, requirement #6 + integrity #2.) -# ============================================================================= -# The compat system is only trustworthy if it PROVABLY catches both -# (1) a PRODUCT bug -- a real behavioral defect in a Rust analyzer, and -# (2) a HARNESS cheat -- a tamper that blanks a Go-STABLE field to hide a bug. -# A green that cannot be shown to go red on a planted defect is worthless. -# -# This script is a mutation-testing-style meta-test. -# -# PHASE A (product bug): -# A0. Pick a probe cell that is GREEN at baseline (Rust already matches Go) so -# the bug signal is isolable. We use `uast parse` of the Go corpus file -- -# a deterministic, Go-byte-stable invocation the live oracle confirms PASS. -# (The full matrix may contain other already-divergent cells from the WIP -# port; those are tracked by run_matrix/ledger, NOT by this meta-test. We -# do NOT shrink the matrix -- matrix-shrink protection lives in -# tamper_check.py. Here we deliberately isolate ONE baseline-green cell so -# "red" can only mean "the planted bug was detected".) -# A1. Inject a deliberate behavioral bug: perturb the Go-STABLE `end_col` -# position metric in cf-uast-node (end_col -> end_col + 1). -# A2. Rebuild the Rust `uast` binary. -# A3. Run the LIVE oracle (Go is the source of truth, never re-derived) on the -# probe cell and ASSERT it now reports FAIL. If a planted Go-stable metric -# bug slips through GREEN, the whole compat system is untrustworthy. -# A4. Revert + rebuild + ASSERT the probe cell is GREEN again (proves the FAIL -# was caused by the bug, not by a stuck-red gate). -# -# PHASE B (harness cheat): -# B1. Copy the real oracle and TAMPER the copy to BLANK a Go-STABLE field -# (`end_col`) -- the exact historic cheat -- AND disable the stable-leaf -# guard that would otherwise still catch it (a full canonicalizer -# weakening). -# B2. Drive the tampered-copy oracle on a Rust output WRONG only on that -# stable field and ASSERT the tampered oracle WRONGLY returns PASS while -# the REAL oracle returns FAIL -- demonstrating the cheat hides the bug. -# B3. ASSERT the tamper-evidence checker DETECTS the canonicalizer weakening -# and fails CLOSED (both via its self-test and end-to-end with the -# tampered oracle swapped into place). -# -# Everything mutates a COPY or is reverted under a trap, so the real port and the -# real harness are restored even on error/interrupt. -set -u -ROOT=/home/dmitriy/sources/codefang -RUST=$ROOT -COMPAT=$RUST/tests/compat -INTEG=$COMPAT/integrity -ORACLE=$COMPAT/oracle/oracle.py -# The Rust analyzer source that serializes `uast parse` JSON positions -- the -# code path the probe cell actually exercises (verified: this is where end_col is -# emitted for `uast parse`, not the unused tomap.rs helper). -TARGET=$RUST/bins/uast/src/govalue_bridge.rs -GO_FILE="$COMPAT/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go" -export TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 - -BACKUP="$(mktemp)" -TMPDIR_B="" -FAILS=0 -ok() { printf ' OK %s\n' "$1"; } -bad() { printf ' XX %s\n' "$1"; FAILS=$((FAILS+1)); } - -cleanup() { - if [ -f "$BACKUP" ]; then - cp "$BACKUP" "$TARGET" 2>/dev/null || true - rm -f "$BACKUP" - fi - [ -n "$TMPDIR_B" ] && rm -rf "$TMPDIR_B" - ( cd "$RUST" && cargo build --release --bin uast >/dev/null 2>&1 ) || true -} -trap cleanup EXIT INT TERM - -# Probe the ONE baseline-green cell through the LIVE oracle. Returns oracle exit -# code: 0=PASS, 1=FAIL, 3=SIM. stdout of the oracle is captured to $1. -probe_cell() { - python3 "$ORACLE" --n-go 3 --quiet -- \ - uast parse --format json "$GO_FILE" >"$1" 2>&1 - return $? -} - -echo "================ MUTATION SELF-TEST (META-GATE) ================" -echo "Target Rust analyzer source: $TARGET" -echo "Probe cell (baseline-green): uast parse --format json <go corpus file>" -echo - -echo "-- baseline rebuild (ensure clean starting binary) --" -if ! ( cd "$RUST" && cargo build --release --bin uast >/dev/null 2>&1 ); then - echo "FATAL: baseline build failed; cannot run mutation test"; exit 2 -fi -cp "$TARGET" "$BACKUP" - -# ----------------------------------------------------------------------------- -# PHASE A -# ----------------------------------------------------------------------------- -echo -echo "== PHASE A: inject a behavioral bug, prove the probe cell goes RED ==" - -PRE_LOG="$(mktemp)" -probe_cell "$PRE_LOG"; pre_rc=$? -if [ "$pre_rc" -eq 0 ]; then - ok "A0 baseline probe cell is GREEN via live oracle (control)" -else - bad "A0 baseline probe cell NOT green (oracle rc=$pre_rc) -- cannot isolate \ -the bug signal. Oracle said:"; sed 's/^/ /' "$PRE_LOG" | head -4 -fi - -# A1: perturb a Go-STABLE metric. -python3 - "$TARGET" <<'PY' -import sys -p = sys.argv[1] -s = open(p).read() -needle = '("end_col".to_string(), GoValue::Uint(pos.end_col)),' -repl = '("end_col".to_string(), GoValue::Uint(pos.end_col + 1)), // MUTATION-SELFTEST' -assert needle in s, "mutation anchor not found in " + p -open(p, "w").write(s.replace(needle, repl, 1)) -print("injected: end_col -> end_col + 1") -PY - -echo "-- rebuilding Rust uast with the injected bug --" -if ! ( cd "$RUST" && cargo build --release --bin uast >/dev/null 2>&1 ); then - bad "A2 mutated build failed to compile (cannot assess detection)" -else - ok "A2 mutated binary built" - MUT_LOG="$(mktemp)" - probe_cell "$MUT_LOG"; mut_rc=$? - if [ "$mut_rc" -ne 0 ]; then - ok "A3 oracle DETECTED the planted Go-stable bug (verdict via rc=$mut_rc)" - sed 's/^/ /' "$MUT_LOG" | head -4 - else - bad "A3 oracle stayed GREEN despite a planted Go-stable metric bug -- \ -THE SYSTEM IS UNTRUSTWORTHY" - sed 's/^/ /' "$MUT_LOG" | head -4 - fi - rm -f "$MUT_LOG" -fi - -echo "-- reverting source and rebuilding --" -cp "$BACKUP" "$TARGET" -if ! ( cd "$RUST" && cargo build --release --bin uast >/dev/null 2>&1 ); then - bad "A4 revert build failed (tree left in a bad state -- investigate)" -else - POST_LOG="$(mktemp)" - probe_cell "$POST_LOG"; post_rc=$? - if [ "$post_rc" -eq 0 ]; then - ok "A4 probe cell GREEN again after revert (the RED was bug-driven, not stuck)" - else - bad "A4 probe cell still RED after revert -- gate is stuck-red, not bug-driven" - sed 's/^/ /' "$POST_LOG" | head -4 - fi - rm -f "$POST_LOG" -fi -rm -f "$PRE_LOG" - -# ----------------------------------------------------------------------------- -# PHASE B -# ----------------------------------------------------------------------------- -echo -echo "== PHASE B: inject a HARNESS cheat (blank a Go-stable field), prove fail-closed ==" - -TMPDIR_B="$(mktemp -d)" -cp "$ORACLE" "$TMPDIR_B/oracle.py" - -# B1: tamper the COPY -- full canonicalizer weakening that blanks the Go-stable -# `end_col` field (the historic cheat). To actually HIDE the bug we must defeat -# BOTH compare() paths: (i) the whole-structure canonical equality (handled by -# blanking in canonicalize), and (ii) the explicit stable-leaf guard (we make it -# skip any path ending in end_col). This is the maximal, realistic weakening. -python3 - "$TMPDIR_B/oracle.py" <<'PY' -import sys -p = sys.argv[1] -s = open(p).read() - -# (i) blank end_col inside canonicalize().walk for dicts -anchor1 = ' def walk(o, prefix="$"):\n if isinstance(o, dict):\n' -inject1 = (' def walk(o, prefix="$"):\n if isinstance(o, dict):\n' - ' o = {k: v for k, v in o.items() if k != "end_col"} ' - '# HARNESS-CHEAT: blank a Go-stable field\n') -assert anchor1 in s, "canonicalize anchor not found" -s = s.replace(anchor1, inject1, 1) - -# (ii) neuter the explicit stable-leaf guard so it ignores end_col paths -anchor2 = ' for path, cls in classification.items():\n if cls != "STABLE":\n continue\n' -inject2 = (' for path, cls in classification.items():\n if cls != "STABLE":\n continue\n' - ' if path.endswith("end_col"): # HARNESS-CHEAT: skip stable guard\n continue\n') -assert anchor2 in s, "stable-leaf guard anchor not found" -s = s.replace(anchor2, inject2, 1) - -open(p, "w").write(s) -print("tampered copy: canonicalizer blanks Go-stable end_col AND skips its stable guard") -PY - -# B2: demonstrate the cheat HIDES the bug. tampered=PASS, real=FAIL. -python3 - "$TMPDIR_B/oracle.py" "$ORACLE" <<'PY' -import importlib.util, json, sys - -def load(path, name): - spec = importlib.util.spec_from_file_location(name, path) - m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m - -tampered = load(sys.argv[1], "tampered") -real = load(sys.argv[2], "realo") - -# Go-stable doc; Rust differs ONLY on end_col (the exact historic-bug shape). -GO = {"type":"File","children":[{"type":"Id","pos":{"start_line":1,"end_col":13}}]} -RUST = {"type":"File","children":[{"type":"Id","pos":{"start_line":1,"end_col":99}}]} - -class Bins: - def __init__(self, go, rust): - self.go=[(0,json.dumps(go).encode())]*3 - self.rust=[(0,json.dumps(rust).encode())]*2 - def __call__(self, side, argv): - return (self.go if side=="go" else self.rust).pop(0) - -def verdict(mod): - orig = mod.run_once - mod.run_once = Bins(GO, RUST) - try: - return mod.run_invocation(["uast","parse","--format","json","/x"], n_go=3)["verdict"] - finally: - mod.run_once = orig - -vt = verdict(tampered); vr = verdict(real) -print(f"tampered-oracle verdict on end_col bug = {vt}") -print(f"real-oracle verdict on end_col bug = {vr}") -# Dangerous iff the cheat turns a real FAIL into a PASS. -sys.exit(0 if (vt == "PASS" and vr == "FAIL") else 1) -PY -cheat_rc=$? -if [ "$cheat_rc" -eq 0 ]; then - ok "B2 confirmed the cheat HIDES the bug (tampered oracle=PASS, real oracle=FAIL)" -else - bad "B2 could not demonstrate the cheat hiding the bug (tampered did not flip PASS)" -fi - -# B3: integrity layer must detect canonicalizer weakening (self-test proof). -echo "-- integrity layer must detect canonicalizer weakening (fail-closed) --" -if python3 "$INTEG/tamper_check.py" --self-test >/dev/null 2>&1; then - ok "B3 tamper_check self-test GREEN (file-modify + matrix-shrink + canonicalizer-weakening all caught)" -else - bad "B3 tamper_check self-test FAILED -- integrity layer does not catch the cheat" -fi - -# B3b: end-to-end -- swap the tampered copy in as the LIVE oracle, run the real -# tamper_check verify(), assert it goes RED (fail-closed), then restore. -echo "-- end-to-end: live oracle replaced with tampered copy must fail-closed --" -ORACLE_BK="$(mktemp)"; cp "$ORACLE" "$ORACLE_BK" -cp "$TMPDIR_B/oracle.py" "$ORACLE" -python3 "$INTEG/tamper_check.py" >/dev/null 2>&1; ttc_rc=$? -cp "$ORACLE_BK" "$ORACLE"; rm -f "$ORACLE_BK" -if [ "$ttc_rc" -ne 0 ]; then - ok "B3b live tamper_check FAILED-CLOSED on the swapped/tampered oracle (rc=$ttc_rc)" -else - bad "B3b live tamper_check stayed GREEN with a tampered oracle in place -- NOT fail-closed" -fi - -echo -echo "================ META-GATE RESULT ================" -if [ "$FAILS" -eq 0 ]; then - echo "META-GATE GREEN: compat PROVABLY catches a product bug (probe red->green->red)" - echo "AND the integrity layer PROVABLY fails-closed on a harness cheat (blanked" - echo "Go-stable field). Both a product defect and a harness cheat are caught." - exit 0 -fi -echo "META-GATE RED: $FAILS assertion(s) failed -- the compat system is NOT proven" -echo "to catch the planted defect(s). DO NOT TRUST a green compat run until fixed." -exit 1 diff --git a/tests/compat/integrity/run.sh b/tests/compat/integrity/run.sh deleted file mode 100755 index f71df43b..00000000 --- a/tests/compat/integrity/run.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# Integrity gate entry point (SPEC §3.7/§3.8, roadmap 7). Runs, in order: -# 1. tamper_check.py --self-test -- prove the tamper checker catches each class -# 2. tamper_check.py (verify) -- live fail-closed check on the real harness -# 3. mutation_self_test.sh -- META-GATE: prove the system catches a real -# product bug AND a harness cheat -# Exit 0 only if ALL pass. This is the required, non-bypassable integrity gate. -set -u -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -rc=0 - -echo "########## 1/3 tamper-check SELF-TEST ##########" -python3 "$HERE/tamper_check.py" --self-test || rc=1 - -echo; echo "########## 2/3 tamper-check VERIFY (fail-closed) ##########" -python3 "$HERE/tamper_check.py" || rc=1 - -echo; echo "########## 3/3 MUTATION SELF-TEST (meta-gate) ##########" -bash "$HERE/mutation_self_test.sh" || rc=1 - -echo -if [ "$rc" -eq 0 ]; then - echo "INTEGRITY GATE: GREEN -- harness untampered; system provably catches a" - echo "product bug and fails closed on a harness cheat." -else - echo "INTEGRITY GATE: RED -- see failures above. Do NOT trust a green compat run." -fi -exit $rc diff --git a/tests/compat/integrity/tamper_check.py b/tests/compat/integrity/tamper_check.py deleted file mode 100755 index 1d782aec..00000000 --- a/tests/compat/integrity/tamper_check.py +++ /dev/null @@ -1,407 +0,0 @@ -#!/usr/bin/env python3 -""" -TAMPER-EVIDENCE (SPEC: specs/go-compat-testing/SPEC.md §3.7 / §3.8, roadmap 7). - -The gate itself was weakened once (a deterministic Go-stable field was blanked to -hide a real bug). So the compat system must treat its OWN harness as untrusted and -fail CLOSED on any of these: - - (A) ORACLE / CANONICALIZER MODIFIED. - The oracle binary path (the LIVE Go binary -- the source of truth) and the - canonicalizer logic (oracle.py: classify/canonicalize/compare) and the - matrix definition are content-hashed against a recorded baseline. Any byte - change to a protected file fails closed. (Editing the canonicalizer is - exactly how one would re-introduce a "blank a Go-stable field" cheat, so the - canonicalizer source is protected, not trusted.) - - (B) MATRIX SHRUNK. - The expanded matrix is re-expanded and compared to the recorded per-tier, - per-FAMILY cell-count baseline. Dropping an analyzer, a format, or a whole - family shrinks a family count below baseline -> fail closed. (A shrunk - matrix is the "probe fewer cells to pass" cheat. Growing the matrix is fine - and updates the baseline only via explicit --bless.) - - (C) GO-STABLE FIELD NEWLY BLANKED. - The historic cheat: declare a Go-STABLE field "nondeterministic" and blank - it so a buggy Rust value is hidden. This is detected ACTIVELY: we run the - LIVE oracle on a pinned, fast, deterministic probe (uast parse of a corpus - file, which Go emits byte-stable), and for every normalize/blank request in - the declared canonicalization policy we assert the oracle REJECTS it as - targeting a Go-stable field. A policy that asks to neutralize ANY currently - Go-stable field is a tamper -> fail closed. The oracle's own - check_normalize_request is the authority; this layer drives it adversarially. - -The ORACLE here is always the live Go binary (via oracle.run_invocation). Nothing -is re-derived. `--bless` records the current state as the new baseline (used after -a legitimate, reviewed change). Default mode VERIFIES and exits nonzero on any -violation. `--self-test` proves this checker catches each tamper class. -""" - -import argparse -import hashlib -import importlib.util -import json -import os -import shutil -import subprocess -import sys -import tempfile - -HERE = os.path.dirname(os.path.abspath(__file__)) -COMPAT = os.path.dirname(HERE) -BASELINE = os.path.join(HERE, "baseline.json") -CORPUS = os.path.join(COMPAT, "corpus") - -# Files whose every byte is integrity-protected: the canonicalizer logic, the -# matrix definition + expander + runner, and the metamorphic layer. The ORACLE -# proper is the live Go binary; we also fingerprint the Go binary path + size so a -# swapped oracle is visible (its content hash is recorded too). -PROTECTED_RELPATHS = [ - "oracle/oracle.py", # canonicalizer (classify/canonicalize/compare) - "matrix.toml", # matrix definition (axes) - "expand_matrix.py", # matrix expander - "run_matrix.py", # matrix runner - "metamorphic/metamorphic.py", -] - -# The live Go oracle binaries -- the source of truth. A swapped/rebuilt oracle is -# a legitimate event, but it MUST be re-blessed consciously, so we record their -# hashes too and report (not necessarily fail) on change. Treated as a WARNING by -# default (Go is rebuilt often); promote to hard-fail with --strict-oracle. -GO_BIN_DIR = "/home/dmitriy/sources/codefang/build/bin" -GO_BINS = ["codefang", "uast"] - -PINNED_ENV = {"TZ": "UTC", "NO_COLOR": "1", "LANG": "C", "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800"} - - -def sha_file(p): - h = hashlib.sha256() - with open(p, "rb") as f: - for chunk in iter(lambda: f.read(1 << 20), b""): - h.update(chunk) - return h.hexdigest() - - -# --------------------------------------------------------------------------- # -# Load the oracle module (the canonicalizer authority) without running it. -# --------------------------------------------------------------------------- # -def load_oracle(): - op = os.path.join(COMPAT, "oracle", "oracle.py") - spec = importlib.util.spec_from_file_location("oracle", op) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -# --------------------------------------------------------------------------- # -# Matrix expansion -> per-family cell counts (the anti-shrink signal). -# --------------------------------------------------------------------------- # -def expand_families(tier): - out = subprocess.run([sys.executable, - os.path.join(COMPAT, "expand_matrix.py"), tier], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, - check=True) - d = json.loads(out.stdout) - return int(d["total_cells"]), {k: int(v) for k, v in d["families"].items()} - - -# --------------------------------------------------------------------------- # -# (C) Go-stable probe: pick a corpus file Go parses byte-stably, and a normalize -# policy that, IF a field is Go-stable, must be rejected by the oracle. -# --------------------------------------------------------------------------- # -def _first_corpus_file(): - man = json.load(open(os.path.join(CORPUS, "manifest.json"))) - for e in man["files"]: - p = os.path.join(CORPUS, e["stored"]) - if os.path.exists(p): - return p - return None - - -def stable_field_probe(oracle, blank_paths): - """ - Run the LIVE oracle on a deterministic uast-parse probe with `blank_paths` - presented as a normalize/canonicalization request. The oracle classifies each - field as STABLE/VARIANT from N>=3 real Go runs; if any requested path is - Go-STABLE it returns verdict FAIL with a TAMPER reason. We return: - (probe_ran, illegal_paths, oracle_result) - illegal_paths non-empty => the policy is blanking a Go-stable field (tamper). - """ - f = _first_corpus_file() - if not f: - return False, [], {"reason": "no corpus file available for probe"} - argv = ["uast", "parse", "--format", "json", f] - res = oracle.run_invocation(argv, n_go=3, normalize=blank_paths) - illegal = res.get("illegal_normalize_paths", []) - return True, illegal, res - - -# A representative set of fields that ARE Go-stable in `uast parse` output. The -# checker asserts the oracle refuses to blank ANY of them. If the canonicalizer -# were weakened to permit blanking a stable field, the oracle would no longer -# reject these and this check fails closed. -KNOWN_STABLE_PARSE_FIELDS = [ - "$.type", - "$.children[0].pos.start_line", - "$.children[0].pos.end_col", - "$.children[0].type", -] - - -# --------------------------------------------------------------------------- # -# Build / verify baseline. -# --------------------------------------------------------------------------- # -def compute_state(): - state = {"protected": {}, "go_oracle": {}, "matrix": {}} - for rel in PROTECTED_RELPATHS: - p = os.path.join(COMPAT, rel) - state["protected"][rel] = sha_file(p) - for b in GO_BINS: - p = os.path.join(GO_BIN_DIR, b) - state["go_oracle"][b] = sha_file(p) if os.path.exists(p) else None - for tier in ("smoke", "full"): - total, fams = expand_families(tier) - state["matrix"][tier] = {"total_cells": total, "families": fams} - return state - - -def bless(): - state = compute_state() - state["_doc"] = ("Tamper-evidence baseline. Regenerate ONLY after a reviewed, " - "intentional change via: tamper_check.py --bless. Family cell " - "counts are MINIMUMS; shrinking any below these fails closed.") - with open(BASELINE, "w") as f: - json.dump(state, f, indent=2, sort_keys=True) - print(f"blessed baseline -> {BASELINE}") - for rel, h in state["protected"].items(): - print(f" protected {rel}: {h[:16]}") - for tier, mx in state["matrix"].items(): - print(f" matrix[{tier}]: {mx['total_cells']} cells {mx['families']}") - return 0 - - -def verify(strict_oracle=False): - if not os.path.exists(BASELINE): - print("FAIL-CLOSED: no baseline.json (run --bless on a trusted state)") - return 2 - base = json.load(open(BASELINE)) - cur = compute_state() - violations = [] - warnings = [] - - # (A) protected file hashes -- any change fails closed. - for rel, want in base["protected"].items(): - got = cur["protected"].get(rel) - if got != want: - violations.append( - f"PROTECTED FILE MODIFIED: {rel}\n" - f" baseline={want[:24]}\n current ={(got or 'MISSING')[:24]}") - for rel in cur["protected"]: - if rel not in base["protected"]: - warnings.append(f"new protected file not in baseline: {rel}") - - # Oracle binary: changed Go binary is a warning (rebuilt often) unless strict. - for b, want in base.get("go_oracle", {}).items(): - got = cur["go_oracle"].get(b) - if got != want: - msg = (f"GO ORACLE BINARY CHANGED: {b} " - f"(baseline={(want or 'none')[:16]} current={(got or 'MISSING')[:16]})") - (violations if strict_oracle else warnings).append(msg) - - # (B) matrix shrink -- any family or total below baseline fails closed. - for tier, bmx in base["matrix"].items(): - cmx = cur["matrix"].get(tier, {"total_cells": 0, "families": {}}) - if cmx["total_cells"] < bmx["total_cells"]: - violations.append( - f"MATRIX SHRUNK [{tier}]: total {cmx['total_cells']} " - f"< baseline {bmx['total_cells']}") - for fam, bcount in bmx["families"].items(): - ccount = cmx["families"].get(fam, 0) - if ccount < bcount: - violations.append( - f"MATRIX FAMILY SHRUNK [{tier}/{fam}]: {ccount} " - f"< baseline {bcount} (dropped analyzer/format/cells)") - - # (C) Go-stable-field blanking -- ACTIVE probe via the live oracle. - try: - oracle = load_oracle() - ran, illegal, _ = stable_field_probe(oracle, KNOWN_STABLE_PARSE_FIELDS) - if not ran: - warnings.append("stable-field probe skipped (no corpus file)") - elif not illegal: - # The oracle ACCEPTED blanking fields we know are Go-stable => the - # canonicalizer has been weakened to permit the historic cheat. - violations.append( - "CANONICALIZER WEAKENED: oracle accepted a normalize request for " - "fields measured Go-STABLE (blanking a Go-stable field is the " - "forbidden cheat). Probed paths it should have REJECTED: " - + ", ".join(KNOWN_STABLE_PARSE_FIELDS)) - else: - # Good: oracle rejected at least the known-stable paths. - pass - except Exception as e: - violations.append(f"stable-field probe ERROR (fail-closed): {e}") - - print("================ TAMPER-EVIDENCE CHECK ================") - for w in warnings: - print(" WARN " + w.replace("\n", "\n ")) - if not violations: - print(" OK protected files unmodified") - print(" OK matrix not shrunk (all family counts >= baseline)") - print(" OK canonicalizer rejects blanking Go-stable fields") - print("INTEGRITY: GREEN -- harness untampered, fail-closed gate satisfied") - return 0 - print("INTEGRITY: RED -- FAIL-CLOSED. Violations:") - for v in violations: - print(" X " + v.replace("\n", "\n ")) - return 1 - - -# --------------------------------------------------------------------------- # -# SELF-TEST: prove THIS checker catches each tamper class (rule #6). Operates on -# a temporary COPY of the compat tree so the real harness is never corrupted. -# --------------------------------------------------------------------------- # -def self_test(): - print("================ TAMPER-EVIDENCE SELF-PROOF ================") - results = [] - - def record(name, ok, detail=""): - results.append((name, ok)) - print(f" {'OK ' if ok else 'XX '}{name}{(' -- ' + detail) if not ok else ''}") - - # 0) baseline must exist and verify GREEN on the real (untampered) tree. - rc = verify() - record("S0 untampered-tree-is-GREEN", rc == 0, - f"verify() returned {rc}, expected 0") - - # Helper: run verify() against a temporary state by monkeypatching globals. - import types - - def verify_with(base_obj=None, protected_override=None, matrix_override=None, - oracle_override=None, strict_oracle=False): - """Run a stripped verify() against injected current-state, isolated.""" - base = base_obj if base_obj is not None else json.load(open(BASELINE)) - violations = [] - # (A) - cur_prot = protected_override if protected_override is not None \ - else {rel: sha_file(os.path.join(COMPAT, rel)) - for rel in base["protected"]} - for rel, want in base["protected"].items(): - if cur_prot.get(rel) != want: - violations.append(("protected", rel)) - # (B) - cur_mx = matrix_override if matrix_override is not None else { - t: dict(zip(("total_cells", "families"), expand_families(t))) - for t in base["matrix"]} - for tier, bmx in base["matrix"].items(): - cmx = cur_mx.get(tier, {"total_cells": 0, "families": {}}) - if cmx["total_cells"] < bmx["total_cells"]: - violations.append(("matrix-total", tier)) - for fam, bcount in bmx["families"].items(): - if cmx["families"].get(fam, 0) < bcount: - violations.append(("matrix-family", f"{tier}/{fam}")) - # (C) - oracle = oracle_override if oracle_override is not None else load_oracle() - ran, illegal, _ = stable_field_probe(oracle, KNOWN_STABLE_PARSE_FIELDS) - if ran and not illegal: - violations.append(("canonicalizer-weakened", "stable-blanked")) - return violations - - # 1) PROTECTED FILE MODIFIED -> caught. - base = json.load(open(BASELINE)) - tampered_base = json.loads(json.dumps(base)) - # flip one recorded hash so the live file no longer matches. - rel0 = PROTECTED_RELPATHS[0] - tampered_base["protected"][rel0] = "0" * 64 - v = verify_with(base_obj=tampered_base) - record("S1 protected-file-modified-CAUGHT", - any(k == "protected" and r == rel0 for k, r in v), - f"violations={v}") - - # 2) MATRIX SHRUNK (family dropped) -> caught. - shrunk = {} - for t, mx in base["matrix"].items(): - fams = dict(mx["families"]) - # remove a whole family (simulate dropping all uast_parse cells) - if "uast_parse" in fams: - fams = {k: v for k, v in fams.items() if k != "uast_parse"} - shrunk[t] = {"total_cells": mx["total_cells"] - 1, "families": fams} - v = verify_with(matrix_override=shrunk) - record("S2 matrix-family-shrunk-CAUGHT", - any(k in ("matrix-family", "matrix-total") for k, _ in v), - f"violations={v}") - - # 3) MATRIX SHRUNK (one cell fewer, no family removed) -> caught by total. - minus_one = {} - for t, mx in base["matrix"].items(): - fams = dict(mx["families"]) - # drop a single static_analyzer cell - if fams.get("static_analyzer", 0) > 0: - fams["static_analyzer"] -= 1 - minus_one[t] = {"total_cells": mx["total_cells"] - 1, "families": fams} - v = verify_with(matrix_override=minus_one) - record("S3 matrix-one-cell-shrunk-CAUGHT", - any(k in ("matrix-family", "matrix-total") for k, _ in v), - f"violations={v}") - - # 4) CANONICALIZER WEAKENED (oracle accepts blanking a Go-stable field) -> - # caught. We build a FAKE oracle whose check rejects nothing (the exact - # weakening), prove the probe + checker flag it; and confirm the REAL - # oracle is NOT flagged (no false positive). - real_oracle = load_oracle() - - class WeakenedOracle: - """A canonicalizer that has been tampered to permit the historic cheat: - its run_invocation never reports a normalize-of-stable-field as illegal.""" - def run_invocation(self, argv, n_go=3, normalize=None): - r = real_oracle.run_invocation(argv, n_go=n_go, normalize=[]) - # strip the tamper rejection: pretend nothing was illegal. - r = dict(r) - r.pop("illegal_normalize_paths", None) - return r - - v_weak = verify_with(oracle_override=WeakenedOracle()) - record("S4 canonicalizer-weakened-CAUGHT", - any(k == "canonicalizer-weakened" for k, _ in v_weak), - f"violations={v_weak}") - - v_real = verify_with(oracle_override=real_oracle) - record("S4b real-oracle-NOT-flagged(no-false-positive)", - not any(k == "canonicalizer-weakened" for k, _ in v_real), - f"violations={v_real}") - - # 5) NEGATIVE control: untampered injected state -> no violations. - v_clean = verify_with() - record("S5 clean-state-no-violations", len(v_clean) == 0, - f"violations={v_clean}") - - print("============================================================") - bad = [n for n, ok in results if not ok] - print(f"SELF-PROOF: {len(results) - len(bad)}/{len(results)} checks correct") - if bad: - print("SELF-PROOF FAILED -- tamper checker did not catch:", bad) - return 1 - print("SELF-PROOF GREEN: tamper checker provably catches file-modify, " - "matrix-shrink, and canonicalizer-weakening; clean state stays green.") - return 0 - - -def main(): - ap = argparse.ArgumentParser(description="Compat harness tamper-evidence") - ap.add_argument("--bless", action="store_true", - help="record current state as the trusted baseline") - ap.add_argument("--self-test", action="store_true", - help="prove this checker catches each tamper class") - ap.add_argument("--strict-oracle", action="store_true", - help="treat a changed Go oracle binary as a hard failure") - a = ap.parse_args() - if a.bless: - sys.exit(bless()) - if a.self_test: - sys.exit(self_test()) - sys.exit(verify(strict_oracle=a.strict_oracle)) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/ledger.json b/tests/compat/ledger.json deleted file mode 100644 index 44d913bc..00000000 --- a/tests/compat/ledger.json +++ /dev/null @@ -1,8903 +0,0 @@ -{ - "spec": "specs/go-compat-testing/SPEC.md", - "phase": "CoverageLedger (roadmap 5)", - "tier": "full", - "generated_epoch": 1785101935, - "elapsed_s": 43.9, - "verdict_tally": { - "PASS": 359, - "FAIL": 1, - "SIM": 0, - "EXPECTED_EMPTY": 145 - }, - "verdict_source": "results/full.json", - "coverage": { - "rust_llvm_cov": { - "ok": true, - "scope": "representative-analyzers(cf-gojson,cf-goyaml,cf-complexity,cf-halstead,cf-comments,cf-imports,cf-composition,cf-cohesion,cf-clones,cf-devs,cf-burndown-core,cf-couples,cf-shotness)", - "scope_kind": "subset", - "branch_coverage_available": false, - "elapsed_s": 3.4, - "totals": { - "lines": { - "covered": 12442, - "count": 19129, - "percent": 65.04260546813738 - }, - "regions": { - "covered": 20952, - "count": 31021, - "percent": 67.54134296121983 - }, - "functions": { - "covered": 1437, - "count": 2039, - "percent": 70.4757233938205 - }, - "branches": { - "covered": null, - "count": null, - "percent": null, - "unavailable": "cargo-llvm-cov --branch is nightly-only; region coverage is the stable branch-reach proxy. Branch number NOT fabricated." - } - }, - "note": "Coverage measures REACH, not correctness. Reported alongside the differential verdict per SPEC \u00a76, never as a substitute." - }, - "matrix_cell": { - "tier": "full", - "total_cells": 505, - "exercised_cells": 505, - "untested_cells": 0, - "percent": 100.0, - "per_family": { - "uast_parse": { - "total": 36, - "exercised": 36, - "percent": 100.0 - }, - "uast_analyze": { - "total": 24, - "exercised": 24, - "percent": 100.0 - }, - "uast_query": { - "total": 36, - "exercised": 36, - "percent": 100.0 - }, - "static_analyzer": { - "total": 147, - "exercised": 147, - "percent": 100.0 - }, - "analyzer_set": { - "total": 21, - "exercised": 21, - "percent": 100.0 - }, - "history_analyzer": { - "total": 210, - "exercised": 210, - "percent": 100.0 - }, - "format_combo": { - "total": 3, - "exercised": 3, - "percent": 100.0 - }, - "plot": { - "total": 19, - "exercised": 19, - "percent": 100.0 - }, - "flag": { - "total": 9, - "exercised": 9, - "percent": 100.0 - } - }, - "results_present": true, - "untested": [] - }, - "per_language_parse": { - "languages_total": 12, - "probed": true, - "table": [ - { - "language": "go", - "ext": "go", - "oracle_uast_bytes": 1256674, - "go_parse_bytes": 1256674, - "rust_parse_bytes": 1256674, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "python", - "ext": "py", - "oracle_uast_bytes": 700431, - "go_parse_bytes": 700431, - "rust_parse_bytes": 700431, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "c", - "ext": "c", - "oracle_uast_bytes": 142327, - "go_parse_bytes": 142327, - "rust_parse_bytes": 142327, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "c-header", - "ext": "h", - "oracle_uast_bytes": 19243, - "go_parse_bytes": 19243, - "rust_parse_bytes": 19243, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "rust", - "ext": "rs", - "oracle_uast_bytes": 125877, - "go_parse_bytes": 125877, - "rust_parse_bytes": 125877, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "typescript", - "ext": "ts", - "oracle_uast_bytes": 2555406, - "go_parse_bytes": 2555406, - "rust_parse_bytes": 2555406, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "tsx", - "ext": "tsx", - "oracle_uast_bytes": 244099, - "go_parse_bytes": 244099, - "rust_parse_bytes": 244099, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "javascript", - "ext": "js", - "oracle_uast_bytes": 54802, - "go_parse_bytes": 54802, - "rust_parse_bytes": 54802, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "json", - "ext": "json", - "oracle_uast_bytes": 308147, - "go_parse_bytes": 308147, - "rust_parse_bytes": 308147, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "yaml", - "ext": "yml", - "oracle_uast_bytes": 150552, - "go_parse_bytes": 150552, - "rust_parse_bytes": 150552, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "cpp", - "ext": "cpp", - "oracle_uast_bytes": 3895930, - "go_parse_bytes": 3895930, - "rust_parse_bytes": 3895930, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - }, - { - "language": "shell", - "ext": "sh", - "oracle_uast_bytes": 148968, - "go_parse_bytes": 148968, - "rust_parse_bytes": 148968, - "go_parses": true, - "rust_parses": true, - "status": "both-parse" - } - ], - "rust_parse_ok": 12, - "percent": 100.0, - "gaps": [] - } - }, - "untested_matrix_cells": [], - "untested_matrix_cell_count": 0, - "known_divergences": [ - { - "source": "corpus/known_gaps.json", - "language": "python", - "sha256": "984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 700431, - "rust_stderr": "Error: failed to parse corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py: parse corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py: no tree" - }, - { - "source": "corpus/known_gaps.json", - "language": "c", - "sha256": "6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 142327, - "rust_stderr": "Error: failed to parse corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c: parse corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c: no tree-s" - }, - { - "source": "corpus/known_gaps.json", - "language": "c-header", - "sha256": "ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 19243, - "rust_stderr": "Error: failed to parse corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h: parse corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h: no tree-s" - }, - { - "source": "corpus/known_gaps.json", - "language": "rust", - "sha256": "e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 125877, - "rust_stderr": "Error: failed to parse corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs: parse corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs: no tree" - }, - { - "source": "corpus/known_gaps.json", - "language": "typescript", - "sha256": "48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 2555406, - "rust_stderr": "Error: failed to parse corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts: parse corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts: no tree" - }, - { - "source": "corpus/known_gaps.json", - "language": "tsx", - "sha256": "29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 244099, - "rust_stderr": "Error: failed to parse corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx: parse corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx: no tr" - }, - { - "source": "corpus/known_gaps.json", - "language": "javascript", - "sha256": "cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 54802, - "rust_stderr": "Error: failed to parse corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js: parse corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js: no tree" - }, - { - "source": "corpus/known_gaps.json", - "language": "json", - "sha256": "9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 308147, - "rust_stderr": "Error: failed to parse corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json: parse corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json: no " - }, - { - "source": "corpus/known_gaps.json", - "language": "yaml", - "sha256": "fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 150552, - "rust_stderr": "Error: failed to parse corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml: parse corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml: no tr" - }, - { - "source": "corpus/known_gaps.json", - "language": "cpp", - "sha256": "8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 3895930, - "rust_stderr": "Error: failed to parse corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp: parse corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp: no tr" - }, - { - "source": "corpus/known_gaps.json", - "language": "shell", - "sha256": "5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58", - "kind": "rust-empty-go-nonempty", - "go_uast_bytes": 148968, - "rust_stderr": "Error: failed to parse corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh: parse corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh: no tree" - }, - { - "source": "results/full.json", - "label": "flag:static/complexity--include-vendored@hercules", - "verdict": "FAIL", - "family": "flag", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--include-vendored" - ] - } - ], - "known_divergence_count": 12, - "go_variant_captures": [ - { - "label": "history/couples", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/couples", - "--format", - "json", - "--limit", - "30", - "/home/dmitriy/sources/hercules" - ], - "oracle_verdict": "PASS", - "field_counts": { - "stable": 76, - "variant": 43 - }, - "n_variant_fields": 43, - "canonical_go_stable": true, - "go_run_shas": [ - "8a9c0355f03493f5244ae7010e686efbdac42d4295bfb5a8eb6986e478c275ed", - "cbd664fac1f8ad5d354d6b45d6a5d6763707f6379c1bfd6a215babaefa0fd380", - "5c40bb2f666b2f4b3f8e2bb87659fab2c4a30ec507636760fe75aebf24cd847b" - ], - "evidence_sample": { - "$.aggregate.avg_coupling_strength": [ - "0.2654884021793944", - "0.26548840217939446" - ], - "$.file_coupling[10].coupling_strength": [ - "0.09523809523809523", - "0.16666666666666666", - "0.18181818181818182" - ], - "$.file_coupling[10].file1": [ - "\".travis.yml\"", - "\"README.md\"", - "\"analyser.go\"" - ], - "$.file_coupling[10].file2": [ - "\"cmd/hercules/main.go\"", - "\"git-git.png\"", - "\"labours.py\"" - ], - "$.file_coupling[11].coupling_strength": [ - "0.11764705882352941", - "0.18181818181818182" - ], - "$.file_coupling[11].file1": [ - "\".travis.yml\"", - "\"README.md\"" - ], - "$.file_coupling[11].file2": [ - "\"analyser.go\"", - "\"rbtree.go\"" - ], - "$.file_coupling[12].coupling_strength": [ - "0.1111111111111111", - "0.11764705882352941", - "0.14285714285714285" - ], - "$.file_coupling[12].file1": [ - "\"README.md\"", - "\"analyser.go\"" - ], - "$.file_coupling[12].file2": [ - "\"file.go\"", - "\"file_test.go\"", - "\"rbtree.go\"" - ], - "$.file_coupling[13].coupling_strength": [ - "0.09523809523809523", - "0.11764705882352941" - ], - "$.file_coupling[13].file2": [ - "\"labours.py\"", - "\"rbtree.go\"" - ] - }, - "evidence_field_paths": [ - "$.aggregate.avg_coupling_strength", - "$.file_coupling[10].coupling_strength", - "$.file_coupling[10].file1", - "$.file_coupling[10].file2", - "$.file_coupling[11].coupling_strength", - "$.file_coupling[11].file1", - "$.file_coupling[11].file2", - "$.file_coupling[12].coupling_strength", - "$.file_coupling[12].file1", - "$.file_coupling[12].file2", - "$.file_coupling[13].coupling_strength", - "$.file_coupling[13].file2", - "$.file_coupling[14].coupling_strength", - "$.file_coupling[14].file1", - "$.file_coupling[14].file2", - "$.file_coupling[15].coupling_strength", - "$.file_coupling[15].file1", - "$.file_coupling[15].file2", - "$.file_coupling[16].coupling_strength", - "$.file_coupling[16].file1", - "$.file_coupling[16].file2", - "$.file_coupling[17].coupling_strength", - "$.file_coupling[17].file1", - "$.file_coupling[17].file2", - "$.file_coupling[18].coupling_strength", - "$.file_coupling[18].file1", - "$.file_coupling[18].file2", - "$.file_coupling[19].coupling_strength", - "$.file_coupling[19].file1", - "$.file_coupling[19].file2", - "$.file_coupling[5].coupling_strength", - "$.file_coupling[5].file1", - "$.file_coupling[5].file2", - "$.file_coupling[6].coupling_strength", - "$.file_coupling[6].file1", - "$.file_coupling[6].file2", - "$.file_coupling[7].coupling_strength", - "$.file_coupling[7].file2", - "$.file_coupling[8].coupling_strength", - "$.file_coupling[8].file2", - "$.file_coupling[9].coupling_strength", - "$.file_coupling[9].file1", - "$.file_coupling[9].file2" - ], - "note": "Go MEASURED VARIANT on these fields (evidence = the differing Go observations). Canonicalization here is measured, not declared." - }, - { - "label": "history/shotness", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/shotness", - "--format", - "json", - "--limit", - "30", - "/home/dmitriy/sources/hercules" - ], - "oracle_verdict": "PASS", - "field_counts": { - "stable": 7956, - "variant": 8261 - }, - "n_variant_fields": 8261, - "canonical_go_stable": false, - "go_run_shas": [ - "abd2a30832350a8658b950699608c768bc2e0aa79c858898094b0faa92008b43", - "881ed34e0a3cacdd57e50d385dc3515e4b1dddb2cb1838fbc270a0a1afc865d9", - "691e23927f0c6af39a3a81c49835482194ed750b06506fd265b10e784471048e" - ], - "evidence_sample": { - "$.aggregate.avg_changes_per_node": [ - "2.202247191011236", - "2.235955056179775", - "2.3373493975903616" - ], - "$.aggregate.avg_coupling_strength": [ - "0.7990881458966574", - "0.7991960420531912", - "0.8006657308150412" - ], - "$.aggregate.total_changes": [ - "194", - "196", - "199" - ], - "$.aggregate.total_couplings": [ - "2585", - "2597", - "2612" - ], - "$.aggregate.total_nodes": [ - "83", - "89" - ], - "$.hotspot_nodes[0].change_count": [ - "10", - "11", - "12" - ], - "$.node_coupling[0].coupling_strength": [ - "0.45454545454545453", - "0.5", - "0.7142857142857143" - ], - "$.node_coupling[0].node1_file": [ - "\"analyser.go\"", - "\"cmd/hercules/main.go\"" - ], - "$.node_coupling[0].node1_name": [ - "\"Analyse\"", - "\"main\"" - ], - "$.node_coupling[0].node2_file": [ - "\"analyser.go\"", - "\"file.go\"" - ], - "$.node_coupling[0].node2_name": [ - "\"Analyse\"", - "\"Dump\"", - "\"handleRename\"" - ], - "$.node_coupling[1000].coupling_strength": [ - "0.5", - "1" - ] - }, - "evidence_field_paths": [ - "$.aggregate.avg_changes_per_node", - "$.aggregate.avg_coupling_strength", - "$.aggregate.total_changes", - "$.aggregate.total_couplings", - "$.aggregate.total_nodes", - "$.hotspot_nodes[0].change_count", - "$.node_coupling[0].coupling_strength", - "$.node_coupling[0].node1_file", - "$.node_coupling[0].node1_name", - "$.node_coupling[0].node2_file", - "$.node_coupling[0].node2_name", - "$.node_coupling[1000].coupling_strength", - "$.node_coupling[1000].node1_file", - "$.node_coupling[1000].node1_name", - "$.node_coupling[1000].node2_file", - "$.node_coupling[1000].node2_name", - "$.node_coupling[1001].node2_file", - "$.node_coupling[1001].node2_name", - "$.node_coupling[1002].coupling_strength", - "$.node_coupling[1002].node2_file", - "$.node_coupling[1002].node2_name", - "$.node_coupling[1003].node2_file", - "$.node_coupling[1003].node2_name", - "$.node_coupling[1004].node2_name", - "$.node_coupling[1005].coupling_strength", - "$.node_coupling[1005].node2_file", - "$.node_coupling[1005].node2_name", - "$.node_coupling[1006].node2_name", - "$.node_coupling[1007].node2_name", - "$.node_coupling[1008].coupling_strength", - "$.node_coupling[1008].node2_file", - "$.node_coupling[1008].node2_name", - "$.node_coupling[1009].node2_name", - "$.node_coupling[100].coupling_strength", - "$.node_coupling[100].node2_file", - "$.node_coupling[100].node2_name", - "$.node_coupling[1010].node2_name", - "$.node_coupling[1011].node2_name", - "$.node_coupling[1012].node1_file", - "$.node_coupling[1012].node1_name", - "$.node_coupling[1012].node2_name", - "$.node_coupling[1013].node2_file", - "$.node_coupling[1013].node2_name", - "$.node_coupling[1014].coupling_strength", - "$.node_coupling[1014].node2_file", - "$.node_coupling[1014].node2_name", - "$.node_coupling[1015].coupling_strength", - "$.node_coupling[1015].node2_file", - "$.node_coupling[1015].node2_name", - "$.node_coupling[1016].coupling_strength", - "$.node_coupling[1016].node2_file", - "$.node_coupling[1016].node2_name", - "$.node_coupling[1017].node2_file", - "$.node_coupling[1017].node2_name", - "$.node_coupling[1018].node2_name", - "$.node_coupling[1019].coupling_strength", - "$.node_coupling[1019].node1_file", - "$.node_coupling[1019].node1_name", - "$.node_coupling[1019].node2_file", - "$.node_coupling[1019].node2_name", - "$.node_coupling[101].coupling_strength", - "$.node_coupling[101].node2_file", - "$.node_coupling[101].node2_name", - "$.node_coupling[1020].coupling_strength", - "$.node_coupling[1020].node2_file", - "$.node_coupling[1020].node2_name", - "$.node_coupling[1021].node2_file", - "$.node_coupling[1021].node2_name", - "$.node_coupling[1022].coupling_strength", - "$.node_coupling[1022].node1_file", - "$.node_coupling[1022].node1_name", - "$.node_coupling[1022].node2_file", - "$.node_coupling[1022].node2_name", - "$.node_coupling[1023].node2_name", - "$.node_coupling[1024].coupling_strength", - "$.node_coupling[1024].node1_file", - "$.node_coupling[1024].node1_name", - "$.node_coupling[1024].node2_name", - "$.node_coupling[1025].node1_file", - "$.node_coupling[1025].node1_name", - "$.node_coupling[1025].node2_name", - "$.node_coupling[1026].node1_file", - "$.node_coupling[1026].node1_name", - "$.node_coupling[1026].node2_file", - "$.node_coupling[1026].node2_name", - "$.node_coupling[1027].coupling_strength", - "$.node_coupling[1027].node1_file", - "$.node_coupling[1027].node1_name", - "$.node_coupling[1027].node2_file", - "$.node_coupling[1027].node2_name", - "$.node_coupling[1028].node1_file", - "$.node_coupling[1028].node1_name", - "$.node_coupling[1028].node2_name", - "$.node_coupling[1029].node1_file", - "$.node_coupling[1029].node1_name", - "$.node_coupling[1029].node2_name", - "$.node_coupling[102].coupling_strength", - "$.node_coupling[102].node1_file", - "$.node_coupling[102].node1_name", - "$.node_coupling[102].node2_file", - "$.node_coupling[102].node2_name", - "$.node_coupling[1030].node1_file", - "$.node_coupling[1030].node1_name", - "$.node_coupling[1030].node2_name", - "$.node_coupling[1031].node1_file", - "$.node_coupling[1031].node1_name", - "$.node_coupling[1031].node2_name", - "$.node_coupling[1032].coupling_strength", - "$.node_coupling[1032].node1_file", - "$.node_coupling[1032].node1_name", - "$.node_coupling[1032].node2_file", - "$.node_coupling[1032].node2_name", - "$.node_coupling[1033].coupling_strength", - "$.node_coupling[1033].node2_name", - "$.node_coupling[1034].coupling_strength", - "$.node_coupling[1034].node1_file", - "$.node_coupling[1034].node1_name", - "$.node_coupling[1034].node2_file", - "$.node_coupling[1034].node2_name", - "$.node_coupling[1035].coupling_strength", - "$.node_coupling[1035].node2_file", - "$.node_coupling[1035].node2_name", - "$.node_coupling[1036].coupling_strength", - "$.node_coupling[1036].node2_name", - "$.node_coupling[1037].coupling_strength", - "$.node_coupling[1037].node2_file", - "$.node_coupling[1037].node2_name", - "$.node_coupling[1038].coupling_strength", - "$.node_coupling[1038].node2_file", - "$.node_coupling[1038].node2_name", - "$.node_coupling[1039].coupling_strength", - "$.node_coupling[1039].node2_name", - "$.node_coupling[103].coupling_strength", - "$.node_coupling[103].node1_file", - "$.node_coupling[103].node1_name", - "$.node_coupling[103].node2_name", - "$.node_coupling[1040].coupling_strength", - "$.node_coupling[1040].node2_file", - "$.node_coupling[1040].node2_name", - "$.node_coupling[1041].node1_file", - "$.node_coupling[1041].node1_name", - "$.node_coupling[1041].node2_name", - "$.node_coupling[1042].coupling_strength", - "$.node_coupling[1042].node2_file", - "$.node_coupling[1042].node2_name", - "$.node_coupling[1043].coupling_strength", - "$.node_coupling[1043].node2_name", - "$.node_coupling[1044].coupling_strength", - "$.node_coupling[1044].node2_file", - "$.node_coupling[1044].node2_name", - "$.node_coupling[1045].coupling_strength", - "$.node_coupling[1045].node2_name", - "$.node_coupling[1046].coupling_strength", - "$.node_coupling[1046].node2_file", - "$.node_coupling[1046].node2_name", - "$.node_coupling[1047].coupling_strength", - "$.node_coupling[1047].node2_file", - "$.node_coupling[1047].node2_name", - "$.node_coupling[1048].coupling_strength", - "$.node_coupling[1048].node2_name", - "$.node_coupling[1049].coupling_strength", - "$.node_coupling[1049].node2_file", - "$.node_coupling[1049].node2_name", - "$.node_coupling[104].coupling_strength", - "$.node_coupling[104].node2_file", - "$.node_coupling[104].node2_name", - "$.node_coupling[1050].coupling_strength", - "$.node_coupling[1050].node2_name", - "$.node_coupling[1051].coupling_strength", - "$.node_coupling[1051].node2_file", - "$.node_coupling[1051].node2_name", - "$.node_coupling[1052].coupling_strength", - "$.node_coupling[1052].node2_name", - "$.node_coupling[1053].coupling_strength", - "$.node_coupling[1053].node2_file", - "$.node_coupling[1053].node2_name", - "$.node_coupling[1054].coupling_strength", - "$.node_coupling[1054].node2_file", - "$.node_coupling[1054].node2_name", - "$.node_coupling[1055].coupling_strength", - "$.node_coupling[1055].node1_file", - "$.node_coupling[1055].node1_name", - "$.node_coupling[1055].node2_file", - "$.node_coupling[1055].node2_name", - "$.node_coupling[1056].coupling_strength", - "$.node_coupling[1056].node2_name", - "$.node_coupling[1057].coupling_strength", - "$.node_coupling[1057].node2_file", - "$.node_coupling[1057].node2_name", - "$.node_coupling[1058].coupling_strength", - "$.node_coupling[1058].node2_file", - "$.node_coupling[1058].node2_name", - "$.node_coupling[1059].coupling_strength", - "$.node_coupling[1059].node2_name", - "$.node_coupling[105].coupling_strength", - "$.node_coupling[105].node2_file", - "$.node_coupling[105].node2_name", - "$.node_coupling[1060].coupling_strength", - "$.node_coupling[1060].node2_name", - "$.node_coupling[1061].coupling_strength", - "$.node_coupling[1061].node1_file", - "$.node_coupling[1061].node1_name", - "$.node_coupling[1061].node2_file", - "$.node_coupling[1061].node2_name", - "$.node_coupling[1062].coupling_strength", - "$.node_coupling[1062].node2_name", - "$.node_coupling[1063].coupling_strength", - "$.node_coupling[1063].node2_name", - "$.node_coupling[1064].coupling_strength", - "$.node_coupling[1064].node2_file", - "$.node_coupling[1064].node2_name", - "$.node_coupling[1065].node1_file", - "$.node_coupling[1065].node1_name", - "$.node_coupling[1065].node2_file", - "$.node_coupling[1065].node2_name", - "$.node_coupling[1066].coupling_strength", - "$.node_coupling[1066].node2_name", - "$.node_coupling[1067].coupling_strength", - "$.node_coupling[1067].node2_name", - "$.node_coupling[1068].coupling_strength", - "$.node_coupling[1068].node1_file", - "$.node_coupling[1068].node1_name", - "$.node_coupling[1068].node2_file", - "$.node_coupling[1068].node2_name", - "$.node_coupling[1069].coupling_strength", - "$.node_coupling[1069].node2_name", - "$.node_coupling[106].coupling_strength", - "$.node_coupling[106].node2_name", - "$.node_coupling[1070].node1_file", - "$.node_coupling[1070].node1_name", - "$.node_coupling[1070].node2_name", - "$.node_coupling[1071].coupling_strength", - "$.node_coupling[1071].node2_file", - "$.node_coupling[1071].node2_name", - "$.node_coupling[1072].coupling_strength", - "$.node_coupling[1072].node2_name", - "$.node_coupling[1073].coupling_strength", - "$.node_coupling[1073].node2_file", - "$.node_coupling[1073].node2_name", - "$.node_coupling[1074].coupling_strength", - "$.node_coupling[1074].node2_name", - "$.node_coupling[1075].coupling_strength", - "$.node_coupling[1075].node2_file", - "$.node_coupling[1075].node2_name", - "$.node_coupling[1076].coupling_strength", - "$.node_coupling[1076].node1_file", - "$.node_coupling[1076].node1_name", - "$.node_coupling[1076].node2_name", - "$.node_coupling[1077].coupling_strength", - "$.node_coupling[1077].node2_file", - "$.node_coupling[1077].node2_name", - "$.node_coupling[1078].coupling_strength", - "$.node_coupling[1078].node2_file", - "$.node_coupling[1078].node2_name", - "$.node_coupling[1079].coupling_strength", - "$.node_coupling[1079].node1_file", - "$.node_coupling[1079].node1_name", - "$.node_coupling[1079].node2_file", - "$.node_coupling[1079].node2_name", - "$.node_coupling[107].coupling_strength", - "$.node_coupling[107].node2_file", - "$.node_coupling[107].node2_name", - "$.node_coupling[1080].coupling_strength", - "$.node_coupling[1080].node1_file", - "$.node_coupling[1080].node1_name", - "$.node_coupling[1080].node2_file", - "$.node_coupling[1080].node2_name", - "$.node_coupling[1081].coupling_strength", - "$.node_coupling[1081].node1_file", - "$.node_coupling[1081].node1_name", - "$.node_coupling[1081].node2_file", - "$.node_coupling[1081].node2_name", - "$.node_coupling[1082].coupling_strength", - "$.node_coupling[1082].node1_file", - "$.node_coupling[1082].node1_name", - "$.node_coupling[1082].node2_name", - "$.node_coupling[1083].coupling_strength", - "$.node_coupling[1083].node1_file", - "$.node_coupling[1083].node1_name", - "$.node_coupling[1083].node2_file", - "$.node_coupling[1083].node2_name", - "$.node_coupling[1084].coupling_strength", - "$.node_coupling[1084].node1_file", - "$.node_coupling[1084].node1_name", - "$.node_coupling[1084].node2_file", - "$.node_coupling[1084].node2_name", - "$.node_coupling[1085].coupling_strength", - "$.node_coupling[1085].node1_file", - "$.node_coupling[1085].node1_name", - "$.node_coupling[1085].node2_file", - "$.node_coupling[1085].node2_name", - "$.node_coupling[1086].node1_file", - "$.node_coupling[1086].node1_name", - "$.node_coupling[1086].node2_file", - "$.node_coupling[1086].node2_name", - "$.node_coupling[1087].node1_name", - "$.node_coupling[1087].node2_name", - "$.node_coupling[1088].coupling_strength", - "$.node_coupling[1088].node1_file", - "$.node_coupling[1088].node1_name", - "$.node_coupling[1088].node2_file", - "$.node_coupling[1088].node2_name", - "$.node_coupling[1089].coupling_strength", - "$.node_coupling[1089].node1_file", - "$.node_coupling[1089].node1_name", - "$.node_coupling[1089].node2_name", - "$.node_coupling[108].coupling_strength", - "$.node_coupling[108].node2_name", - "$.node_coupling[1090].node1_name", - "$.node_coupling[1090].node2_name", - "$.node_coupling[1091].node1_name", - "$.node_coupling[1091].node2_name", - "$.node_coupling[1092].node2_name", - "$.node_coupling[1093].node2_name", - "$.node_coupling[1094].node2_name", - "$.node_coupling[1095].node2_name", - "$.node_coupling[1096].coupling_strength", - "$.node_coupling[1096].node2_file", - "$.node_coupling[1096].node2_name", - "$.node_coupling[1097].node2_name", - "$.node_coupling[1098].coupling_strength", - "$.node_coupling[1098].node1_name", - "$.node_coupling[1098].node2_file", - "$.node_coupling[1098].node2_name", - "$.node_coupling[1099].node1_name", - "$.node_coupling[1099].node2_name", - "$.node_coupling[109].coupling_strength", - "$.node_coupling[109].node2_file", - "$.node_coupling[109].node2_name", - "$.node_coupling[10].coupling_strength", - "$.node_coupling[10].node1_file", - "$.node_coupling[10].node1_name", - "$.node_coupling[10].node2_file", - "$.node_coupling[10].node2_name", - "$.node_coupling[1100].coupling_strength", - "$.node_coupling[1100].node2_file", - "$.node_coupling[1100].node2_name", - "$.node_coupling[1101].node2_name", - "$.node_coupling[1102].node1_name", - "$.node_coupling[1102].node2_name", - "$.node_coupling[1103].coupling_strength", - "$.node_coupling[1103].node2_file", - "$.node_coupling[1103].node2_name", - "$.node_coupling[1104].node1_name", - "$.node_coupling[1104].node2_name", - "$.node_coupling[1105].coupling_strength", - "$.node_coupling[1105].node2_file", - "$.node_coupling[1105].node2_name", - "$.node_coupling[1106].node2_name", - "$.node_coupling[1107].node2_name", - "$.node_coupling[1108].node2_file", - "$.node_coupling[1108].node2_name", - "$.node_coupling[1109].coupling_strength", - "$.node_coupling[1109].node2_file", - "$.node_coupling[1109].node2_name", - "$.node_coupling[110].coupling_strength", - "$.node_coupling[110].node2_file", - "$.node_coupling[110].node2_name", - "$.node_coupling[1110].node1_name", - "$.node_coupling[1110].node2_name", - "$.node_coupling[1111].coupling_strength", - "$.node_coupling[1111].node2_file", - "$.node_coupling[1111].node2_name", - "$.node_coupling[1112].node2_file", - "$.node_coupling[1112].node2_name", - "$.node_coupling[1113].coupling_strength", - "$.node_coupling[1113].node2_file", - "$.node_coupling[1113].node2_name", - "$.node_coupling[1114].coupling_strength", - "$.node_coupling[1114].node2_file", - "$.node_coupling[1114].node2_name", - "$.node_coupling[1115].node2_name", - "$.node_coupling[1116].node2_file", - "$.node_coupling[1116].node2_name", - "$.node_coupling[1117].coupling_strength", - "$.node_coupling[1117].node2_file", - "$.node_coupling[1117].node2_name", - "$.node_coupling[1118].node2_file", - "$.node_coupling[1118].node2_name", - "$.node_coupling[1119].coupling_strength", - "$.node_coupling[1119].node1_name", - "$.node_coupling[1119].node2_file", - "$.node_coupling[1119].node2_name", - "$.node_coupling[111].node1_file", - "$.node_coupling[111].node1_name", - "$.node_coupling[111].node2_file", - "$.node_coupling[111].node2_name", - "$.node_coupling[1120].coupling_strength", - "$.node_coupling[1120].node2_file", - "$.node_coupling[1120].node2_name", - "$.node_coupling[1121].node1_name", - "$.node_coupling[1121].node2_name", - "$.node_coupling[1122].node2_name", - "$.node_coupling[1123].coupling_strength", - "$.node_coupling[1123].node2_file", - "$.node_coupling[1123].node2_name", - "$.node_coupling[1124].coupling_strength", - "$.node_coupling[1124].node2_file", - "$.node_coupling[1124].node2_name", - "$.node_coupling[1125].node2_file", - "$.node_coupling[1125].node2_name", - "$.node_coupling[1126].node1_name", - "$.node_coupling[1126].node2_name", - "$.node_coupling[1127].coupling_strength", - "$.node_coupling[1127].node2_file", - "$.node_coupling[1127].node2_name", - "$.node_coupling[1128].node2_file", - "$.node_coupling[1128].node2_name", - "$.node_coupling[1129].node2_file", - "$.node_coupling[1129].node2_name", - "$.node_coupling[112].coupling_strength", - "$.node_coupling[112].node2_file", - "$.node_coupling[112].node2_name", - "$.node_coupling[1130].node2_name", - "$.node_coupling[1131].coupling_strength", - "$.node_coupling[1131].node2_file", - "$.node_coupling[1131].node2_name", - "$.node_coupling[1132].node2_file", - "$.node_coupling[1132].node2_name", - "$.node_coupling[1133].node1_file", - "$.node_coupling[1133].node1_name", - "$.node_coupling[1133].node2_file", - "$.node_coupling[1133].node2_name", - "$.node_coupling[1134].node1_file", - "$.node_coupling[1134].node1_name", - "$.node_coupling[1134].node2_file", - "$.node_coupling[1134].node2_name", - "$.node_coupling[1135].node1_file", - "$.node_coupling[1135].node1_name", - "$.node_coupling[1135].node2_file", - "$.node_coupling[1135].node2_name", - "$.node_coupling[1136].node1_file", - "$.node_coupling[1136].node1_name", - "$.node_coupling[1136].node2_file", - "$.node_coupling[1136].node2_name", - "$.node_coupling[1137].coupling_strength", - "$.node_coupling[1137].node1_file", - "$.node_coupling[1137].node1_name", - "$.node_coupling[1137].node2_file", - "$.node_coupling[1137].node2_name", - "$.node_coupling[1138].coupling_strength", - "$.node_coupling[1138].node1_file", - "$.node_coupling[1138].node1_name", - "$.node_coupling[1138].node2_file", - "$.node_coupling[1138].node2_name", - "$.node_coupling[1139].node1_file", - "$.node_coupling[1139].node1_name", - "$.node_coupling[1139].node2_file", - "$.node_coupling[1139].node2_name", - "$.node_coupling[113].coupling_strength", - "$.node_coupling[113].node2_file", - "$.node_coupling[113].node2_name", - "$.node_coupling[1140].node1_file", - "$.node_coupling[1140].node1_name", - "$.node_coupling[1140].node2_file", - "$.node_coupling[1140].node2_name", - "$.node_coupling[1141].coupling_strength", - "$.node_coupling[1141].node1_file", - "$.node_coupling[1141].node1_name", - "$.node_coupling[1141].node2_file", - "$.node_coupling[1141].node2_name", - "$.node_coupling[1142].node1_file", - "$.node_coupling[1142].node1_name", - "$.node_coupling[1142].node2_name", - "$.node_coupling[1143].node1_file", - "$.node_coupling[1143].node1_name", - "$.node_coupling[1143].node2_name", - "$.node_coupling[1144].node1_file", - "$.node_coupling[1144].node1_name", - "$.node_coupling[1144].node2_file", - "$.node_coupling[1144].node2_name", - "$.node_coupling[1145].node1_file", - "$.node_coupling[1145].node1_name", - "$.node_coupling[1145].node2_name", - "$.node_coupling[1146].coupling_strength", - "$.node_coupling[1146].node1_file", - "$.node_coupling[1146].node1_name", - "$.node_coupling[1146].node2_file", - "$.node_coupling[1146].node2_name", - "$.node_coupling[1147].node2_name", - "$.node_coupling[1148].node2_name", - "$.node_coupling[1149].node2_file", - "$.node_coupling[1149].node2_name", - "$.node_coupling[114].coupling_strength", - "$.node_coupling[114].node2_file", - "$.node_coupling[114].node2_name", - "$.node_coupling[1150].node2_name", - "$.node_coupling[1151].node1_file", - "$.node_coupling[1151].node1_name", - "$.node_coupling[1151].node2_file", - "$.node_coupling[1151].node2_name", - "$.node_coupling[1152].coupling_strength", - "$.node_coupling[1152].node2_file", - "$.node_coupling[1152].node2_name", - "$.node_coupling[1153].node2_file", - "$.node_coupling[1153].node2_name", - "$.node_coupling[1154].node2_file", - "$.node_coupling[1154].node2_name", - "$.node_coupling[1155].coupling_strength", - "$.node_coupling[1155].node2_file", - "$.node_coupling[1155].node2_name", - "$.node_coupling[1156].node2_file", - "$.node_coupling[1156].node2_name", - "$.node_coupling[1157].node2_name", - "$.node_coupling[1158].coupling_strength", - "$.node_coupling[1158].node2_file", - "$.node_coupling[1158].node2_name", - "$.node_coupling[1159].node1_file", - "$.node_coupling[1159].node1_name", - "$.node_coupling[1159].node2_name", - "$.node_coupling[115].coupling_strength", - "$.node_coupling[115].node2_file", - "$.node_coupling[115].node2_name", - "$.node_coupling[1160].coupling_strength", - "$.node_coupling[1160].node2_file", - "$.node_coupling[1160].node2_name", - "$.node_coupling[1161].coupling_strength", - "$.node_coupling[1161].node2_file", - "$.node_coupling[1161].node2_name", - "$.node_coupling[1162].coupling_strength", - "$.node_coupling[1162].node2_file", - "$.node_coupling[1162].node2_name", - "$.node_coupling[1163].coupling_strength", - "$.node_coupling[1163].node2_file", - "$.node_coupling[1163].node2_name", - "$.node_coupling[1164].coupling_strength", - "$.node_coupling[1164].node2_file", - "$.node_coupling[1164].node2_name", - "$.node_coupling[1165].node2_file", - "$.node_coupling[1165].node2_name", - "$.node_coupling[1166].node2_file", - "$.node_coupling[1166].node2_name", - "$.node_coupling[1167].coupling_strength", - "$.node_coupling[1167].node2_file", - "$.node_coupling[1167].node2_name", - "$.node_coupling[1168].coupling_strength", - "$.node_coupling[1168].node2_file", - "$.node_coupling[1168].node2_name", - "$.node_coupling[1169].node2_file", - "$.node_coupling[1169].node2_name", - "$.node_coupling[116].coupling_strength", - "$.node_coupling[116].node2_file", - "$.node_coupling[116].node2_name", - "$.node_coupling[1170].node1_file", - "$.node_coupling[1170].node1_name", - "$.node_coupling[1170].node2_name", - "$.node_coupling[1171].coupling_strength", - "$.node_coupling[1171].node2_file", - "$.node_coupling[1171].node2_name", - "$.node_coupling[1172].coupling_strength", - "$.node_coupling[1172].node2_file", - "$.node_coupling[1172].node2_name", - "$.node_coupling[1173].node2_name", - "$.node_coupling[1174].coupling_strength", - "$.node_coupling[1174].node1_file", - "$.node_coupling[1174].node1_name", - "$.node_coupling[1174].node2_file", - "$.node_coupling[1174].node2_name", - "$.node_coupling[1175].coupling_strength", - "$.node_coupling[1175].node2_file", - "$.node_coupling[1175].node2_name", - "$.node_coupling[1176].node2_name", - "$.node_coupling[1177].node1_file", - "$.node_coupling[1177].node1_name", - "$.node_coupling[1177].node2_name", - "$.node_coupling[1178].node2_name", - "$.node_coupling[1179].coupling_strength", - "$.node_coupling[1179].node2_file", - "$.node_coupling[1179].node2_name", - "$.node_coupling[117].coupling_strength", - "$.node_coupling[117].node2_name", - "$.node_coupling[1180].node1_file", - "$.node_coupling[1180].node1_name", - "$.node_coupling[1180].node2_file", - "$.node_coupling[1180].node2_name", - "$.node_coupling[1181].node2_name", - "$.node_coupling[1182].coupling_strength", - "$.node_coupling[1182].node2_file", - "$.node_coupling[1182].node2_name", - "$.node_coupling[1183].coupling_strength", - "$.node_coupling[1183].node2_file", - "$.node_coupling[1183].node2_name", - "$.node_coupling[1184].node2_name", - "$.node_coupling[1185].node1_file", - "$.node_coupling[1185].node1_name", - "$.node_coupling[1185].node2_name", - "$.node_coupling[1186].coupling_strength", - "$.node_coupling[1186].node1_file", - "$.node_coupling[1186].node1_name", - "$.node_coupling[1186].node2_file", - "$.node_coupling[1186].node2_name", - "$.node_coupling[1187].node1_file", - "$.node_coupling[1187].node1_name", - "$.node_coupling[1187].node2_name", - "$.node_coupling[1188].node1_file", - "$.node_coupling[1188].node1_name", - "$.node_coupling[1188].node2_file", - "$.node_coupling[1188].node2_name", - "$.node_coupling[1189].node1_file", - "$.node_coupling[1189].node1_name", - "$.node_coupling[1189].node2_name", - "$.node_coupling[118].coupling_strength", - "$.node_coupling[118].node1_file", - "$.node_coupling[118].node1_name", - "$.node_coupling[118].node2_file", - "$.node_coupling[118].node2_name", - "$.node_coupling[1190].node1_file", - "$.node_coupling[1190].node1_name", - "$.node_coupling[1190].node2_file", - "$.node_coupling[1190].node2_name", - "$.node_coupling[1191].node1_file", - "$.node_coupling[1191].node1_name", - "$.node_coupling[1191].node2_name", - "$.node_coupling[1192].node1_file", - "$.node_coupling[1192].node1_name", - "$.node_coupling[1192].node2_name", - "$.node_coupling[1193].node1_file", - "$.node_coupling[1193].node1_name", - "$.node_coupling[1193].node2_name", - "$.node_coupling[1194].node1_file", - "$.node_coupling[1194].node1_name", - "$.node_coupling[1194].node2_file", - "$.node_coupling[1194].node2_name", - "$.node_coupling[1195].node1_file", - "$.node_coupling[1195].node1_name", - "$.node_coupling[1195].node2_name", - "$.node_coupling[1196].node1_file", - "$.node_coupling[1196].node1_name", - "$.node_coupling[1196].node2_name", - "$.node_coupling[1197].coupling_strength", - "$.node_coupling[1197].node1_file", - "$.node_coupling[1197].node1_name", - "$.node_coupling[1197].node2_file", - "$.node_coupling[1197].node2_name", - "$.node_coupling[1198].node1_file", - "$.node_coupling[1198].node1_name", - "$.node_coupling[1198].node2_name", - "$.node_coupling[1199].coupling_strength", - "$.node_coupling[1199].node2_file", - "$.node_coupling[1199].node2_name", - "$.node_coupling[119].coupling_strength", - "$.node_coupling[119].node2_file", - "$.node_coupling[119].node2_name", - "$.node_coupling[11].coupling_strength", - "$.node_coupling[11].node1_name", - "$.node_coupling[11].node2_file", - "$.node_coupling[11].node2_name", - "$.node_coupling[1200].node2_file", - "$.node_coupling[1200].node2_name", - "$.node_coupling[1201].node2_name", - "$.node_coupling[1202].node1_name", - "$.node_coupling[1202].node2_file", - "$.node_coupling[1202].node2_name", - "$.node_coupling[1203].node2_name", - "$.node_coupling[1204].coupling_strength", - "$.node_coupling[1204].node2_file", - "$.node_coupling[1204].node2_name", - "$.node_coupling[1205].node2_name", - "$.node_coupling[1206].coupling_strength", - "$.node_coupling[1206].node2_file", - "$.node_coupling[1206].node2_name", - "$.node_coupling[1207].node2_file", - "$.node_coupling[1207].node2_name", - "$.node_coupling[1208].node2_name", - "$.node_coupling[1209].node2_file", - "$.node_coupling[1209].node2_name", - "$.node_coupling[120].coupling_strength", - "$.node_coupling[120].node2_name", - "$.node_coupling[1210].node1_name", - "$.node_coupling[1210].node2_file", - "$.node_coupling[1210].node2_name", - "$.node_coupling[1211].coupling_strength", - "$.node_coupling[1211].node2_file", - "$.node_coupling[1211].node2_name", - "$.node_coupling[1212].coupling_strength", - "$.node_coupling[1212].node2_file", - "$.node_coupling[1212].node2_name", - "$.node_coupling[1213].node2_file", - "$.node_coupling[1213].node2_name", - "$.node_coupling[1214].coupling_strength", - "$.node_coupling[1214].node2_file", - "$.node_coupling[1214].node2_name", - "$.node_coupling[1215].node1_name", - "$.node_coupling[1215].node2_name", - "$.node_coupling[1216].node2_name", - "$.node_coupling[1217].coupling_strength", - "$.node_coupling[1217].node2_file", - "$.node_coupling[1217].node2_name", - "$.node_coupling[1218].node2_name", - "$.node_coupling[1219].node2_name", - "$.node_coupling[121].node1_file", - "$.node_coupling[121].node1_name", - "$.node_coupling[121].node2_name", - "$.node_coupling[1220].node2_file", - "$.node_coupling[1220].node2_name", - "$.node_coupling[1221].node1_name", - "$.node_coupling[1221].node2_name", - "$.node_coupling[1222].coupling_strength", - "$.node_coupling[1222].node2_file", - "$.node_coupling[1222].node2_name", - "$.node_coupling[1223].node1_name", - "$.node_coupling[1223].node2_name", - "$.node_coupling[1224].node2_name", - "$.node_coupling[1225].coupling_strength", - "$.node_coupling[1225].node1_name", - "$.node_coupling[1225].node2_file", - "$.node_coupling[1225].node2_name", - "$.node_coupling[1226].coupling_strength", - "$.node_coupling[1226].node2_file", - "$.node_coupling[1226].node2_name", - "$.node_coupling[1227].node2_file", - "$.node_coupling[1227].node2_name", - "$.node_coupling[1228].coupling_strength", - "$.node_coupling[1228].node2_file", - "$.node_coupling[1228].node2_name", - "$.node_coupling[1229].node2_name", - "$.node_coupling[122].coupling_strength", - "$.node_coupling[122].node2_file", - "$.node_coupling[122].node2_name", - "$.node_coupling[1230].node2_file", - "$.node_coupling[1230].node2_name", - "$.node_coupling[1231].node2_name", - "$.node_coupling[1232].coupling_strength", - "$.node_coupling[1232].node1_name", - "$.node_coupling[1232].node2_file", - "$.node_coupling[1232].node2_name", - "$.node_coupling[1233].coupling_strength", - "$.node_coupling[1233].node2_file", - "$.node_coupling[1233].node2_name", - "$.node_coupling[1234].node2_name", - "$.node_coupling[1235].node2_name", - "$.node_coupling[1236].node2_name", - "$.node_coupling[1237].coupling_strength", - "$.node_coupling[1237].node2_file", - "$.node_coupling[1237].node2_name", - "$.node_coupling[1238].coupling_strength", - "$.node_coupling[1238].node1_name", - "$.node_coupling[1238].node2_file", - "$.node_coupling[1238].node2_name", - "$.node_coupling[1239].coupling_strength", - "$.node_coupling[1239].node1_name", - "$.node_coupling[1239].node2_file", - "$.node_coupling[1239].node2_name", - "$.node_coupling[123].coupling_strength", - "$.node_coupling[123].node2_file", - "$.node_coupling[123].node2_name", - "$.node_coupling[1240].coupling_strength", - "$.node_coupling[1240].node1_name", - "$.node_coupling[1240].node2_file", - "$.node_coupling[1240].node2_name", - "$.node_coupling[1241].coupling_strength", - "$.node_coupling[1241].node1_name", - "$.node_coupling[1241].node2_file", - "$.node_coupling[1241].node2_name", - "$.node_coupling[1242].node1_name", - "$.node_coupling[1242].node2_file", - "$.node_coupling[1242].node2_name", - "$.node_coupling[1243].node1_name", - "$.node_coupling[1243].node2_file", - "$.node_coupling[1243].node2_name", - "$.node_coupling[1244].coupling_strength", - "$.node_coupling[1244].node1_name", - "$.node_coupling[1244].node2_file", - "$.node_coupling[1244].node2_name", - "$.node_coupling[1245].node1_name", - "$.node_coupling[1245].node2_name", - "$.node_coupling[1246].node1_name", - "$.node_coupling[1246].node2_name", - "$.node_coupling[1247].coupling_strength", - "$.node_coupling[1247].node1_name", - "$.node_coupling[1247].node2_file", - "$.node_coupling[1247].node2_name", - "$.node_coupling[1248].coupling_strength", - "$.node_coupling[1248].node1_file", - "$.node_coupling[1248].node1_name", - "$.node_coupling[1248].node2_name", - "$.node_coupling[1249].node1_name", - "$.node_coupling[1249].node2_name", - "$.node_coupling[124].coupling_strength", - "$.node_coupling[124].node2_name", - "$.node_coupling[1250].node1_name", - "$.node_coupling[1250].node2_file", - "$.node_coupling[1250].node2_name", - "$.node_coupling[1251].node1_name", - "$.node_coupling[1251].node2_file", - "$.node_coupling[1251].node2_name", - "$.node_coupling[1252].node1_name", - "$.node_coupling[1252].node2_name", - "$.node_coupling[1253].coupling_strength", - "$.node_coupling[1253].node2_file", - "$.node_coupling[1253].node2_name", - "$.node_coupling[1254].node2_name", - "$.node_coupling[1255].node2_file", - "$.node_coupling[1255].node2_name", - "$.node_coupling[1256].node2_name", - "$.node_coupling[1257].coupling_strength", - "$.node_coupling[1257].node2_file", - "$.node_coupling[1257].node2_name", - "$.node_coupling[1258].node1_name", - "$.node_coupling[1258].node2_name", - "$.node_coupling[1259].coupling_strength", - "$.node_coupling[1259].node2_file", - "$.node_coupling[1259].node2_name", - "$.node_coupling[125].coupling_strength", - "$.node_coupling[125].node2_file", - "$.node_coupling[125].node2_name", - "$.node_coupling[1260].node2_name", - "$.node_coupling[1261].node2_file", - "$.node_coupling[1261].node2_name", - "$.node_coupling[1262].coupling_strength", - "$.node_coupling[1262].node2_file", - "$.node_coupling[1262].node2_name", - "$.node_coupling[1263].node2_file", - "$.node_coupling[1263].node2_name", - "$.node_coupling[1264].coupling_strength", - "$.node_coupling[1264].node1_name", - "$.node_coupling[1264].node2_file", - "$.node_coupling[1264].node2_name", - "$.node_coupling[1265].coupling_strength", - "$.node_coupling[1265].node2_file", - "$.node_coupling[1265].node2_name", - "$.node_coupling[1266].coupling_strength", - "$.node_coupling[1266].node2_file", - "$.node_coupling[1266].node2_name", - "$.node_coupling[1267].node2_file", - "$.node_coupling[1267].node2_name", - "$.node_coupling[1268].coupling_strength", - "$.node_coupling[1268].node2_file", - "$.node_coupling[1268].node2_name", - "$.node_coupling[1269].node2_name", - "$.node_coupling[126].coupling_strength", - "$.node_coupling[126].node2_file", - "$.node_coupling[126].node2_name", - "$.node_coupling[1270].coupling_strength", - "$.node_coupling[1270].node2_file", - "$.node_coupling[1270].node2_name", - "$.node_coupling[1271].node2_file", - "$.node_coupling[1271].node2_name", - "$.node_coupling[1272].coupling_strength", - "$.node_coupling[1272].node2_file", - "$.node_coupling[1272].node2_name", - "$.node_coupling[1273].node1_name", - "$.node_coupling[1273].node2_name", - "$.node_coupling[1274].coupling_strength", - "$.node_coupling[1274].node2_file", - "$.node_coupling[1274].node2_name", - "$.node_coupling[1275].node1_name", - "$.node_coupling[1275].node2_name", - "$.node_coupling[1276].coupling_strength", - "$.node_coupling[1276].node2_file", - "$.node_coupling[1276].node2_name", - "$.node_coupling[1277].node2_name", - "$.node_coupling[1278].node1_name", - "$.node_coupling[1278].node2_name", - "$.node_coupling[1279].node2_name", - "$.node_coupling[127].coupling_strength", - "$.node_coupling[127].node2_file", - "$.node_coupling[127].node2_name", - "$.node_coupling[1280].node2_file", - "$.node_coupling[1280].node2_name", - "$.node_coupling[1281].coupling_strength", - "$.node_coupling[1281].node2_file", - "$.node_coupling[1281].node2_name", - "$.node_coupling[1282].node1_name", - "$.node_coupling[1282].node2_file", - "$.node_coupling[1282].node2_name", - "$.node_coupling[1283].coupling_strength", - "$.node_coupling[1283].node1_name", - "$.node_coupling[1283].node2_file", - "$.node_coupling[1283].node2_name", - "$.node_coupling[1284].coupling_strength", - "$.node_coupling[1284].node1_name", - "$.node_coupling[1284].node2_file", - "$.node_coupling[1284].node2_name", - "$.node_coupling[1285].coupling_strength", - "$.node_coupling[1285].node2_file", - "$.node_coupling[1285].node2_name", - "$.node_coupling[1286].node2_file", - "$.node_coupling[1286].node2_name", - "$.node_coupling[1287].node1_name", - "$.node_coupling[1287].node2_file", - "$.node_coupling[1287].node2_name", - "$.node_coupling[1288].node2_name", - "$.node_coupling[1289].coupling_strength", - "$.node_coupling[1289].node2_file", - "$.node_coupling[1289].node2_name", - "$.node_coupling[128].coupling_strength", - "$.node_coupling[128].node2_name", - "$.node_coupling[1290].coupling_strength", - "$.node_coupling[1290].node2_file", - "$.node_coupling[1290].node2_name", - "$.node_coupling[1291].node2_file", - "$.node_coupling[1291].node2_name", - "$.node_coupling[1292].coupling_strength", - "$.node_coupling[1292].node1_file", - "$.node_coupling[1292].node1_name", - "$.node_coupling[1292].node2_file", - "$.node_coupling[1292].node2_name", - "$.node_coupling[1293].node1_file", - "$.node_coupling[1293].node1_name", - "$.node_coupling[1293].node2_name", - "$.node_coupling[1294].node1_file", - "$.node_coupling[1294].node1_name", - "$.node_coupling[1294].node2_name", - "$.node_coupling[1295].node1_file", - "$.node_coupling[1295].node1_name", - "$.node_coupling[1295].node2_name", - "$.node_coupling[1296].node1_name", - "$.node_coupling[1296].node2_name", - "$.node_coupling[1297].node1_file", - "$.node_coupling[1297].node1_name", - "$.node_coupling[1297].node2_name", - "$.node_coupling[1298].coupling_strength", - "$.node_coupling[1298].node1_file", - "$.node_coupling[1298].node1_name", - "$.node_coupling[1298].node2_file", - "$.node_coupling[1298].node2_name", - "$.node_coupling[1299].node1_name", - "$.node_coupling[1299].node2_file", - "$.node_coupling[1299].node2_name", - "$.node_coupling[129].coupling_strength", - "$.node_coupling[129].node2_name", - "$.node_coupling[12].co_changes", - "$.node_coupling[12].coupling_strength", - "$.node_coupling[12].node1_file", - "$.node_coupling[12].node1_name", - "$.node_coupling[12].node2_file", - "$.node_coupling[12].node2_name", - "$.node_coupling[1300].coupling_strength", - "$.node_coupling[1300].node1_file", - "$.node_coupling[1300].node1_name", - "$.node_coupling[1300].node2_file", - "$.node_coupling[1300].node2_name", - "$.node_coupling[1301].node1_file", - "$.node_coupling[1301].node1_name", - "$.node_coupling[1301].node2_name", - "$.node_coupling[1302].coupling_strength", - "$.node_coupling[1302].node1_file", - "$.node_coupling[1302].node1_name", - "$.node_coupling[1302].node2_file", - "$.node_coupling[1302].node2_name", - "$.node_coupling[1303].coupling_strength", - "$.node_coupling[1303].node1_file", - "$.node_coupling[1303].node1_name", - "$.node_coupling[1303].node2_file", - "$.node_coupling[1303].node2_name", - "$.node_coupling[1304].coupling_strength", - "$.node_coupling[1304].node1_file", - "$.node_coupling[1304].node1_name", - "$.node_coupling[1304].node2_file", - "$.node_coupling[1304].node2_name", - "$.node_coupling[1305].node1_file", - "$.node_coupling[1305].node1_name", - "$.node_coupling[1305].node2_file", - "$.node_coupling[1305].node2_name", - "$.node_coupling[1306].coupling_strength", - "$.node_coupling[1306].node1_file", - "$.node_coupling[1306].node1_name", - "$.node_coupling[1306].node2_file", - "$.node_coupling[1306].node2_name", - "$.node_coupling[1307].node1_file", - "$.node_coupling[1307].node1_name", - "$.node_coupling[1307].node2_file", - "$.node_coupling[1307].node2_name", - "$.node_coupling[1308].coupling_strength", - "$.node_coupling[1308].node1_file", - "$.node_coupling[1308].node1_name", - "$.node_coupling[1308].node2_file", - "$.node_coupling[1308].node2_name", - "$.node_coupling[1309].node1_file", - "$.node_coupling[1309].node1_name", - "$.node_coupling[1309].node2_name", - "$.node_coupling[130].coupling_strength", - "$.node_coupling[130].node1_file", - "$.node_coupling[130].node1_name", - "$.node_coupling[130].node2_file", - "$.node_coupling[130].node2_name", - "$.node_coupling[1310].node1_file", - "$.node_coupling[1310].node1_name", - "$.node_coupling[1310].node2_name", - "$.node_coupling[1311].node1_file", - "$.node_coupling[1311].node1_name", - "$.node_coupling[1311].node2_file", - "$.node_coupling[1311].node2_name", - "$.node_coupling[1312].node1_file", - "$.node_coupling[1312].node1_name", - "$.node_coupling[1312].node2_file", - "$.node_coupling[1312].node2_name", - "$.node_coupling[1313].coupling_strength", - "$.node_coupling[1313].node2_file", - "$.node_coupling[1313].node2_name", - "$.node_coupling[1314].coupling_strength", - "$.node_coupling[1314].node2_file", - "$.node_coupling[1314].node2_name", - "$.node_coupling[1315].coupling_strength", - "$.node_coupling[1315].node2_file", - "$.node_coupling[1315].node2_name", - "$.node_coupling[1316].coupling_strength", - "$.node_coupling[1316].node2_file", - "$.node_coupling[1316].node2_name", - "$.node_coupling[1317].coupling_strength", - "$.node_coupling[1317].node1_file", - "$.node_coupling[1317].node1_name", - "$.node_coupling[1317].node2_name", - "$.node_coupling[1318].coupling_strength", - "$.node_coupling[1318].node1_file", - "$.node_coupling[1318].node1_name", - "$.node_coupling[1318].node2_name", - "$.node_coupling[1319].coupling_strength", - "$.node_coupling[1319].node2_file", - "$.node_coupling[1319].node2_name", - "$.node_coupling[131].coupling_strength", - "$.node_coupling[131].node1_file", - "$.node_coupling[131].node1_name", - "$.node_coupling[131].node2_name", - "$.node_coupling[1320].coupling_strength", - "$.node_coupling[1320].node2_file", - "$.node_coupling[1320].node2_name", - "$.node_coupling[1321].coupling_strength", - "$.node_coupling[1321].node2_name", - "$.node_coupling[1322].coupling_strength", - "$.node_coupling[1322].node2_file", - "$.node_coupling[1322].node2_name", - "$.node_coupling[1323].coupling_strength", - "$.node_coupling[1323].node2_name", - "$.node_coupling[1324].coupling_strength", - "$.node_coupling[1324].node2_file", - "$.node_coupling[1324].node2_name", - "$.node_coupling[1325].coupling_strength", - "$.node_coupling[1325].node2_name", - "$.node_coupling[1326].coupling_strength", - "$.node_coupling[1326].node2_file", - "$.node_coupling[1326].node2_name", - "$.node_coupling[1327].coupling_strength", - "$.node_coupling[1327].node2_name", - "$.node_coupling[1328].coupling_strength", - "$.node_coupling[1328].node1_file", - "$.node_coupling[1328].node1_name", - "$.node_coupling[1328].node2_name", - "$.node_coupling[1329].coupling_strength", - "$.node_coupling[1329].node2_file", - "$.node_coupling[1329].node2_name", - "$.node_coupling[132].coupling_strength", - "$.node_coupling[132].node1_file", - "$.node_coupling[132].node1_name", - "$.node_coupling[132].node2_file", - "$.node_coupling[132].node2_name", - "$.node_coupling[1330].coupling_strength", - "$.node_coupling[1330].node2_file", - "$.node_coupling[1330].node2_name", - "$.node_coupling[1331].coupling_strength", - "$.node_coupling[1331].node2_name", - "$.node_coupling[1332].coupling_strength", - "$.node_coupling[1332].node2_file", - "$.node_coupling[1332].node2_name", - "$.node_coupling[1333].coupling_strength", - "$.node_coupling[1333].node1_file", - "$.node_coupling[1333].node1_name", - "$.node_coupling[1333].node2_file", - "$.node_coupling[1333].node2_name", - "$.node_coupling[1334].coupling_strength", - "$.node_coupling[1334].node2_name", - "$.node_coupling[1335].coupling_strength", - "$.node_coupling[1335].node2_name", - "$.node_coupling[1336].coupling_strength", - "$.node_coupling[1336].node2_name", - "$.node_coupling[1337].coupling_strength", - "$.node_coupling[1337].node2_name", - "$.node_coupling[1338].coupling_strength", - "$.node_coupling[1338].node2_file", - "$.node_coupling[1338].node2_name", - "$.node_coupling[1339].coupling_strength", - "$.node_coupling[1339].node1_file", - "$.node_coupling[1339].node1_name", - "$.node_coupling[1339].node2_file", - "$.node_coupling[1339].node2_name", - "$.node_coupling[133].coupling_strength", - "$.node_coupling[133].node2_name", - "$.node_coupling[1340].coupling_strength", - "$.node_coupling[1340].node2_name", - "$.node_coupling[1341].coupling_strength", - "$.node_coupling[1341].node2_file", - "$.node_coupling[1341].node2_name", - "$.node_coupling[1342].coupling_strength", - "$.node_coupling[1342].node1_file", - "$.node_coupling[1342].node1_name", - "$.node_coupling[1342].node2_name", - "$.node_coupling[1343].coupling_strength", - "$.node_coupling[1343].node1_file", - "$.node_coupling[1343].node1_name", - "$.node_coupling[1343].node2_file", - "$.node_coupling[1343].node2_name", - "$.node_coupling[1344].coupling_strength", - "$.node_coupling[1344].node1_file", - "$.node_coupling[1344].node1_name", - "$.node_coupling[1344].node2_file", - "$.node_coupling[1344].node2_name", - "$.node_coupling[1345].coupling_strength", - "$.node_coupling[1345].node1_file", - "$.node_coupling[1345].node1_name", - "$.node_coupling[1345].node2_name", - "$.node_coupling[1346].coupling_strength", - "$.node_coupling[1346].node1_file", - "$.node_coupling[1346].node1_name", - "$.node_coupling[1346].node2_file", - "$.node_coupling[1346].node2_name", - "$.node_coupling[1347].coupling_strength", - "$.node_coupling[1347].node1_file", - "$.node_coupling[1347].node1_name", - "$.node_coupling[1347].node2_file", - "$.node_coupling[1347].node2_name", - "$.node_coupling[1348].coupling_strength", - "$.node_coupling[1348].node1_file", - "$.node_coupling[1348].node1_name", - "$.node_coupling[1348].node2_file", - "$.node_coupling[1348].node2_name", - "$.node_coupling[1349].coupling_strength", - "$.node_coupling[1349].node1_file", - "$.node_coupling[1349].node1_name", - "$.node_coupling[1349].node2_file", - "$.node_coupling[1349].node2_name", - "$.node_coupling[134].node1_file", - "$.node_coupling[134].node1_name", - "$.node_coupling[134].node2_name", - "$.node_coupling[1350].coupling_strength", - "$.node_coupling[1350].node1_file", - "$.node_coupling[1350].node1_name", - "$.node_coupling[1350].node2_file", - "$.node_coupling[1350].node2_name", - "$.node_coupling[1351].coupling_strength", - "$.node_coupling[1351].node1_file", - "$.node_coupling[1351].node1_name", - "$.node_coupling[1351].node2_name", - "$.node_coupling[1352].coupling_strength", - "$.node_coupling[1352].node1_file", - "$.node_coupling[1352].node1_name", - "$.node_coupling[1352].node2_file", - "$.node_coupling[1352].node2_name", - "$.node_coupling[1353].coupling_strength", - "$.node_coupling[1353].node1_file", - "$.node_coupling[1353].node1_name", - "$.node_coupling[1353].node2_name", - "$.node_coupling[1354].coupling_strength", - "$.node_coupling[1354].node1_file", - "$.node_coupling[1354].node1_name", - "$.node_coupling[1354].node2_file", - "$.node_coupling[1354].node2_name", - "$.node_coupling[1355].coupling_strength", - "$.node_coupling[1355].node1_file", - "$.node_coupling[1355].node1_name", - "$.node_coupling[1355].node2_name", - "$.node_coupling[1356].coupling_strength", - "$.node_coupling[1356].node1_file", - "$.node_coupling[1356].node1_name", - "$.node_coupling[1356].node2_file", - "$.node_coupling[1356].node2_name", - "$.node_coupling[1357].coupling_strength", - "$.node_coupling[1357].node1_file", - "$.node_coupling[1357].node1_name", - "$.node_coupling[1357].node2_file", - "$.node_coupling[1357].node2_name", - "$.node_coupling[1358].coupling_strength", - "$.node_coupling[1358].node1_file", - "$.node_coupling[1358].node1_name", - "$.node_coupling[1358].node2_name", - "$.node_coupling[1359].coupling_strength", - "$.node_coupling[1359].node1_file", - "$.node_coupling[1359].node1_name", - "$.node_coupling[1359].node2_name", - "$.node_coupling[135].coupling_strength", - "$.node_coupling[135].node1_file", - "$.node_coupling[135].node1_name", - "$.node_coupling[135].node2_name", - "$.node_coupling[1360].coupling_strength", - "$.node_coupling[1360].node2_name", - "$.node_coupling[1361].coupling_strength", - "$.node_coupling[1361].node2_file", - "$.node_coupling[1361].node2_name", - "$.node_coupling[1362].coupling_strength", - "$.node_coupling[1362].node2_name", - "$.node_coupling[1363].coupling_strength", - "$.node_coupling[1363].node1_file", - "$.node_coupling[1363].node1_name", - "$.node_coupling[1363].node2_file", - "$.node_coupling[1363].node2_name", - "$.node_coupling[1364].coupling_strength", - "$.node_coupling[1364].node2_file", - "$.node_coupling[1364].node2_name", - "$.node_coupling[1365].coupling_strength", - "$.node_coupling[1365].node1_file", - "$.node_coupling[1365].node1_name", - "$.node_coupling[1365].node2_file", - "$.node_coupling[1365].node2_name", - "$.node_coupling[1366].coupling_strength", - "$.node_coupling[1366].node2_file", - "$.node_coupling[1366].node2_name", - "$.node_coupling[1367].coupling_strength", - "$.node_coupling[1367].node1_file", - "$.node_coupling[1367].node1_name", - "$.node_coupling[1367].node2_file", - "$.node_coupling[1367].node2_name", - "$.node_coupling[1368].coupling_strength", - "$.node_coupling[1368].node2_name", - "$.node_coupling[1369].coupling_strength", - "$.node_coupling[1369].node2_file", - "$.node_coupling[1369].node2_name", - "$.node_coupling[136].coupling_strength", - "$.node_coupling[136].node2_file", - "$.node_coupling[136].node2_name", - "$.node_coupling[1370].coupling_strength", - "$.node_coupling[1370].node2_file", - "$.node_coupling[1370].node2_name", - "$.node_coupling[1371].coupling_strength", - "$.node_coupling[1371].node2_file", - "$.node_coupling[1371].node2_name", - "$.node_coupling[1372].coupling_strength", - "$.node_coupling[1372].node1_file", - "$.node_coupling[1372].node1_name", - "$.node_coupling[1372].node2_name", - "$.node_coupling[1373].coupling_strength", - "$.node_coupling[1373].node2_name", - "$.node_coupling[1374].coupling_strength", - "$.node_coupling[1374].node2_file", - "$.node_coupling[1374].node2_name", - "$.node_coupling[1375].coupling_strength", - "$.node_coupling[1375].node1_file", - "$.node_coupling[1375].node1_name", - "$.node_coupling[1375].node2_file", - "$.node_coupling[1375].node2_name", - "$.node_coupling[1376].coupling_strength", - "$.node_coupling[1376].node2_name", - "$.node_coupling[1377].coupling_strength", - "$.node_coupling[1377].node2_name", - "$.node_coupling[1378].coupling_strength", - "$.node_coupling[1378].node2_name", - "$.node_coupling[1379].coupling_strength", - "$.node_coupling[1379].node1_file", - "$.node_coupling[1379].node1_name", - "$.node_coupling[1379].node2_name", - "$.node_coupling[137].coupling_strength", - "$.node_coupling[137].node2_file", - "$.node_coupling[137].node2_name", - "$.node_coupling[1380].coupling_strength", - "$.node_coupling[1380].node2_name", - "$.node_coupling[1381].coupling_strength", - "$.node_coupling[1381].node1_file", - "$.node_coupling[1381].node1_name", - "$.node_coupling[1381].node2_file", - "$.node_coupling[1381].node2_name", - "$.node_coupling[1382].coupling_strength", - "$.node_coupling[1382].node2_file", - "$.node_coupling[1382].node2_name", - "$.node_coupling[1383].coupling_strength", - "$.node_coupling[1383].node1_file", - "$.node_coupling[1383].node1_name", - "$.node_coupling[1383].node2_file", - "$.node_coupling[1383].node2_name", - "$.node_coupling[1384].coupling_strength", - "$.node_coupling[1384].node1_file", - "$.node_coupling[1384].node1_name", - "$.node_coupling[1384].node2_name", - "$.node_coupling[1385].coupling_strength", - "$.node_coupling[1385].node1_file", - "$.node_coupling[1385].node1_name", - "$.node_coupling[1385].node2_file", - "$.node_coupling[1385].node2_name", - "$.node_coupling[1386].coupling_strength", - "$.node_coupling[1386].node2_name", - "$.node_coupling[1387].coupling_strength", - "$.node_coupling[1387].node1_file", - "$.node_coupling[1387].node1_name", - "$.node_coupling[1387].node2_name", - "$.node_coupling[1388].coupling_strength", - "$.node_coupling[1388].node2_name", - "$.node_coupling[1389].coupling_strength", - "$.node_coupling[1389].node1_file", - "$.node_coupling[1389].node1_name", - "$.node_coupling[1389].node2_name", - "$.node_coupling[138].coupling_strength", - "$.node_coupling[138].node2_name", - "$.node_coupling[1390].coupling_strength", - "$.node_coupling[1390].node2_name", - "$.node_coupling[1391].coupling_strength", - "$.node_coupling[1391].node1_file", - "$.node_coupling[1391].node1_name", - "$.node_coupling[1391].node2_file", - "$.node_coupling[1391].node2_name", - "$.node_coupling[1392].coupling_strength", - "$.node_coupling[1392].node1_file", - "$.node_coupling[1392].node1_name", - "$.node_coupling[1392].node2_name", - "$.node_coupling[1393].coupling_strength", - "$.node_coupling[1393].node1_file", - "$.node_coupling[1393].node1_name", - "$.node_coupling[1393].node2_name", - "$.node_coupling[1394].coupling_strength", - "$.node_coupling[1394].node1_file", - "$.node_coupling[1394].node1_name", - "$.node_coupling[1394].node2_file", - "$.node_coupling[1394].node2_name", - "$.node_coupling[1395].coupling_strength", - "$.node_coupling[1395].node1_file", - "$.node_coupling[1395].node1_name", - "$.node_coupling[1395].node2_name", - "$.node_coupling[1396].coupling_strength", - "$.node_coupling[1396].node1_file", - "$.node_coupling[1396].node1_name", - "$.node_coupling[1396].node2_file", - "$.node_coupling[1396].node2_name", - "$.node_coupling[1397].coupling_strength", - "$.node_coupling[1397].node1_file", - "$.node_coupling[1397].node1_name", - "$.node_coupling[1397].node2_file", - "$.node_coupling[1397].node2_name", - "$.node_coupling[1398].coupling_strength", - "$.node_coupling[1398].node1_file", - "$.node_coupling[1398].node1_name", - "$.node_coupling[1398].node2_name", - "$.node_coupling[1399].coupling_strength", - "$.node_coupling[1399].node1_file", - "$.node_coupling[1399].node1_name", - "$.node_coupling[1399].node2_file", - "$.node_coupling[1399].node2_name", - "$.node_coupling[139].coupling_strength", - "$.node_coupling[139].node2_name", - "$.node_coupling[13].co_changes", - "$.node_coupling[13].coupling_strength", - "$.node_coupling[13].node1_file", - "$.node_coupling[13].node1_name", - "$.node_coupling[13].node2_file", - "$.node_coupling[13].node2_name", - "$.node_coupling[1400].node1_file", - "$.node_coupling[1400].node1_name", - "$.node_coupling[1400].node2_name", - "$.node_coupling[1401].coupling_strength", - "$.node_coupling[1401].node1_file", - "$.node_coupling[1401].node1_name", - "$.node_coupling[1401].node2_file", - "$.node_coupling[1401].node2_name", - "$.node_coupling[1402].coupling_strength", - "$.node_coupling[1402].node1_file", - "$.node_coupling[1402].node1_name", - "$.node_coupling[1402].node2_name", - "$.node_coupling[1403].coupling_strength", - "$.node_coupling[1403].node1_file", - "$.node_coupling[1403].node1_name", - "$.node_coupling[1403].node2_file", - "$.node_coupling[1403].node2_name", - "$.node_coupling[1404].coupling_strength", - "$.node_coupling[1404].node1_file", - "$.node_coupling[1404].node1_name", - "$.node_coupling[1404].node2_file", - "$.node_coupling[1404].node2_name", - "$.node_coupling[1405].coupling_strength", - "$.node_coupling[1405].node1_file", - "$.node_coupling[1405].node1_name", - "$.node_coupling[1405].node2_file", - "$.node_coupling[1405].node2_name", - "$.node_coupling[1406].coupling_strength", - "$.node_coupling[1406].node1_file", - "$.node_coupling[1406].node1_name", - "$.node_coupling[1406].node2_name", - "$.node_coupling[1407].coupling_strength", - "$.node_coupling[1407].node1_file", - "$.node_coupling[1407].node1_name", - "$.node_coupling[1407].node2_file", - "$.node_coupling[1407].node2_name", - "$.node_coupling[1408].node1_file", - "$.node_coupling[1408].node1_name", - "$.node_coupling[1408].node2_name", - "$.node_coupling[1409].coupling_strength", - "$.node_coupling[1409].node1_file", - "$.node_coupling[1409].node1_name", - "$.node_coupling[1409].node2_file", - "$.node_coupling[1409].node2_name", - "$.node_coupling[140].coupling_strength", - "$.node_coupling[140].node2_file", - "$.node_coupling[140].node2_name", - "$.node_coupling[1410].coupling_strength", - "$.node_coupling[1410].node1_file", - "$.node_coupling[1410].node1_name", - "$.node_coupling[1410].node2_file", - "$.node_coupling[1410].node2_name", - "$.node_coupling[1411].coupling_strength", - "$.node_coupling[1411].node1_file", - "$.node_coupling[1411].node1_name", - "$.node_coupling[1411].node2_file", - "$.node_coupling[1411].node2_name", - "$.node_coupling[1412].coupling_strength", - "$.node_coupling[1412].node1_file", - "$.node_coupling[1412].node1_name", - "$.node_coupling[1412].node2_name", - "$.node_coupling[1413].coupling_strength", - "$.node_coupling[1413].node1_file", - "$.node_coupling[1413].node1_name", - "$.node_coupling[1413].node2_name", - "$.node_coupling[1414].coupling_strength", - "$.node_coupling[1414].node1_file", - "$.node_coupling[1414].node1_name", - "$.node_coupling[1414].node2_name", - "$.node_coupling[1415].node2_name", - "$.node_coupling[1416].node2_name", - "$.node_coupling[1417].node2_name", - "$.node_coupling[1418].coupling_strength", - "$.node_coupling[1418].node1_name", - "$.node_coupling[1418].node2_file", - "$.node_coupling[1418].node2_name", - "$.node_coupling[1419].node1_name", - "$.node_coupling[1419].node2_name", - "$.node_coupling[141].coupling_strength", - "$.node_coupling[141].node2_file", - "$.node_coupling[141].node2_name", - "$.node_coupling[1420].node1_name", - "$.node_coupling[1420].node2_name", - "$.node_coupling[1421].node1_name", - "$.node_coupling[1421].node2_file", - "$.node_coupling[1421].node2_name", - "$.node_coupling[1422].coupling_strength", - "$.node_coupling[1422].node1_name", - "$.node_coupling[1422].node2_file", - "$.node_coupling[1422].node2_name", - "$.node_coupling[1423].node1_name", - "$.node_coupling[1423].node2_name", - "$.node_coupling[1424].coupling_strength", - "$.node_coupling[1424].node1_name", - "$.node_coupling[1424].node2_file", - "$.node_coupling[1424].node2_name", - "$.node_coupling[1425].coupling_strength", - "$.node_coupling[1425].node1_name", - "$.node_coupling[1425].node2_file", - "$.node_coupling[1425].node2_name", - "$.node_coupling[1426].node2_file", - "$.node_coupling[1426].node2_name", - "$.node_coupling[1427].node2_file", - "$.node_coupling[1427].node2_name", - "$.node_coupling[1428].coupling_strength", - "$.node_coupling[1428].node1_file", - "$.node_coupling[1428].node1_name", - "$.node_coupling[1428].node2_file", - "$.node_coupling[1428].node2_name", - "$.node_coupling[1429].node2_name", - "$.node_coupling[142].coupling_strength", - "$.node_coupling[142].node2_file", - "$.node_coupling[142].node2_name", - "$.node_coupling[1430].node2_name", - "$.node_coupling[1431].coupling_strength", - "$.node_coupling[1431].node2_file", - "$.node_coupling[1431].node2_name", - "$.node_coupling[1432].coupling_strength", - "$.node_coupling[1432].node1_file", - "$.node_coupling[1432].node1_name", - "$.node_coupling[1432].node2_file", - "$.node_coupling[1432].node2_name", - "$.node_coupling[1433].coupling_strength", - "$.node_coupling[1433].node2_file", - "$.node_coupling[1433].node2_name", - "$.node_coupling[1434].coupling_strength", - "$.node_coupling[1434].node1_file", - "$.node_coupling[1434].node1_name", - "$.node_coupling[1434].node2_file", - "$.node_coupling[1434].node2_name", - "$.node_coupling[1435].node2_name", - "$.node_coupling[1436].node2_name", - "$.node_coupling[1437].node2_name", - "$.node_coupling[1438].coupling_strength", - "$.node_coupling[1438].node1_name", - "$.node_coupling[1438].node2_file", - "$.node_coupling[1438].node2_name", - "$.node_coupling[1439].node1_name", - "$.node_coupling[1439].node2_name", - "$.node_coupling[143].coupling_strength", - "$.node_coupling[143].node2_file", - "$.node_coupling[143].node2_name", - "$.node_coupling[1440].coupling_strength", - "$.node_coupling[1440].node1_name", - "$.node_coupling[1440].node2_file", - "$.node_coupling[1440].node2_name", - "$.node_coupling[1441].node1_name", - "$.node_coupling[1441].node2_name", - "$.node_coupling[1442].node1_name", - "$.node_coupling[1442].node2_file", - "$.node_coupling[1442].node2_name", - "$.node_coupling[1443].coupling_strength", - "$.node_coupling[1443].node1_file", - "$.node_coupling[1443].node1_name", - "$.node_coupling[1443].node2_name", - "$.node_coupling[1444].node1_name", - "$.node_coupling[1444].node2_name", - "$.node_coupling[1445].node1_name", - "$.node_coupling[1445].node2_name", - "$.node_coupling[1446].coupling_strength", - "$.node_coupling[1446].node1_name", - "$.node_coupling[1446].node2_file", - "$.node_coupling[1446].node2_name", - "$.node_coupling[1447].node1_name", - "$.node_coupling[1447].node2_name", - "$.node_coupling[1448].coupling_strength", - "$.node_coupling[1448].node1_name", - "$.node_coupling[1448].node2_file", - "$.node_coupling[1448].node2_name", - "$.node_coupling[1449].node1_name", - "$.node_coupling[1449].node2_name", - "$.node_coupling[144].coupling_strength", - "$.node_coupling[144].node2_file", - "$.node_coupling[144].node2_name", - "$.node_coupling[1450].coupling_strength", - "$.node_coupling[1450].node1_name", - "$.node_coupling[1450].node2_file", - "$.node_coupling[1450].node2_name", - "$.node_coupling[1451].node2_name", - "$.node_coupling[1452].node1_name", - "$.node_coupling[1452].node2_name", - "$.node_coupling[1453].coupling_strength", - "$.node_coupling[1453].node1_name", - "$.node_coupling[1453].node2_file", - "$.node_coupling[1453].node2_name", - "$.node_coupling[1454].node1_name", - "$.node_coupling[1454].node2_file", - "$.node_coupling[1454].node2_name", - "$.node_coupling[1455].node1_name", - "$.node_coupling[1455].node2_file", - "$.node_coupling[1455].node2_name", - "$.node_coupling[1456].coupling_strength", - "$.node_coupling[1456].node1_name", - "$.node_coupling[1456].node2_file", - "$.node_coupling[1456].node2_name", - "$.node_coupling[1457].node1_name", - "$.node_coupling[1457].node2_name", - "$.node_coupling[1458].node1_name", - "$.node_coupling[1458].node2_file", - "$.node_coupling[1458].node2_name", - "$.node_coupling[1459].node1_name", - "$.node_coupling[1459].node2_name", - "$.node_coupling[145].coupling_strength", - "$.node_coupling[145].node2_file", - "$.node_coupling[145].node2_name", - "$.node_coupling[1460].coupling_strength", - "$.node_coupling[1460].node1_name", - "$.node_coupling[1460].node2_file", - "$.node_coupling[1460].node2_name", - "$.node_coupling[1461].node1_name", - "$.node_coupling[1461].node2_name", - "$.node_coupling[1462].node1_name", - "$.node_coupling[1462].node2_name", - "$.node_coupling[1463].node1_name", - "$.node_coupling[1463].node2_file", - "$.node_coupling[1463].node2_name", - "$.node_coupling[1464].node1_name", - "$.node_coupling[1464].node2_name", - "$.node_coupling[1465].node1_name", - "$.node_coupling[1465].node2_name", - "$.node_coupling[1466].node2_name", - "$.node_coupling[1467].coupling_strength", - "$.node_coupling[1467].node2_file", - "$.node_coupling[1467].node2_name", - "$.node_coupling[1468].coupling_strength", - "$.node_coupling[1468].node2_file", - "$.node_coupling[1468].node2_name", - "$.node_coupling[1469].coupling_strength", - "$.node_coupling[1469].node2_file", - "$.node_coupling[1469].node2_name", - "$.node_coupling[146].coupling_strength", - "$.node_coupling[146].node1_name", - "$.node_coupling[146].node2_name", - "$.node_coupling[1470].coupling_strength", - "$.node_coupling[1470].node2_file", - "$.node_coupling[1470].node2_name", - "$.node_coupling[1471].coupling_strength", - "$.node_coupling[1471].node2_file", - "$.node_coupling[1471].node2_name", - "$.node_coupling[1472].coupling_strength", - "$.node_coupling[1472].node2_file", - "$.node_coupling[1472].node2_name", - "$.node_coupling[1473].node2_name", - "$.node_coupling[1474].node2_name", - "$.node_coupling[1475].node2_name", - "$.node_coupling[1476].node2_name", - "$.node_coupling[1477].node2_name", - "$.node_coupling[1478].node1_name", - "$.node_coupling[1478].node2_file", - "$.node_coupling[1478].node2_name", - "$.node_coupling[1479].node2_file", - "$.node_coupling[1479].node2_name", - "$.node_coupling[147].coupling_strength", - "$.node_coupling[147].node1_name", - "$.node_coupling[147].node2_file", - "$.node_coupling[147].node2_name", - "$.node_coupling[1480].coupling_strength", - "$.node_coupling[1480].node2_file", - "$.node_coupling[1480].node2_name", - "$.node_coupling[1481].node2_file", - "$.node_coupling[1481].node2_name", - "$.node_coupling[1482].node2_file", - "$.node_coupling[1482].node2_name", - "$.node_coupling[1483].coupling_strength", - "$.node_coupling[1483].node1_name", - "$.node_coupling[1483].node2_file", - "$.node_coupling[1483].node2_name", - "$.node_coupling[1484].coupling_strength", - "$.node_coupling[1484].node2_file", - "$.node_coupling[1484].node2_name", - "$.node_coupling[1485].coupling_strength", - "$.node_coupling[1485].node2_file", - "$.node_coupling[1485].node2_name", - "$.node_coupling[1486].coupling_strength", - "$.node_coupling[1486].node2_file", - "$.node_coupling[1486].node2_name", - "$.node_coupling[1487].node2_name", - "$.node_coupling[1488].node2_file", - "$.node_coupling[1488].node2_name", - "$.node_coupling[1489].node2_name", - "$.node_coupling[148].coupling_strength", - "$.node_coupling[148].node1_name", - "$.node_coupling[148].node2_file", - "$.node_coupling[148].node2_name", - "$.node_coupling[1490].node2_name", - "$.node_coupling[1491].node1_file", - "$.node_coupling[1491].node1_name", - "$.node_coupling[1491].node2_file", - "$.node_coupling[1491].node2_name", - "$.node_coupling[1492].node1_file", - "$.node_coupling[1492].node1_name", - "$.node_coupling[1492].node2_name", - "$.node_coupling[1493].node1_file", - "$.node_coupling[1493].node1_name", - "$.node_coupling[1493].node2_name", - "$.node_coupling[1494].node1_file", - "$.node_coupling[1494].node1_name", - "$.node_coupling[1494].node2_file", - "$.node_coupling[1494].node2_name", - "$.node_coupling[1495].node1_name", - "$.node_coupling[1495].node2_file", - "$.node_coupling[1495].node2_name", - "$.node_coupling[1496].node1_file", - "$.node_coupling[1496].node1_name", - "$.node_coupling[1496].node2_file", - "$.node_coupling[1496].node2_name", - "$.node_coupling[1497].coupling_strength", - "$.node_coupling[1497].node1_file", - "$.node_coupling[1497].node1_name", - "$.node_coupling[1497].node2_file", - "$.node_coupling[1497].node2_name", - "$.node_coupling[1498].node1_file", - "$.node_coupling[1498].node1_name", - "$.node_coupling[1498].node2_name", - "$.node_coupling[1499].node1_file", - "$.node_coupling[1499].node1_name", - "$.node_coupling[1499].node2_name", - "$.node_coupling[149].coupling_strength", - "$.node_coupling[149].node1_name", - "$.node_coupling[149].node2_file", - "$.node_coupling[149].node2_name", - "$.node_coupling[14].co_changes", - "$.node_coupling[14].coupling_strength", - "$.node_coupling[14].node1_file", - "$.node_coupling[14].node1_name", - "$.node_coupling[14].node2_file", - "$.node_coupling[14].node2_name", - "$.node_coupling[1500].node1_file", - "$.node_coupling[1500].node1_name", - "$.node_coupling[1500].node2_name", - "$.node_coupling[1501].coupling_strength", - "$.node_coupling[1501].node1_file", - "$.node_coupling[1501].node1_name", - "$.node_coupling[1501].node2_file", - "$.node_coupling[1501].node2_name", - "$.node_coupling[1502].node1_file", - "$.node_coupling[1502].node1_name", - "$.node_coupling[1502].node2_name", - "$.node_coupling[1503].coupling_strength", - "$.node_coupling[1503].node1_file", - "$.node_coupling[1503].node1_name", - "$.node_coupling[1503].node2_file", - "$.node_coupling[1503].node2_name", - "$.node_coupling[1504].node1_file", - "$.node_coupling[1504].node1_name", - "$.node_coupling[1504].node2_file", - "$.node_coupling[1504].node2_name", - "$.node_coupling[1505].node1_file", - "$.node_coupling[1505].node1_name", - "$.node_coupling[1505].node2_name", - "$.node_coupling[1506].node1_file", - "$.node_coupling[1506].node1_name", - "$.node_coupling[1506].node2_name", - "$.node_coupling[1507].node1_file", - "$.node_coupling[1507].node1_name", - "$.node_coupling[1507].node2_name", - "$.node_coupling[1508].node1_file", - "$.node_coupling[1508].node1_name", - "$.node_coupling[1508].node2_file", - "$.node_coupling[1508].node2_name", - "$.node_coupling[1509].coupling_strength", - "$.node_coupling[1509].node1_file", - "$.node_coupling[1509].node1_name", - "$.node_coupling[1509].node2_file", - "$.node_coupling[1509].node2_name", - "$.node_coupling[150].coupling_strength", - "$.node_coupling[150].node1_name", - "$.node_coupling[150].node2_name", - "$.node_coupling[1510].node1_file", - "$.node_coupling[1510].node1_name", - "$.node_coupling[1510].node2_file", - "$.node_coupling[1510].node2_name", - "$.node_coupling[1511].coupling_strength", - "$.node_coupling[1511].node1_file", - "$.node_coupling[1511].node1_name", - "$.node_coupling[1511].node2_file", - "$.node_coupling[1511].node2_name", - "$.node_coupling[1512].node1_file", - "$.node_coupling[1512].node1_name", - "$.node_coupling[1512].node2_name", - "$.node_coupling[1513].node2_name", - "$.node_coupling[1514].node2_name", - "$.node_coupling[1515].coupling_strength", - "$.node_coupling[1515].node1_file", - "$.node_coupling[1515].node1_name", - "$.node_coupling[1515].node2_file", - "$.node_coupling[1515].node2_name", - "$.node_coupling[1516].node2_name", - "$.node_coupling[1517].coupling_strength", - "$.node_coupling[1517].node2_file", - "$.node_coupling[1517].node2_name", - "$.node_coupling[1518].node2_name", - "$.node_coupling[1519].node2_name", - "$.node_coupling[151].coupling_strength", - "$.node_coupling[151].node1_file", - "$.node_coupling[151].node1_name", - "$.node_coupling[151].node2_file", - "$.node_coupling[151].node2_name", - "$.node_coupling[1520].coupling_strength", - "$.node_coupling[1520].node2_file", - "$.node_coupling[1520].node2_name", - "$.node_coupling[1521].node1_file", - "$.node_coupling[1521].node1_name", - "$.node_coupling[1521].node2_name", - "$.node_coupling[1522].node2_name", - "$.node_coupling[1523].node2_file", - "$.node_coupling[1523].node2_name", - "$.node_coupling[1524].node2_file", - "$.node_coupling[1524].node2_name", - "$.node_coupling[1525].node2_file", - "$.node_coupling[1525].node2_name", - "$.node_coupling[1526].coupling_strength", - "$.node_coupling[1526].node2_file", - "$.node_coupling[1526].node2_name", - "$.node_coupling[1527].node2_name", - "$.node_coupling[1528].node2_file", - "$.node_coupling[1528].node2_name", - "$.node_coupling[1529].coupling_strength", - "$.node_coupling[1529].node2_file", - "$.node_coupling[1529].node2_name", - "$.node_coupling[152].coupling_strength", - "$.node_coupling[152].node1_file", - "$.node_coupling[152].node1_name", - "$.node_coupling[152].node2_file", - "$.node_coupling[152].node2_name", - "$.node_coupling[1530].coupling_strength", - "$.node_coupling[1530].node1_file", - "$.node_coupling[1530].node1_name", - "$.node_coupling[1530].node2_file", - "$.node_coupling[1530].node2_name", - "$.node_coupling[1531].coupling_strength", - "$.node_coupling[1531].node2_file", - "$.node_coupling[1531].node2_name", - "$.node_coupling[1532].coupling_strength", - "$.node_coupling[1532].node2_file", - "$.node_coupling[1532].node2_name", - "$.node_coupling[1533].coupling_strength", - "$.node_coupling[1533].node2_file", - "$.node_coupling[1533].node2_name", - "$.node_coupling[1534].node2_name", - "$.node_coupling[1535].coupling_strength", - "$.node_coupling[1535].node2_file", - "$.node_coupling[1535].node2_name", - "$.node_coupling[1536].node2_name", - "$.node_coupling[1537].coupling_strength", - "$.node_coupling[1537].node1_name", - "$.node_coupling[1537].node2_file", - "$.node_coupling[1537].node2_name", - "$.node_coupling[1538].node1_file", - "$.node_coupling[1538].node1_name", - "$.node_coupling[1538].node2_name", - "$.node_coupling[1539].node1_name", - "$.node_coupling[1539].node2_file", - "$.node_coupling[1539].node2_name", - "$.node_coupling[153].coupling_strength", - "$.node_coupling[153].node1_name", - "$.node_coupling[153].node2_file", - "$.node_coupling[153].node2_name", - "$.node_coupling[1540].coupling_strength", - "$.node_coupling[1540].node1_name", - "$.node_coupling[1540].node2_file", - "$.node_coupling[1540].node2_name", - "$.node_coupling[1541].node1_name", - "$.node_coupling[1541].node2_name", - "$.node_coupling[1542].node1_name", - "$.node_coupling[1542].node2_name", - "$.node_coupling[1543].node1_name", - "$.node_coupling[1543].node2_name", - "$.node_coupling[1544].node1_name", - "$.node_coupling[1544].node2_name", - "$.node_coupling[1545].coupling_strength", - "$.node_coupling[1545].node1_name", - "$.node_coupling[1545].node2_file", - "$.node_coupling[1545].node2_name", - "$.node_coupling[1546].coupling_strength", - "$.node_coupling[1546].node1_name", - "$.node_coupling[1546].node2_file", - "$.node_coupling[1546].node2_name", - "$.node_coupling[1547].coupling_strength", - "$.node_coupling[1547].node1_name", - "$.node_coupling[1547].node2_file", - "$.node_coupling[1547].node2_name", - "$.node_coupling[1548].node1_name", - "$.node_coupling[1548].node2_file", - "$.node_coupling[1548].node2_name", - "$.node_coupling[1549].node1_name", - "$.node_coupling[1549].node2_file", - "$.node_coupling[1549].node2_name", - "$.node_coupling[154].coupling_strength", - "$.node_coupling[154].node2_file", - "$.node_coupling[154].node2_name", - "$.node_coupling[1550].coupling_strength", - "$.node_coupling[1550].node1_file", - "$.node_coupling[1550].node1_name", - "$.node_coupling[1550].node2_file", - "$.node_coupling[1550].node2_name", - "$.node_coupling[1551].coupling_strength", - "$.node_coupling[1551].node1_name", - "$.node_coupling[1551].node2_file", - "$.node_coupling[1551].node2_name", - "$.node_coupling[1552].node1_name", - "$.node_coupling[1552].node2_name", - "$.node_coupling[1553].node1_name", - "$.node_coupling[1553].node2_file", - "$.node_coupling[1553].node2_name", - "$.node_coupling[1554].node1_name", - "$.node_coupling[1554].node2_name", - "$.node_coupling[1555].node1_name", - "$.node_coupling[1555].node2_name", - "$.node_coupling[1556].coupling_strength", - "$.node_coupling[1556].node1_file", - "$.node_coupling[1556].node1_name", - "$.node_coupling[1556].node2_file", - "$.node_coupling[1556].node2_name", - "$.node_coupling[1557].coupling_strength", - "$.node_coupling[1557].node1_name", - "$.node_coupling[1557].node2_file", - "$.node_coupling[1557].node2_name", - "$.node_coupling[1558].node1_name", - "$.node_coupling[1558].node2_file", - "$.node_coupling[1558].node2_name", - "$.node_coupling[1559].node2_name", - "$.node_coupling[155].coupling_strength", - "$.node_coupling[155].node2_file", - "$.node_coupling[155].node2_name", - "$.node_coupling[1560].coupling_strength", - "$.node_coupling[1560].node2_file", - "$.node_coupling[1560].node2_name", - "$.node_coupling[1561].node2_file", - "$.node_coupling[1561].node2_name", - "$.node_coupling[1562].node2_name", - "$.node_coupling[1563].coupling_strength", - "$.node_coupling[1563].node2_file", - "$.node_coupling[1563].node2_name", - "$.node_coupling[1564].node2_file", - "$.node_coupling[1564].node2_name", - "$.node_coupling[1565].node2_name", - "$.node_coupling[1566].node2_name", - "$.node_coupling[1567].node1_file", - "$.node_coupling[1567].node1_name", - "$.node_coupling[1567].node2_name", - "$.node_coupling[1568].coupling_strength", - "$.node_coupling[1568].node2_file", - "$.node_coupling[1568].node2_name", - "$.node_coupling[1569].coupling_strength", - "$.node_coupling[1569].node2_file", - "$.node_coupling[1569].node2_name", - "$.node_coupling[156].coupling_strength", - "$.node_coupling[156].node2_file", - "$.node_coupling[156].node2_name", - "$.node_coupling[1570].node2_name", - "$.node_coupling[1571].node2_name", - "$.node_coupling[1572].coupling_strength", - "$.node_coupling[1572].node2_file", - "$.node_coupling[1572].node2_name", - "$.node_coupling[1573].node2_name", - "$.node_coupling[1574].node2_name", - "$.node_coupling[1575].coupling_strength", - "$.node_coupling[1575].node2_file", - "$.node_coupling[1575].node2_name", - "$.node_coupling[1576].node2_name", - "$.node_coupling[1577].node2_name", - "$.node_coupling[1578].node2_file", - "$.node_coupling[1578].node2_name", - "$.node_coupling[1579].node2_name", - "$.node_coupling[157].coupling_strength", - "$.node_coupling[157].node2_file", - "$.node_coupling[157].node2_name", - "$.node_coupling[1580].node2_name", - "$.node_coupling[1581].coupling_strength", - "$.node_coupling[1581].node2_file", - "$.node_coupling[1581].node2_name", - "$.node_coupling[1582].coupling_strength", - "$.node_coupling[1582].node1_file", - "$.node_coupling[1582].node1_name", - "$.node_coupling[1582].node2_file", - "$.node_coupling[1582].node2_name", - "$.node_coupling[1583].coupling_strength", - "$.node_coupling[1583].node1_file", - "$.node_coupling[1583].node1_name", - "$.node_coupling[1583].node2_file", - "$.node_coupling[1583].node2_name", - "$.node_coupling[1584].node1_file", - "$.node_coupling[1584].node1_name", - "$.node_coupling[1584].node2_file", - "$.node_coupling[1584].node2_name", - "$.node_coupling[1585].node1_file", - "$.node_coupling[1585].node1_name", - "$.node_coupling[1585].node2_name", - "$.node_coupling[1586].coupling_strength", - "$.node_coupling[1586].node1_file", - "$.node_coupling[1586].node1_name", - "$.node_coupling[1586].node2_file", - "$.node_coupling[1586].node2_name", - "$.node_coupling[1587].node1_file", - "$.node_coupling[1587].node1_name", - "$.node_coupling[1587].node2_name", - "$.node_coupling[1588].node1_file", - "$.node_coupling[1588].node1_name", - "$.node_coupling[1588].node2_file", - "$.node_coupling[1588].node2_name", - "$.node_coupling[1589].coupling_strength", - "$.node_coupling[1589].node1_file", - "$.node_coupling[1589].node1_name", - "$.node_coupling[1589].node2_file", - "$.node_coupling[1589].node2_name", - "$.node_coupling[158].coupling_strength", - "$.node_coupling[158].node1_file", - "$.node_coupling[158].node1_name", - "$.node_coupling[158].node2_file", - "$.node_coupling[158].node2_name", - "$.node_coupling[1590].coupling_strength", - "$.node_coupling[1590].node1_file", - "$.node_coupling[1590].node1_name", - "$.node_coupling[1590].node2_file", - "$.node_coupling[1590].node2_name", - "$.node_coupling[1591].node1_file", - "$.node_coupling[1591].node1_name", - "$.node_coupling[1591].node2_name", - "$.node_coupling[1592].node1_file", - "$.node_coupling[1592].node1_name", - "$.node_coupling[1592].node2_name", - "$.node_coupling[1593].coupling_strength", - "$.node_coupling[1593].node1_file", - "$.node_coupling[1593].node1_name", - "$.node_coupling[1593].node2_file", - "$.node_coupling[1593].node2_name", - "$.node_coupling[1594].node1_file", - "$.node_coupling[1594].node1_name", - "$.node_coupling[1594].node2_name", - "$.node_coupling[1595].node1_file", - "$.node_coupling[1595].node1_name", - "$.node_coupling[1595].node2_file", - "$.node_coupling[1595].node2_name", - "$.node_coupling[1596].coupling_strength", - "$.node_coupling[1596].node1_file", - "$.node_coupling[1596].node1_name", - "$.node_coupling[1596].node2_file", - "$.node_coupling[1596].node2_name", - "$.node_coupling[1597].coupling_strength", - "$.node_coupling[1597].node1_file", - "$.node_coupling[1597].node1_name", - "$.node_coupling[1597].node2_file", - "$.node_coupling[1597].node2_name", - "$.node_coupling[1598].node1_file", - "$.node_coupling[1598].node1_name", - "$.node_coupling[1598].node2_file", - "$.node_coupling[1598].node2_name", - "$.node_coupling[1599].node1_file", - "$.node_coupling[1599].node1_name", - "$.node_coupling[1599].node2_name", - "$.node_coupling[159].coupling_strength", - "$.node_coupling[159].node2_file", - "$.node_coupling[159].node2_name", - "$.node_coupling[15].coupling_strength", - "$.node_coupling[15].node1_file", - "$.node_coupling[15].node1_name", - "$.node_coupling[15].node2_file", - "$.node_coupling[15].node2_name", - "$.node_coupling[1600].node1_file", - "$.node_coupling[1600].node1_name", - "$.node_coupling[1600].node2_name", - "$.node_coupling[1601].coupling_strength", - "$.node_coupling[1601].node1_file", - "$.node_coupling[1601].node1_name", - "$.node_coupling[1601].node2_file", - "$.node_coupling[1601].node2_name", - "$.node_coupling[1602].node1_file", - "$.node_coupling[1602].node1_name", - "$.node_coupling[1602].node2_name", - "$.node_coupling[1603].coupling_strength", - "$.node_coupling[1603].node1_file", - "$.node_coupling[1603].node1_name", - "$.node_coupling[1603].node2_file", - "$.node_coupling[1603].node2_name", - "$.node_coupling[1604].node2_name", - "$.node_coupling[1605].node2_file", - "$.node_coupling[1605].node2_name", - "$.node_coupling[1606].coupling_strength", - "$.node_coupling[1606].node2_file", - "$.node_coupling[1606].node2_name", - "$.node_coupling[1607].coupling_strength", - "$.node_coupling[1607].node2_file", - "$.node_coupling[1607].node2_name", - "$.node_coupling[1608].coupling_strength", - "$.node_coupling[1608].node2_file", - "$.node_coupling[1608].node2_name", - "$.node_coupling[1609].coupling_strength", - "$.node_coupling[1609].node2_file", - "$.node_coupling[1609].node2_name", - "$.node_coupling[160].node1_file", - "$.node_coupling[160].node1_name", - "$.node_coupling[160].node2_file", - "$.node_coupling[160].node2_name", - "$.node_coupling[1610].node2_name", - "$.node_coupling[1611].node1_file", - "$.node_coupling[1611].node1_name", - "$.node_coupling[1611].node2_name", - "$.node_coupling[1612].node2_name", - "$.node_coupling[1613].node1_file", - "$.node_coupling[1613].node1_name", - "$.node_coupling[1613].node2_name", - "$.node_coupling[1614].node2_name", - "$.node_coupling[1615].node1_file", - "$.node_coupling[1615].node1_name", - "$.node_coupling[1615].node2_name", - "$.node_coupling[1616].coupling_strength", - "$.node_coupling[1616].node2_file", - "$.node_coupling[1616].node2_name", - "$.node_coupling[1617].coupling_strength", - "$.node_coupling[1617].node2_file", - "$.node_coupling[1617].node2_name", - "$.node_coupling[1618].node2_name", - "$.node_coupling[1619].coupling_strength", - "$.node_coupling[1619].node2_file", - "$.node_coupling[1619].node2_name", - "$.node_coupling[161].coupling_strength", - "$.node_coupling[161].node2_file", - "$.node_coupling[161].node2_name", - "$.node_coupling[1620].node2_name", - "$.node_coupling[1621].node2_name", - "$.node_coupling[1622].node2_name", - "$.node_coupling[1623].coupling_strength", - "$.node_coupling[1623].node2_file", - "$.node_coupling[1623].node2_name", - "$.node_coupling[1624].node2_name", - "$.node_coupling[1625].coupling_strength", - "$.node_coupling[1625].node2_file", - "$.node_coupling[1625].node2_name", - "$.node_coupling[1626].node1_file", - "$.node_coupling[1626].node1_name", - "$.node_coupling[1626].node2_name", - "$.node_coupling[1627].node1_file", - "$.node_coupling[1627].node1_name", - "$.node_coupling[1627].node2_file", - "$.node_coupling[1627].node2_name", - "$.node_coupling[1628].coupling_strength", - "$.node_coupling[1628].node1_file", - "$.node_coupling[1628].node1_name", - "$.node_coupling[1628].node2_file", - "$.node_coupling[1628].node2_name", - "$.node_coupling[1629].node1_file", - "$.node_coupling[1629].node1_name", - "$.node_coupling[1629].node2_name", - "$.node_coupling[162].coupling_strength", - "$.node_coupling[162].node1_file", - "$.node_coupling[162].node1_name", - "$.node_coupling[162].node2_name", - "$.node_coupling[1630].coupling_strength", - "$.node_coupling[1630].node1_file", - "$.node_coupling[1630].node1_name", - "$.node_coupling[1630].node2_file", - "$.node_coupling[1630].node2_name", - "$.node_coupling[1631].node1_file", - "$.node_coupling[1631].node1_name", - "$.node_coupling[1631].node2_name", - "$.node_coupling[1632].node1_file", - "$.node_coupling[1632].node1_name", - "$.node_coupling[1632].node2_name", - "$.node_coupling[1633].node1_file", - "$.node_coupling[1633].node1_name", - "$.node_coupling[1633].node2_name", - "$.node_coupling[1634].coupling_strength", - "$.node_coupling[1634].node1_file", - "$.node_coupling[1634].node1_name", - "$.node_coupling[1634].node2_file", - "$.node_coupling[1634].node2_name", - "$.node_coupling[1635].node1_file", - "$.node_coupling[1635].node1_name", - "$.node_coupling[1635].node2_file", - "$.node_coupling[1635].node2_name", - "$.node_coupling[1636].node1_file", - "$.node_coupling[1636].node1_name", - "$.node_coupling[1636].node2_name", - "$.node_coupling[1637].node1_file", - "$.node_coupling[1637].node1_name", - "$.node_coupling[1637].node2_name", - "$.node_coupling[1638].coupling_strength", - "$.node_coupling[1638].node1_file", - "$.node_coupling[1638].node1_name", - "$.node_coupling[1638].node2_file", - "$.node_coupling[1638].node2_name", - "$.node_coupling[1639].coupling_strength", - "$.node_coupling[1639].node1_file", - "$.node_coupling[1639].node1_name", - "$.node_coupling[1639].node2_file", - "$.node_coupling[1639].node2_name", - "$.node_coupling[163].coupling_strength", - "$.node_coupling[163].node1_file", - "$.node_coupling[163].node1_name", - "$.node_coupling[163].node2_name", - "$.node_coupling[1640].coupling_strength", - "$.node_coupling[1640].node1_file", - "$.node_coupling[1640].node1_name", - "$.node_coupling[1640].node2_file", - "$.node_coupling[1640].node2_name", - "$.node_coupling[1641].node1_file", - "$.node_coupling[1641].node1_name", - "$.node_coupling[1641].node2_file", - "$.node_coupling[1641].node2_name", - "$.node_coupling[1642].node1_file", - "$.node_coupling[1642].node1_name", - "$.node_coupling[1642].node2_file", - "$.node_coupling[1642].node2_name", - "$.node_coupling[1643].node1_file", - "$.node_coupling[1643].node1_name", - "$.node_coupling[1643].node2_file", - "$.node_coupling[1643].node2_name", - "$.node_coupling[1644].coupling_strength", - "$.node_coupling[1644].node1_file", - "$.node_coupling[1644].node1_name", - "$.node_coupling[1644].node2_file", - "$.node_coupling[1644].node2_name", - "$.node_coupling[1645].coupling_strength", - "$.node_coupling[1645].node1_file", - "$.node_coupling[1645].node1_name", - "$.node_coupling[1645].node2_name", - "$.node_coupling[1646].node1_file", - "$.node_coupling[1646].node1_name", - "$.node_coupling[1646].node2_name", - "$.node_coupling[1647].node1_file", - "$.node_coupling[1647].node1_name", - "$.node_coupling[1647].node2_name", - "$.node_coupling[1648].node1_file", - "$.node_coupling[1648].node1_name", - "$.node_coupling[1648].node2_file", - "$.node_coupling[1648].node2_name", - "$.node_coupling[1649].node2_name", - "$.node_coupling[164].coupling_strength", - "$.node_coupling[164].node1_file", - "$.node_coupling[164].node1_name", - "$.node_coupling[164].node2_file", - "$.node_coupling[164].node2_name", - "$.node_coupling[1650].node2_name", - "$.node_coupling[1651].coupling_strength", - "$.node_coupling[1651].node1_file", - "$.node_coupling[1651].node1_name", - "$.node_coupling[1651].node2_file", - "$.node_coupling[1651].node2_name", - "$.node_coupling[1652].coupling_strength", - "$.node_coupling[1652].node2_file", - "$.node_coupling[1652].node2_name", - "$.node_coupling[1653].node2_name", - "$.node_coupling[1654].coupling_strength", - "$.node_coupling[1654].node2_file", - "$.node_coupling[1654].node2_name", - "$.node_coupling[1655].node2_name", - "$.node_coupling[1656].node2_name", - "$.node_coupling[1657].node2_name", - "$.node_coupling[1658].coupling_strength", - "$.node_coupling[1658].node2_file", - "$.node_coupling[1658].node2_name", - "$.node_coupling[1659].node2_file", - "$.node_coupling[1659].node2_name", - "$.node_coupling[165].coupling_strength", - "$.node_coupling[165].node2_name", - "$.node_coupling[1660].node2_name", - "$.node_coupling[1661].node2_name", - "$.node_coupling[1662].coupling_strength", - "$.node_coupling[1662].node2_file", - "$.node_coupling[1662].node2_name", - "$.node_coupling[1663].node2_name", - "$.node_coupling[1664].node2_name", - "$.node_coupling[1665].coupling_strength", - "$.node_coupling[1665].node1_file", - "$.node_coupling[1665].node1_name", - "$.node_coupling[1665].node2_file", - "$.node_coupling[1665].node2_name", - "$.node_coupling[1666].node2_name", - "$.node_coupling[1667].coupling_strength", - "$.node_coupling[1667].node2_file", - "$.node_coupling[1667].node2_name", - "$.node_coupling[1668].node2_name", - "$.node_coupling[1669].coupling_strength", - "$.node_coupling[1669].node1_file", - "$.node_coupling[1669].node1_name", - "$.node_coupling[1669].node2_name", - "$.node_coupling[166].coupling_strength", - "$.node_coupling[166].node2_name", - "$.node_coupling[1670].coupling_strength", - "$.node_coupling[1670].node1_file", - "$.node_coupling[1670].node1_name", - "$.node_coupling[1670].node2_file", - "$.node_coupling[1670].node2_name", - "$.node_coupling[1671].coupling_strength", - "$.node_coupling[1671].node1_file", - "$.node_coupling[1671].node1_name", - "$.node_coupling[1671].node2_name", - "$.node_coupling[1672].coupling_strength", - "$.node_coupling[1672].node1_file", - "$.node_coupling[1672].node1_name", - "$.node_coupling[1672].node2_name", - "$.node_coupling[1673].coupling_strength", - "$.node_coupling[1673].node1_file", - "$.node_coupling[1673].node1_name", - "$.node_coupling[1673].node2_file", - "$.node_coupling[1673].node2_name", - "$.node_coupling[1674].coupling_strength", - "$.node_coupling[1674].node1_file", - "$.node_coupling[1674].node1_name", - "$.node_coupling[1674].node2_name", - "$.node_coupling[1675].coupling_strength", - "$.node_coupling[1675].node1_file", - "$.node_coupling[1675].node1_name", - "$.node_coupling[1675].node2_file", - "$.node_coupling[1675].node2_name", - "$.node_coupling[1676].coupling_strength", - "$.node_coupling[1676].node1_file", - "$.node_coupling[1676].node1_name", - "$.node_coupling[1676].node2_name", - "$.node_coupling[1677].coupling_strength", - "$.node_coupling[1677].node1_file", - "$.node_coupling[1677].node1_name", - "$.node_coupling[1677].node2_name", - "$.node_coupling[1678].node1_file", - "$.node_coupling[1678].node1_name", - "$.node_coupling[1678].node2_file", - "$.node_coupling[1678].node2_name", - "$.node_coupling[1679].coupling_strength", - "$.node_coupling[1679].node1_file", - "$.node_coupling[1679].node1_name", - "$.node_coupling[1679].node2_name", - "$.node_coupling[167].coupling_strength", - "$.node_coupling[167].node2_name", - "$.node_coupling[1680].coupling_strength", - "$.node_coupling[1680].node1_file", - "$.node_coupling[1680].node1_name", - "$.node_coupling[1680].node2_name", - "$.node_coupling[1681].coupling_strength", - "$.node_coupling[1681].node1_file", - "$.node_coupling[1681].node1_name", - "$.node_coupling[1681].node2_file", - "$.node_coupling[1681].node2_name", - "$.node_coupling[1682].coupling_strength", - "$.node_coupling[1682].node1_file", - "$.node_coupling[1682].node1_name", - "$.node_coupling[1682].node2_file", - "$.node_coupling[1682].node2_name", - "$.node_coupling[1683].coupling_strength", - "$.node_coupling[1683].node1_file", - "$.node_coupling[1683].node1_name", - "$.node_coupling[1683].node2_name", - "$.node_coupling[1684].coupling_strength", - "$.node_coupling[1684].node1_file", - "$.node_coupling[1684].node1_name", - "$.node_coupling[1684].node2_name", - "$.node_coupling[1685].coupling_strength", - "$.node_coupling[1685].node1_file", - "$.node_coupling[1685].node1_name", - "$.node_coupling[1685].node2_name", - "$.node_coupling[1686].coupling_strength", - "$.node_coupling[1686].node1_file", - "$.node_coupling[1686].node1_name", - "$.node_coupling[1686].node2_name", - "$.node_coupling[1687].coupling_strength", - "$.node_coupling[1687].node1_file", - "$.node_coupling[1687].node1_name", - "$.node_coupling[1687].node2_file", - "$.node_coupling[1687].node2_name", - "$.node_coupling[1688].coupling_strength", - "$.node_coupling[1688].node1_file", - "$.node_coupling[1688].node1_name", - "$.node_coupling[1688].node2_file", - "$.node_coupling[1688].node2_name", - "$.node_coupling[1689].coupling_strength", - "$.node_coupling[1689].node1_file", - "$.node_coupling[1689].node1_name", - "$.node_coupling[1689].node2_name", - "$.node_coupling[168].coupling_strength", - "$.node_coupling[168].node2_file", - "$.node_coupling[168].node2_name", - "$.node_coupling[1690].coupling_strength", - "$.node_coupling[1690].node1_file", - "$.node_coupling[1690].node1_name", - "$.node_coupling[1690].node2_file", - "$.node_coupling[1690].node2_name", - "$.node_coupling[1691].coupling_strength", - "$.node_coupling[1691].node1_file", - "$.node_coupling[1691].node1_name", - "$.node_coupling[1691].node2_name", - "$.node_coupling[1692].coupling_strength", - "$.node_coupling[1692].node1_file", - "$.node_coupling[1692].node1_name", - "$.node_coupling[1692].node2_file", - "$.node_coupling[1692].node2_name", - "$.node_coupling[1693].coupling_strength", - "$.node_coupling[1693].node2_file", - "$.node_coupling[1693].node2_name", - "$.node_coupling[1694].coupling_strength", - "$.node_coupling[1694].node2_file", - "$.node_coupling[1694].node2_name", - "$.node_coupling[1695].coupling_strength", - "$.node_coupling[1695].node1_file", - "$.node_coupling[1695].node1_name", - "$.node_coupling[1695].node2_file", - "$.node_coupling[1695].node2_name", - "$.node_coupling[1696].coupling_strength", - "$.node_coupling[1696].node2_file", - "$.node_coupling[1696].node2_name", - "$.node_coupling[1697].coupling_strength", - "$.node_coupling[1697].node2_name", - "$.node_coupling[1698].coupling_strength", - "$.node_coupling[1698].node2_name", - "$.node_coupling[1699].coupling_strength", - "$.node_coupling[1699].node2_name", - "$.node_coupling[169].coupling_strength", - "$.node_coupling[169].node2_name", - "$.node_coupling[16].node1_file", - "$.node_coupling[16].node1_name", - "$.node_coupling[16].node2_file", - "$.node_coupling[16].node2_name", - "$.node_coupling[1700].coupling_strength", - "$.node_coupling[1700].node2_name", - "$.node_coupling[1701].coupling_strength", - "$.node_coupling[1701].node2_name", - "$.node_coupling[1702].coupling_strength", - "$.node_coupling[1702].node2_name", - "$.node_coupling[1703].coupling_strength", - "$.node_coupling[1703].node2_name", - "$.node_coupling[1704].coupling_strength", - "$.node_coupling[1704].node2_name", - "$.node_coupling[1705].coupling_strength", - "$.node_coupling[1705].node2_name", - "$.node_coupling[1706].coupling_strength", - "$.node_coupling[1706].node1_file", - "$.node_coupling[1706].node1_name", - "$.node_coupling[1706].node2_file", - "$.node_coupling[1706].node2_name", - "$.node_coupling[1707].coupling_strength", - "$.node_coupling[1707].node2_name", - "$.node_coupling[1708].coupling_strength", - "$.node_coupling[1708].node2_file", - "$.node_coupling[1708].node2_name", - "$.node_coupling[1709].coupling_strength", - "$.node_coupling[1709].node1_file", - "$.node_coupling[1709].node1_name", - "$.node_coupling[1709].node2_file", - "$.node_coupling[1709].node2_name", - "$.node_coupling[170].coupling_strength", - "$.node_coupling[170].node2_name", - "$.node_coupling[1710].coupling_strength", - "$.node_coupling[1710].node2_name", - "$.node_coupling[1711].coupling_strength", - "$.node_coupling[1711].node2_file", - "$.node_coupling[1711].node2_name", - "$.node_coupling[1712].coupling_strength", - "$.node_coupling[1712].node2_file", - "$.node_coupling[1712].node2_name", - "$.node_coupling[1713].coupling_strength", - "$.node_coupling[1713].node1_file", - "$.node_coupling[1713].node1_name", - "$.node_coupling[1713].node2_file", - "$.node_coupling[1713].node2_name", - "$.node_coupling[1714].coupling_strength", - "$.node_coupling[1714].node1_file", - "$.node_coupling[1714].node1_name", - "$.node_coupling[1714].node2_file", - "$.node_coupling[1714].node2_name", - "$.node_coupling[1715].coupling_strength", - "$.node_coupling[1715].node1_file", - "$.node_coupling[1715].node1_name", - "$.node_coupling[1715].node2_name", - "$.node_coupling[1716].coupling_strength", - "$.node_coupling[1716].node1_file", - "$.node_coupling[1716].node1_name", - "$.node_coupling[1716].node2_name", - "$.node_coupling[1717].coupling_strength", - "$.node_coupling[1717].node1_file", - "$.node_coupling[1717].node1_name", - "$.node_coupling[1717].node2_name", - "$.node_coupling[1718].coupling_strength", - "$.node_coupling[1718].node1_file", - "$.node_coupling[1718].node1_name", - "$.node_coupling[1718].node2_name", - "$.node_coupling[1719].coupling_strength", - "$.node_coupling[1719].node1_file", - "$.node_coupling[1719].node1_name", - "$.node_coupling[1719].node2_file", - "$.node_coupling[1719].node2_name", - "$.node_coupling[171].node2_file", - "$.node_coupling[171].node2_name", - "$.node_coupling[1720].coupling_strength", - "$.node_coupling[1720].node1_file", - "$.node_coupling[1720].node1_name", - "$.node_coupling[1720].node2_name", - "$.node_coupling[1721].coupling_strength", - "$.node_coupling[1721].node1_file", - "$.node_coupling[1721].node1_name", - "$.node_coupling[1721].node2_file", - "$.node_coupling[1721].node2_name", - "$.node_coupling[1722].coupling_strength", - "$.node_coupling[1722].node1_file", - "$.node_coupling[1722].node1_name", - "$.node_coupling[1722].node2_name", - "$.node_coupling[1723].coupling_strength", - "$.node_coupling[1723].node1_file", - "$.node_coupling[1723].node1_name", - "$.node_coupling[1723].node2_file", - "$.node_coupling[1723].node2_name", - "$.node_coupling[1724].coupling_strength", - "$.node_coupling[1724].node1_file", - "$.node_coupling[1724].node1_name", - "$.node_coupling[1724].node2_name", - "$.node_coupling[1725].coupling_strength", - "$.node_coupling[1725].node1_file", - "$.node_coupling[1725].node1_name", - "$.node_coupling[1725].node2_file", - "$.node_coupling[1725].node2_name", - "$.node_coupling[1726].coupling_strength", - "$.node_coupling[1726].node1_file", - "$.node_coupling[1726].node1_name", - "$.node_coupling[1726].node2_name", - "$.node_coupling[1727].coupling_strength", - "$.node_coupling[1727].node1_file", - "$.node_coupling[1727].node1_name", - "$.node_coupling[1727].node2_file", - "$.node_coupling[1727].node2_name", - "$.node_coupling[1728].coupling_strength", - "$.node_coupling[1728].node1_name", - "$.node_coupling[1728].node2_file", - "$.node_coupling[1728].node2_name", - "$.node_coupling[1729].coupling_strength", - "$.node_coupling[1729].node1_file", - "$.node_coupling[1729].node1_name", - "$.node_coupling[1729].node2_name", - "$.node_coupling[172].coupling_strength", - "$.node_coupling[172].node2_file", - "$.node_coupling[172].node2_name", - "$.node_coupling[1730].coupling_strength", - "$.node_coupling[1730].node1_file", - "$.node_coupling[1730].node1_name", - "$.node_coupling[1730].node2_name", - "$.node_coupling[1731].coupling_strength", - "$.node_coupling[1731].node1_file", - "$.node_coupling[1731].node1_name", - "$.node_coupling[1731].node2_file", - "$.node_coupling[1731].node2_name", - "$.node_coupling[1732].node1_name", - "$.node_coupling[1732].node2_name", - "$.node_coupling[1733].coupling_strength", - "$.node_coupling[1733].node1_file", - "$.node_coupling[1733].node1_name", - "$.node_coupling[1733].node2_file", - "$.node_coupling[1733].node2_name", - "$.node_coupling[1734].coupling_strength", - "$.node_coupling[1734].node1_file", - "$.node_coupling[1734].node1_name", - "$.node_coupling[1734].node2_name", - "$.node_coupling[1735].node2_name", - "$.node_coupling[1736].node2_name", - "$.node_coupling[1737].coupling_strength", - "$.node_coupling[1737].node2_file", - "$.node_coupling[1737].node2_name", - "$.node_coupling[1738].coupling_strength", - "$.node_coupling[1738].node2_file", - "$.node_coupling[1738].node2_name", - "$.node_coupling[1739].coupling_strength", - "$.node_coupling[1739].node2_file", - "$.node_coupling[1739].node2_name", - "$.node_coupling[173].coupling_strength", - "$.node_coupling[173].node2_file", - "$.node_coupling[173].node2_name", - "$.node_coupling[1740].coupling_strength", - "$.node_coupling[1740].node2_file", - "$.node_coupling[1740].node2_name", - "$.node_coupling[1741].coupling_strength", - "$.node_coupling[1741].node2_file", - "$.node_coupling[1741].node2_name", - "$.node_coupling[1742].node2_file", - "$.node_coupling[1742].node2_name", - "$.node_coupling[1743].node2_file", - "$.node_coupling[1743].node2_name", - "$.node_coupling[1744].node2_name", - "$.node_coupling[1745].node2_name", - "$.node_coupling[1746].coupling_strength", - "$.node_coupling[1746].node2_file", - "$.node_coupling[1746].node2_name", - "$.node_coupling[1747].node2_name", - "$.node_coupling[1748].coupling_strength", - "$.node_coupling[1748].node1_name", - "$.node_coupling[1748].node2_file", - "$.node_coupling[1748].node2_name", - "$.node_coupling[1749].node1_name", - "$.node_coupling[1749].node2_name", - "$.node_coupling[174].coupling_strength", - "$.node_coupling[174].node2_name", - "$.node_coupling[1750].node2_name", - "$.node_coupling[1751].coupling_strength", - "$.node_coupling[1751].node2_file", - "$.node_coupling[1751].node2_name", - "$.node_coupling[1752].node2_name", - "$.node_coupling[1753].node2_name", - "$.node_coupling[1754].node2_name", - "$.node_coupling[1755].node1_name", - "$.node_coupling[1755].node2_name", - "$.node_coupling[1756].coupling_strength", - "$.node_coupling[1756].node1_name", - "$.node_coupling[1756].node2_file", - "$.node_coupling[1756].node2_name", - "$.node_coupling[1757].coupling_strength", - "$.node_coupling[1757].node1_name", - "$.node_coupling[1757].node2_file", - "$.node_coupling[1757].node2_name", - "$.node_coupling[1758].node1_name", - "$.node_coupling[1758].node2_name", - "$.node_coupling[1759].node1_name", - "$.node_coupling[1759].node2_name", - "$.node_coupling[175].coupling_strength", - "$.node_coupling[175].node1_file", - "$.node_coupling[175].node1_name", - "$.node_coupling[175].node2_file", - "$.node_coupling[175].node2_name", - "$.node_coupling[1760].coupling_strength", - "$.node_coupling[1760].node1_name", - "$.node_coupling[1760].node2_file", - "$.node_coupling[1760].node2_name", - "$.node_coupling[1761].node1_name", - "$.node_coupling[1761].node2_name", - "$.node_coupling[1762].node1_name", - "$.node_coupling[1762].node2_name", - "$.node_coupling[1763].node1_name", - "$.node_coupling[1763].node2_name", - "$.node_coupling[1764].node1_name", - "$.node_coupling[1764].node2_name", - "$.node_coupling[1765].node1_name", - "$.node_coupling[1765].node2_file", - "$.node_coupling[1765].node2_name", - "$.node_coupling[1766].node1_name", - "$.node_coupling[1766].node2_name", - "$.node_coupling[1767].coupling_strength", - "$.node_coupling[1767].node1_name", - "$.node_coupling[1767].node2_file", - "$.node_coupling[1767].node2_name", - "$.node_coupling[1768].coupling_strength", - "$.node_coupling[1768].node1_name", - "$.node_coupling[1768].node2_file", - "$.node_coupling[1768].node2_name", - "$.node_coupling[1769].node1_name", - "$.node_coupling[1769].node2_name", - "$.node_coupling[176].coupling_strength", - "$.node_coupling[176].node2_file", - "$.node_coupling[176].node2_name", - "$.node_coupling[1770].node1_name", - "$.node_coupling[1770].node2_file", - "$.node_coupling[1770].node2_name", - "$.node_coupling[1771].coupling_strength", - "$.node_coupling[1771].node1_name", - "$.node_coupling[1771].node2_file", - "$.node_coupling[1771].node2_name", - "$.node_coupling[1772].coupling_strength", - "$.node_coupling[1772].node1_name", - "$.node_coupling[1772].node2_file", - "$.node_coupling[1772].node2_name", - "$.node_coupling[1773].coupling_strength", - "$.node_coupling[1773].node1_name", - "$.node_coupling[1773].node2_file", - "$.node_coupling[1773].node2_name", - "$.node_coupling[1774].node1_name", - "$.node_coupling[1774].node2_name", - "$.node_coupling[1775].node1_name", - "$.node_coupling[1775].node2_name", - "$.node_coupling[1776].node2_file", - "$.node_coupling[1776].node2_name", - "$.node_coupling[1777].node2_name", - "$.node_coupling[1778].coupling_strength", - "$.node_coupling[1778].node1_name", - "$.node_coupling[1778].node2_file", - "$.node_coupling[1778].node2_name", - "$.node_coupling[1779].coupling_strength", - "$.node_coupling[1779].node2_file", - "$.node_coupling[1779].node2_name", - "$.node_coupling[177].coupling_strength", - "$.node_coupling[177].node2_file", - "$.node_coupling[177].node2_name", - "$.node_coupling[1780].node1_name", - "$.node_coupling[1780].node2_file", - "$.node_coupling[1780].node2_name", - "$.node_coupling[1781].coupling_strength", - "$.node_coupling[1781].node2_file", - "$.node_coupling[1781].node2_name", - "$.node_coupling[1782].coupling_strength", - "$.node_coupling[1782].node2_file", - "$.node_coupling[1782].node2_name", - "$.node_coupling[1783].node2_name", - "$.node_coupling[1784].node1_name", - "$.node_coupling[1784].node2_name", - "$.node_coupling[1785].coupling_strength", - "$.node_coupling[1785].node2_file", - "$.node_coupling[1785].node2_name", - "$.node_coupling[1786].node1_name", - "$.node_coupling[1786].node2_name", - "$.node_coupling[1787].node2_name", - "$.node_coupling[1788].node2_name", - "$.node_coupling[1789].node1_name", - "$.node_coupling[1789].node2_name", - "$.node_coupling[178].coupling_strength", - "$.node_coupling[178].node2_name", - "$.node_coupling[1790].node2_file", - "$.node_coupling[1790].node2_name", - "$.node_coupling[1791].node1_name", - "$.node_coupling[1791].node2_name", - "$.node_coupling[1792].coupling_strength", - "$.node_coupling[1792].node2_file", - "$.node_coupling[1792].node2_name", - "$.node_coupling[1793].node2_file", - "$.node_coupling[1793].node2_name", - "$.node_coupling[1794].coupling_strength", - "$.node_coupling[1794].node2_file", - "$.node_coupling[1794].node2_name", - "$.node_coupling[1795].coupling_strength", - "$.node_coupling[1795].node1_file", - "$.node_coupling[1795].node1_name", - "$.node_coupling[1795].node2_name", - "$.node_coupling[1796].coupling_strength", - "$.node_coupling[1796].node1_file", - "$.node_coupling[1796].node1_name", - "$.node_coupling[1796].node2_name", - "$.node_coupling[1797].coupling_strength", - "$.node_coupling[1797].node1_file", - "$.node_coupling[1797].node1_name", - "$.node_coupling[1797].node2_name", - "$.node_coupling[1798].coupling_strength", - "$.node_coupling[1798].node1_file", - "$.node_coupling[1798].node1_name", - "$.node_coupling[1798].node2_name", - "$.node_coupling[1799].coupling_strength", - "$.node_coupling[1799].node1_file", - "$.node_coupling[1799].node1_name", - "$.node_coupling[1799].node2_name", - "$.node_coupling[179].coupling_strength", - "$.node_coupling[179].node2_file", - "$.node_coupling[179].node2_name", - "$.node_coupling[17].node1_file", - "$.node_coupling[17].node1_name", - "$.node_coupling[17].node2_name", - "$.node_coupling[1800].coupling_strength", - "$.node_coupling[1800].node1_file", - "$.node_coupling[1800].node1_name", - "$.node_coupling[1800].node2_file", - "$.node_coupling[1800].node2_name", - "$.node_coupling[1801].coupling_strength", - "$.node_coupling[1801].node1_file", - "$.node_coupling[1801].node1_name", - "$.node_coupling[1801].node2_name", - "$.node_coupling[1802].coupling_strength", - "$.node_coupling[1802].node1_file", - "$.node_coupling[1802].node1_name", - "$.node_coupling[1802].node2_name", - "$.node_coupling[1803].coupling_strength", - "$.node_coupling[1803].node1_file", - "$.node_coupling[1803].node1_name", - "$.node_coupling[1803].node2_name", - "$.node_coupling[1804].coupling_strength", - "$.node_coupling[1804].node1_file", - "$.node_coupling[1804].node1_name", - "$.node_coupling[1804].node2_file", - "$.node_coupling[1804].node2_name", - "$.node_coupling[1805].coupling_strength", - "$.node_coupling[1805].node1_file", - "$.node_coupling[1805].node1_name", - "$.node_coupling[1805].node2_name", - "$.node_coupling[1806].coupling_strength", - "$.node_coupling[1806].node1_file", - "$.node_coupling[1806].node1_name", - "$.node_coupling[1806].node2_name", - "$.node_coupling[1807].coupling_strength", - "$.node_coupling[1807].node1_file", - "$.node_coupling[1807].node1_name", - "$.node_coupling[1807].node2_file", - "$.node_coupling[1807].node2_name", - "$.node_coupling[1808].coupling_strength", - "$.node_coupling[1808].node1_file", - "$.node_coupling[1808].node1_name", - "$.node_coupling[1808].node2_name", - "$.node_coupling[1809].coupling_strength", - "$.node_coupling[1809].node1_file", - "$.node_coupling[1809].node1_name", - "$.node_coupling[1809].node2_file", - "$.node_coupling[1809].node2_name", - "$.node_coupling[180].coupling_strength", - "$.node_coupling[180].node1_file", - "$.node_coupling[180].node1_name", - "$.node_coupling[180].node2_name", - "$.node_coupling[1810].coupling_strength", - "$.node_coupling[1810].node1_file", - "$.node_coupling[1810].node1_name", - "$.node_coupling[1810].node2_name", - "$.node_coupling[1811].coupling_strength", - "$.node_coupling[1811].node1_file", - "$.node_coupling[1811].node1_name", - "$.node_coupling[1811].node2_name", - "$.node_coupling[1812].coupling_strength", - "$.node_coupling[1812].node1_file", - "$.node_coupling[1812].node1_name", - "$.node_coupling[1812].node2_name", - "$.node_coupling[1813].coupling_strength", - "$.node_coupling[1813].node1_file", - "$.node_coupling[1813].node1_name", - "$.node_coupling[1813].node2_name", - "$.node_coupling[1814].coupling_strength", - "$.node_coupling[1814].node1_file", - "$.node_coupling[1814].node1_name", - "$.node_coupling[1814].node2_name", - "$.node_coupling[1815].coupling_strength", - "$.node_coupling[1815].node1_file", - "$.node_coupling[1815].node1_name", - "$.node_coupling[1815].node2_name", - "$.node_coupling[1816].coupling_strength", - "$.node_coupling[1816].node2_file", - "$.node_coupling[1816].node2_name", - "$.node_coupling[1817].coupling_strength", - "$.node_coupling[1817].node1_file", - "$.node_coupling[1817].node1_name", - "$.node_coupling[1817].node2_file", - "$.node_coupling[1817].node2_name", - "$.node_coupling[1818].coupling_strength", - "$.node_coupling[1818].node2_name", - "$.node_coupling[1819].coupling_strength", - "$.node_coupling[1819].node2_name", - "$.node_coupling[181].coupling_strength", - "$.node_coupling[181].node2_file", - "$.node_coupling[181].node2_name", - "$.node_coupling[1820].coupling_strength", - "$.node_coupling[1820].node1_file", - "$.node_coupling[1820].node1_name", - "$.node_coupling[1820].node2_name", - "$.node_coupling[1821].coupling_strength", - "$.node_coupling[1821].node1_file", - "$.node_coupling[1821].node1_name", - "$.node_coupling[1821].node2_file", - "$.node_coupling[1821].node2_name", - "$.node_coupling[1822].coupling_strength", - "$.node_coupling[1822].node1_file", - "$.node_coupling[1822].node1_name", - "$.node_coupling[1822].node2_name", - "$.node_coupling[1823].coupling_strength", - "$.node_coupling[1823].node2_file", - "$.node_coupling[1823].node2_name", - "$.node_coupling[1824].coupling_strength", - "$.node_coupling[1824].node2_name", - "$.node_coupling[1825].coupling_strength", - "$.node_coupling[1825].node1_file", - "$.node_coupling[1825].node1_name", - "$.node_coupling[1825].node2_file", - "$.node_coupling[1825].node2_name", - "$.node_coupling[1826].coupling_strength", - "$.node_coupling[1826].node2_name", - "$.node_coupling[1827].coupling_strength", - "$.node_coupling[1827].node2_file", - "$.node_coupling[1827].node2_name", - "$.node_coupling[1828].coupling_strength", - "$.node_coupling[1828].node1_file", - "$.node_coupling[1828].node1_name", - "$.node_coupling[1828].node2_name", - "$.node_coupling[1829].coupling_strength", - "$.node_coupling[1829].node2_name", - "$.node_coupling[182].coupling_strength", - "$.node_coupling[182].node2_file", - "$.node_coupling[182].node2_name", - "$.node_coupling[1830].coupling_strength", - "$.node_coupling[1830].node2_file", - "$.node_coupling[1830].node2_name", - "$.node_coupling[1831].coupling_strength", - "$.node_coupling[1831].node2_file", - "$.node_coupling[1831].node2_name", - "$.node_coupling[1832].coupling_strength", - "$.node_coupling[1832].node1_file", - "$.node_coupling[1832].node1_name", - "$.node_coupling[1832].node2_file", - "$.node_coupling[1832].node2_name", - "$.node_coupling[1833].coupling_strength", - "$.node_coupling[1833].node2_name", - "$.node_coupling[1834].coupling_strength", - "$.node_coupling[1834].node2_file", - "$.node_coupling[1834].node2_name", - "$.node_coupling[1835].coupling_strength", - "$.node_coupling[1835].node2_file", - "$.node_coupling[1835].node2_name", - "$.node_coupling[1836].coupling_strength", - "$.node_coupling[1836].node2_file", - "$.node_coupling[1836].node2_name", - "$.node_coupling[1837].coupling_strength", - "$.node_coupling[1837].node1_file", - "$.node_coupling[1837].node1_name", - "$.node_coupling[1837].node2_file", - "$.node_coupling[1837].node2_name", - "$.node_coupling[1838].node1_file", - "$.node_coupling[1838].node1_name", - "$.node_coupling[1838].node2_name", - "$.node_coupling[1839].coupling_strength", - "$.node_coupling[1839].node1_file", - "$.node_coupling[1839].node1_name", - "$.node_coupling[1839].node2_name", - "$.node_coupling[183].coupling_strength", - "$.node_coupling[183].node2_file", - "$.node_coupling[183].node2_name", - "$.node_coupling[1840].coupling_strength", - "$.node_coupling[1840].node1_file", - "$.node_coupling[1840].node1_name", - "$.node_coupling[1840].node2_name", - "$.node_coupling[1841].coupling_strength", - "$.node_coupling[1841].node1_file", - "$.node_coupling[1841].node1_name", - "$.node_coupling[1841].node2_name", - "$.node_coupling[1842].coupling_strength", - "$.node_coupling[1842].node1_file", - "$.node_coupling[1842].node1_name", - "$.node_coupling[1842].node2_file", - "$.node_coupling[1842].node2_name", - "$.node_coupling[1843].coupling_strength", - "$.node_coupling[1843].node1_file", - "$.node_coupling[1843].node1_name", - "$.node_coupling[1843].node2_name", - "$.node_coupling[1844].coupling_strength", - "$.node_coupling[1844].node1_file", - "$.node_coupling[1844].node1_name", - "$.node_coupling[1844].node2_file", - "$.node_coupling[1844].node2_name", - "$.node_coupling[1845].coupling_strength", - "$.node_coupling[1845].node1_file", - "$.node_coupling[1845].node1_name", - "$.node_coupling[1845].node2_name", - "$.node_coupling[1846].node1_file", - "$.node_coupling[1846].node1_name", - "$.node_coupling[1846].node2_file", - "$.node_coupling[1846].node2_name", - "$.node_coupling[1847].node1_file", - "$.node_coupling[1847].node1_name", - "$.node_coupling[1847].node2_name", - "$.node_coupling[1848].node1_file", - "$.node_coupling[1848].node1_name", - "$.node_coupling[1848].node2_file", - "$.node_coupling[1848].node2_name", - "$.node_coupling[1849].node1_file", - "$.node_coupling[1849].node1_name", - "$.node_coupling[1849].node2_name", - "$.node_coupling[184].coupling_strength", - "$.node_coupling[184].node2_file", - "$.node_coupling[184].node2_name", - "$.node_coupling[1850].node1_file", - "$.node_coupling[1850].node1_name", - "$.node_coupling[1850].node2_name", - "$.node_coupling[1851].node1_file", - "$.node_coupling[1851].node1_name", - "$.node_coupling[1851].node2_name", - "$.node_coupling[1852].node1_file", - "$.node_coupling[1852].node1_name", - "$.node_coupling[1852].node2_file", - "$.node_coupling[1852].node2_name", - "$.node_coupling[1853].coupling_strength", - "$.node_coupling[1853].node1_file", - "$.node_coupling[1853].node1_name", - "$.node_coupling[1853].node2_file", - "$.node_coupling[1853].node2_name", - "$.node_coupling[1854].node1_file", - "$.node_coupling[1854].node1_name", - "$.node_coupling[1854].node2_name", - "$.node_coupling[1855].node2_name", - "$.node_coupling[1856].node1_name", - "$.node_coupling[1856].node2_name", - "$.node_coupling[1857].node2_name", - "$.node_coupling[1858].node2_name", - "$.node_coupling[1859].node2_file", - "$.node_coupling[1859].node2_name", - "$.node_coupling[185].coupling_strength", - "$.node_coupling[185].node2_file", - "$.node_coupling[185].node2_name", - "$.node_coupling[1860].node2_name", - "$.node_coupling[1861].node2_name", - "$.node_coupling[1862].node2_name", - "$.node_coupling[1863].node1_name", - "$.node_coupling[1863].node2_name", - "$.node_coupling[1864].coupling_strength", - "$.node_coupling[1864].node2_file", - "$.node_coupling[1864].node2_name", - "$.node_coupling[1865].coupling_strength", - "$.node_coupling[1865].node2_file", - "$.node_coupling[1865].node2_name", - "$.node_coupling[1866].node2_name", - "$.node_coupling[1867].coupling_strength", - "$.node_coupling[1867].node1_file", - "$.node_coupling[1867].node1_name", - "$.node_coupling[1867].node2_name", - "$.node_coupling[1868].coupling_strength", - "$.node_coupling[1868].node2_file", - "$.node_coupling[1868].node2_name", - "$.node_coupling[1869].node2_file", - "$.node_coupling[1869].node2_name", - "$.node_coupling[186].coupling_strength", - "$.node_coupling[186].node1_file", - "$.node_coupling[186].node1_name", - "$.node_coupling[186].node2_name", - "$.node_coupling[1870].coupling_strength", - "$.node_coupling[1870].node2_file", - "$.node_coupling[1870].node2_name", - "$.node_coupling[1871].coupling_strength", - "$.node_coupling[1871].node2_file", - "$.node_coupling[1871].node2_name", - "$.node_coupling[1872].node2_name", - "$.node_coupling[1873].node2_file", - "$.node_coupling[1873].node2_name", - "$.node_coupling[1874].node2_name", - "$.node_coupling[1875].node1_name", - "$.node_coupling[1875].node2_name", - "$.node_coupling[1876].coupling_strength", - "$.node_coupling[1876].node1_name", - "$.node_coupling[1876].node2_file", - "$.node_coupling[1876].node2_name", - "$.node_coupling[1877].coupling_strength", - "$.node_coupling[1877].node1_name", - "$.node_coupling[1877].node2_file", - "$.node_coupling[1877].node2_name", - "$.node_coupling[1878].node1_name", - "$.node_coupling[1878].node2_name", - "$.node_coupling[1879].coupling_strength", - "$.node_coupling[1879].node1_name", - "$.node_coupling[1879].node2_file", - "$.node_coupling[1879].node2_name", - "$.node_coupling[187].coupling_strength", - "$.node_coupling[187].node2_file", - "$.node_coupling[187].node2_name", - "$.node_coupling[1880].node1_name", - "$.node_coupling[1880].node2_file", - "$.node_coupling[1880].node2_name", - "$.node_coupling[1881].node1_name", - "$.node_coupling[1881].node2_name", - "$.node_coupling[1882].node1_name", - "$.node_coupling[1882].node2_name", - "$.node_coupling[1883].coupling_strength", - "$.node_coupling[1883].node1_name", - "$.node_coupling[1883].node2_file", - "$.node_coupling[1883].node2_name", - "$.node_coupling[1884].coupling_strength", - "$.node_coupling[1884].node1_name", - "$.node_coupling[1884].node2_file", - "$.node_coupling[1884].node2_name", - "$.node_coupling[1885].node1_name", - "$.node_coupling[1885].node2_name", - "$.node_coupling[1886].coupling_strength", - "$.node_coupling[1886].node1_name", - "$.node_coupling[1886].node2_file", - "$.node_coupling[1886].node2_name", - "$.node_coupling[1887].node1_name", - "$.node_coupling[1887].node2_name", - "$.node_coupling[1888].coupling_strength", - "$.node_coupling[1888].node1_name", - "$.node_coupling[1888].node2_file", - "$.node_coupling[1888].node2_name", - "$.node_coupling[1889].coupling_strength", - "$.node_coupling[1889].node1_file", - "$.node_coupling[1889].node1_name", - "$.node_coupling[1889].node2_file", - "$.node_coupling[1889].node2_name", - "$.node_coupling[188].coupling_strength", - "$.node_coupling[188].node2_file", - "$.node_coupling[188].node2_name", - "$.node_coupling[1890].node1_name", - "$.node_coupling[1890].node2_name", - "$.node_coupling[1891].node1_name", - "$.node_coupling[1891].node2_name", - "$.node_coupling[1892].node1_name", - "$.node_coupling[1892].node2_name", - "$.node_coupling[1893].coupling_strength", - "$.node_coupling[1893].node2_file", - "$.node_coupling[1893].node2_name", - "$.node_coupling[1894].node2_name", - "$.node_coupling[1895].node2_name", - "$.node_coupling[1896].node2_name", - "$.node_coupling[1897].node2_name", - "$.node_coupling[1898].node2_file", - "$.node_coupling[1898].node2_name", - "$.node_coupling[1899].coupling_strength", - "$.node_coupling[1899].node2_file", - "$.node_coupling[1899].node2_name", - "$.node_coupling[189].coupling_strength", - "$.node_coupling[189].node2_name", - "$.node_coupling[18].node1_file", - "$.node_coupling[18].node1_name", - "$.node_coupling[18].node2_name", - "$.node_coupling[1900].node1_name", - "$.node_coupling[1900].node2_name", - "$.node_coupling[1901].node2_file", - "$.node_coupling[1901].node2_name", - "$.node_coupling[1902].node2_name", - "$.node_coupling[1903].coupling_strength", - "$.node_coupling[1903].node1_name", - "$.node_coupling[1903].node2_file", - "$.node_coupling[1903].node2_name", - "$.node_coupling[1904].coupling_strength", - "$.node_coupling[1904].node2_file", - "$.node_coupling[1904].node2_name", - "$.node_coupling[1905].coupling_strength", - "$.node_coupling[1905].node2_file", - "$.node_coupling[1905].node2_name", - "$.node_coupling[1906].node2_name", - "$.node_coupling[1907].node2_file", - "$.node_coupling[1907].node2_name", - "$.node_coupling[1908].node2_name", - "$.node_coupling[1909].coupling_strength", - "$.node_coupling[1909].node2_file", - "$.node_coupling[1909].node2_name", - "$.node_coupling[190].coupling_strength", - "$.node_coupling[190].node2_file", - "$.node_coupling[190].node2_name", - "$.node_coupling[1910].coupling_strength", - "$.node_coupling[1910].node1_file", - "$.node_coupling[1910].node1_name", - "$.node_coupling[1910].node2_name", - "$.node_coupling[1911].coupling_strength", - "$.node_coupling[1911].node1_name", - "$.node_coupling[1911].node2_file", - "$.node_coupling[1911].node2_name", - "$.node_coupling[1912].coupling_strength", - "$.node_coupling[1912].node1_name", - "$.node_coupling[1912].node2_file", - "$.node_coupling[1912].node2_name", - "$.node_coupling[1913].coupling_strength", - "$.node_coupling[1913].node1_name", - "$.node_coupling[1913].node2_file", - "$.node_coupling[1913].node2_name", - "$.node_coupling[1914].node1_name", - "$.node_coupling[1914].node2_name", - "$.node_coupling[1915].node1_name", - "$.node_coupling[1915].node2_name", - "$.node_coupling[1916].node1_name", - "$.node_coupling[1916].node2_name", - "$.node_coupling[1917].node1_name", - "$.node_coupling[1917].node2_file", - "$.node_coupling[1917].node2_name", - "$.node_coupling[1918].coupling_strength", - "$.node_coupling[1918].node1_name", - "$.node_coupling[1918].node2_file", - "$.node_coupling[1918].node2_name", - "$.node_coupling[1919].node1_name", - "$.node_coupling[1919].node2_name", - "$.node_coupling[191].coupling_strength", - "$.node_coupling[191].node2_file", - "$.node_coupling[191].node2_name", - "$.node_coupling[1920].node1_name", - "$.node_coupling[1920].node2_file", - "$.node_coupling[1920].node2_name", - "$.node_coupling[1921].coupling_strength", - "$.node_coupling[1921].node1_name", - "$.node_coupling[1921].node2_file", - "$.node_coupling[1921].node2_name", - "$.node_coupling[1922].node1_name", - "$.node_coupling[1922].node2_name", - "$.node_coupling[1923].node1_name", - "$.node_coupling[1923].node2_name", - "$.node_coupling[1924].coupling_strength", - "$.node_coupling[1924].node1_name", - "$.node_coupling[1924].node2_file", - "$.node_coupling[1924].node2_name", - "$.node_coupling[1925].node1_name", - "$.node_coupling[1925].node2_file", - "$.node_coupling[1925].node2_name", - "$.node_coupling[1926].coupling_strength", - "$.node_coupling[1926].node1_name", - "$.node_coupling[1926].node2_file", - "$.node_coupling[1926].node2_name", - "$.node_coupling[1927].node1_name", - "$.node_coupling[1927].node2_name", - "$.node_coupling[1928].coupling_strength", - "$.node_coupling[1928].node1_file", - "$.node_coupling[1928].node1_name", - "$.node_coupling[1928].node2_file", - "$.node_coupling[1928].node2_name", - "$.node_coupling[1929].node1_name", - "$.node_coupling[1929].node2_name", - "$.node_coupling[192].coupling_strength", - "$.node_coupling[192].node2_file", - "$.node_coupling[192].node2_name", - "$.node_coupling[1930].node2_file", - "$.node_coupling[1930].node2_name", - "$.node_coupling[1931].node2_name", - "$.node_coupling[1932].coupling_strength", - "$.node_coupling[1932].node2_file", - "$.node_coupling[1932].node2_name", - "$.node_coupling[1933].node1_name", - "$.node_coupling[1933].node2_name", - "$.node_coupling[1934].coupling_strength", - "$.node_coupling[1934].node2_file", - "$.node_coupling[1934].node2_name", - "$.node_coupling[1935].node1_name", - "$.node_coupling[1935].node2_name", - "$.node_coupling[1936].node2_name", - "$.node_coupling[1937].node2_name", - "$.node_coupling[1938].coupling_strength", - "$.node_coupling[1938].node2_file", - "$.node_coupling[1938].node2_name", - "$.node_coupling[1939].coupling_strength", - "$.node_coupling[1939].node2_file", - "$.node_coupling[1939].node2_name", - "$.node_coupling[193].coupling_strength", - "$.node_coupling[193].node2_name", - "$.node_coupling[1940].node2_name", - "$.node_coupling[1941].coupling_strength", - "$.node_coupling[1941].node2_file", - "$.node_coupling[1941].node2_name", - "$.node_coupling[1942].node2_name", - "$.node_coupling[1943].coupling_strength", - "$.node_coupling[1943].node1_file", - "$.node_coupling[1943].node1_name", - "$.node_coupling[1943].node2_name", - "$.node_coupling[1944].coupling_strength", - "$.node_coupling[1944].node1_file", - "$.node_coupling[1944].node1_name", - "$.node_coupling[1944].node2_name", - "$.node_coupling[1945].node2_name", - "$.node_coupling[1946].coupling_strength", - "$.node_coupling[1946].node2_file", - "$.node_coupling[1946].node2_name", - "$.node_coupling[1947].coupling_strength", - "$.node_coupling[1947].node2_file", - "$.node_coupling[1947].node2_name", - "$.node_coupling[1948].coupling_strength", - "$.node_coupling[1948].node1_name", - "$.node_coupling[1948].node2_file", - "$.node_coupling[1948].node2_name", - "$.node_coupling[1949].coupling_strength", - "$.node_coupling[1949].node1_file", - "$.node_coupling[1949].node1_name", - "$.node_coupling[1949].node2_file", - "$.node_coupling[1949].node2_name", - "$.node_coupling[194].coupling_strength", - "$.node_coupling[194].node2_file", - "$.node_coupling[194].node2_name", - "$.node_coupling[1950].coupling_strength", - "$.node_coupling[1950].node1_name", - "$.node_coupling[1950].node2_file", - "$.node_coupling[1950].node2_name", - "$.node_coupling[1951].node1_name", - "$.node_coupling[1951].node2_name", - "$.node_coupling[1952].node1_name", - "$.node_coupling[1952].node2_name", - "$.node_coupling[1953].node1_name", - "$.node_coupling[1953].node2_file", - "$.node_coupling[1953].node2_name", - "$.node_coupling[1954].coupling_strength", - "$.node_coupling[1954].node1_name", - "$.node_coupling[1954].node2_file", - "$.node_coupling[1954].node2_name", - "$.node_coupling[1955].coupling_strength", - "$.node_coupling[1955].node1_name", - "$.node_coupling[1955].node2_file", - "$.node_coupling[1955].node2_name", - "$.node_coupling[1956].node1_name", - "$.node_coupling[1956].node2_file", - "$.node_coupling[1956].node2_name", - "$.node_coupling[1957].node1_name", - "$.node_coupling[1957].node2_name", - "$.node_coupling[1958].node1_name", - "$.node_coupling[1958].node2_name", - "$.node_coupling[1959].coupling_strength", - "$.node_coupling[1959].node1_file", - "$.node_coupling[1959].node1_name", - "$.node_coupling[1959].node2_name", - "$.node_coupling[195].coupling_strength", - "$.node_coupling[195].node2_name", - "$.node_coupling[1960].node1_name", - "$.node_coupling[1960].node2_file", - "$.node_coupling[1960].node2_name", - "$.node_coupling[1961].node1_name", - "$.node_coupling[1961].node2_name", - "$.node_coupling[1962].node1_name", - "$.node_coupling[1962].node2_name", - "$.node_coupling[1963].node1_name", - "$.node_coupling[1963].node2_file", - "$.node_coupling[1963].node2_name", - "$.node_coupling[1964].coupling_strength", - "$.node_coupling[1964].node1_name", - "$.node_coupling[1964].node2_file", - "$.node_coupling[1964].node2_name", - "$.node_coupling[1965].node1_name", - "$.node_coupling[1965].node2_name", - "$.node_coupling[1966].coupling_strength", - "$.node_coupling[1966].node2_file", - "$.node_coupling[1966].node2_name", - "$.node_coupling[1967].node2_file", - "$.node_coupling[1967].node2_name", - "$.node_coupling[1968].node2_name", - "$.node_coupling[1969].coupling_strength", - "$.node_coupling[1969].node1_file", - "$.node_coupling[1969].node1_name", - "$.node_coupling[1969].node2_name", - "$.node_coupling[196].coupling_strength", - "$.node_coupling[196].node2_name", - "$.node_coupling[1970].coupling_strength", - "$.node_coupling[1970].node2_file", - "$.node_coupling[1970].node2_name", - "$.node_coupling[1971].node2_name", - "$.node_coupling[1972].coupling_strength", - "$.node_coupling[1972].node2_file", - "$.node_coupling[1972].node2_name", - "$.node_coupling[1973].node2_name", - "$.node_coupling[1974].coupling_strength", - "$.node_coupling[1974].node1_name", - "$.node_coupling[1974].node2_file", - "$.node_coupling[1974].node2_name", - "$.node_coupling[1975].node2_name", - "$.node_coupling[1976].node2_name", - "$.node_coupling[1977].node2_file", - "$.node_coupling[1977].node2_name", - "$.node_coupling[1978].coupling_strength", - "$.node_coupling[1978].node2_file", - "$.node_coupling[1978].node2_name", - "$.node_coupling[1979].coupling_strength", - "$.node_coupling[1979].node2_file", - "$.node_coupling[1979].node2_name", - "$.node_coupling[197].node2_file", - "$.node_coupling[197].node2_name", - "$.node_coupling[1980].node2_name", - "$.node_coupling[1981].node2_name", - "$.node_coupling[1982].node2_name", - "$.node_coupling[1983].coupling_strength", - "$.node_coupling[1983].node1_name", - "$.node_coupling[1983].node2_file", - "$.node_coupling[1983].node2_name", - "$.node_coupling[1984].node1_name", - "$.node_coupling[1984].node2_name", - "$.node_coupling[1985].node1_name", - "$.node_coupling[1985].node2_name", - "$.node_coupling[1986].coupling_strength", - "$.node_coupling[1986].node1_name", - "$.node_coupling[1986].node2_file", - "$.node_coupling[1986].node2_name", - "$.node_coupling[1987].coupling_strength", - "$.node_coupling[1987].node1_name", - "$.node_coupling[1987].node2_file", - "$.node_coupling[1987].node2_name", - "$.node_coupling[1988].node1_name", - "$.node_coupling[1988].node2_name", - "$.node_coupling[1989].coupling_strength", - "$.node_coupling[1989].node1_name", - "$.node_coupling[1989].node2_file", - "$.node_coupling[1989].node2_name", - "$.node_coupling[198].coupling_strength", - "$.node_coupling[198].node1_file", - "$.node_coupling[198].node1_name", - "$.node_coupling[198].node2_file", - "$.node_coupling[198].node2_name", - "$.node_coupling[1990].coupling_strength", - "$.node_coupling[1990].node1_file", - "$.node_coupling[1990].node1_name", - "$.node_coupling[1990].node2_name", - "$.node_coupling[1991].node1_name", - "$.node_coupling[1991].node2_name", - "$.node_coupling[1992].node1_name", - "$.node_coupling[1992].node2_name", - "$.node_coupling[1993].node1_name", - "$.node_coupling[1993].node2_file", - "$.node_coupling[1993].node2_name", - "$.node_coupling[1994].coupling_strength", - "$.node_coupling[1994].node1_name", - "$.node_coupling[1994].node2_file", - "$.node_coupling[1994].node2_name", - "$.node_coupling[1995].coupling_strength", - "$.node_coupling[1995].node1_file", - "$.node_coupling[1995].node1_name", - "$.node_coupling[1995].node2_name", - "$.node_coupling[1996].coupling_strength", - "$.node_coupling[1996].node1_file", - "$.node_coupling[1996].node1_name", - "$.node_coupling[1996].node2_file", - "$.node_coupling[1996].node2_name", - "$.node_coupling[1997].node1_name", - "$.node_coupling[1997].node2_name", - "$.node_coupling[1998].coupling_strength", - "$.node_coupling[1998].node1_name", - "$.node_coupling[1998].node2_file", - "$.node_coupling[1998].node2_name", - "$.node_coupling[1999].coupling_strength", - "$.node_coupling[1999].node1_name", - "$.node_coupling[1999].node2_file", - "$.node_coupling[1999].node2_name", - "$.node_coupling[199].node2_file", - "$.node_coupling[199].node2_name", - "$.node_coupling[19].coupling_strength", - "$.node_coupling[19].node1_file", - "$.node_coupling[19].node1_name", - "$.node_coupling[19].node2_file", - "$.node_coupling[19].node2_name", - "$.node_coupling[1].co_changes", - "$.node_coupling[1].coupling_strength", - "$.node_coupling[1].node1_file", - "$.node_coupling[1].node1_name", - "$.node_coupling[1].node2_name", - "$.node_coupling[2000].coupling_strength", - "$.node_coupling[2000].node1_name", - "$.node_coupling[2000].node2_file", - "$.node_coupling[2000].node2_name", - "$.node_coupling[2001].coupling_strength", - "$.node_coupling[2001].node2_file", - "$.node_coupling[2001].node2_name", - "$.node_coupling[2002].coupling_strength", - "$.node_coupling[2002].node2_file", - "$.node_coupling[2002].node2_name", - "$.node_coupling[2003].coupling_strength", - "$.node_coupling[2003].node1_file", - "$.node_coupling[2003].node1_name", - "$.node_coupling[2003].node2_file", - "$.node_coupling[2003].node2_name", - "$.node_coupling[2004].coupling_strength", - "$.node_coupling[2004].node2_file", - "$.node_coupling[2004].node2_name", - "$.node_coupling[2005].node2_name", - "$.node_coupling[2006].coupling_strength", - "$.node_coupling[2006].node1_file", - "$.node_coupling[2006].node1_name", - "$.node_coupling[2006].node2_file", - "$.node_coupling[2006].node2_name", - "$.node_coupling[2007].coupling_strength", - "$.node_coupling[2007].node2_file", - "$.node_coupling[2007].node2_name", - "$.node_coupling[2008].coupling_strength", - "$.node_coupling[2008].node1_file", - "$.node_coupling[2008].node1_name", - "$.node_coupling[2008].node2_name", - "$.node_coupling[2009].coupling_strength", - "$.node_coupling[2009].node1_file", - "$.node_coupling[2009].node1_name", - "$.node_coupling[2009].node2_file", - "$.node_coupling[2009].node2_name", - "$.node_coupling[200].coupling_strength", - "$.node_coupling[200].node1_file", - "$.node_coupling[200].node1_name", - "$.node_coupling[200].node2_file", - "$.node_coupling[200].node2_name", - "$.node_coupling[2010].node2_name", - "$.node_coupling[2011].node2_name", - "$.node_coupling[2012].node2_name", - "$.node_coupling[2013].node2_name", - "$.node_coupling[2014].coupling_strength", - "$.node_coupling[2014].node2_file", - "$.node_coupling[2014].node2_name", - "$.node_coupling[2015].coupling_strength", - "$.node_coupling[2015].node2_file", - "$.node_coupling[2015].node2_name", - "$.node_coupling[2016].node2_file", - "$.node_coupling[2016].node2_name", - "$.node_coupling[2017].coupling_strength", - "$.node_coupling[2017].node1_name", - "$.node_coupling[2017].node2_file", - "$.node_coupling[2017].node2_name", - "$.node_coupling[2018].node1_name", - "$.node_coupling[2018].node2_name", - "$.node_coupling[2019].node1_name", - "$.node_coupling[2019].node2_name", - "$.node_coupling[201].coupling_strength", - "$.node_coupling[201].node2_file", - "$.node_coupling[201].node2_name", - "$.node_coupling[2020].node1_name", - "$.node_coupling[2020].node2_name", - "$.node_coupling[2021].coupling_strength", - "$.node_coupling[2021].node1_name", - "$.node_coupling[2021].node2_file", - "$.node_coupling[2021].node2_name", - "$.node_coupling[2022].node1_name", - "$.node_coupling[2022].node2_name", - "$.node_coupling[2023].node1_name", - "$.node_coupling[2023].node2_name", - "$.node_coupling[2024].node1_name", - "$.node_coupling[2024].node2_name", - "$.node_coupling[2025].node1_name", - "$.node_coupling[2025].node2_name", - "$.node_coupling[2026].node1_name", - "$.node_coupling[2026].node2_name", - "$.node_coupling[2027].coupling_strength", - "$.node_coupling[2027].node1_file", - "$.node_coupling[2027].node1_name", - "$.node_coupling[2027].node2_file", - "$.node_coupling[2027].node2_name", - "$.node_coupling[2028].node1_name", - "$.node_coupling[2028].node2_name", - "$.node_coupling[2029].node1_name", - "$.node_coupling[2029].node2_name", - "$.node_coupling[202].coupling_strength", - "$.node_coupling[202].node2_file", - "$.node_coupling[202].node2_name", - "$.node_coupling[2030].node1_name", - "$.node_coupling[2030].node2_name", - "$.node_coupling[2031].coupling_strength", - "$.node_coupling[2031].node1_name", - "$.node_coupling[2031].node2_file", - "$.node_coupling[2031].node2_name", - "$.node_coupling[2032].node1_name", - "$.node_coupling[2032].node2_file", - "$.node_coupling[2032].node2_name", - "$.node_coupling[2033].coupling_strength", - "$.node_coupling[2033].node1_name", - "$.node_coupling[2033].node2_file", - "$.node_coupling[2033].node2_name", - "$.node_coupling[2034].node1_name", - "$.node_coupling[2034].node2_file", - "$.node_coupling[2034].node2_name", - "$.node_coupling[2035].node2_name", - "$.node_coupling[2036].node2_name", - "$.node_coupling[2037].node2_file", - "$.node_coupling[2037].node2_name", - "$.node_coupling[2038].coupling_strength", - "$.node_coupling[2038].node2_file", - "$.node_coupling[2038].node2_name", - "$.node_coupling[2039].coupling_strength", - "$.node_coupling[2039].node2_file", - "$.node_coupling[2039].node2_name", - "$.node_coupling[203].coupling_strength", - "$.node_coupling[203].node1_file", - "$.node_coupling[203].node1_name", - "$.node_coupling[203].node2_name", - "$.node_coupling[2040].node2_name", - "$.node_coupling[2041].node2_name", - "$.node_coupling[2042].node2_name", - "$.node_coupling[2043].node2_name", - "$.node_coupling[2044].coupling_strength", - "$.node_coupling[2044].node2_file", - "$.node_coupling[2044].node2_name", - "$.node_coupling[2045].coupling_strength", - "$.node_coupling[2045].node2_file", - "$.node_coupling[2045].node2_name", - "$.node_coupling[2046].node2_name", - "$.node_coupling[2047].coupling_strength", - "$.node_coupling[2047].node2_file", - "$.node_coupling[2047].node2_name", - "$.node_coupling[2048].node2_name", - "$.node_coupling[2049].node2_name", - "$.node_coupling[204].coupling_strength", - "$.node_coupling[204].node1_file", - "$.node_coupling[204].node1_name", - "$.node_coupling[204].node2_file", - "$.node_coupling[204].node2_name", - "$.node_coupling[2050].node1_name", - "$.node_coupling[2050].node2_name", - "$.node_coupling[2051].node1_name", - "$.node_coupling[2051].node2_name", - "$.node_coupling[2052].coupling_strength", - "$.node_coupling[2052].node1_name", - "$.node_coupling[2052].node2_file", - "$.node_coupling[2052].node2_name", - "$.node_coupling[2053].coupling_strength", - "$.node_coupling[2053].node1_file", - "$.node_coupling[2053].node1_name", - "$.node_coupling[2053].node2_name", - "$.node_coupling[2054].coupling_strength", - "$.node_coupling[2054].node1_file", - "$.node_coupling[2054].node1_name", - "$.node_coupling[2054].node2_file", - "$.node_coupling[2054].node2_name", - "$.node_coupling[2055].node1_file", - "$.node_coupling[2055].node1_name", - "$.node_coupling[2055].node2_name", - "$.node_coupling[2056].coupling_strength", - "$.node_coupling[2056].node1_file", - "$.node_coupling[2056].node1_name", - "$.node_coupling[2056].node2_file", - "$.node_coupling[2056].node2_name", - "$.node_coupling[2057].coupling_strength", - "$.node_coupling[2057].node1_file", - "$.node_coupling[2057].node1_name", - "$.node_coupling[2057].node2_file", - "$.node_coupling[2057].node2_name", - "$.node_coupling[2058].coupling_strength", - "$.node_coupling[2058].node1_file", - "$.node_coupling[2058].node1_name", - "$.node_coupling[2058].node2_file", - "$.node_coupling[2058].node2_name", - "$.node_coupling[2059].coupling_strength", - "$.node_coupling[2059].node1_file", - "$.node_coupling[2059].node1_name", - "$.node_coupling[2059].node2_name", - "$.node_coupling[205].coupling_strength", - "$.node_coupling[205].node2_file", - "$.node_coupling[205].node2_name", - "$.node_coupling[2060].coupling_strength", - "$.node_coupling[2060].node1_file", - "$.node_coupling[2060].node1_name", - "$.node_coupling[2060].node2_name", - "$.node_coupling[2061].coupling_strength", - "$.node_coupling[2061].node1_file", - "$.node_coupling[2061].node1_name", - "$.node_coupling[2061].node2_name", - "$.node_coupling[2062].coupling_strength", - "$.node_coupling[2062].node1_file", - "$.node_coupling[2062].node1_name", - "$.node_coupling[2062].node2_file", - "$.node_coupling[2062].node2_name", - "$.node_coupling[2063].coupling_strength", - "$.node_coupling[2063].node1_file", - "$.node_coupling[2063].node1_name", - "$.node_coupling[2063].node2_file", - "$.node_coupling[2063].node2_name", - "$.node_coupling[2064].coupling_strength", - "$.node_coupling[2064].node1_file", - "$.node_coupling[2064].node1_name", - "$.node_coupling[2064].node2_file", - "$.node_coupling[2064].node2_name", - "$.node_coupling[2065].coupling_strength", - "$.node_coupling[2065].node1_file", - "$.node_coupling[2065].node1_name", - "$.node_coupling[2065].node2_name", - "$.node_coupling[2066].coupling_strength", - "$.node_coupling[2066].node1_file", - "$.node_coupling[2066].node1_name", - "$.node_coupling[2066].node2_name", - "$.node_coupling[2067].coupling_strength", - "$.node_coupling[2067].node1_file", - "$.node_coupling[2067].node1_name", - "$.node_coupling[2067].node2_file", - "$.node_coupling[2067].node2_name", - "$.node_coupling[2068].coupling_strength", - "$.node_coupling[2068].node2_name", - "$.node_coupling[2069].coupling_strength", - "$.node_coupling[2069].node2_name", - "$.node_coupling[206].coupling_strength", - "$.node_coupling[206].node2_file", - "$.node_coupling[206].node2_name", - "$.node_coupling[2070].coupling_strength", - "$.node_coupling[2070].node2_name", - "$.node_coupling[2071].coupling_strength", - "$.node_coupling[2071].node2_name", - "$.node_coupling[2072].coupling_strength", - "$.node_coupling[2072].node1_file", - "$.node_coupling[2072].node1_name", - "$.node_coupling[2072].node2_file", - "$.node_coupling[2072].node2_name", - "$.node_coupling[2073].coupling_strength", - "$.node_coupling[2073].node2_name", - "$.node_coupling[2074].coupling_strength", - "$.node_coupling[2074].node2_name", - "$.node_coupling[2075].coupling_strength", - "$.node_coupling[2075].node1_file", - "$.node_coupling[2075].node1_name", - "$.node_coupling[2075].node2_file", - "$.node_coupling[2075].node2_name", - "$.node_coupling[2076].coupling_strength", - "$.node_coupling[2076].node2_file", - "$.node_coupling[2076].node2_name", - "$.node_coupling[2077].coupling_strength", - "$.node_coupling[2077].node2_file", - "$.node_coupling[2077].node2_name", - "$.node_coupling[2078].coupling_strength", - "$.node_coupling[2078].node1_name", - "$.node_coupling[2078].node2_file", - "$.node_coupling[2078].node2_name", - "$.node_coupling[2079].coupling_strength", - "$.node_coupling[2079].node2_file", - "$.node_coupling[2079].node2_name", - "$.node_coupling[207].coupling_strength", - "$.node_coupling[207].node1_file", - "$.node_coupling[207].node1_name", - "$.node_coupling[207].node2_name", - "$.node_coupling[2080].coupling_strength", - "$.node_coupling[2080].node2_file", - "$.node_coupling[2080].node2_name", - "$.node_coupling[2081].coupling_strength", - "$.node_coupling[2081].node2_file", - "$.node_coupling[2081].node2_name", - "$.node_coupling[2082].coupling_strength", - "$.node_coupling[2082].node2_file", - "$.node_coupling[2082].node2_name", - "$.node_coupling[2083].coupling_strength", - "$.node_coupling[2083].node1_file", - "$.node_coupling[2083].node1_name", - "$.node_coupling[2083].node2_file", - "$.node_coupling[2083].node2_name", - "$.node_coupling[2084].coupling_strength", - "$.node_coupling[2084].node1_file", - "$.node_coupling[2084].node1_name", - "$.node_coupling[2084].node2_name", - "$.node_coupling[2085].coupling_strength", - "$.node_coupling[2085].node1_file", - "$.node_coupling[2085].node1_name", - "$.node_coupling[2085].node2_file", - "$.node_coupling[2085].node2_name", - "$.node_coupling[2086].coupling_strength", - "$.node_coupling[2086].node1_file", - "$.node_coupling[2086].node1_name", - "$.node_coupling[2086].node2_file", - "$.node_coupling[2086].node2_name", - "$.node_coupling[2087].coupling_strength", - "$.node_coupling[2087].node1_file", - "$.node_coupling[2087].node1_name", - "$.node_coupling[2087].node2_file", - "$.node_coupling[2087].node2_name", - "$.node_coupling[2088].coupling_strength", - "$.node_coupling[2088].node1_file", - "$.node_coupling[2088].node1_name", - "$.node_coupling[2088].node2_name", - "$.node_coupling[2089].coupling_strength", - "$.node_coupling[2089].node2_file", - "$.node_coupling[2089].node2_name", - "$.node_coupling[208].coupling_strength", - "$.node_coupling[208].node2_file", - "$.node_coupling[208].node2_name", - "$.node_coupling[2090].coupling_strength", - "$.node_coupling[2090].node1_file", - "$.node_coupling[2090].node1_name", - "$.node_coupling[2090].node2_name", - "$.node_coupling[2091].coupling_strength", - "$.node_coupling[2091].node1_file", - "$.node_coupling[2091].node1_name", - "$.node_coupling[2091].node2_name", - "$.node_coupling[2092].coupling_strength", - "$.node_coupling[2092].node1_file", - "$.node_coupling[2092].node1_name", - "$.node_coupling[2092].node2_file", - "$.node_coupling[2092].node2_name", - "$.node_coupling[2093].coupling_strength", - "$.node_coupling[2093].node1_file", - "$.node_coupling[2093].node1_name", - "$.node_coupling[2093].node2_name", - "$.node_coupling[2094].coupling_strength", - "$.node_coupling[2094].node1_file", - "$.node_coupling[2094].node1_name", - "$.node_coupling[2094].node2_name", - "$.node_coupling[2095].coupling_strength", - "$.node_coupling[2095].node1_file", - "$.node_coupling[2095].node1_name", - "$.node_coupling[2095].node2_name", - "$.node_coupling[2096].coupling_strength", - "$.node_coupling[2096].node1_file", - "$.node_coupling[2096].node1_name", - "$.node_coupling[2096].node2_file", - "$.node_coupling[2096].node2_name", - "$.node_coupling[2097].coupling_strength", - "$.node_coupling[2097].node1_file", - "$.node_coupling[2097].node1_name", - "$.node_coupling[2097].node2_name", - "$.node_coupling[2098].coupling_strength", - "$.node_coupling[2098].node1_file", - "$.node_coupling[2098].node1_name", - "$.node_coupling[2098].node2_name", - "$.node_coupling[2099].coupling_strength", - "$.node_coupling[2099].node1_file", - "$.node_coupling[2099].node1_name", - "$.node_coupling[2099].node2_name", - "$.node_coupling[209].coupling_strength", - "$.node_coupling[209].node2_file", - "$.node_coupling[209].node2_name", - "$.node_coupling[20].coupling_strength", - "$.node_coupling[20].node1_file", - "$.node_coupling[20].node1_name", - "$.node_coupling[20].node2_file", - "$.node_coupling[20].node2_name", - "$.node_coupling[2100].coupling_strength", - "$.node_coupling[2100].node1_name", - "$.node_coupling[2100].node2_file", - "$.node_coupling[2100].node2_name", - "$.node_coupling[2101].node1_name", - "$.node_coupling[2101].node2_name", - "$.node_coupling[2102].node1_name", - "$.node_coupling[2102].node2_name", - "$.node_coupling[2103].coupling_strength", - "$.node_coupling[2103].node1_name", - "$.node_coupling[2103].node2_file", - "$.node_coupling[2103].node2_name", - "$.node_coupling[2104].coupling_strength", - "$.node_coupling[2104].node2_file", - "$.node_coupling[2104].node2_name", - "$.node_coupling[2105].node2_name", - "$.node_coupling[2106].node2_name", - "$.node_coupling[2107].coupling_strength", - "$.node_coupling[2107].node2_file", - "$.node_coupling[2107].node2_name", - "$.node_coupling[2108].coupling_strength", - "$.node_coupling[2108].node2_file", - "$.node_coupling[2108].node2_name", - "$.node_coupling[2109].node2_name", - "$.node_coupling[210].coupling_strength", - "$.node_coupling[210].node1_file", - "$.node_coupling[210].node1_name", - "$.node_coupling[210].node2_file", - "$.node_coupling[210].node2_name", - "$.node_coupling[2110].node2_file", - "$.node_coupling[2110].node2_name", - "$.node_coupling[2111].node2_name", - "$.node_coupling[2112].node2_name", - "$.node_coupling[2113].coupling_strength", - "$.node_coupling[2113].node2_file", - "$.node_coupling[2113].node2_name", - "$.node_coupling[2114].coupling_strength", - "$.node_coupling[2114].node1_name", - "$.node_coupling[2114].node2_file", - "$.node_coupling[2114].node2_name", - "$.node_coupling[2115].coupling_strength", - "$.node_coupling[2115].node1_file", - "$.node_coupling[2115].node1_name", - "$.node_coupling[2115].node2_file", - "$.node_coupling[2115].node2_name", - "$.node_coupling[2116].coupling_strength", - "$.node_coupling[2116].node1_name", - "$.node_coupling[2116].node2_file", - "$.node_coupling[2116].node2_name", - "$.node_coupling[2117].coupling_strength", - "$.node_coupling[2117].node1_name", - "$.node_coupling[2117].node2_file", - "$.node_coupling[2117].node2_name", - "$.node_coupling[2118].coupling_strength", - "$.node_coupling[2118].node1_name", - "$.node_coupling[2118].node2_file", - "$.node_coupling[2118].node2_name", - "$.node_coupling[2119].node1_name", - "$.node_coupling[2119].node2_name", - "$.node_coupling[211].coupling_strength", - "$.node_coupling[211].node2_file", - "$.node_coupling[211].node2_name", - "$.node_coupling[2120].coupling_strength", - "$.node_coupling[2120].node1_name", - "$.node_coupling[2120].node2_file", - "$.node_coupling[2120].node2_name", - "$.node_coupling[2121].coupling_strength", - "$.node_coupling[2121].node1_name", - "$.node_coupling[2121].node2_file", - "$.node_coupling[2121].node2_name", - "$.node_coupling[2122].node1_name", - "$.node_coupling[2122].node2_name", - "$.node_coupling[2123].node1_name", - "$.node_coupling[2123].node2_name", - "$.node_coupling[2124].node1_name", - "$.node_coupling[2124].node2_name", - "$.node_coupling[2125].node1_name", - "$.node_coupling[2125].node2_file", - "$.node_coupling[2125].node2_name", - "$.node_coupling[2126].coupling_strength", - "$.node_coupling[2126].node1_name", - "$.node_coupling[2126].node2_file", - "$.node_coupling[2126].node2_name", - "$.node_coupling[2127].node1_name", - "$.node_coupling[2127].node2_name", - "$.node_coupling[2128].coupling_strength", - "$.node_coupling[2128].node1_name", - "$.node_coupling[2128].node2_file", - "$.node_coupling[2128].node2_name", - "$.node_coupling[2129].coupling_strength", - "$.node_coupling[2129].node1_name", - "$.node_coupling[2129].node2_file", - "$.node_coupling[2129].node2_name", - "$.node_coupling[212].coupling_strength", - "$.node_coupling[212].node2_file", - "$.node_coupling[212].node2_name", - "$.node_coupling[2130].node1_name", - "$.node_coupling[2130].node2_name", - "$.node_coupling[2131].coupling_strength", - "$.node_coupling[2131].node1_name", - "$.node_coupling[2131].node2_file", - "$.node_coupling[2131].node2_name", - "$.node_coupling[2132].node1_name", - "$.node_coupling[2132].node2_name", - "$.node_coupling[2133].node1_name", - "$.node_coupling[2133].node2_name", - "$.node_coupling[2134].node1_name", - "$.node_coupling[2134].node2_name", - "$.node_coupling[2135].coupling_strength", - "$.node_coupling[2135].node2_file", - "$.node_coupling[2135].node2_name", - "$.node_coupling[2136].coupling_strength", - "$.node_coupling[2136].node1_file", - "$.node_coupling[2136].node1_name", - "$.node_coupling[2136].node2_file", - "$.node_coupling[2136].node2_name", - "$.node_coupling[2137].node2_name", - "$.node_coupling[2138].node2_file", - "$.node_coupling[2138].node2_name", - "$.node_coupling[2139].node2_file", - "$.node_coupling[2139].node2_name", - "$.node_coupling[213].coupling_strength", - "$.node_coupling[213].node1_file", - "$.node_coupling[213].node1_name", - "$.node_coupling[213].node2_file", - "$.node_coupling[213].node2_name", - "$.node_coupling[2140].coupling_strength", - "$.node_coupling[2140].node2_file", - "$.node_coupling[2140].node2_name", - "$.node_coupling[2141].node2_name", - "$.node_coupling[2142].coupling_strength", - "$.node_coupling[2142].node2_file", - "$.node_coupling[2142].node2_name", - "$.node_coupling[2143].node1_file", - "$.node_coupling[2143].node1_name", - "$.node_coupling[2143].node2_name", - "$.node_coupling[2144].node1_name", - "$.node_coupling[2144].node2_name", - "$.node_coupling[2145].coupling_strength", - "$.node_coupling[2145].node1_name", - "$.node_coupling[2145].node2_file", - "$.node_coupling[2145].node2_name", - "$.node_coupling[2146].coupling_strength", - "$.node_coupling[2146].node1_name", - "$.node_coupling[2146].node2_file", - "$.node_coupling[2146].node2_name", - "$.node_coupling[2147].node1_name", - "$.node_coupling[2147].node2_name", - "$.node_coupling[2148].node1_name", - "$.node_coupling[2148].node2_name", - "$.node_coupling[2149].coupling_strength", - "$.node_coupling[2149].node1_name", - "$.node_coupling[2149].node2_file", - "$.node_coupling[2149].node2_name", - "$.node_coupling[214].coupling_strength", - "$.node_coupling[214].node1_file", - "$.node_coupling[214].node1_name", - "$.node_coupling[214].node2_file", - "$.node_coupling[214].node2_name", - "$.node_coupling[2150].node1_name", - "$.node_coupling[2150].node2_name", - "$.node_coupling[2151].node1_name", - "$.node_coupling[2151].node2_name", - "$.node_coupling[2152].node1_name", - "$.node_coupling[2152].node2_file", - "$.node_coupling[2152].node2_name", - "$.node_coupling[2153].node1_name", - "$.node_coupling[2153].node2_file", - "$.node_coupling[2153].node2_name", - "$.node_coupling[2154].coupling_strength", - "$.node_coupling[2154].node1_name", - "$.node_coupling[2154].node2_file", - "$.node_coupling[2154].node2_name", - "$.node_coupling[2155].node1_name", - "$.node_coupling[2155].node2_file", - "$.node_coupling[2155].node2_name", - "$.node_coupling[2156].node1_name", - "$.node_coupling[2156].node2_name", - "$.node_coupling[2157].coupling_strength", - "$.node_coupling[2157].node1_file", - "$.node_coupling[2157].node1_name", - "$.node_coupling[2157].node2_file", - "$.node_coupling[2157].node2_name", - "$.node_coupling[2158].node1_name", - "$.node_coupling[2158].node2_name", - "$.node_coupling[2159].coupling_strength", - "$.node_coupling[2159].node1_name", - "$.node_coupling[2159].node2_name", - "$.node_coupling[215].coupling_strength", - "$.node_coupling[215].node2_file", - "$.node_coupling[215].node2_name", - "$.node_coupling[2160].node1_name", - "$.node_coupling[2160].node2_name", - "$.node_coupling[2161].node1_name", - "$.node_coupling[2161].node2_name", - "$.node_coupling[2162].coupling_strength", - "$.node_coupling[2162].node1_name", - "$.node_coupling[2162].node2_file", - "$.node_coupling[2162].node2_name", - "$.node_coupling[2163].node1_name", - "$.node_coupling[2163].node2_name", - "$.node_coupling[2164].node1_name", - "$.node_coupling[2164].node2_name", - "$.node_coupling[2165].coupling_strength", - "$.node_coupling[2165].node1_file", - "$.node_coupling[2165].node1_name", - "$.node_coupling[2165].node2_name", - "$.node_coupling[2166].node2_name", - "$.node_coupling[2167].coupling_strength", - "$.node_coupling[2167].node2_file", - "$.node_coupling[2167].node2_name", - "$.node_coupling[2168].node2_name", - "$.node_coupling[2169].node2_name", - "$.node_coupling[216].coupling_strength", - "$.node_coupling[216].node1_name", - "$.node_coupling[216].node2_file", - "$.node_coupling[216].node2_name", - "$.node_coupling[2170].node2_name", - "$.node_coupling[2171].coupling_strength", - "$.node_coupling[2171].node2_file", - "$.node_coupling[2171].node2_name", - "$.node_coupling[2172].node2_name", - "$.node_coupling[2173].coupling_strength", - "$.node_coupling[2173].node1_name", - "$.node_coupling[2173].node2_file", - "$.node_coupling[2173].node2_name", - "$.node_coupling[2174].coupling_strength", - "$.node_coupling[2174].node1_name", - "$.node_coupling[2174].node2_file", - "$.node_coupling[2174].node2_name", - "$.node_coupling[2175].node1_name", - "$.node_coupling[2175].node2_file", - "$.node_coupling[2175].node2_name", - "$.node_coupling[2176].coupling_strength", - "$.node_coupling[2176].node1_name", - "$.node_coupling[2176].node2_file", - "$.node_coupling[2176].node2_name", - "$.node_coupling[2177].coupling_strength", - "$.node_coupling[2177].node1_name", - "$.node_coupling[2177].node2_file", - "$.node_coupling[2177].node2_name", - "$.node_coupling[2178].coupling_strength", - "$.node_coupling[2178].node1_name", - "$.node_coupling[2178].node2_file", - "$.node_coupling[2178].node2_name", - "$.node_coupling[2179].coupling_strength", - "$.node_coupling[2179].node1_name", - "$.node_coupling[2179].node2_file", - "$.node_coupling[2179].node2_name", - "$.node_coupling[217].coupling_strength", - "$.node_coupling[217].node1_name", - "$.node_coupling[217].node2_name", - "$.node_coupling[2180].coupling_strength", - "$.node_coupling[2180].node1_name", - "$.node_coupling[2180].node2_file", - "$.node_coupling[2180].node2_name", - "$.node_coupling[2181].node1_name", - "$.node_coupling[2181].node2_name", - "$.node_coupling[2182].node1_name", - "$.node_coupling[2182].node2_name", - "$.node_coupling[2183].coupling_strength", - "$.node_coupling[2183].node1_name", - "$.node_coupling[2183].node2_file", - "$.node_coupling[2183].node2_name", - "$.node_coupling[2184].node1_name", - "$.node_coupling[2184].node2_file", - "$.node_coupling[2184].node2_name", - "$.node_coupling[2185].coupling_strength", - "$.node_coupling[2185].node1_name", - "$.node_coupling[2185].node2_file", - "$.node_coupling[2185].node2_name", - "$.node_coupling[2186].coupling_strength", - "$.node_coupling[2186].node1_name", - "$.node_coupling[2186].node2_file", - "$.node_coupling[2186].node2_name", - "$.node_coupling[2187].coupling_strength", - "$.node_coupling[2187].node1_name", - "$.node_coupling[2187].node2_file", - "$.node_coupling[2187].node2_name", - "$.node_coupling[2188].coupling_strength", - "$.node_coupling[2188].node1_name", - "$.node_coupling[2188].node2_file", - "$.node_coupling[2188].node2_name", - "$.node_coupling[2189].node1_name", - "$.node_coupling[2189].node2_name", - "$.node_coupling[218].coupling_strength", - "$.node_coupling[218].node1_name", - "$.node_coupling[218].node2_name", - "$.node_coupling[2190].coupling_strength", - "$.node_coupling[2190].node1_file", - "$.node_coupling[2190].node1_name", - "$.node_coupling[2190].node2_file", - "$.node_coupling[2190].node2_name", - "$.node_coupling[2191].node1_name", - "$.node_coupling[2191].node2_name", - "$.node_coupling[2192].node1_name", - "$.node_coupling[2192].node2_name", - "$.node_coupling[2193].coupling_strength", - "$.node_coupling[2193].node1_name", - "$.node_coupling[2193].node2_file", - "$.node_coupling[2193].node2_name", - "$.node_coupling[2194].coupling_strength", - "$.node_coupling[2194].node2_file", - "$.node_coupling[2194].node2_name", - "$.node_coupling[2195].node2_name", - "$.node_coupling[2196].node2_name", - "$.node_coupling[2197].node2_name", - "$.node_coupling[2198].node2_name", - "$.node_coupling[2199].coupling_strength", - "$.node_coupling[2199].node2_file", - "$.node_coupling[2199].node2_name", - "$.node_coupling[219].coupling_strength", - "$.node_coupling[219].node1_name", - "$.node_coupling[219].node2_file", - "$.node_coupling[219].node2_name", - "$.node_coupling[21].node2_file", - "$.node_coupling[21].node2_name", - "$.node_coupling[2200].coupling_strength", - "$.node_coupling[2200].node2_file", - "$.node_coupling[2200].node2_name", - "$.node_coupling[2201].coupling_strength", - "$.node_coupling[2201].node1_file", - "$.node_coupling[2201].node1_name", - "$.node_coupling[2201].node2_file", - "$.node_coupling[2201].node2_name", - "$.node_coupling[2202].node1_name", - "$.node_coupling[2202].node2_name", - "$.node_coupling[2203].node1_name", - "$.node_coupling[2203].node2_name", - "$.node_coupling[2204].coupling_strength", - "$.node_coupling[2204].node1_name", - "$.node_coupling[2204].node2_file", - "$.node_coupling[2204].node2_name", - "$.node_coupling[2205].node1_name", - "$.node_coupling[2205].node2_file", - "$.node_coupling[2205].node2_name", - "$.node_coupling[2206].node1_name", - "$.node_coupling[2206].node2_file", - "$.node_coupling[2206].node2_name", - "$.node_coupling[2207].coupling_strength", - "$.node_coupling[2207].node1_name", - "$.node_coupling[2207].node2_file", - "$.node_coupling[2207].node2_name", - "$.node_coupling[2208].coupling_strength", - "$.node_coupling[2208].node1_name", - "$.node_coupling[2208].node2_file", - "$.node_coupling[2208].node2_name", - "$.node_coupling[2209].node1_name", - "$.node_coupling[2209].node2_name", - "$.node_coupling[220].coupling_strength", - "$.node_coupling[220].node1_name", - "$.node_coupling[220].node2_file", - "$.node_coupling[220].node2_name", - "$.node_coupling[2210].coupling_strength", - "$.node_coupling[2210].node1_name", - "$.node_coupling[2210].node2_file", - "$.node_coupling[2210].node2_name", - "$.node_coupling[2211].node1_name", - "$.node_coupling[2211].node2_name", - "$.node_coupling[2212].node1_name", - "$.node_coupling[2212].node2_name", - "$.node_coupling[2213].node1_name", - "$.node_coupling[2213].node2_name", - "$.node_coupling[2214].node1_name", - "$.node_coupling[2214].node2_name", - "$.node_coupling[2215].node1_name", - "$.node_coupling[2215].node2_name", - "$.node_coupling[2216].coupling_strength", - "$.node_coupling[2216].node1_name", - "$.node_coupling[2216].node2_file", - "$.node_coupling[2216].node2_name", - "$.node_coupling[2217].node1_name", - "$.node_coupling[2217].node2_name", - "$.node_coupling[2218].coupling_strength", - "$.node_coupling[2218].node1_name", - "$.node_coupling[2218].node2_file", - "$.node_coupling[2218].node2_name", - "$.node_coupling[2219].coupling_strength", - "$.node_coupling[2219].node1_name", - "$.node_coupling[2219].node2_file", - "$.node_coupling[2219].node2_name", - "$.node_coupling[221].coupling_strength", - "$.node_coupling[221].node1_name", - "$.node_coupling[221].node2_file", - "$.node_coupling[221].node2_name", - "$.node_coupling[2220].node1_name", - "$.node_coupling[2220].node2_name", - "$.node_coupling[2221].node1_name", - "$.node_coupling[2221].node2_name", - "$.node_coupling[2222].node2_name", - "$.node_coupling[2223].coupling_strength", - "$.node_coupling[2223].node2_file", - "$.node_coupling[2223].node2_name", - "$.node_coupling[2224].coupling_strength", - "$.node_coupling[2224].node2_file", - "$.node_coupling[2224].node2_name", - "$.node_coupling[2225].node2_name", - "$.node_coupling[2226].node2_name", - "$.node_coupling[2227].coupling_strength", - "$.node_coupling[2227].node1_file", - "$.node_coupling[2227].node1_name", - "$.node_coupling[2227].node2_file", - "$.node_coupling[2227].node2_name", - "$.node_coupling[2228].node1_file", - "$.node_coupling[2228].node1_name", - "$.node_coupling[2228].node2_name", - "$.node_coupling[2229].node1_file", - "$.node_coupling[2229].node1_name", - "$.node_coupling[2229].node2_file", - "$.node_coupling[2229].node2_name", - "$.node_coupling[222].coupling_strength", - "$.node_coupling[222].node1_name", - "$.node_coupling[222].node2_name", - "$.node_coupling[2230].node1_name", - "$.node_coupling[2230].node2_file", - "$.node_coupling[2230].node2_name", - "$.node_coupling[2231].node1_name", - "$.node_coupling[2231].node2_file", - "$.node_coupling[2231].node2_name", - "$.node_coupling[2232].node1_name", - "$.node_coupling[2232].node2_name", - "$.node_coupling[2233].node1_name", - "$.node_coupling[2233].node2_file", - "$.node_coupling[2233].node2_name", - "$.node_coupling[2234].node1_name", - "$.node_coupling[2234].node2_name", - "$.node_coupling[2235].coupling_strength", - "$.node_coupling[2235].node1_name", - "$.node_coupling[2235].node2_file", - "$.node_coupling[2235].node2_name", - "$.node_coupling[2236].coupling_strength", - "$.node_coupling[2236].node1_file", - "$.node_coupling[2236].node1_name", - "$.node_coupling[2236].node2_name", - "$.node_coupling[2237].coupling_strength", - "$.node_coupling[2237].node1_name", - "$.node_coupling[2237].node2_file", - "$.node_coupling[2237].node2_name", - "$.node_coupling[2238].coupling_strength", - "$.node_coupling[2238].node1_name", - "$.node_coupling[2238].node2_file", - "$.node_coupling[2238].node2_name", - "$.node_coupling[2239].coupling_strength", - "$.node_coupling[2239].node1_name", - "$.node_coupling[2239].node2_file", - "$.node_coupling[2239].node2_name", - "$.node_coupling[223].node2_file", - "$.node_coupling[223].node2_name", - "$.node_coupling[2240].coupling_strength", - "$.node_coupling[2240].node1_name", - "$.node_coupling[2240].node2_file", - "$.node_coupling[2240].node2_name", - "$.node_coupling[2241].node1_name", - "$.node_coupling[2241].node2_file", - "$.node_coupling[2241].node2_name", - "$.node_coupling[2242].coupling_strength", - "$.node_coupling[2242].node1_name", - "$.node_coupling[2242].node2_file", - "$.node_coupling[2242].node2_name", - "$.node_coupling[2243].node1_name", - "$.node_coupling[2243].node2_name", - "$.node_coupling[2244].node1_name", - "$.node_coupling[2244].node2_name", - "$.node_coupling[2245].node1_name", - "$.node_coupling[2245].node2_name", - "$.node_coupling[2246].node1_name", - "$.node_coupling[2246].node2_name", - "$.node_coupling[2247].coupling_strength", - "$.node_coupling[2247].node1_name", - "$.node_coupling[2247].node2_file", - "$.node_coupling[2247].node2_name", - "$.node_coupling[2248].node1_file", - "$.node_coupling[2248].node1_name", - "$.node_coupling[2248].node2_name", - "$.node_coupling[2249].node1_name", - "$.node_coupling[2249].node2_name", - "$.node_coupling[224].node2_file", - "$.node_coupling[224].node2_name", - "$.node_coupling[2250].node1_name", - "$.node_coupling[2250].node2_name", - "$.node_coupling[2251].node1_name", - "$.node_coupling[2251].node2_name", - "$.node_coupling[2252].coupling_strength", - "$.node_coupling[2252].node2_file", - "$.node_coupling[2252].node2_name", - "$.node_coupling[2253].node2_file", - "$.node_coupling[2253].node2_name", - "$.node_coupling[2254].coupling_strength", - "$.node_coupling[2254].node2_file", - "$.node_coupling[2254].node2_name", - "$.node_coupling[2255].coupling_strength", - "$.node_coupling[2255].node2_file", - "$.node_coupling[2255].node2_name", - "$.node_coupling[2256].coupling_strength", - "$.node_coupling[2256].node1_name", - "$.node_coupling[2256].node2_file", - "$.node_coupling[2256].node2_name", - "$.node_coupling[2257].node1_name", - "$.node_coupling[2257].node2_name", - "$.node_coupling[2258].node1_name", - "$.node_coupling[2258].node2_name", - "$.node_coupling[2259].coupling_strength", - "$.node_coupling[2259].node1_file", - "$.node_coupling[2259].node1_name", - "$.node_coupling[2259].node2_name", - "$.node_coupling[225].node1_file", - "$.node_coupling[225].node1_name", - "$.node_coupling[225].node2_file", - "$.node_coupling[225].node2_name", - "$.node_coupling[2260].coupling_strength", - "$.node_coupling[2260].node1_name", - "$.node_coupling[2260].node2_file", - "$.node_coupling[2260].node2_name", - "$.node_coupling[2261].node1_name", - "$.node_coupling[2261].node2_name", - "$.node_coupling[2262].coupling_strength", - "$.node_coupling[2262].node1_name", - "$.node_coupling[2262].node2_file", - "$.node_coupling[2262].node2_name", - "$.node_coupling[2263].coupling_strength", - "$.node_coupling[2263].node1_name", - "$.node_coupling[2263].node2_file", - "$.node_coupling[2263].node2_name", - "$.node_coupling[2264].node1_name", - "$.node_coupling[2264].node2_name", - "$.node_coupling[2265].node1_name", - "$.node_coupling[2265].node2_file", - "$.node_coupling[2265].node2_name", - "$.node_coupling[2266].coupling_strength", - "$.node_coupling[2266].node1_name", - "$.node_coupling[2266].node2_file", - "$.node_coupling[2266].node2_name", - "$.node_coupling[2267].node1_name", - "$.node_coupling[2267].node2_file", - "$.node_coupling[2267].node2_name", - "$.node_coupling[2268].coupling_strength", - "$.node_coupling[2268].node1_file", - "$.node_coupling[2268].node1_name", - "$.node_coupling[2268].node2_name", - "$.node_coupling[2269].node1_name", - "$.node_coupling[2269].node2_name", - "$.node_coupling[226].node1_file", - "$.node_coupling[226].node1_name", - "$.node_coupling[226].node2_name", - "$.node_coupling[2270].coupling_strength", - "$.node_coupling[2270].node1_name", - "$.node_coupling[2270].node2_file", - "$.node_coupling[2270].node2_name", - "$.node_coupling[2271].node1_name", - "$.node_coupling[2271].node2_file", - "$.node_coupling[2271].node2_name", - "$.node_coupling[2272].node1_name", - "$.node_coupling[2272].node2_name", - "$.node_coupling[2273].node1_name", - "$.node_coupling[2273].node2_name", - "$.node_coupling[2274].coupling_strength", - "$.node_coupling[2274].node1_name", - "$.node_coupling[2274].node2_file", - "$.node_coupling[2274].node2_name", - "$.node_coupling[2275].coupling_strength", - "$.node_coupling[2275].node1_name", - "$.node_coupling[2275].node2_file", - "$.node_coupling[2275].node2_name", - "$.node_coupling[2276].node1_name", - "$.node_coupling[2276].node2_name", - "$.node_coupling[2277].coupling_strength", - "$.node_coupling[2277].node2_file", - "$.node_coupling[2277].node2_name", - "$.node_coupling[2278].coupling_strength", - "$.node_coupling[2278].node2_name", - "$.node_coupling[2279].node2_file", - "$.node_coupling[2279].node2_name", - "$.node_coupling[227].coupling_strength", - "$.node_coupling[227].node2_file", - "$.node_coupling[227].node2_name", - "$.node_coupling[2280].coupling_strength", - "$.node_coupling[2280].node2_name", - "$.node_coupling[2281].node1_file", - "$.node_coupling[2281].node1_name", - "$.node_coupling[2281].node2_name", - "$.node_coupling[2282].coupling_strength", - "$.node_coupling[2282].node1_file", - "$.node_coupling[2282].node1_name", - "$.node_coupling[2282].node2_file", - "$.node_coupling[2282].node2_name", - "$.node_coupling[2283].node1_file", - "$.node_coupling[2283].node1_name", - "$.node_coupling[2283].node2_name", - "$.node_coupling[2284].coupling_strength", - "$.node_coupling[2284].node1_file", - "$.node_coupling[2284].node1_name", - "$.node_coupling[2284].node2_file", - "$.node_coupling[2284].node2_name", - "$.node_coupling[2285].node1_file", - "$.node_coupling[2285].node1_name", - "$.node_coupling[2285].node2_name", - "$.node_coupling[2286].node1_file", - "$.node_coupling[2286].node1_name", - "$.node_coupling[2286].node2_name", - "$.node_coupling[2287].co_changes", - "$.node_coupling[2287].coupling_strength", - "$.node_coupling[2287].node1_name", - "$.node_coupling[2287].node2_file", - "$.node_coupling[2287].node2_name", - "$.node_coupling[2288].co_changes", - "$.node_coupling[2288].coupling_strength", - "$.node_coupling[2288].node1_name", - "$.node_coupling[2288].node2_file", - "$.node_coupling[2288].node2_name", - "$.node_coupling[2289].coupling_strength", - "$.node_coupling[2289].node1_file", - "$.node_coupling[2289].node1_name", - "$.node_coupling[2289].node2_file", - "$.node_coupling[2289].node2_name", - "$.node_coupling[228].node2_file", - "$.node_coupling[228].node2_name", - "$.node_coupling[2290].coupling_strength", - "$.node_coupling[2290].node1_file", - "$.node_coupling[2290].node1_name", - "$.node_coupling[2290].node2_file", - "$.node_coupling[2290].node2_name", - "$.node_coupling[2291].coupling_strength", - "$.node_coupling[2291].node1_file", - "$.node_coupling[2291].node1_name", - "$.node_coupling[2291].node2_file", - "$.node_coupling[2291].node2_name", - "$.node_coupling[2292].node1_file", - "$.node_coupling[2292].node1_name", - "$.node_coupling[2292].node2_name", - "$.node_coupling[2293].coupling_strength", - "$.node_coupling[2293].node1_file", - "$.node_coupling[2293].node1_name", - "$.node_coupling[2293].node2_name", - "$.node_coupling[2294].coupling_strength", - "$.node_coupling[2294].node1_name", - "$.node_coupling[2295].coupling_strength", - "$.node_coupling[2295].node1_name", - "$.node_coupling[2296].coupling_strength", - "$.node_coupling[2296].node1_file", - "$.node_coupling[2296].node1_name", - "$.node_coupling[2296].node2_file", - "$.node_coupling[2296].node2_name", - "$.node_coupling[2297].coupling_strength", - "$.node_coupling[2297].node1_file", - "$.node_coupling[2297].node1_name", - "$.node_coupling[2297].node2_file", - "$.node_coupling[2297].node2_name", - "$.node_coupling[2298].coupling_strength", - "$.node_coupling[2298].node1_file", - "$.node_coupling[2298].node1_name", - "$.node_coupling[2298].node2_file", - "$.node_coupling[2298].node2_name", - "$.node_coupling[2299].coupling_strength", - "$.node_coupling[2299].node2_file", - "$.node_coupling[2299].node2_name", - "$.node_coupling[229].node1_file", - "$.node_coupling[229].node1_name", - "$.node_coupling[229].node2_file", - "$.node_coupling[229].node2_name", - "$.node_coupling[22].coupling_strength", - "$.node_coupling[22].node2_file", - "$.node_coupling[22].node2_name", - "$.node_coupling[2300].coupling_strength", - "$.node_coupling[2300].node2_file", - "$.node_coupling[2300].node2_name", - "$.node_coupling[2301].coupling_strength", - "$.node_coupling[2301].node2_file", - "$.node_coupling[2301].node2_name", - "$.node_coupling[2302].coupling_strength", - "$.node_coupling[2302].node2_file", - "$.node_coupling[2302].node2_name", - "$.node_coupling[2303].coupling_strength", - "$.node_coupling[2303].node2_name", - "$.node_coupling[2304].coupling_strength", - "$.node_coupling[2304].node2_file", - "$.node_coupling[2304].node2_name", - "$.node_coupling[2305].coupling_strength", - "$.node_coupling[2305].node1_file", - "$.node_coupling[2305].node1_name", - "$.node_coupling[2305].node2_file", - "$.node_coupling[2305].node2_name", - "$.node_coupling[2306].coupling_strength", - "$.node_coupling[2306].node2_file", - "$.node_coupling[2306].node2_name", - "$.node_coupling[2307].coupling_strength", - "$.node_coupling[2307].node2_name", - "$.node_coupling[2308].coupling_strength", - "$.node_coupling[2308].node2_file", - "$.node_coupling[2308].node2_name", - "$.node_coupling[2309].coupling_strength", - "$.node_coupling[2309].node2_file", - "$.node_coupling[2309].node2_name", - "$.node_coupling[230].coupling_strength", - "$.node_coupling[230].node1_file", - "$.node_coupling[230].node1_name", - "$.node_coupling[230].node2_file", - "$.node_coupling[230].node2_name", - "$.node_coupling[2310].coupling_strength", - "$.node_coupling[2310].node2_file", - "$.node_coupling[2310].node2_name", - "$.node_coupling[2311].coupling_strength", - "$.node_coupling[2311].node1_name", - "$.node_coupling[2311].node2_file", - "$.node_coupling[2311].node2_name", - "$.node_coupling[2312].coupling_strength", - "$.node_coupling[2312].node2_file", - "$.node_coupling[2312].node2_name", - "$.node_coupling[2313].coupling_strength", - "$.node_coupling[2313].node1_name", - "$.node_coupling[2313].node2_file", - "$.node_coupling[2313].node2_name", - "$.node_coupling[2314].coupling_strength", - "$.node_coupling[2314].node1_file", - "$.node_coupling[2314].node1_name", - "$.node_coupling[2314].node2_file", - "$.node_coupling[2314].node2_name", - "$.node_coupling[2315].coupling_strength", - "$.node_coupling[2315].node1_file", - "$.node_coupling[2315].node1_name", - "$.node_coupling[2315].node2_file", - "$.node_coupling[2315].node2_name", - "$.node_coupling[2316].coupling_strength", - "$.node_coupling[2316].node1_file", - "$.node_coupling[2316].node1_name", - "$.node_coupling[2316].node2_file", - "$.node_coupling[2316].node2_name", - "$.node_coupling[2317].coupling_strength", - "$.node_coupling[2317].node2_name", - "$.node_coupling[2318].coupling_strength", - "$.node_coupling[2318].node2_name", - "$.node_coupling[2319].coupling_strength", - "$.node_coupling[2319].node1_file", - "$.node_coupling[2319].node1_name", - "$.node_coupling[2319].node2_file", - "$.node_coupling[2319].node2_name", - "$.node_coupling[231].coupling_strength", - "$.node_coupling[231].node2_file", - "$.node_coupling[231].node2_name", - "$.node_coupling[2320].coupling_strength", - "$.node_coupling[2320].node2_file", - "$.node_coupling[2320].node2_name", - "$.node_coupling[2321].coupling_strength", - "$.node_coupling[2321].node2_name", - "$.node_coupling[2322].coupling_strength", - "$.node_coupling[2322].node2_file", - "$.node_coupling[2322].node2_name", - "$.node_coupling[2323].coupling_strength", - "$.node_coupling[2323].node2_file", - "$.node_coupling[2323].node2_name", - "$.node_coupling[2324].coupling_strength", - "$.node_coupling[2324].node2_name", - "$.node_coupling[2325].coupling_strength", - "$.node_coupling[2325].node1_file", - "$.node_coupling[2325].node1_name", - "$.node_coupling[2325].node2_file", - "$.node_coupling[2325].node2_name", - "$.node_coupling[2326].coupling_strength", - "$.node_coupling[2326].node2_file", - "$.node_coupling[2326].node2_name", - "$.node_coupling[2327].coupling_strength", - "$.node_coupling[2327].node1_file", - "$.node_coupling[2327].node1_name", - "$.node_coupling[2327].node2_file", - "$.node_coupling[2327].node2_name", - "$.node_coupling[2328].coupling_strength", - "$.node_coupling[2328].node1_name", - "$.node_coupling[2328].node2_file", - "$.node_coupling[2328].node2_name", - "$.node_coupling[2329].coupling_strength", - "$.node_coupling[2329].node1_file", - "$.node_coupling[2329].node1_name", - "$.node_coupling[2329].node2_file", - "$.node_coupling[2329].node2_name", - "$.node_coupling[232].coupling_strength", - "$.node_coupling[232].node2_file", - "$.node_coupling[232].node2_name", - "$.node_coupling[2330].coupling_strength", - "$.node_coupling[2330].node1_file", - "$.node_coupling[2330].node1_name", - "$.node_coupling[2330].node2_file", - "$.node_coupling[2330].node2_name", - "$.node_coupling[2331].coupling_strength", - "$.node_coupling[2331].node1_file", - "$.node_coupling[2331].node1_name", - "$.node_coupling[2331].node2_file", - "$.node_coupling[2331].node2_name", - "$.node_coupling[2332].coupling_strength", - "$.node_coupling[2332].node1_file", - "$.node_coupling[2332].node1_name", - "$.node_coupling[2332].node2_file", - "$.node_coupling[2332].node2_name", - "$.node_coupling[2333].coupling_strength", - "$.node_coupling[2333].node1_file", - "$.node_coupling[2333].node1_name", - "$.node_coupling[2333].node2_file", - "$.node_coupling[2333].node2_name", - "$.node_coupling[2334].coupling_strength", - "$.node_coupling[2334].node1_file", - "$.node_coupling[2334].node1_name", - "$.node_coupling[2334].node2_file", - "$.node_coupling[2334].node2_name", - "$.node_coupling[2335].coupling_strength", - "$.node_coupling[2335].node1_file", - "$.node_coupling[2335].node1_name", - "$.node_coupling[2335].node2_file", - "$.node_coupling[2335].node2_name", - "$.node_coupling[2336].coupling_strength", - "$.node_coupling[2336].node1_file", - "$.node_coupling[2336].node1_name", - "$.node_coupling[2337].coupling_strength", - "$.node_coupling[2337].node1_file", - "$.node_coupling[2337].node1_name", - "$.node_coupling[2337].node2_file", - "$.node_coupling[2337].node2_name", - "$.node_coupling[2338].coupling_strength", - "$.node_coupling[2338].node1_file", - "$.node_coupling[2338].node1_name", - "$.node_coupling[2338].node2_file", - "$.node_coupling[2338].node2_name", - "$.node_coupling[2339].coupling_strength", - "$.node_coupling[2339].node1_file", - "$.node_coupling[2339].node1_name", - "$.node_coupling[2339].node2_file", - "$.node_coupling[2339].node2_name", - "$.node_coupling[233].node2_file", - "$.node_coupling[233].node2_name", - "$.node_coupling[2340].coupling_strength", - "$.node_coupling[2340].node1_name", - "$.node_coupling[2340].node2_file", - "$.node_coupling[2340].node2_name", - "$.node_coupling[2341].coupling_strength", - "$.node_coupling[2341].node1_name", - "$.node_coupling[2341].node2_file", - "$.node_coupling[2341].node2_name", - "$.node_coupling[2342].coupling_strength", - "$.node_coupling[2342].node1_name", - "$.node_coupling[2342].node2_file", - "$.node_coupling[2342].node2_name", - "$.node_coupling[2343].coupling_strength", - "$.node_coupling[2343].node1_file", - "$.node_coupling[2343].node1_name", - "$.node_coupling[2343].node2_file", - "$.node_coupling[2343].node2_name", - "$.node_coupling[2344].coupling_strength", - "$.node_coupling[2344].node1_name", - "$.node_coupling[2344].node2_file", - "$.node_coupling[2344].node2_name", - "$.node_coupling[2345].coupling_strength", - "$.node_coupling[2345].node1_file", - "$.node_coupling[2345].node1_name", - "$.node_coupling[2345].node2_file", - "$.node_coupling[2345].node2_name", - "$.node_coupling[2346].coupling_strength", - "$.node_coupling[2346].node1_file", - "$.node_coupling[2346].node1_name", - "$.node_coupling[2346].node2_file", - "$.node_coupling[2346].node2_name", - "$.node_coupling[2347].coupling_strength", - "$.node_coupling[2347].node1_file", - "$.node_coupling[2347].node1_name", - "$.node_coupling[2347].node2_file", - "$.node_coupling[2347].node2_name", - "$.node_coupling[2348].coupling_strength", - "$.node_coupling[2348].node1_file", - "$.node_coupling[2348].node1_name", - "$.node_coupling[2348].node2_file", - "$.node_coupling[2348].node2_name", - "$.node_coupling[2349].coupling_strength", - "$.node_coupling[2349].node1_name", - "$.node_coupling[234].node2_file", - "$.node_coupling[234].node2_name", - "$.node_coupling[2350].coupling_strength", - "$.node_coupling[2350].node1_name", - "$.node_coupling[2350].node2_file", - "$.node_coupling[2350].node2_name", - "$.node_coupling[2351].coupling_strength", - "$.node_coupling[2351].node1_name", - "$.node_coupling[2351].node2_file", - "$.node_coupling[2351].node2_name", - "$.node_coupling[2352].coupling_strength", - "$.node_coupling[2352].node1_name", - "$.node_coupling[2353].coupling_strength", - "$.node_coupling[2353].node1_file", - "$.node_coupling[2353].node1_name", - "$.node_coupling[2353].node2_file", - "$.node_coupling[2353].node2_name", - "$.node_coupling[2354].coupling_strength", - "$.node_coupling[2354].node1_file", - "$.node_coupling[2354].node1_name", - "$.node_coupling[2354].node2_name", - "$.node_coupling[2355].coupling_strength", - "$.node_coupling[2355].node1_file", - "$.node_coupling[2355].node1_name", - "$.node_coupling[2355].node2_file", - "$.node_coupling[2355].node2_name", - "$.node_coupling[2356].coupling_strength", - "$.node_coupling[2356].node1_file", - "$.node_coupling[2356].node1_name", - "$.node_coupling[2356].node2_file", - "$.node_coupling[2356].node2_name", - "$.node_coupling[2357].coupling_strength", - "$.node_coupling[2357].node1_file", - "$.node_coupling[2357].node1_name", - "$.node_coupling[2358].coupling_strength", - "$.node_coupling[2358].node1_file", - "$.node_coupling[2358].node1_name", - "$.node_coupling[2358].node2_file", - "$.node_coupling[2358].node2_name", - "$.node_coupling[2359].coupling_strength", - "$.node_coupling[2359].node1_file", - "$.node_coupling[2359].node1_name", - "$.node_coupling[2359].node2_file", - "$.node_coupling[2359].node2_name", - "$.node_coupling[235].node2_file", - "$.node_coupling[235].node2_name", - "$.node_coupling[2360].coupling_strength", - "$.node_coupling[2360].node1_file", - "$.node_coupling[2360].node1_name", - "$.node_coupling[2360].node2_name", - "$.node_coupling[2361].coupling_strength", - "$.node_coupling[2361].node1_file", - "$.node_coupling[2361].node1_name", - "$.node_coupling[2361].node2_file", - "$.node_coupling[2361].node2_name", - "$.node_coupling[2362].coupling_strength", - "$.node_coupling[2362].node1_file", - "$.node_coupling[2362].node1_name", - "$.node_coupling[2362].node2_file", - "$.node_coupling[2362].node2_name", - "$.node_coupling[2363].coupling_strength", - "$.node_coupling[2363].node1_file", - "$.node_coupling[2363].node1_name", - "$.node_coupling[2363].node2_file", - "$.node_coupling[2363].node2_name", - "$.node_coupling[2364].coupling_strength", - "$.node_coupling[2364].node1_file", - "$.node_coupling[2364].node1_name", - "$.node_coupling[2364].node2_file", - "$.node_coupling[2364].node2_name", - "$.node_coupling[2365].coupling_strength", - "$.node_coupling[2365].node1_file", - "$.node_coupling[2365].node1_name", - "$.node_coupling[2365].node2_file", - "$.node_coupling[2365].node2_name", - "$.node_coupling[2366].coupling_strength", - "$.node_coupling[2366].node1_file", - "$.node_coupling[2366].node1_name", - "$.node_coupling[2366].node2_file", - "$.node_coupling[2366].node2_name", - "$.node_coupling[2367].coupling_strength", - "$.node_coupling[2367].node1_file", - "$.node_coupling[2367].node1_name", - "$.node_coupling[2367].node2_file", - "$.node_coupling[2367].node2_name", - "$.node_coupling[2368].coupling_strength", - "$.node_coupling[2368].node1_file", - "$.node_coupling[2368].node1_name", - "$.node_coupling[2368].node2_file", - "$.node_coupling[2368].node2_name", - "$.node_coupling[2369].coupling_strength", - "$.node_coupling[2369].node1_file", - "$.node_coupling[2369].node1_name", - "$.node_coupling[2369].node2_file", - "$.node_coupling[2369].node2_name", - "$.node_coupling[236].coupling_strength", - "$.node_coupling[236].node2_file", - "$.node_coupling[236].node2_name", - "$.node_coupling[2370].coupling_strength", - "$.node_coupling[2370].node1_file", - "$.node_coupling[2370].node1_name", - "$.node_coupling[2370].node2_file", - "$.node_coupling[2370].node2_name", - "$.node_coupling[2371].coupling_strength", - "$.node_coupling[2371].node1_file", - "$.node_coupling[2371].node1_name", - "$.node_coupling[2371].node2_file", - "$.node_coupling[2371].node2_name", - "$.node_coupling[2372].coupling_strength", - "$.node_coupling[2372].node1_file", - "$.node_coupling[2372].node1_name", - "$.node_coupling[2372].node2_file", - "$.node_coupling[2372].node2_name", - "$.node_coupling[2373].coupling_strength", - "$.node_coupling[2373].node1_file", - "$.node_coupling[2373].node1_name", - "$.node_coupling[2373].node2_file", - "$.node_coupling[2373].node2_name", - "$.node_coupling[2374].coupling_strength", - "$.node_coupling[2374].node1_file", - "$.node_coupling[2374].node1_name", - "$.node_coupling[2374].node2_file", - "$.node_coupling[2374].node2_name", - "$.node_coupling[2375].coupling_strength", - "$.node_coupling[2375].node1_file", - "$.node_coupling[2375].node1_name", - "$.node_coupling[2375].node2_file", - "$.node_coupling[2375].node2_name", - "$.node_coupling[2376].coupling_strength", - "$.node_coupling[2376].node1_file", - "$.node_coupling[2376].node1_name", - "$.node_coupling[2376].node2_file", - "$.node_coupling[2376].node2_name", - "$.node_coupling[2377].coupling_strength", - "$.node_coupling[2377].node1_file", - "$.node_coupling[2377].node1_name", - "$.node_coupling[2377].node2_file", - "$.node_coupling[2377].node2_name", - "$.node_coupling[2378].coupling_strength", - "$.node_coupling[2378].node1_file", - "$.node_coupling[2378].node1_name", - "$.node_coupling[2378].node2_file", - "$.node_coupling[2378].node2_name", - "$.node_coupling[2379].coupling_strength", - "$.node_coupling[2379].node1_file", - "$.node_coupling[2379].node1_name", - "$.node_coupling[2379].node2_file", - "$.node_coupling[2379].node2_name", - "$.node_coupling[237].node1_file", - "$.node_coupling[237].node1_name", - "$.node_coupling[237].node2_name", - "$.node_coupling[2380].node2_name", - "$.node_coupling[2381].node2_name", - "$.node_coupling[2382].node2_file", - "$.node_coupling[2382].node2_name", - "$.node_coupling[2383].node2_name", - "$.node_coupling[2384].coupling_strength", - "$.node_coupling[2384].node1_file", - "$.node_coupling[2384].node1_name", - "$.node_coupling[2384].node2_file", - "$.node_coupling[2384].node2_name", - "$.node_coupling[2385].node2_file", - "$.node_coupling[2385].node2_name", - "$.node_coupling[2386].node2_file", - "$.node_coupling[2386].node2_name", - "$.node_coupling[2387].coupling_strength", - "$.node_coupling[2387].node1_file", - "$.node_coupling[2387].node1_name", - "$.node_coupling[2387].node2_file", - "$.node_coupling[2387].node2_name", - "$.node_coupling[2388].coupling_strength", - "$.node_coupling[2388].node2_file", - "$.node_coupling[2388].node2_name", - "$.node_coupling[2389].coupling_strength", - "$.node_coupling[2389].node2_file", - "$.node_coupling[2389].node2_name", - "$.node_coupling[238].coupling_strength", - "$.node_coupling[238].node2_file", - "$.node_coupling[238].node2_name", - "$.node_coupling[2390].node2_name", - "$.node_coupling[2391].coupling_strength", - "$.node_coupling[2391].node1_file", - "$.node_coupling[2391].node1_name", - "$.node_coupling[2391].node2_file", - "$.node_coupling[2391].node2_name", - "$.node_coupling[2392].coupling_strength", - "$.node_coupling[2392].node2_file", - "$.node_coupling[2392].node2_name", - "$.node_coupling[2393].node2_name", - "$.node_coupling[2394].node2_file", - "$.node_coupling[2394].node2_name", - "$.node_coupling[2395].coupling_strength", - "$.node_coupling[2395].node2_file", - "$.node_coupling[2395].node2_name", - "$.node_coupling[2396].coupling_strength", - "$.node_coupling[2396].node1_file", - "$.node_coupling[2396].node1_name", - "$.node_coupling[2396].node2_file", - "$.node_coupling[2396].node2_name", - "$.node_coupling[2397].node2_file", - "$.node_coupling[2397].node2_name", - "$.node_coupling[2398].node2_file", - "$.node_coupling[2398].node2_name", - "$.node_coupling[2399].node2_name", - "$.node_coupling[239].node2_name", - "$.node_coupling[23].node2_name", - "$.node_coupling[2400].node2_name", - "$.node_coupling[2401].node2_file", - "$.node_coupling[2401].node2_name", - "$.node_coupling[2402].coupling_strength", - "$.node_coupling[2402].node2_file", - "$.node_coupling[2402].node2_name", - "$.node_coupling[2403].node2_file", - "$.node_coupling[2403].node2_name", - "$.node_coupling[2404].node2_name", - "$.node_coupling[2405].node2_file", - "$.node_coupling[2405].node2_name", - "$.node_coupling[2406].node2_file", - "$.node_coupling[2406].node2_name", - "$.node_coupling[2407].node2_name", - "$.node_coupling[2408].node2_file", - "$.node_coupling[2408].node2_name", - "$.node_coupling[2409].node2_file", - "$.node_coupling[2409].node2_name", - "$.node_coupling[240].node2_file", - "$.node_coupling[240].node2_name", - "$.node_coupling[2410].node2_name", - "$.node_coupling[2411].node2_name", - "$.node_coupling[2412].coupling_strength", - "$.node_coupling[2412].node1_file", - "$.node_coupling[2412].node1_name", - "$.node_coupling[2412].node2_file", - "$.node_coupling[2412].node2_name", - "$.node_coupling[2413].coupling_strength", - "$.node_coupling[2413].node1_file", - "$.node_coupling[2413].node1_name", - "$.node_coupling[2413].node2_file", - "$.node_coupling[2413].node2_name", - "$.node_coupling[2414].node2_name", - "$.node_coupling[2415].node2_file", - "$.node_coupling[2415].node2_name", - "$.node_coupling[2416].coupling_strength", - "$.node_coupling[2416].node1_file", - "$.node_coupling[2416].node1_name", - "$.node_coupling[2416].node2_file", - "$.node_coupling[2416].node2_name", - "$.node_coupling[2417].coupling_strength", - "$.node_coupling[2417].node1_file", - "$.node_coupling[2417].node1_name", - "$.node_coupling[2417].node2_file", - "$.node_coupling[2417].node2_name", - "$.node_coupling[2418].coupling_strength", - "$.node_coupling[2418].node1_file", - "$.node_coupling[2418].node1_name", - "$.node_coupling[2418].node2_name", - "$.node_coupling[2419].coupling_strength", - "$.node_coupling[2419].node1_file", - "$.node_coupling[2419].node2_file", - "$.node_coupling[2419].node2_name", - "$.node_coupling[241].node2_file", - "$.node_coupling[241].node2_name", - "$.node_coupling[2420].coupling_strength", - "$.node_coupling[2420].node1_file", - "$.node_coupling[2420].node1_name", - "$.node_coupling[2420].node2_file", - "$.node_coupling[2420].node2_name", - "$.node_coupling[2421].coupling_strength", - "$.node_coupling[2421].node1_file", - "$.node_coupling[2421].node1_name", - "$.node_coupling[2421].node2_file", - "$.node_coupling[2421].node2_name", - "$.node_coupling[2422].coupling_strength", - "$.node_coupling[2422].node1_file", - "$.node_coupling[2422].node1_name", - "$.node_coupling[2422].node2_file", - "$.node_coupling[2422].node2_name", - "$.node_coupling[2423].coupling_strength", - "$.node_coupling[2423].node1_file", - "$.node_coupling[2423].node1_name", - "$.node_coupling[2423].node2_name", - "$.node_coupling[2424].coupling_strength", - "$.node_coupling[2424].node1_file", - "$.node_coupling[2424].node1_name", - "$.node_coupling[2424].node2_file", - "$.node_coupling[2424].node2_name", - "$.node_coupling[2425].coupling_strength", - "$.node_coupling[2425].node1_file", - "$.node_coupling[2425].node1_name", - "$.node_coupling[2425].node2_file", - "$.node_coupling[2425].node2_name", - "$.node_coupling[2426].node1_file", - "$.node_coupling[2426].node1_name", - "$.node_coupling[2426].node2_file", - "$.node_coupling[2426].node2_name", - "$.node_coupling[2427].node1_file", - "$.node_coupling[2427].node1_name", - "$.node_coupling[2427].node2_file", - "$.node_coupling[2427].node2_name", - "$.node_coupling[2428].coupling_strength", - "$.node_coupling[2428].node1_file", - "$.node_coupling[2428].node1_name", - "$.node_coupling[2428].node2_file", - "$.node_coupling[2428].node2_name", - "$.node_coupling[2429].coupling_strength", - "$.node_coupling[2429].node1_file", - "$.node_coupling[2429].node1_name", - "$.node_coupling[2429].node2_file", - "$.node_coupling[2429].node2_name", - "$.node_coupling[242].coupling_strength", - "$.node_coupling[242].node2_file", - "$.node_coupling[242].node2_name", - "$.node_coupling[2430].coupling_strength", - "$.node_coupling[2430].node1_file", - "$.node_coupling[2430].node1_name", - "$.node_coupling[2430].node2_file", - "$.node_coupling[2430].node2_name", - "$.node_coupling[2431].coupling_strength", - "$.node_coupling[2431].node1_file", - "$.node_coupling[2431].node1_name", - "$.node_coupling[2431].node2_file", - "$.node_coupling[2431].node2_name", - "$.node_coupling[2432].coupling_strength", - "$.node_coupling[2432].node1_file", - "$.node_coupling[2432].node1_name", - "$.node_coupling[2432].node2_file", - "$.node_coupling[2432].node2_name", - "$.node_coupling[2433].coupling_strength", - "$.node_coupling[2433].node1_file", - "$.node_coupling[2433].node1_name", - "$.node_coupling[2433].node2_file", - "$.node_coupling[2433].node2_name", - "$.node_coupling[2434].coupling_strength", - "$.node_coupling[2434].node1_file", - "$.node_coupling[2434].node1_name", - "$.node_coupling[2434].node2_file", - "$.node_coupling[2434].node2_name", - "$.node_coupling[2435].coupling_strength", - "$.node_coupling[2435].node1_file", - "$.node_coupling[2435].node1_name", - "$.node_coupling[2435].node2_file", - "$.node_coupling[2435].node2_name", - "$.node_coupling[2436].node1_file", - "$.node_coupling[2436].node1_name", - "$.node_coupling[2436].node2_name", - "$.node_coupling[2437].coupling_strength", - "$.node_coupling[2437].node1_file", - "$.node_coupling[2437].node1_name", - "$.node_coupling[2437].node2_file", - "$.node_coupling[2437].node2_name", - "$.node_coupling[2438].coupling_strength", - "$.node_coupling[2438].node1_file", - "$.node_coupling[2438].node1_name", - "$.node_coupling[2438].node2_file", - "$.node_coupling[2438].node2_name", - "$.node_coupling[2439].coupling_strength", - "$.node_coupling[2439].node1_file", - "$.node_coupling[2439].node1_name", - "$.node_coupling[2439].node2_file", - "$.node_coupling[2439].node2_name", - "$.node_coupling[243].node2_file", - "$.node_coupling[243].node2_name", - "$.node_coupling[2440].coupling_strength", - "$.node_coupling[2440].node1_file", - "$.node_coupling[2440].node1_name", - "$.node_coupling[2440].node2_file", - "$.node_coupling[2440].node2_name", - "$.node_coupling[2441].coupling_strength", - "$.node_coupling[2441].node1_file", - "$.node_coupling[2441].node1_name", - "$.node_coupling[2441].node2_file", - "$.node_coupling[2441].node2_name", - "$.node_coupling[2442].coupling_strength", - "$.node_coupling[2442].node1_file", - "$.node_coupling[2442].node1_name", - "$.node_coupling[2442].node2_file", - "$.node_coupling[2442].node2_name", - "$.node_coupling[2443].coupling_strength", - "$.node_coupling[2443].node1_file", - "$.node_coupling[2443].node1_name", - "$.node_coupling[2443].node2_file", - "$.node_coupling[2443].node2_name", - "$.node_coupling[2444].coupling_strength", - "$.node_coupling[2444].node1_file", - "$.node_coupling[2444].node1_name", - "$.node_coupling[2444].node2_file", - "$.node_coupling[2444].node2_name", - "$.node_coupling[2445].coupling_strength", - "$.node_coupling[2445].node1_file", - "$.node_coupling[2445].node1_name", - "$.node_coupling[2445].node2_file", - "$.node_coupling[2445].node2_name", - "$.node_coupling[2446].coupling_strength", - "$.node_coupling[2446].node1_file", - "$.node_coupling[2446].node1_name", - "$.node_coupling[2446].node2_file", - "$.node_coupling[2446].node2_name", - "$.node_coupling[2447].coupling_strength", - "$.node_coupling[2447].node1_file", - "$.node_coupling[2447].node1_name", - "$.node_coupling[2447].node2_file", - "$.node_coupling[2447].node2_name", - "$.node_coupling[2448].coupling_strength", - "$.node_coupling[2448].node1_file", - "$.node_coupling[2448].node1_name", - "$.node_coupling[2449].coupling_strength", - "$.node_coupling[2449].node1_file", - "$.node_coupling[2449].node1_name", - "$.node_coupling[2449].node2_file", - "$.node_coupling[2449].node2_name", - "$.node_coupling[244].coupling_strength", - "$.node_coupling[244].node2_file", - "$.node_coupling[244].node2_name", - "$.node_coupling[2450].coupling_strength", - "$.node_coupling[2450].node1_file", - "$.node_coupling[2450].node1_name", - "$.node_coupling[2450].node2_file", - "$.node_coupling[2450].node2_name", - "$.node_coupling[2451].coupling_strength", - "$.node_coupling[2451].node1_file", - "$.node_coupling[2451].node1_name", - "$.node_coupling[2451].node2_file", - "$.node_coupling[2451].node2_name", - "$.node_coupling[2452].coupling_strength", - "$.node_coupling[2452].node1_file", - "$.node_coupling[2452].node1_name", - "$.node_coupling[2452].node2_file", - "$.node_coupling[2452].node2_name", - "$.node_coupling[2453].coupling_strength", - "$.node_coupling[2453].node1_file", - "$.node_coupling[2453].node1_name", - "$.node_coupling[2453].node2_file", - "$.node_coupling[2453].node2_name", - "$.node_coupling[2454].coupling_strength", - "$.node_coupling[2454].node1_name", - "$.node_coupling[2454].node2_file", - "$.node_coupling[2454].node2_name", - "$.node_coupling[2455].coupling_strength", - "$.node_coupling[2455].node1_file", - "$.node_coupling[2455].node1_name", - "$.node_coupling[2455].node2_file", - "$.node_coupling[2455].node2_name", - "$.node_coupling[2456].coupling_strength", - "$.node_coupling[2456].node1_file", - "$.node_coupling[2456].node1_name", - "$.node_coupling[2456].node2_file", - "$.node_coupling[2456].node2_name", - "$.node_coupling[2457].coupling_strength", - "$.node_coupling[2457].node1_file", - "$.node_coupling[2457].node1_name", - "$.node_coupling[2457].node2_file", - "$.node_coupling[2457].node2_name", - "$.node_coupling[2458].coupling_strength", - "$.node_coupling[2458].node1_file", - "$.node_coupling[2458].node1_name", - "$.node_coupling[2458].node2_file", - "$.node_coupling[2458].node2_name", - "$.node_coupling[2459].coupling_strength", - "$.node_coupling[2459].node1_file", - "$.node_coupling[2459].node1_name", - "$.node_coupling[2459].node2_file", - "$.node_coupling[2459].node2_name", - "$.node_coupling[245].node2_file", - "$.node_coupling[245].node2_name", - "$.node_coupling[2460].coupling_strength", - "$.node_coupling[2460].node1_file", - "$.node_coupling[2460].node1_name", - "$.node_coupling[2460].node2_file", - "$.node_coupling[2460].node2_name", - "$.node_coupling[2461].coupling_strength", - "$.node_coupling[2461].node1_file", - "$.node_coupling[2461].node1_name", - "$.node_coupling[2461].node2_name", - "$.node_coupling[2462].coupling_strength", - "$.node_coupling[2462].node1_file", - "$.node_coupling[2462].node1_name", - "$.node_coupling[2462].node2_file", - "$.node_coupling[2462].node2_name", - "$.node_coupling[2463].coupling_strength", - "$.node_coupling[2463].node1_file", - "$.node_coupling[2463].node1_name", - "$.node_coupling[2463].node2_file", - "$.node_coupling[2463].node2_name", - "$.node_coupling[2464].coupling_strength", - "$.node_coupling[2464].node1_file", - "$.node_coupling[2464].node1_name", - "$.node_coupling[2464].node2_file", - "$.node_coupling[2464].node2_name", - "$.node_coupling[2465].coupling_strength", - "$.node_coupling[2465].node1_file", - "$.node_coupling[2465].node1_name", - "$.node_coupling[2465].node2_file", - "$.node_coupling[2465].node2_name", - "$.node_coupling[2466].coupling_strength", - "$.node_coupling[2466].node1_file", - "$.node_coupling[2466].node1_name", - "$.node_coupling[2466].node2_file", - "$.node_coupling[2466].node2_name", - "$.node_coupling[2467].coupling_strength", - "$.node_coupling[2467].node1_file", - "$.node_coupling[2467].node1_name", - "$.node_coupling[2467].node2_file", - "$.node_coupling[2467].node2_name", - "$.node_coupling[2468].coupling_strength", - "$.node_coupling[2468].node1_file", - "$.node_coupling[2468].node1_name", - "$.node_coupling[2468].node2_file", - "$.node_coupling[2468].node2_name", - "$.node_coupling[2469].coupling_strength", - "$.node_coupling[2469].node1_file", - "$.node_coupling[2469].node1_name", - "$.node_coupling[2469].node2_file", - "$.node_coupling[2469].node2_name", - "$.node_coupling[246].node2_file", - "$.node_coupling[246].node2_name", - "$.node_coupling[2470].coupling_strength", - "$.node_coupling[2470].node1_file", - "$.node_coupling[2470].node1_name", - "$.node_coupling[2470].node2_file", - "$.node_coupling[2470].node2_name", - "$.node_coupling[2471].coupling_strength", - "$.node_coupling[2471].node1_file", - "$.node_coupling[2471].node1_name", - "$.node_coupling[2471].node2_file", - "$.node_coupling[2471].node2_name", - "$.node_coupling[2472].coupling_strength", - "$.node_coupling[2472].node1_file", - "$.node_coupling[2472].node1_name", - "$.node_coupling[2472].node2_file", - "$.node_coupling[2472].node2_name", - "$.node_coupling[2473].coupling_strength", - "$.node_coupling[2473].node1_file", - "$.node_coupling[2473].node1_name", - "$.node_coupling[2473].node2_file", - "$.node_coupling[2473].node2_name", - "$.node_coupling[2474].coupling_strength", - "$.node_coupling[2474].node1_file", - "$.node_coupling[2474].node1_name", - "$.node_coupling[2474].node2_file", - "$.node_coupling[2474].node2_name", - "$.node_coupling[2475].coupling_strength", - "$.node_coupling[2475].node1_file", - "$.node_coupling[2475].node1_name", - "$.node_coupling[2475].node2_file", - "$.node_coupling[2475].node2_name", - "$.node_coupling[2476].coupling_strength", - "$.node_coupling[2476].node1_file", - "$.node_coupling[2476].node1_name", - "$.node_coupling[2476].node2_file", - "$.node_coupling[2476].node2_name", - "$.node_coupling[2477].coupling_strength", - "$.node_coupling[2477].node1_file", - "$.node_coupling[2477].node1_name", - "$.node_coupling[2477].node2_file", - "$.node_coupling[2477].node2_name", - "$.node_coupling[2478].coupling_strength", - "$.node_coupling[2478].node1_file", - "$.node_coupling[2478].node1_name", - "$.node_coupling[2478].node2_name", - "$.node_coupling[2479].coupling_strength", - "$.node_coupling[2479].node1_file", - "$.node_coupling[2479].node1_name", - "$.node_coupling[2479].node2_name", - "$.node_coupling[247].node1_file", - "$.node_coupling[247].node1_name", - "$.node_coupling[247].node2_file", - "$.node_coupling[247].node2_name", - "$.node_coupling[2480].coupling_strength", - "$.node_coupling[2480].node1_name", - "$.node_coupling[2480].node2_file", - "$.node_coupling[2480].node2_name", - "$.node_coupling[2481].coupling_strength", - "$.node_coupling[2481].node1_file", - "$.node_coupling[2481].node1_name", - "$.node_coupling[2481].node2_file", - "$.node_coupling[2481].node2_name", - "$.node_coupling[2482].coupling_strength", - "$.node_coupling[2482].node1_file", - "$.node_coupling[2482].node1_name", - "$.node_coupling[2482].node2_file", - "$.node_coupling[2482].node2_name", - "$.node_coupling[2483].coupling_strength", - "$.node_coupling[2483].node1_file", - "$.node_coupling[2483].node1_name", - "$.node_coupling[2483].node2_file", - "$.node_coupling[2483].node2_name", - "$.node_coupling[2484].coupling_strength", - "$.node_coupling[2484].node1_file", - "$.node_coupling[2484].node1_name", - "$.node_coupling[2484].node2_file", - "$.node_coupling[2484].node2_name", - "$.node_coupling[2485].coupling_strength", - "$.node_coupling[2485].node1_file", - "$.node_coupling[2485].node1_name", - "$.node_coupling[2485].node2_file", - "$.node_coupling[2485].node2_name", - "$.node_coupling[2486].coupling_strength", - "$.node_coupling[2486].node1_file", - "$.node_coupling[2486].node1_name", - "$.node_coupling[2486].node2_file", - "$.node_coupling[2486].node2_name", - "$.node_coupling[2487].coupling_strength", - "$.node_coupling[2487].node1_file", - "$.node_coupling[2487].node1_name", - "$.node_coupling[2487].node2_name", - "$.node_coupling[2488].coupling_strength", - "$.node_coupling[2488].node1_file", - "$.node_coupling[2488].node1_name", - "$.node_coupling[2488].node2_file", - "$.node_coupling[2488].node2_name", - "$.node_coupling[2489].coupling_strength", - "$.node_coupling[2489].node1_file", - "$.node_coupling[2489].node1_name", - "$.node_coupling[2489].node2_file", - "$.node_coupling[2489].node2_name", - "$.node_coupling[248].node2_file", - "$.node_coupling[248].node2_name", - "$.node_coupling[2490].coupling_strength", - "$.node_coupling[2490].node1_file", - "$.node_coupling[2490].node1_name", - "$.node_coupling[2490].node2_file", - "$.node_coupling[2490].node2_name", - "$.node_coupling[2491].coupling_strength", - "$.node_coupling[2491].node1_file", - "$.node_coupling[2491].node1_name", - "$.node_coupling[2491].node2_name", - "$.node_coupling[2492].coupling_strength", - "$.node_coupling[2492].node1_file", - "$.node_coupling[2492].node1_name", - "$.node_coupling[2492].node2_file", - "$.node_coupling[2492].node2_name", - "$.node_coupling[2493].coupling_strength", - "$.node_coupling[2493].node1_file", - "$.node_coupling[2493].node1_name", - "$.node_coupling[2493].node2_name", - "$.node_coupling[2494].coupling_strength", - "$.node_coupling[2494].node1_file", - "$.node_coupling[2494].node1_name", - "$.node_coupling[2494].node2_file", - "$.node_coupling[2494].node2_name", - "$.node_coupling[2495].coupling_strength", - "$.node_coupling[2495].node1_file", - "$.node_coupling[2495].node1_name", - "$.node_coupling[2495].node2_name", - "$.node_coupling[2496].coupling_strength", - "$.node_coupling[2496].node1_file", - "$.node_coupling[2496].node1_name", - "$.node_coupling[2496].node2_file", - "$.node_coupling[2496].node2_name", - "$.node_coupling[2497].coupling_strength", - "$.node_coupling[2497].node1_file", - "$.node_coupling[2497].node1_name", - "$.node_coupling[2497].node2_name", - "$.node_coupling[2498].coupling_strength", - "$.node_coupling[2498].node1_file", - "$.node_coupling[2498].node1_name", - "$.node_coupling[2498].node2_file", - "$.node_coupling[2498].node2_name", - "$.node_coupling[2499].coupling_strength", - "$.node_coupling[2499].node1_file", - "$.node_coupling[2499].node1_name", - "$.node_coupling[2499].node2_file", - "$.node_coupling[2499].node2_name", - "$.node_coupling[249].node2_file", - "$.node_coupling[249].node2_name", - "$.node_coupling[24].node2_file", - "$.node_coupling[24].node2_name", - "$.node_coupling[2500].coupling_strength", - "$.node_coupling[2500].node1_file", - "$.node_coupling[2500].node1_name", - "$.node_coupling[2500].node2_file", - "$.node_coupling[2500].node2_name", - "$.node_coupling[2501].coupling_strength", - "$.node_coupling[2501].node1_file", - "$.node_coupling[2501].node1_name", - "$.node_coupling[2501].node2_file", - "$.node_coupling[2501].node2_name", - "$.node_coupling[2502].coupling_strength", - "$.node_coupling[2502].node1_file", - "$.node_coupling[2502].node1_name", - "$.node_coupling[2502].node2_file", - "$.node_coupling[2502].node2_name", - "$.node_coupling[2503].coupling_strength", - "$.node_coupling[2503].node1_file", - "$.node_coupling[2503].node1_name", - "$.node_coupling[2503].node2_file", - "$.node_coupling[2503].node2_name", - "$.node_coupling[2504].node2_file", - "$.node_coupling[2504].node2_name", - "$.node_coupling[2505].coupling_strength", - "$.node_coupling[2505].node2_file", - "$.node_coupling[2505].node2_name", - "$.node_coupling[2506].coupling_strength", - "$.node_coupling[2506].node2_file", - "$.node_coupling[2506].node2_name", - "$.node_coupling[2507].coupling_strength", - "$.node_coupling[2507].node2_file", - "$.node_coupling[2507].node2_name", - "$.node_coupling[2508].node2_file", - "$.node_coupling[2508].node2_name", - "$.node_coupling[2509].coupling_strength", - "$.node_coupling[2509].node2_file", - "$.node_coupling[2509].node2_name", - "$.node_coupling[250].node2_name", - "$.node_coupling[2510].node2_name", - "$.node_coupling[2511].coupling_strength", - "$.node_coupling[2511].node2_file", - "$.node_coupling[2511].node2_name", - "$.node_coupling[2512].node2_file", - "$.node_coupling[2512].node2_name", - "$.node_coupling[2513].node2_name", - "$.node_coupling[2514].coupling_strength", - "$.node_coupling[2514].node2_file", - "$.node_coupling[2514].node2_name", - "$.node_coupling[2515].node2_file", - "$.node_coupling[2515].node2_name", - "$.node_coupling[2516].node2_file", - "$.node_coupling[2516].node2_name", - "$.node_coupling[2517].node2_file", - "$.node_coupling[2517].node2_name", - "$.node_coupling[2518].node2_file", - "$.node_coupling[2518].node2_name", - "$.node_coupling[2519].coupling_strength", - "$.node_coupling[2519].node1_file", - "$.node_coupling[2519].node1_name", - "$.node_coupling[2519].node2_file", - "$.node_coupling[2519].node2_name", - "$.node_coupling[251].node1_file", - "$.node_coupling[251].node1_name", - "$.node_coupling[251].node2_name", - "$.node_coupling[2520].node2_name", - "$.node_coupling[2521].coupling_strength", - "$.node_coupling[2521].node2_file", - "$.node_coupling[2521].node2_name", - "$.node_coupling[2522].node2_name", - "$.node_coupling[2523].coupling_strength", - "$.node_coupling[2523].node2_file", - "$.node_coupling[2523].node2_name", - "$.node_coupling[2524].node2_file", - "$.node_coupling[2524].node2_name", - "$.node_coupling[2525].coupling_strength", - "$.node_coupling[2525].node2_file", - "$.node_coupling[2525].node2_name", - "$.node_coupling[2526].coupling_strength", - "$.node_coupling[2526].node1_name", - "$.node_coupling[2526].node2_file", - "$.node_coupling[2526].node2_name", - "$.node_coupling[2527].node1_name", - "$.node_coupling[2527].node2_file", - "$.node_coupling[2527].node2_name", - "$.node_coupling[2528].coupling_strength", - "$.node_coupling[2528].node1_name", - "$.node_coupling[2528].node2_file", - "$.node_coupling[2528].node2_name", - "$.node_coupling[2529].node1_name", - "$.node_coupling[2529].node2_file", - "$.node_coupling[2529].node2_name", - "$.node_coupling[252].node2_name", - "$.node_coupling[2530].coupling_strength", - "$.node_coupling[2530].node1_name", - "$.node_coupling[2530].node2_file", - "$.node_coupling[2530].node2_name", - "$.node_coupling[2531].coupling_strength", - "$.node_coupling[2531].node1_name", - "$.node_coupling[2531].node2_file", - "$.node_coupling[2531].node2_name", - "$.node_coupling[2532].node1_name", - "$.node_coupling[2532].node2_file", - "$.node_coupling[2532].node2_name", - "$.node_coupling[2533].coupling_strength", - "$.node_coupling[2533].node1_file", - "$.node_coupling[2533].node1_name", - "$.node_coupling[2533].node2_file", - "$.node_coupling[2533].node2_name", - "$.node_coupling[2534].coupling_strength", - "$.node_coupling[2534].node1_name", - "$.node_coupling[2534].node2_name", - "$.node_coupling[2535].coupling_strength", - "$.node_coupling[2535].node1_name", - "$.node_coupling[2535].node2_file", - "$.node_coupling[2535].node2_name", - "$.node_coupling[2536].coupling_strength", - "$.node_coupling[2536].node1_name", - "$.node_coupling[2536].node2_file", - "$.node_coupling[2536].node2_name", - "$.node_coupling[2537].coupling_strength", - "$.node_coupling[2537].node1_name", - "$.node_coupling[2537].node2_file", - "$.node_coupling[2537].node2_name", - "$.node_coupling[2538].coupling_strength", - "$.node_coupling[2538].node1_file", - "$.node_coupling[2538].node1_name", - "$.node_coupling[2538].node2_file", - "$.node_coupling[2538].node2_name", - "$.node_coupling[2539].coupling_strength", - "$.node_coupling[2539].node1_name", - "$.node_coupling[2539].node2_file", - "$.node_coupling[2539].node2_name", - "$.node_coupling[253].node2_file", - "$.node_coupling[253].node2_name", - "$.node_coupling[2540].coupling_strength", - "$.node_coupling[2540].node1_name", - "$.node_coupling[2540].node2_file", - "$.node_coupling[2540].node2_name", - "$.node_coupling[2541].node1_name", - "$.node_coupling[2541].node2_file", - "$.node_coupling[2541].node2_name", - "$.node_coupling[2542].coupling_strength", - "$.node_coupling[2542].node1_name", - "$.node_coupling[2542].node2_file", - "$.node_coupling[2542].node2_name", - "$.node_coupling[2543].coupling_strength", - "$.node_coupling[2543].node1_name", - "$.node_coupling[2543].node2_file", - "$.node_coupling[2543].node2_name", - "$.node_coupling[2544].coupling_strength", - "$.node_coupling[2544].node1_file", - "$.node_coupling[2544].node1_name", - "$.node_coupling[2544].node2_file", - "$.node_coupling[2544].node2_name", - "$.node_coupling[2545].coupling_strength", - "$.node_coupling[2545].node1_name", - "$.node_coupling[2545].node2_file", - "$.node_coupling[2545].node2_name", - "$.node_coupling[2546].coupling_strength", - "$.node_coupling[2546].node1_name", - "$.node_coupling[2546].node2_file", - "$.node_coupling[2546].node2_name", - "$.node_coupling[2547].coupling_strength", - "$.node_coupling[2547].node1_name", - "$.node_coupling[2547].node2_file", - "$.node_coupling[2547].node2_name", - "$.node_coupling[2548].coupling_strength", - "$.node_coupling[2548].node1_name", - "$.node_coupling[2548].node2_file", - "$.node_coupling[2548].node2_name", - "$.node_coupling[2549].coupling_strength", - "$.node_coupling[2549].node1_name", - "$.node_coupling[2549].node2_file", - "$.node_coupling[2549].node2_name", - "$.node_coupling[254].node2_file", - "$.node_coupling[254].node2_name", - "$.node_coupling[2550].coupling_strength", - "$.node_coupling[2550].node1_name", - "$.node_coupling[2550].node2_file", - "$.node_coupling[2550].node2_name", - "$.node_coupling[2551].node1_name", - "$.node_coupling[2551].node2_file", - "$.node_coupling[2551].node2_name", - "$.node_coupling[2552].coupling_strength", - "$.node_coupling[2552].node1_name", - "$.node_coupling[2552].node2_file", - "$.node_coupling[2552].node2_name", - "$.node_coupling[2553].coupling_strength", - "$.node_coupling[2553].node1_name", - "$.node_coupling[2553].node2_file", - "$.node_coupling[2553].node2_name", - "$.node_coupling[2554].coupling_strength", - "$.node_coupling[2554].node1_name", - "$.node_coupling[2554].node2_file", - "$.node_coupling[2554].node2_name", - "$.node_coupling[2555].coupling_strength", - "$.node_coupling[2555].node1_name", - "$.node_coupling[2555].node2_file", - "$.node_coupling[2555].node2_name", - "$.node_coupling[2556].coupling_strength", - "$.node_coupling[2556].node1_file", - "$.node_coupling[2556].node1_name", - "$.node_coupling[2556].node2_file", - "$.node_coupling[2556].node2_name", - "$.node_coupling[2557].coupling_strength", - "$.node_coupling[2557].node1_name", - "$.node_coupling[2557].node2_file", - "$.node_coupling[2557].node2_name", - "$.node_coupling[2558].coupling_strength", - "$.node_coupling[2558].node1_name", - "$.node_coupling[2558].node2_file", - "$.node_coupling[2558].node2_name", - "$.node_coupling[2559].coupling_strength", - "$.node_coupling[2559].node1_file", - "$.node_coupling[2559].node1_name", - "$.node_coupling[2559].node2_file", - "$.node_coupling[2559].node2_name", - "$.node_coupling[255].node2_file", - "$.node_coupling[255].node2_name", - "$.node_coupling[2560].coupling_strength", - "$.node_coupling[2560].node1_name", - "$.node_coupling[2560].node2_file", - "$.node_coupling[2560].node2_name", - "$.node_coupling[2561].coupling_strength", - "$.node_coupling[2561].node1_name", - "$.node_coupling[2561].node2_file", - "$.node_coupling[2561].node2_name", - "$.node_coupling[2562].coupling_strength", - "$.node_coupling[2562].node1_name", - "$.node_coupling[2562].node2_file", - "$.node_coupling[2562].node2_name", - "$.node_coupling[2563].coupling_strength", - "$.node_coupling[2563].node1_name", - "$.node_coupling[2563].node2_file", - "$.node_coupling[2563].node2_name", - "$.node_coupling[2564].coupling_strength", - "$.node_coupling[2564].node1_name", - "$.node_coupling[2564].node2_file", - "$.node_coupling[2564].node2_name", - "$.node_coupling[2565].coupling_strength", - "$.node_coupling[2565].node1_name", - "$.node_coupling[2565].node2_file", - "$.node_coupling[2565].node2_name", - "$.node_coupling[2566].node1_name", - "$.node_coupling[2567].coupling_strength", - "$.node_coupling[2567].node1_name", - "$.node_coupling[2567].node2_file", - "$.node_coupling[2567].node2_name", - "$.node_coupling[2568].coupling_strength", - "$.node_coupling[2568].node1_name", - "$.node_coupling[2568].node2_file", - "$.node_coupling[2568].node2_name", - "$.node_coupling[2569].coupling_strength", - "$.node_coupling[2569].node1_name", - "$.node_coupling[2569].node2_file", - "$.node_coupling[2569].node2_name", - "$.node_coupling[256].node2_file", - "$.node_coupling[256].node2_name", - "$.node_coupling[2570].coupling_strength", - "$.node_coupling[2570].node1_name", - "$.node_coupling[2570].node2_file", - "$.node_coupling[2570].node2_name", - "$.node_coupling[2571].coupling_strength", - "$.node_coupling[2571].node1_name", - "$.node_coupling[2571].node2_file", - "$.node_coupling[2571].node2_name", - "$.node_coupling[2572].coupling_strength", - "$.node_coupling[2572].node1_name", - "$.node_coupling[2572].node2_file", - "$.node_coupling[2572].node2_name", - "$.node_coupling[2573].coupling_strength", - "$.node_coupling[2573].node1_name", - "$.node_coupling[2573].node2_file", - "$.node_coupling[2573].node2_name", - "$.node_coupling[2574].coupling_strength", - "$.node_coupling[2574].node1_name", - "$.node_coupling[2574].node2_file", - "$.node_coupling[2574].node2_name", - "$.node_coupling[2575].coupling_strength", - "$.node_coupling[2575].node1_file", - "$.node_coupling[2575].node1_name", - "$.node_coupling[2575].node2_name", - "$.node_coupling[2576].coupling_strength", - "$.node_coupling[2576].node1_name", - "$.node_coupling[2576].node2_file", - "$.node_coupling[2576].node2_name", - "$.node_coupling[2577].coupling_strength", - "$.node_coupling[2577].node1_file", - "$.node_coupling[2577].node1_name", - "$.node_coupling[2577].node2_file", - "$.node_coupling[2577].node2_name", - "$.node_coupling[2578].coupling_strength", - "$.node_coupling[2578].node1_file", - "$.node_coupling[2578].node1_name", - "$.node_coupling[2578].node2_file", - "$.node_coupling[2578].node2_name", - "$.node_coupling[2579].coupling_strength", - "$.node_coupling[2579].node1_file", - "$.node_coupling[2579].node1_name", - "$.node_coupling[2579].node2_file", - "$.node_coupling[2579].node2_name", - "$.node_coupling[257].node2_file", - "$.node_coupling[257].node2_name", - "$.node_coupling[2580].coupling_strength", - "$.node_coupling[2580].node1_file", - "$.node_coupling[2580].node1_name", - "$.node_coupling[2580].node2_file", - "$.node_coupling[2580].node2_name", - "$.node_coupling[2581].coupling_strength", - "$.node_coupling[2581].node1_file", - "$.node_coupling[2581].node1_name", - "$.node_coupling[2581].node2_file", - "$.node_coupling[2581].node2_name", - "$.node_coupling[2582].coupling_strength", - "$.node_coupling[2582].node1_file", - "$.node_coupling[2582].node1_name", - "$.node_coupling[2582].node2_file", - "$.node_coupling[2582].node2_name", - "$.node_coupling[2583].coupling_strength", - "$.node_coupling[2583].node1_file", - "$.node_coupling[2583].node1_name", - "$.node_coupling[2583].node2_file", - "$.node_coupling[2583].node2_name", - "$.node_coupling[2584].coupling_strength", - "$.node_coupling[2584].node1_file", - "$.node_coupling[2584].node1_name", - "$.node_coupling[2584].node2_file", - "$.node_coupling[2584].node2_name", - "$.node_coupling[2585].co_changes", - "$.node_coupling[2585].coupling_strength", - "$.node_coupling[2585].node1_file", - "$.node_coupling[2585].node1_name", - "$.node_coupling[2585].node2_file", - "$.node_coupling[2585].node2_name", - "$.node_coupling[2586].co_changes", - "$.node_coupling[2586].coupling_strength", - "$.node_coupling[2586].node1_file", - "$.node_coupling[2586].node1_name", - "$.node_coupling[2586].node2_file", - "$.node_coupling[2586].node2_name", - "$.node_coupling[2587].co_changes", - "$.node_coupling[2587].coupling_strength", - "$.node_coupling[2587].node1_file", - "$.node_coupling[2587].node1_name", - "$.node_coupling[2587].node2_file", - "$.node_coupling[2587].node2_name", - "$.node_coupling[2588].co_changes", - "$.node_coupling[2588].coupling_strength", - "$.node_coupling[2588].node1_file", - "$.node_coupling[2588].node1_name", - "$.node_coupling[2588].node2_file", - "$.node_coupling[2588].node2_name", - "$.node_coupling[2589].co_changes", - "$.node_coupling[2589].coupling_strength", - "$.node_coupling[2589].node1_file", - "$.node_coupling[2589].node1_name", - "$.node_coupling[2589].node2_file", - "$.node_coupling[2589].node2_name", - "$.node_coupling[258].node1_file", - "$.node_coupling[258].node1_name", - "$.node_coupling[258].node2_name", - "$.node_coupling[2590].co_changes", - "$.node_coupling[2590].coupling_strength", - "$.node_coupling[2590].node1_file", - "$.node_coupling[2590].node1_name", - "$.node_coupling[2590].node2_file", - "$.node_coupling[2590].node2_name", - "$.node_coupling[2591].co_changes", - "$.node_coupling[2591].coupling_strength", - "$.node_coupling[2591].node1_file", - "$.node_coupling[2591].node1_name", - "$.node_coupling[2591].node2_file", - "$.node_coupling[2591].node2_name", - "$.node_coupling[2592].co_changes", - "$.node_coupling[2592].coupling_strength", - "$.node_coupling[2592].node1_file", - "$.node_coupling[2592].node1_name", - "$.node_coupling[2592].node2_file", - "$.node_coupling[2592].node2_name", - "$.node_coupling[2593].co_changes", - "$.node_coupling[2593].coupling_strength", - "$.node_coupling[2593].node1_file", - "$.node_coupling[2593].node1_name", - "$.node_coupling[2593].node2_file", - "$.node_coupling[2593].node2_name", - "$.node_coupling[2594].co_changes", - "$.node_coupling[2594].coupling_strength", - "$.node_coupling[2594].node1_file", - "$.node_coupling[2594].node1_name", - "$.node_coupling[2594].node2_file", - "$.node_coupling[2594].node2_name", - "$.node_coupling[2595].co_changes", - "$.node_coupling[2595].coupling_strength", - "$.node_coupling[2595].node1_file", - "$.node_coupling[2595].node1_name", - "$.node_coupling[2595].node2_file", - "$.node_coupling[2595].node2_name", - "$.node_coupling[2596].co_changes", - "$.node_coupling[2596].coupling_strength", - "$.node_coupling[2596].node1_file", - "$.node_coupling[2596].node1_name", - "$.node_coupling[2596].node2_file", - "$.node_coupling[2596].node2_name", - "$.node_coupling[2597].co_changes", - "$.node_coupling[2597].coupling_strength", - "$.node_coupling[2597].node1_file", - "$.node_coupling[2597].node1_name", - "$.node_coupling[2597].node2_file", - "$.node_coupling[2597].node2_name", - "$.node_coupling[2598].co_changes", - "$.node_coupling[2598].coupling_strength", - "$.node_coupling[2598].node1_file", - "$.node_coupling[2598].node1_name", - "$.node_coupling[2598].node2_file", - "$.node_coupling[2598].node2_name", - "$.node_coupling[2599].co_changes", - "$.node_coupling[2599].coupling_strength", - "$.node_coupling[2599].node1_file", - "$.node_coupling[2599].node1_name", - "$.node_coupling[2599].node2_file", - "$.node_coupling[2599].node2_name", - "$.node_coupling[259].coupling_strength", - "$.node_coupling[259].node2_file", - "$.node_coupling[259].node2_name", - "$.node_coupling[25].coupling_strength", - "$.node_coupling[25].node2_file", - "$.node_coupling[25].node2_name", - "$.node_coupling[2600].co_changes", - "$.node_coupling[2600].coupling_strength", - "$.node_coupling[2600].node1_file", - "$.node_coupling[2600].node1_name", - "$.node_coupling[2600].node2_file", - "$.node_coupling[2600].node2_name", - "$.node_coupling[2601].co_changes", - "$.node_coupling[2601].coupling_strength", - "$.node_coupling[2601].node1_file", - "$.node_coupling[2601].node1_name", - "$.node_coupling[2601].node2_file", - "$.node_coupling[2601].node2_name", - "$.node_coupling[2602].co_changes", - "$.node_coupling[2602].coupling_strength", - "$.node_coupling[2602].node1_file", - "$.node_coupling[2602].node1_name", - "$.node_coupling[2602].node2_file", - "$.node_coupling[2602].node2_name", - "$.node_coupling[2603].co_changes", - "$.node_coupling[2603].coupling_strength", - "$.node_coupling[2603].node1_file", - "$.node_coupling[2603].node1_name", - "$.node_coupling[2603].node2_file", - "$.node_coupling[2603].node2_name", - "$.node_coupling[2604].co_changes", - "$.node_coupling[2604].coupling_strength", - "$.node_coupling[2604].node1_file", - "$.node_coupling[2604].node1_name", - "$.node_coupling[2604].node2_file", - "$.node_coupling[2604].node2_name", - "$.node_coupling[2605].co_changes", - "$.node_coupling[2605].coupling_strength", - "$.node_coupling[2605].node1_file", - "$.node_coupling[2605].node1_name", - "$.node_coupling[2605].node2_file", - "$.node_coupling[2605].node2_name", - "$.node_coupling[2606].co_changes", - "$.node_coupling[2606].coupling_strength", - "$.node_coupling[2606].node1_file", - "$.node_coupling[2606].node1_name", - "$.node_coupling[2606].node2_file", - "$.node_coupling[2606].node2_name", - "$.node_coupling[2607].co_changes", - "$.node_coupling[2607].coupling_strength", - "$.node_coupling[2607].node1_file", - "$.node_coupling[2607].node1_name", - "$.node_coupling[2607].node2_file", - "$.node_coupling[2607].node2_name", - "$.node_coupling[2608].co_changes", - "$.node_coupling[2608].coupling_strength", - "$.node_coupling[2608].node1_file", - "$.node_coupling[2608].node1_name", - "$.node_coupling[2608].node2_file", - "$.node_coupling[2608].node2_name", - "$.node_coupling[2609].co_changes", - "$.node_coupling[2609].coupling_strength", - "$.node_coupling[2609].node1_file", - "$.node_coupling[2609].node1_name", - "$.node_coupling[2609].node2_file", - "$.node_coupling[2609].node2_name", - "$.node_coupling[260].coupling_strength", - "$.node_coupling[260].node2_file", - "$.node_coupling[260].node2_name", - "$.node_coupling[2610].co_changes", - "$.node_coupling[2610].coupling_strength", - "$.node_coupling[2610].node1_file", - "$.node_coupling[2610].node1_name", - "$.node_coupling[2610].node2_file", - "$.node_coupling[2610].node2_name", - "$.node_coupling[2611].co_changes", - "$.node_coupling[2611].coupling_strength", - "$.node_coupling[2611].node1_file", - "$.node_coupling[2611].node1_name", - "$.node_coupling[2611].node2_file", - "$.node_coupling[2611].node2_name", - "$.node_coupling[261].node2_file", - "$.node_coupling[261].node2_name", - "$.node_coupling[262].coupling_strength", - "$.node_coupling[262].node2_file", - "$.node_coupling[262].node2_name", - "$.node_coupling[263].coupling_strength", - "$.node_coupling[263].node2_file", - "$.node_coupling[263].node2_name", - "$.node_coupling[264].node1_file", - "$.node_coupling[264].node1_name", - "$.node_coupling[264].node2_file", - "$.node_coupling[264].node2_name", - "$.node_coupling[265].node2_name", - "$.node_coupling[266].node2_file", - "$.node_coupling[266].node2_name", - "$.node_coupling[267].coupling_strength", - "$.node_coupling[267].node2_file", - "$.node_coupling[267].node2_name", - "$.node_coupling[268].coupling_strength", - "$.node_coupling[268].node2_file", - "$.node_coupling[268].node2_name", - "$.node_coupling[269].coupling_strength", - "$.node_coupling[269].node2_file", - "$.node_coupling[269].node2_name", - "$.node_coupling[26].node2_name", - "$.node_coupling[270].node2_file", - "$.node_coupling[270].node2_name", - "$.node_coupling[271].node2_file", - "$.node_coupling[271].node2_name", - "$.node_coupling[272].node2_file", - "$.node_coupling[272].node2_name", - "$.node_coupling[273].coupling_strength", - "$.node_coupling[273].node2_file", - "$.node_coupling[273].node2_name", - "$.node_coupling[274].coupling_strength", - "$.node_coupling[274].node2_file", - "$.node_coupling[274].node2_name", - "$.node_coupling[275].coupling_strength", - "$.node_coupling[275].node2_file", - "$.node_coupling[275].node2_name", - "$.node_coupling[276].coupling_strength", - "$.node_coupling[276].node2_file", - "$.node_coupling[276].node2_name", - "$.node_coupling[277].coupling_strength", - "$.node_coupling[277].node2_file", - "$.node_coupling[277].node2_name", - "$.node_coupling[278].node2_name", - "$.node_coupling[279].node2_file", - "$.node_coupling[279].node2_name", - "$.node_coupling[27].node1_file", - "$.node_coupling[27].node1_name", - "$.node_coupling[27].node2_name", - "$.node_coupling[280].node2_name", - "$.node_coupling[281].node2_file", - "$.node_coupling[281].node2_name", - "$.node_coupling[282].node2_file", - "$.node_coupling[282].node2_name", - "$.node_coupling[283].node1_file", - "$.node_coupling[283].node1_name", - "$.node_coupling[283].node2_file", - "$.node_coupling[283].node2_name", - "$.node_coupling[284].coupling_strength", - "$.node_coupling[284].node1_name", - "$.node_coupling[284].node2_file", - "$.node_coupling[284].node2_name", - "$.node_coupling[285].coupling_strength", - "$.node_coupling[285].node1_name", - "$.node_coupling[285].node2_file", - "$.node_coupling[285].node2_name", - "$.node_coupling[286].coupling_strength", - "$.node_coupling[286].node1_name", - "$.node_coupling[286].node2_file", - "$.node_coupling[286].node2_name", - "$.node_coupling[287].node1_name", - "$.node_coupling[287].node2_file", - "$.node_coupling[287].node2_name", - "$.node_coupling[288].node1_name", - "$.node_coupling[288].node2_name", - "$.node_coupling[289].coupling_strength", - "$.node_coupling[289].node1_name", - "$.node_coupling[289].node2_file", - "$.node_coupling[289].node2_name", - "$.node_coupling[28].node2_name", - "$.node_coupling[290].node1_name", - "$.node_coupling[290].node2_name", - "$.node_coupling[291].coupling_strength", - "$.node_coupling[291].node1_file", - "$.node_coupling[291].node1_name", - "$.node_coupling[291].node2_file", - "$.node_coupling[291].node2_name", - "$.node_coupling[292].coupling_strength", - "$.node_coupling[292].node2_file", - "$.node_coupling[292].node2_name", - "$.node_coupling[293].node2_file", - "$.node_coupling[293].node2_name", - "$.node_coupling[294].node2_file", - "$.node_coupling[294].node2_name", - "$.node_coupling[295].node2_file", - "$.node_coupling[295].node2_name", - "$.node_coupling[296].node2_file", - "$.node_coupling[296].node2_name", - "$.node_coupling[297].coupling_strength", - "$.node_coupling[297].node1_file", - "$.node_coupling[297].node1_name", - "$.node_coupling[297].node2_file", - "$.node_coupling[297].node2_name", - "$.node_coupling[298].node2_file", - "$.node_coupling[298].node2_name", - "$.node_coupling[299].node2_name", - "$.node_coupling[29].node2_file", - "$.node_coupling[29].node2_name", - "$.node_coupling[2].co_changes", - "$.node_coupling[2].coupling_strength", - "$.node_coupling[2].node1_file", - "$.node_coupling[2].node1_name", - "$.node_coupling[2].node2_name", - "$.node_coupling[300].node2_name", - "$.node_coupling[301].node2_name", - "$.node_coupling[302].coupling_strength", - "$.node_coupling[302].node1_file", - "$.node_coupling[302].node1_name", - "$.node_coupling[302].node2_file", - "$.node_coupling[302].node2_name", - "$.node_coupling[303].node2_file", - "$.node_coupling[303].node2_name", - "$.node_coupling[304].node2_file", - "$.node_coupling[304].node2_name", - "$.node_coupling[305].coupling_strength", - "$.node_coupling[305].node2_file", - "$.node_coupling[305].node2_name", - "$.node_coupling[306].node2_file", - "$.node_coupling[306].node2_name", - "$.node_coupling[307].node1_file", - "$.node_coupling[307].node1_name", - "$.node_coupling[307].node2_name", - "$.node_coupling[308].node2_file", - "$.node_coupling[308].node2_name", - "$.node_coupling[309].node2_file", - "$.node_coupling[309].node2_name", - "$.node_coupling[30].node2_name", - "$.node_coupling[310].node1_file", - "$.node_coupling[310].node1_name", - "$.node_coupling[310].node2_file", - "$.node_coupling[310].node2_name", - "$.node_coupling[311].node2_file", - "$.node_coupling[311].node2_name", - "$.node_coupling[312].node2_name", - "$.node_coupling[313].coupling_strength", - "$.node_coupling[313].node2_file", - "$.node_coupling[313].node2_name", - "$.node_coupling[314].node2_file", - "$.node_coupling[314].node2_name", - "$.node_coupling[315].coupling_strength", - "$.node_coupling[315].node2_file", - "$.node_coupling[315].node2_name", - "$.node_coupling[316].coupling_strength", - "$.node_coupling[316].node2_file", - "$.node_coupling[316].node2_name", - "$.node_coupling[317].node2_file", - "$.node_coupling[317].node2_name", - "$.node_coupling[318].node1_file", - "$.node_coupling[318].node1_name", - "$.node_coupling[318].node2_name", - "$.node_coupling[319].coupling_strength", - "$.node_coupling[319].node1_file", - "$.node_coupling[319].node1_name", - "$.node_coupling[319].node2_file", - "$.node_coupling[319].node2_name", - "$.node_coupling[31].node2_file", - "$.node_coupling[31].node2_name", - "$.node_coupling[320].coupling_strength", - "$.node_coupling[320].node2_file", - "$.node_coupling[320].node2_name", - "$.node_coupling[321].node2_name", - "$.node_coupling[322].node2_file", - "$.node_coupling[322].node2_name", - "$.node_coupling[323].node1_file", - "$.node_coupling[323].node1_name", - "$.node_coupling[323].node2_name", - "$.node_coupling[324].node2_file", - "$.node_coupling[324].node2_name", - "$.node_coupling[325].node2_file", - "$.node_coupling[325].node2_name", - "$.node_coupling[326].node1_file", - "$.node_coupling[326].node1_name", - "$.node_coupling[326].node2_name", - "$.node_coupling[327].node2_name", - "$.node_coupling[328].node2_file", - "$.node_coupling[328].node2_name", - "$.node_coupling[329].node2_file", - "$.node_coupling[329].node2_name", - "$.node_coupling[32].node2_file", - "$.node_coupling[32].node2_name", - "$.node_coupling[330].node2_file", - "$.node_coupling[330].node2_name", - "$.node_coupling[331].node2_file", - "$.node_coupling[331].node2_name", - "$.node_coupling[332].node2_name", - "$.node_coupling[333].node2_file", - "$.node_coupling[333].node2_name", - "$.node_coupling[334].coupling_strength", - "$.node_coupling[334].node2_file", - "$.node_coupling[334].node2_name", - "$.node_coupling[335].node2_name", - "$.node_coupling[336].coupling_strength", - "$.node_coupling[336].node2_file", - "$.node_coupling[336].node2_name", - "$.node_coupling[337].node2_name", - "$.node_coupling[338].coupling_strength", - "$.node_coupling[338].node2_file", - "$.node_coupling[338].node2_name", - "$.node_coupling[339].node2_file", - "$.node_coupling[339].node2_name", - "$.node_coupling[33].coupling_strength", - "$.node_coupling[33].node2_file", - "$.node_coupling[33].node2_name", - "$.node_coupling[340].node2_file", - "$.node_coupling[340].node2_name", - "$.node_coupling[341].coupling_strength", - "$.node_coupling[341].node2_file", - "$.node_coupling[341].node2_name", - "$.node_coupling[342].node2_name", - "$.node_coupling[343].coupling_strength", - "$.node_coupling[343].node2_file", - "$.node_coupling[343].node2_name", - "$.node_coupling[344].node2_file", - "$.node_coupling[344].node2_name", - "$.node_coupling[345].coupling_strength", - "$.node_coupling[345].node2_file", - "$.node_coupling[345].node2_name", - "$.node_coupling[346].coupling_strength", - "$.node_coupling[346].node2_file", - "$.node_coupling[346].node2_name", - "$.node_coupling[347].coupling_strength", - "$.node_coupling[347].node2_file", - "$.node_coupling[347].node2_name", - "$.node_coupling[348].node2_name", - "$.node_coupling[349].coupling_strength", - "$.node_coupling[349].node2_name", - "$.node_coupling[34].node2_file", - "$.node_coupling[34].node2_name", - "$.node_coupling[350].node2_file", - "$.node_coupling[350].node2_name", - "$.node_coupling[351].coupling_strength", - "$.node_coupling[351].node1_file", - "$.node_coupling[351].node1_name", - "$.node_coupling[351].node2_file", - "$.node_coupling[351].node2_name", - "$.node_coupling[352].coupling_strength", - "$.node_coupling[352].node1_name", - "$.node_coupling[352].node2_file", - "$.node_coupling[352].node2_name", - "$.node_coupling[353].coupling_strength", - "$.node_coupling[353].node1_name", - "$.node_coupling[353].node2_file", - "$.node_coupling[353].node2_name", - "$.node_coupling[354].coupling_strength", - "$.node_coupling[354].node1_file", - "$.node_coupling[354].node1_name", - "$.node_coupling[354].node2_file", - "$.node_coupling[354].node2_name", - "$.node_coupling[355].coupling_strength", - "$.node_coupling[355].node1_name", - "$.node_coupling[355].node2_file", - "$.node_coupling[355].node2_name", - "$.node_coupling[356].coupling_strength", - "$.node_coupling[356].node1_name", - "$.node_coupling[356].node2_file", - "$.node_coupling[356].node2_name", - "$.node_coupling[357].coupling_strength", - "$.node_coupling[357].node1_name", - "$.node_coupling[357].node2_file", - "$.node_coupling[357].node2_name", - "$.node_coupling[358].node1_file", - "$.node_coupling[358].node1_name", - "$.node_coupling[358].node2_name", - "$.node_coupling[359].coupling_strength", - "$.node_coupling[359].node2_file", - "$.node_coupling[359].node2_name", - "$.node_coupling[35].node2_name", - "$.node_coupling[360].coupling_strength", - "$.node_coupling[360].node2_file", - "$.node_coupling[360].node2_name", - "$.node_coupling[361].coupling_strength", - "$.node_coupling[361].node2_name", - "$.node_coupling[362].coupling_strength", - "$.node_coupling[362].node2_file", - "$.node_coupling[362].node2_name", - "$.node_coupling[363].coupling_strength", - "$.node_coupling[363].node2_file", - "$.node_coupling[363].node2_name", - "$.node_coupling[364].coupling_strength", - "$.node_coupling[364].node2_file", - "$.node_coupling[364].node2_name", - "$.node_coupling[365].coupling_strength", - "$.node_coupling[365].node2_name", - "$.node_coupling[366].coupling_strength", - "$.node_coupling[366].node2_file", - "$.node_coupling[366].node2_name", - "$.node_coupling[367].node1_file", - "$.node_coupling[367].node1_name", - "$.node_coupling[367].node2_name", - "$.node_coupling[368].coupling_strength", - "$.node_coupling[368].node1_file", - "$.node_coupling[368].node1_name", - "$.node_coupling[368].node2_file", - "$.node_coupling[368].node2_name", - "$.node_coupling[369].coupling_strength", - "$.node_coupling[369].node2_file", - "$.node_coupling[369].node2_name", - "$.node_coupling[36].node2_name", - "$.node_coupling[370].coupling_strength", - "$.node_coupling[370].node2_file", - "$.node_coupling[370].node2_name", - "$.node_coupling[371].coupling_strength", - "$.node_coupling[371].node2_file", - "$.node_coupling[371].node2_name", - "$.node_coupling[372].coupling_strength", - "$.node_coupling[372].node2_file", - "$.node_coupling[372].node2_name", - "$.node_coupling[373].coupling_strength", - "$.node_coupling[373].node2_file", - "$.node_coupling[373].node2_name", - "$.node_coupling[374].coupling_strength", - "$.node_coupling[374].node2_name", - "$.node_coupling[375].coupling_strength", - "$.node_coupling[375].node2_file", - "$.node_coupling[375].node2_name", - "$.node_coupling[376].coupling_strength", - "$.node_coupling[376].node1_file", - "$.node_coupling[376].node1_name", - "$.node_coupling[376].node2_file", - "$.node_coupling[376].node2_name", - "$.node_coupling[377].coupling_strength", - "$.node_coupling[377].node2_file", - "$.node_coupling[377].node2_name", - "$.node_coupling[378].coupling_strength", - "$.node_coupling[378].node2_file", - "$.node_coupling[378].node2_name", - "$.node_coupling[379].coupling_strength", - "$.node_coupling[379].node2_name", - "$.node_coupling[37].node2_file", - "$.node_coupling[37].node2_name", - "$.node_coupling[380].coupling_strength", - "$.node_coupling[380].node2_file", - "$.node_coupling[380].node2_name", - "$.node_coupling[381].coupling_strength", - "$.node_coupling[381].node2_file", - "$.node_coupling[381].node2_name", - "$.node_coupling[382].coupling_strength", - "$.node_coupling[382].node2_file", - "$.node_coupling[382].node2_name", - "$.node_coupling[383].coupling_strength", - "$.node_coupling[383].node2_file", - "$.node_coupling[383].node2_name", - "$.node_coupling[384].coupling_strength", - "$.node_coupling[384].node2_file", - "$.node_coupling[384].node2_name", - "$.node_coupling[385].coupling_strength", - "$.node_coupling[385].node2_name", - "$.node_coupling[386].coupling_strength", - "$.node_coupling[386].node2_file", - "$.node_coupling[386].node2_name", - "$.node_coupling[387].coupling_strength", - "$.node_coupling[387].node1_file", - "$.node_coupling[387].node1_name", - "$.node_coupling[387].node2_file", - "$.node_coupling[387].node2_name", - "$.node_coupling[388].coupling_strength", - "$.node_coupling[388].node2_file", - "$.node_coupling[388].node2_name", - "$.node_coupling[389].coupling_strength", - "$.node_coupling[389].node2_file", - "$.node_coupling[389].node2_name", - "$.node_coupling[38].coupling_strength", - "$.node_coupling[38].node2_file", - "$.node_coupling[38].node2_name", - "$.node_coupling[390].coupling_strength", - "$.node_coupling[390].node2_file", - "$.node_coupling[390].node2_name", - "$.node_coupling[391].coupling_strength", - "$.node_coupling[391].node2_file", - "$.node_coupling[391].node2_name", - "$.node_coupling[392].coupling_strength", - "$.node_coupling[392].node1_file", - "$.node_coupling[392].node1_name", - "$.node_coupling[392].node2_name", - "$.node_coupling[393].coupling_strength", - "$.node_coupling[393].node2_name", - "$.node_coupling[394].node1_file", - "$.node_coupling[394].node1_name", - "$.node_coupling[394].node2_name", - "$.node_coupling[395].node1_file", - "$.node_coupling[395].node1_name", - "$.node_coupling[395].node2_file", - "$.node_coupling[395].node2_name", - "$.node_coupling[396].coupling_strength", - "$.node_coupling[396].node2_file", - "$.node_coupling[396].node2_name", - "$.node_coupling[397].node2_name", - "$.node_coupling[398].coupling_strength", - "$.node_coupling[398].node2_file", - "$.node_coupling[398].node2_name", - "$.node_coupling[399].coupling_strength", - "$.node_coupling[399].node1_file", - "$.node_coupling[399].node1_name", - "$.node_coupling[399].node2_file", - "$.node_coupling[399].node2_name", - "$.node_coupling[39].node2_name", - "$.node_coupling[3].co_changes", - "$.node_coupling[3].coupling_strength", - "$.node_coupling[3].node1_file", - "$.node_coupling[3].node1_name", - "$.node_coupling[3].node2_name", - "$.node_coupling[400].coupling_strength", - "$.node_coupling[400].node2_name", - "$.node_coupling[401].coupling_strength", - "$.node_coupling[401].node2_name", - "$.node_coupling[402].coupling_strength", - "$.node_coupling[402].node2_file", - "$.node_coupling[402].node2_name", - "$.node_coupling[403].coupling_strength", - "$.node_coupling[403].node2_file", - "$.node_coupling[403].node2_name", - "$.node_coupling[404].coupling_strength", - "$.node_coupling[404].node2_name", - "$.node_coupling[405].coupling_strength", - "$.node_coupling[405].node1_file", - "$.node_coupling[405].node1_name", - "$.node_coupling[405].node2_name", - "$.node_coupling[406].coupling_strength", - "$.node_coupling[406].node2_file", - "$.node_coupling[406].node2_name", - "$.node_coupling[407].coupling_strength", - "$.node_coupling[407].node2_file", - "$.node_coupling[407].node2_name", - "$.node_coupling[408].coupling_strength", - "$.node_coupling[408].node2_file", - "$.node_coupling[408].node2_name", - "$.node_coupling[409].coupling_strength", - "$.node_coupling[409].node2_file", - "$.node_coupling[409].node2_name", - "$.node_coupling[40].node2_file", - "$.node_coupling[40].node2_name", - "$.node_coupling[410].coupling_strength", - "$.node_coupling[410].node2_file", - "$.node_coupling[410].node2_name", - "$.node_coupling[411].coupling_strength", - "$.node_coupling[411].node2_file", - "$.node_coupling[411].node2_name", - "$.node_coupling[412].coupling_strength", - "$.node_coupling[412].node2_file", - "$.node_coupling[412].node2_name", - "$.node_coupling[413].coupling_strength", - "$.node_coupling[413].node2_file", - "$.node_coupling[413].node2_name", - "$.node_coupling[414].coupling_strength", - "$.node_coupling[414].node2_file", - "$.node_coupling[414].node2_name", - "$.node_coupling[415].coupling_strength", - "$.node_coupling[415].node2_file", - "$.node_coupling[415].node2_name", - "$.node_coupling[416].coupling_strength", - "$.node_coupling[416].node2_name", - "$.node_coupling[417].coupling_strength", - "$.node_coupling[417].node2_file", - "$.node_coupling[417].node2_name", - "$.node_coupling[418].coupling_strength", - "$.node_coupling[418].node2_file", - "$.node_coupling[418].node2_name", - "$.node_coupling[419].node1_name", - "$.node_coupling[419].node2_file", - "$.node_coupling[419].node2_name", - "$.node_coupling[41].node2_file", - "$.node_coupling[41].node2_name", - "$.node_coupling[420].node1_name", - "$.node_coupling[420].node2_name", - "$.node_coupling[421].node1_name", - "$.node_coupling[421].node2_file", - "$.node_coupling[421].node2_name", - "$.node_coupling[422].coupling_strength", - "$.node_coupling[422].node1_name", - "$.node_coupling[422].node2_file", - "$.node_coupling[422].node2_name", - "$.node_coupling[423].node1_name", - "$.node_coupling[423].node2_file", - "$.node_coupling[423].node2_name", - "$.node_coupling[424].node2_name", - "$.node_coupling[425].coupling_strength", - "$.node_coupling[425].node2_file", - "$.node_coupling[425].node2_name", - "$.node_coupling[426].node2_file", - "$.node_coupling[426].node2_name", - "$.node_coupling[427].coupling_strength", - "$.node_coupling[427].node2_file", - "$.node_coupling[427].node2_name", - "$.node_coupling[428].node2_file", - "$.node_coupling[428].node2_name", - "$.node_coupling[429].node1_file", - "$.node_coupling[429].node1_name", - "$.node_coupling[429].node2_name", - "$.node_coupling[42].node1_file", - "$.node_coupling[42].node1_name", - "$.node_coupling[42].node2_name", - "$.node_coupling[430].node2_file", - "$.node_coupling[430].node2_name", - "$.node_coupling[431].node2_name", - "$.node_coupling[432].node1_file", - "$.node_coupling[432].node1_name", - "$.node_coupling[432].node2_file", - "$.node_coupling[432].node2_name", - "$.node_coupling[433].coupling_strength", - "$.node_coupling[433].node2_file", - "$.node_coupling[433].node2_name", - "$.node_coupling[434].coupling_strength", - "$.node_coupling[434].node2_file", - "$.node_coupling[434].node2_name", - "$.node_coupling[435].node2_name", - "$.node_coupling[436].node2_file", - "$.node_coupling[436].node2_name", - "$.node_coupling[437].node2_file", - "$.node_coupling[437].node2_name", - "$.node_coupling[438].node2_name", - "$.node_coupling[439].node2_file", - "$.node_coupling[439].node2_name", - "$.node_coupling[43].coupling_strength", - "$.node_coupling[43].node1_file", - "$.node_coupling[43].node1_name", - "$.node_coupling[43].node2_file", - "$.node_coupling[43].node2_name", - "$.node_coupling[440].coupling_strength", - "$.node_coupling[440].node2_file", - "$.node_coupling[440].node2_name", - "$.node_coupling[441].node2_name", - "$.node_coupling[442].node2_name", - "$.node_coupling[443].node2_file", - "$.node_coupling[443].node2_name", - "$.node_coupling[444].coupling_strength", - "$.node_coupling[444].node2_file", - "$.node_coupling[444].node2_name", - "$.node_coupling[445].coupling_strength", - "$.node_coupling[445].node2_file", - "$.node_coupling[445].node2_name", - "$.node_coupling[446].node2_file", - "$.node_coupling[446].node2_name", - "$.node_coupling[447].node2_file", - "$.node_coupling[447].node2_name", - "$.node_coupling[448].node2_name", - "$.node_coupling[449].node2_file", - "$.node_coupling[449].node2_name", - "$.node_coupling[44].node2_file", - "$.node_coupling[44].node2_name", - "$.node_coupling[450].node2_file", - "$.node_coupling[450].node2_name", - "$.node_coupling[451].coupling_strength", - "$.node_coupling[451].node2_file", - "$.node_coupling[451].node2_name", - "$.node_coupling[452].node2_name", - "$.node_coupling[453].node2_file", - "$.node_coupling[453].node2_name", - "$.node_coupling[454].coupling_strength", - "$.node_coupling[454].node2_file", - "$.node_coupling[454].node2_name", - "$.node_coupling[455].node2_name", - "$.node_coupling[456].node2_file", - "$.node_coupling[456].node2_name", - "$.node_coupling[457].node1_file", - "$.node_coupling[457].node1_name", - "$.node_coupling[457].node2_name", - "$.node_coupling[458].coupling_strength", - "$.node_coupling[458].node2_file", - "$.node_coupling[458].node2_name", - "$.node_coupling[459].node2_file", - "$.node_coupling[459].node2_name", - "$.node_coupling[45].node2_file", - "$.node_coupling[45].node2_name", - "$.node_coupling[460].node2_file", - "$.node_coupling[460].node2_name", - "$.node_coupling[461].coupling_strength", - "$.node_coupling[461].node2_file", - "$.node_coupling[461].node2_name", - "$.node_coupling[462].node2_file", - "$.node_coupling[462].node2_name", - "$.node_coupling[463].node2_name", - "$.node_coupling[464].coupling_strength", - "$.node_coupling[464].node2_file", - "$.node_coupling[464].node2_name", - "$.node_coupling[465].coupling_strength", - "$.node_coupling[465].node2_file", - "$.node_coupling[465].node2_name", - "$.node_coupling[466].coupling_strength", - "$.node_coupling[466].node2_file", - "$.node_coupling[466].node2_name", - "$.node_coupling[467].node1_file", - "$.node_coupling[467].node1_name", - "$.node_coupling[467].node2_name", - "$.node_coupling[468].coupling_strength", - "$.node_coupling[468].node2_file", - "$.node_coupling[468].node2_name", - "$.node_coupling[469].node2_name", - "$.node_coupling[46].node2_file", - "$.node_coupling[46].node2_name", - "$.node_coupling[470].node1_file", - "$.node_coupling[470].node1_name", - "$.node_coupling[470].node2_file", - "$.node_coupling[470].node2_name", - "$.node_coupling[471].node2_name", - "$.node_coupling[472].node2_file", - "$.node_coupling[472].node2_name", - "$.node_coupling[473].node2_file", - "$.node_coupling[473].node2_name", - "$.node_coupling[474].coupling_strength", - "$.node_coupling[474].node2_file", - "$.node_coupling[474].node2_name", - "$.node_coupling[475].coupling_strength", - "$.node_coupling[475].node2_file", - "$.node_coupling[475].node2_name", - "$.node_coupling[476].node1_file", - "$.node_coupling[476].node1_name", - "$.node_coupling[476].node2_name", - "$.node_coupling[477].node2_file", - "$.node_coupling[477].node2_name", - "$.node_coupling[478].coupling_strength", - "$.node_coupling[478].node2_file", - "$.node_coupling[478].node2_name", - "$.node_coupling[479].coupling_strength", - "$.node_coupling[479].node1_file", - "$.node_coupling[479].node1_name", - "$.node_coupling[479].node2_file", - "$.node_coupling[479].node2_name", - "$.node_coupling[47].node2_name", - "$.node_coupling[480].node1_file", - "$.node_coupling[480].node1_name", - "$.node_coupling[480].node2_name", - "$.node_coupling[481].coupling_strength", - "$.node_coupling[481].node2_file", - "$.node_coupling[481].node2_name", - "$.node_coupling[482].coupling_strength", - "$.node_coupling[482].node2_file", - "$.node_coupling[482].node2_name", - "$.node_coupling[483].node1_file", - "$.node_coupling[483].node1_name", - "$.node_coupling[483].node2_name", - "$.node_coupling[484].coupling_strength", - "$.node_coupling[484].node1_file", - "$.node_coupling[484].node1_name", - "$.node_coupling[484].node2_file", - "$.node_coupling[484].node2_name", - "$.node_coupling[485].node1_file", - "$.node_coupling[485].node1_name", - "$.node_coupling[485].node2_file", - "$.node_coupling[485].node2_name", - "$.node_coupling[486].node1_name", - "$.node_coupling[486].node2_name", - "$.node_coupling[487].node1_file", - "$.node_coupling[487].node1_name", - "$.node_coupling[487].node2_name", - "$.node_coupling[488].node1_file", - "$.node_coupling[488].node1_name", - "$.node_coupling[488].node2_file", - "$.node_coupling[488].node2_name", - "$.node_coupling[489].coupling_strength", - "$.node_coupling[489].node2_file", - "$.node_coupling[489].node2_name", - "$.node_coupling[48].coupling_strength", - "$.node_coupling[48].node2_file", - "$.node_coupling[48].node2_name", - "$.node_coupling[490].coupling_strength", - "$.node_coupling[490].node1_file", - "$.node_coupling[490].node1_name", - "$.node_coupling[490].node2_name", - "$.node_coupling[491].coupling_strength", - "$.node_coupling[491].node1_file", - "$.node_coupling[491].node1_name", - "$.node_coupling[491].node2_name", - "$.node_coupling[492].coupling_strength", - "$.node_coupling[492].node2_file", - "$.node_coupling[492].node2_name", - "$.node_coupling[493].coupling_strength", - "$.node_coupling[493].node2_name", - "$.node_coupling[494].coupling_strength", - "$.node_coupling[494].node2_file", - "$.node_coupling[494].node2_name", - "$.node_coupling[495].coupling_strength", - "$.node_coupling[495].node2_file", - "$.node_coupling[495].node2_name", - "$.node_coupling[496].coupling_strength", - "$.node_coupling[496].node2_name", - "$.node_coupling[497].node1_file", - "$.node_coupling[497].node1_name", - "$.node_coupling[497].node2_file", - "$.node_coupling[497].node2_name", - "$.node_coupling[498].coupling_strength", - "$.node_coupling[498].node2_file", - "$.node_coupling[498].node2_name", - "$.node_coupling[499].coupling_strength", - "$.node_coupling[499].node1_file", - "$.node_coupling[499].node1_name", - "$.node_coupling[499].node2_name", - "$.node_coupling[49].coupling_strength", - "$.node_coupling[49].node2_file", - "$.node_coupling[49].node2_name", - "$.node_coupling[4].co_changes", - "$.node_coupling[4].coupling_strength", - "$.node_coupling[4].node1_name", - "$.node_coupling[4].node2_name", - "$.node_coupling[500].coupling_strength", - "$.node_coupling[500].node1_file", - "$.node_coupling[500].node1_name", - "$.node_coupling[500].node2_file", - "$.node_coupling[500].node2_name", - "$.node_coupling[501].node1_file", - "$.node_coupling[501].node1_name", - "$.node_coupling[501].node2_file", - "$.node_coupling[501].node2_name", - "$.node_coupling[502].coupling_strength", - "$.node_coupling[502].node2_file", - "$.node_coupling[502].node2_name", - "$.node_coupling[503].coupling_strength", - "$.node_coupling[503].node2_name", - "$.node_coupling[504].coupling_strength", - "$.node_coupling[504].node2_file", - "$.node_coupling[504].node2_name", - "$.node_coupling[505].coupling_strength", - "$.node_coupling[505].node2_file", - "$.node_coupling[505].node2_name", - "$.node_coupling[506].coupling_strength", - "$.node_coupling[506].node2_file", - "$.node_coupling[506].node2_name", - "$.node_coupling[507].coupling_strength", - "$.node_coupling[507].node2_name", - "$.node_coupling[508].coupling_strength", - "$.node_coupling[508].node2_file", - "$.node_coupling[508].node2_name", - "$.node_coupling[509].coupling_strength", - "$.node_coupling[509].node1_file", - "$.node_coupling[509].node1_name", - "$.node_coupling[509].node2_file", - "$.node_coupling[509].node2_name", - "$.node_coupling[50].node2_file", - "$.node_coupling[50].node2_name", - "$.node_coupling[510].coupling_strength", - "$.node_coupling[510].node2_file", - "$.node_coupling[510].node2_name", - "$.node_coupling[511].coupling_strength", - "$.node_coupling[511].node2_file", - "$.node_coupling[511].node2_name", - "$.node_coupling[512].coupling_strength", - "$.node_coupling[512].node2_file", - "$.node_coupling[512].node2_name", - "$.node_coupling[513].node1_file", - "$.node_coupling[513].node1_name", - "$.node_coupling[513].node2_name", - "$.node_coupling[514].coupling_strength", - "$.node_coupling[514].node2_file", - "$.node_coupling[514].node2_name", - "$.node_coupling[515].coupling_strength", - "$.node_coupling[515].node1_file", - "$.node_coupling[515].node1_name", - "$.node_coupling[515].node2_file", - "$.node_coupling[515].node2_name", - "$.node_coupling[516].coupling_strength", - "$.node_coupling[516].node1_file", - "$.node_coupling[516].node1_name", - "$.node_coupling[516].node2_file", - "$.node_coupling[516].node2_name", - "$.node_coupling[517].coupling_strength", - "$.node_coupling[517].node2_name", - "$.node_coupling[518].coupling_strength", - "$.node_coupling[518].node2_file", - "$.node_coupling[518].node2_name", - "$.node_coupling[519].coupling_strength", - "$.node_coupling[519].node2_name", - "$.node_coupling[51].coupling_strength", - "$.node_coupling[51].node2_file", - "$.node_coupling[51].node2_name", - "$.node_coupling[520].node1_file", - "$.node_coupling[520].node1_name", - "$.node_coupling[520].node2_name", - "$.node_coupling[521].coupling_strength", - "$.node_coupling[521].node1_file", - "$.node_coupling[521].node1_name", - "$.node_coupling[521].node2_file", - "$.node_coupling[521].node2_name", - "$.node_coupling[522].node1_file", - "$.node_coupling[522].node1_name", - "$.node_coupling[522].node2_file", - "$.node_coupling[522].node2_name", - "$.node_coupling[523].coupling_strength", - "$.node_coupling[523].node2_file", - "$.node_coupling[523].node2_name", - "$.node_coupling[524].coupling_strength", - "$.node_coupling[524].node2_file", - "$.node_coupling[524].node2_name", - "$.node_coupling[525].coupling_strength", - "$.node_coupling[525].node2_name", - "$.node_coupling[526].coupling_strength", - "$.node_coupling[526].node2_file", - "$.node_coupling[526].node2_name", - "$.node_coupling[527].coupling_strength", - "$.node_coupling[527].node2_name", - "$.node_coupling[528].coupling_strength", - "$.node_coupling[528].node2_file", - "$.node_coupling[528].node2_name", - "$.node_coupling[529].coupling_strength", - "$.node_coupling[529].node2_file", - "$.node_coupling[529].node2_name", - "$.node_coupling[52].node2_file", - "$.node_coupling[52].node2_name", - "$.node_coupling[530].coupling_strength", - "$.node_coupling[530].node2_file", - "$.node_coupling[530].node2_name", - "$.node_coupling[531].coupling_strength", - "$.node_coupling[531].node2_file", - "$.node_coupling[531].node2_name", - "$.node_coupling[532].coupling_strength", - "$.node_coupling[532].node2_file", - "$.node_coupling[532].node2_name", - "$.node_coupling[533].coupling_strength", - "$.node_coupling[533].node2_file", - "$.node_coupling[533].node2_name", - "$.node_coupling[534].coupling_strength", - "$.node_coupling[534].node2_file", - "$.node_coupling[534].node2_name", - "$.node_coupling[535].coupling_strength", - "$.node_coupling[535].node2_file", - "$.node_coupling[535].node2_name", - "$.node_coupling[536].coupling_strength", - "$.node_coupling[536].node2_file", - "$.node_coupling[536].node2_name", - "$.node_coupling[537].coupling_strength", - "$.node_coupling[537].node2_file", - "$.node_coupling[537].node2_name", - "$.node_coupling[538].coupling_strength", - "$.node_coupling[538].node2_name", - "$.node_coupling[539].coupling_strength", - "$.node_coupling[539].node2_name", - "$.node_coupling[53].node1_file", - "$.node_coupling[53].node1_name", - "$.node_coupling[53].node2_name", - "$.node_coupling[540].coupling_strength", - "$.node_coupling[540].node2_file", - "$.node_coupling[540].node2_name", - "$.node_coupling[541].coupling_strength", - "$.node_coupling[541].node2_file", - "$.node_coupling[541].node2_name", - "$.node_coupling[542].coupling_strength", - "$.node_coupling[542].node2_file", - "$.node_coupling[542].node2_name", - "$.node_coupling[543].coupling_strength", - "$.node_coupling[543].node2_name", - "$.node_coupling[544].coupling_strength", - "$.node_coupling[544].node2_file", - "$.node_coupling[544].node2_name", - "$.node_coupling[545].coupling_strength", - "$.node_coupling[545].node2_name", - "$.node_coupling[546].coupling_strength", - "$.node_coupling[546].node2_file", - "$.node_coupling[546].node2_name", - "$.node_coupling[547].coupling_strength", - "$.node_coupling[547].node2_file", - "$.node_coupling[547].node2_name", - "$.node_coupling[548].coupling_strength", - "$.node_coupling[548].node1_name", - "$.node_coupling[548].node2_file", - "$.node_coupling[548].node2_name", - "$.node_coupling[549].coupling_strength", - "$.node_coupling[549].node1_name", - "$.node_coupling[549].node2_file", - "$.node_coupling[549].node2_name", - "$.node_coupling[54].node2_file", - "$.node_coupling[54].node2_name", - "$.node_coupling[550].coupling_strength", - "$.node_coupling[550].node1_name", - "$.node_coupling[550].node2_name", - "$.node_coupling[551].coupling_strength", - "$.node_coupling[551].node1_name", - "$.node_coupling[551].node2_file", - "$.node_coupling[551].node2_name", - "$.node_coupling[552].coupling_strength", - "$.node_coupling[552].node1_name", - "$.node_coupling[552].node2_file", - "$.node_coupling[552].node2_name", - "$.node_coupling[553].node2_file", - "$.node_coupling[553].node2_name", - "$.node_coupling[554].node1_file", - "$.node_coupling[554].node1_name", - "$.node_coupling[554].node2_file", - "$.node_coupling[554].node2_name", - "$.node_coupling[555].coupling_strength", - "$.node_coupling[555].node2_file", - "$.node_coupling[555].node2_name", - "$.node_coupling[556].coupling_strength", - "$.node_coupling[556].node2_file", - "$.node_coupling[556].node2_name", - "$.node_coupling[557].coupling_strength", - "$.node_coupling[557].node1_file", - "$.node_coupling[557].node1_name", - "$.node_coupling[557].node2_file", - "$.node_coupling[557].node2_name", - "$.node_coupling[558].node2_name", - "$.node_coupling[559].coupling_strength", - "$.node_coupling[559].node2_file", - "$.node_coupling[559].node2_name", - "$.node_coupling[55].node2_file", - "$.node_coupling[55].node2_name", - "$.node_coupling[560].coupling_strength", - "$.node_coupling[560].node2_file", - "$.node_coupling[560].node2_name", - "$.node_coupling[561].node2_file", - "$.node_coupling[561].node2_name", - "$.node_coupling[562].node2_file", - "$.node_coupling[562].node2_name", - "$.node_coupling[563].node2_name", - "$.node_coupling[564].node2_file", - "$.node_coupling[564].node2_name", - "$.node_coupling[565].node2_name", - "$.node_coupling[566].node1_file", - "$.node_coupling[566].node1_name", - "$.node_coupling[566].node2_file", - "$.node_coupling[566].node2_name", - "$.node_coupling[567].node2_file", - "$.node_coupling[567].node2_name", - "$.node_coupling[568].node1_file", - "$.node_coupling[568].node1_name", - "$.node_coupling[568].node2_file", - "$.node_coupling[568].node2_name", - "$.node_coupling[569].node2_file", - "$.node_coupling[569].node2_name", - "$.node_coupling[56].node1_file", - "$.node_coupling[56].node1_name", - "$.node_coupling[56].node2_name", - "$.node_coupling[570].node2_file", - "$.node_coupling[570].node2_name", - "$.node_coupling[571].node1_file", - "$.node_coupling[571].node1_name", - "$.node_coupling[571].node2_file", - "$.node_coupling[571].node2_name", - "$.node_coupling[572].coupling_strength", - "$.node_coupling[572].node2_file", - "$.node_coupling[572].node2_name", - "$.node_coupling[573].node2_name", - "$.node_coupling[574].node2_name", - "$.node_coupling[575].node2_name", - "$.node_coupling[576].node2_file", - "$.node_coupling[576].node2_name", - "$.node_coupling[577].node2_file", - "$.node_coupling[577].node2_name", - "$.node_coupling[578].node2_file", - "$.node_coupling[578].node2_name", - "$.node_coupling[579].node2_name", - "$.node_coupling[57].node2_file", - "$.node_coupling[57].node2_name", - "$.node_coupling[580].node2_name", - "$.node_coupling[581].node2_name", - "$.node_coupling[582].coupling_strength", - "$.node_coupling[582].node2_file", - "$.node_coupling[582].node2_name", - "$.node_coupling[583].node1_file", - "$.node_coupling[583].node1_name", - "$.node_coupling[583].node2_file", - "$.node_coupling[583].node2_name", - "$.node_coupling[584].coupling_strength", - "$.node_coupling[584].node2_file", - "$.node_coupling[584].node2_name", - "$.node_coupling[585].node2_file", - "$.node_coupling[585].node2_name", - "$.node_coupling[586].coupling_strength", - "$.node_coupling[586].node2_file", - "$.node_coupling[586].node2_name", - "$.node_coupling[587].coupling_strength", - "$.node_coupling[587].node2_file", - "$.node_coupling[587].node2_name", - "$.node_coupling[588].coupling_strength", - "$.node_coupling[588].node2_file", - "$.node_coupling[588].node2_name", - "$.node_coupling[589].coupling_strength", - "$.node_coupling[589].node2_file", - "$.node_coupling[589].node2_name", - "$.node_coupling[58].node2_file", - "$.node_coupling[58].node2_name", - "$.node_coupling[590].node2_name", - "$.node_coupling[591].coupling_strength", - "$.node_coupling[591].node2_file", - "$.node_coupling[591].node2_name", - "$.node_coupling[592].coupling_strength", - "$.node_coupling[592].node2_file", - "$.node_coupling[592].node2_name", - "$.node_coupling[593].node2_file", - "$.node_coupling[593].node2_name", - "$.node_coupling[594].node2_name", - "$.node_coupling[595].node2_file", - "$.node_coupling[595].node2_name", - "$.node_coupling[596].node1_file", - "$.node_coupling[596].node1_name", - "$.node_coupling[596].node2_file", - "$.node_coupling[596].node2_name", - "$.node_coupling[597].coupling_strength", - "$.node_coupling[597].node2_file", - "$.node_coupling[597].node2_name", - "$.node_coupling[598].node1_file", - "$.node_coupling[598].node1_name", - "$.node_coupling[598].node2_name", - "$.node_coupling[599].node2_name", - "$.node_coupling[59].coupling_strength", - "$.node_coupling[59].node2_file", - "$.node_coupling[59].node2_name", - "$.node_coupling[5].co_changes", - "$.node_coupling[5].coupling_strength", - "$.node_coupling[5].node1_file", - "$.node_coupling[5].node1_name", - "$.node_coupling[5].node2_name", - "$.node_coupling[600].coupling_strength", - "$.node_coupling[600].node1_file", - "$.node_coupling[600].node1_name", - "$.node_coupling[600].node2_file", - "$.node_coupling[600].node2_name", - "$.node_coupling[601].coupling_strength", - "$.node_coupling[601].node1_file", - "$.node_coupling[601].node1_name", - "$.node_coupling[601].node2_file", - "$.node_coupling[601].node2_name", - "$.node_coupling[602].node2_file", - "$.node_coupling[602].node2_name", - "$.node_coupling[603].node1_file", - "$.node_coupling[603].node1_name", - "$.node_coupling[603].node2_file", - "$.node_coupling[603].node2_name", - "$.node_coupling[604].coupling_strength", - "$.node_coupling[604].node2_file", - "$.node_coupling[604].node2_name", - "$.node_coupling[605].coupling_strength", - "$.node_coupling[605].node2_file", - "$.node_coupling[605].node2_name", - "$.node_coupling[606].node1_file", - "$.node_coupling[606].node1_name", - "$.node_coupling[606].node2_file", - "$.node_coupling[606].node2_name", - "$.node_coupling[607].coupling_strength", - "$.node_coupling[607].node2_file", - "$.node_coupling[607].node2_name", - "$.node_coupling[608].node2_name", - "$.node_coupling[609].node2_name", - "$.node_coupling[60].coupling_strength", - "$.node_coupling[60].node2_file", - "$.node_coupling[60].node2_name", - "$.node_coupling[610].coupling_strength", - "$.node_coupling[610].node2_file", - "$.node_coupling[610].node2_name", - "$.node_coupling[611].coupling_strength", - "$.node_coupling[611].node1_name", - "$.node_coupling[611].node2_file", - "$.node_coupling[611].node2_name", - "$.node_coupling[612].node1_name", - "$.node_coupling[612].node2_file", - "$.node_coupling[612].node2_name", - "$.node_coupling[613].node1_name", - "$.node_coupling[613].node2_name", - "$.node_coupling[614].node1_name", - "$.node_coupling[614].node2_file", - "$.node_coupling[614].node2_name", - "$.node_coupling[615].node1_file", - "$.node_coupling[615].node1_name", - "$.node_coupling[615].node2_file", - "$.node_coupling[615].node2_name", - "$.node_coupling[616].coupling_strength", - "$.node_coupling[616].node2_file", - "$.node_coupling[616].node2_name", - "$.node_coupling[617].node2_file", - "$.node_coupling[617].node2_name", - "$.node_coupling[618].node2_file", - "$.node_coupling[618].node2_name", - "$.node_coupling[619].coupling_strength", - "$.node_coupling[619].node2_file", - "$.node_coupling[619].node2_name", - "$.node_coupling[61].coupling_strength", - "$.node_coupling[61].node2_file", - "$.node_coupling[61].node2_name", - "$.node_coupling[620].coupling_strength", - "$.node_coupling[620].node2_file", - "$.node_coupling[620].node2_name", - "$.node_coupling[621].node2_file", - "$.node_coupling[621].node2_name", - "$.node_coupling[622].node2_file", - "$.node_coupling[622].node2_name", - "$.node_coupling[623].node2_name", - "$.node_coupling[624].coupling_strength", - "$.node_coupling[624].node2_file", - "$.node_coupling[624].node2_name", - "$.node_coupling[625].node2_name", - "$.node_coupling[626].node2_file", - "$.node_coupling[626].node2_name", - "$.node_coupling[627].coupling_strength", - "$.node_coupling[627].node1_file", - "$.node_coupling[627].node1_name", - "$.node_coupling[627].node2_file", - "$.node_coupling[627].node2_name", - "$.node_coupling[628].coupling_strength", - "$.node_coupling[628].node2_file", - "$.node_coupling[628].node2_name", - "$.node_coupling[629].coupling_strength", - "$.node_coupling[629].node2_file", - "$.node_coupling[629].node2_name", - "$.node_coupling[62].node2_file", - "$.node_coupling[62].node2_name", - "$.node_coupling[630].node2_file", - "$.node_coupling[630].node2_name", - "$.node_coupling[631].node2_file", - "$.node_coupling[631].node2_name", - "$.node_coupling[632].coupling_strength", - "$.node_coupling[632].node2_file", - "$.node_coupling[632].node2_name", - "$.node_coupling[633].node1_file", - "$.node_coupling[633].node1_name", - "$.node_coupling[633].node2_name", - "$.node_coupling[634].node2_file", - "$.node_coupling[634].node2_name", - "$.node_coupling[635].coupling_strength", - "$.node_coupling[635].node1_file", - "$.node_coupling[635].node1_name", - "$.node_coupling[635].node2_file", - "$.node_coupling[635].node2_name", - "$.node_coupling[636].coupling_strength", - "$.node_coupling[636].node2_file", - "$.node_coupling[636].node2_name", - "$.node_coupling[637].node2_name", - "$.node_coupling[638].node1_file", - "$.node_coupling[638].node1_name", - "$.node_coupling[638].node2_name", - "$.node_coupling[639].node1_file", - "$.node_coupling[639].node1_name", - "$.node_coupling[639].node2_file", - "$.node_coupling[639].node2_name", - "$.node_coupling[63].coupling_strength", - "$.node_coupling[63].node2_file", - "$.node_coupling[63].node2_name", - "$.node_coupling[640].coupling_strength", - "$.node_coupling[640].node2_file", - "$.node_coupling[640].node2_name", - "$.node_coupling[641].coupling_strength", - "$.node_coupling[641].node2_file", - "$.node_coupling[641].node2_name", - "$.node_coupling[642].node2_file", - "$.node_coupling[642].node2_name", - "$.node_coupling[643].node2_file", - "$.node_coupling[643].node2_name", - "$.node_coupling[644].node1_file", - "$.node_coupling[644].node1_name", - "$.node_coupling[644].node2_file", - "$.node_coupling[644].node2_name", - "$.node_coupling[645].node2_file", - "$.node_coupling[645].node2_name", - "$.node_coupling[646].node2_file", - "$.node_coupling[646].node2_name", - "$.node_coupling[647].coupling_strength", - "$.node_coupling[647].node2_file", - "$.node_coupling[647].node2_name", - "$.node_coupling[648].node2_name", - "$.node_coupling[649].node2_file", - "$.node_coupling[649].node2_name", - "$.node_coupling[64].node1_file", - "$.node_coupling[64].node1_name", - "$.node_coupling[64].node2_file", - "$.node_coupling[64].node2_name", - "$.node_coupling[650].node2_file", - "$.node_coupling[650].node2_name", - "$.node_coupling[651].node2_file", - "$.node_coupling[651].node2_name", - "$.node_coupling[652].node2_name", - "$.node_coupling[653].node2_file", - "$.node_coupling[653].node2_name", - "$.node_coupling[654].coupling_strength", - "$.node_coupling[654].node2_file", - "$.node_coupling[654].node2_name", - "$.node_coupling[655].coupling_strength", - "$.node_coupling[655].node1_file", - "$.node_coupling[655].node1_name", - "$.node_coupling[655].node2_file", - "$.node_coupling[655].node2_name", - "$.node_coupling[656].coupling_strength", - "$.node_coupling[656].node2_file", - "$.node_coupling[656].node2_name", - "$.node_coupling[657].node2_file", - "$.node_coupling[657].node2_name", - "$.node_coupling[658].node1_file", - "$.node_coupling[658].node1_name", - "$.node_coupling[658].node2_name", - "$.node_coupling[659].node2_file", - "$.node_coupling[659].node2_name", - "$.node_coupling[65].node2_file", - "$.node_coupling[65].node2_name", - "$.node_coupling[660].coupling_strength", - "$.node_coupling[660].node2_file", - "$.node_coupling[660].node2_name", - "$.node_coupling[661].node2_name", - "$.node_coupling[662].node2_file", - "$.node_coupling[662].node2_name", - "$.node_coupling[663].node1_file", - "$.node_coupling[663].node1_name", - "$.node_coupling[663].node2_file", - "$.node_coupling[663].node2_name", - "$.node_coupling[664].node2_name", - "$.node_coupling[665].node2_file", - "$.node_coupling[665].node2_name", - "$.node_coupling[666].node2_file", - "$.node_coupling[666].node2_name", - "$.node_coupling[667].node1_file", - "$.node_coupling[667].node1_name", - "$.node_coupling[667].node2_file", - "$.node_coupling[667].node2_name", - "$.node_coupling[668].coupling_strength", - "$.node_coupling[668].node2_file", - "$.node_coupling[668].node2_name", - "$.node_coupling[669].node2_name", - "$.node_coupling[66].coupling_strength", - "$.node_coupling[66].node2_file", - "$.node_coupling[66].node2_name", - "$.node_coupling[670].coupling_strength", - "$.node_coupling[670].node2_file", - "$.node_coupling[670].node2_name", - "$.node_coupling[671].coupling_strength", - "$.node_coupling[671].node2_file", - "$.node_coupling[671].node2_name", - "$.node_coupling[672].node2_name", - "$.node_coupling[673].node1_name", - "$.node_coupling[673].node2_file", - "$.node_coupling[673].node2_name", - "$.node_coupling[674].node1_name", - "$.node_coupling[674].node2_name", - "$.node_coupling[675].node1_name", - "$.node_coupling[675].node2_file", - "$.node_coupling[675].node2_name", - "$.node_coupling[676].node1_name", - "$.node_coupling[676].node2_file", - "$.node_coupling[676].node2_name", - "$.node_coupling[677].node1_name", - "$.node_coupling[677].node2_name", - "$.node_coupling[678].node2_file", - "$.node_coupling[678].node2_name", - "$.node_coupling[679].node1_file", - "$.node_coupling[679].node1_name", - "$.node_coupling[679].node2_file", - "$.node_coupling[679].node2_name", - "$.node_coupling[67].node2_file", - "$.node_coupling[67].node2_name", - "$.node_coupling[680].coupling_strength", - "$.node_coupling[680].node2_file", - "$.node_coupling[680].node2_name", - "$.node_coupling[681].coupling_strength", - "$.node_coupling[681].node2_file", - "$.node_coupling[681].node2_name", - "$.node_coupling[682].node2_name", - "$.node_coupling[683].node1_file", - "$.node_coupling[683].node1_name", - "$.node_coupling[683].node2_name", - "$.node_coupling[684].node2_name", - "$.node_coupling[685].node1_file", - "$.node_coupling[685].node1_name", - "$.node_coupling[685].node2_file", - "$.node_coupling[685].node2_name", - "$.node_coupling[686].coupling_strength", - "$.node_coupling[686].node2_file", - "$.node_coupling[686].node2_name", - "$.node_coupling[687].coupling_strength", - "$.node_coupling[687].node1_file", - "$.node_coupling[687].node1_name", - "$.node_coupling[687].node2_file", - "$.node_coupling[687].node2_name", - "$.node_coupling[688].coupling_strength", - "$.node_coupling[688].node2_file", - "$.node_coupling[688].node2_name", - "$.node_coupling[689].node2_file", - "$.node_coupling[689].node2_name", - "$.node_coupling[68].coupling_strength", - "$.node_coupling[68].node2_file", - "$.node_coupling[68].node2_name", - "$.node_coupling[690].node2_file", - "$.node_coupling[690].node2_name", - "$.node_coupling[691].node2_file", - "$.node_coupling[691].node2_name", - "$.node_coupling[692].node1_file", - "$.node_coupling[692].node1_name", - "$.node_coupling[692].node2_file", - "$.node_coupling[692].node2_name", - "$.node_coupling[693].coupling_strength", - "$.node_coupling[693].node2_file", - "$.node_coupling[693].node2_name", - "$.node_coupling[694].coupling_strength", - "$.node_coupling[694].node2_file", - "$.node_coupling[694].node2_name", - "$.node_coupling[695].node1_file", - "$.node_coupling[695].node1_name", - "$.node_coupling[695].node2_file", - "$.node_coupling[695].node2_name", - "$.node_coupling[696].node1_file", - "$.node_coupling[696].node1_name", - "$.node_coupling[696].node2_name", - "$.node_coupling[697].node2_name", - "$.node_coupling[698].coupling_strength", - "$.node_coupling[698].node2_file", - "$.node_coupling[698].node2_name", - "$.node_coupling[699].node2_file", - "$.node_coupling[699].node2_name", - "$.node_coupling[69].coupling_strength", - "$.node_coupling[69].node2_file", - "$.node_coupling[69].node2_name", - "$.node_coupling[6].coupling_strength", - "$.node_coupling[6].node1_file", - "$.node_coupling[6].node1_name", - "$.node_coupling[6].node2_file", - "$.node_coupling[6].node2_name", - "$.node_coupling[700].node1_file", - "$.node_coupling[700].node1_name", - "$.node_coupling[700].node2_file", - "$.node_coupling[700].node2_name", - "$.node_coupling[701].coupling_strength", - "$.node_coupling[701].node2_file", - "$.node_coupling[701].node2_name", - "$.node_coupling[702].node2_file", - "$.node_coupling[702].node2_name", - "$.node_coupling[703].coupling_strength", - "$.node_coupling[703].node2_file", - "$.node_coupling[703].node2_name", - "$.node_coupling[704].node2_name", - "$.node_coupling[705].node1_file", - "$.node_coupling[705].node1_name", - "$.node_coupling[705].node2_name", - "$.node_coupling[706].coupling_strength", - "$.node_coupling[706].node2_file", - "$.node_coupling[706].node2_name", - "$.node_coupling[707].node2_file", - "$.node_coupling[707].node2_name", - "$.node_coupling[708].coupling_strength", - "$.node_coupling[708].node2_file", - "$.node_coupling[708].node2_name", - "$.node_coupling[709].node2_file", - "$.node_coupling[709].node2_name", - "$.node_coupling[70].coupling_strength", - "$.node_coupling[70].node2_file", - "$.node_coupling[70].node2_name", - "$.node_coupling[710].node2_name", - "$.node_coupling[711].node1_file", - "$.node_coupling[711].node1_name", - "$.node_coupling[711].node2_name", - "$.node_coupling[712].node2_name", - "$.node_coupling[713].node2_file", - "$.node_coupling[713].node2_name", - "$.node_coupling[714].coupling_strength", - "$.node_coupling[714].node2_file", - "$.node_coupling[714].node2_name", - "$.node_coupling[715].node2_file", - "$.node_coupling[715].node2_name", - "$.node_coupling[716].node2_name", - "$.node_coupling[717].node2_name", - "$.node_coupling[718].node1_file", - "$.node_coupling[718].node1_name", - "$.node_coupling[718].node2_name", - "$.node_coupling[719].node2_name", - "$.node_coupling[71].node2_name", - "$.node_coupling[720].node2_file", - "$.node_coupling[720].node2_name", - "$.node_coupling[721].coupling_strength", - "$.node_coupling[721].node2_file", - "$.node_coupling[721].node2_name", - "$.node_coupling[722].node2_file", - "$.node_coupling[722].node2_name", - "$.node_coupling[723].node1_file", - "$.node_coupling[723].node1_name", - "$.node_coupling[723].node2_name", - "$.node_coupling[724].coupling_strength", - "$.node_coupling[724].node2_file", - "$.node_coupling[724].node2_name", - "$.node_coupling[725].coupling_strength", - "$.node_coupling[725].node2_file", - "$.node_coupling[725].node2_name", - "$.node_coupling[726].coupling_strength", - "$.node_coupling[726].node2_file", - "$.node_coupling[726].node2_name", - "$.node_coupling[727].coupling_strength", - "$.node_coupling[727].node2_file", - "$.node_coupling[727].node2_name", - "$.node_coupling[728].node2_name", - "$.node_coupling[729].node2_file", - "$.node_coupling[729].node2_name", - "$.node_coupling[72].node2_file", - "$.node_coupling[72].node2_name", - "$.node_coupling[730].node2_name", - "$.node_coupling[731].node2_name", - "$.node_coupling[732].coupling_strength", - "$.node_coupling[732].node1_file", - "$.node_coupling[732].node1_name", - "$.node_coupling[732].node2_file", - "$.node_coupling[732].node2_name", - "$.node_coupling[733].node2_name", - "$.node_coupling[734].node1_name", - "$.node_coupling[734].node2_file", - "$.node_coupling[734].node2_name", - "$.node_coupling[735].node1_name", - "$.node_coupling[735].node2_name", - "$.node_coupling[736].node1_name", - "$.node_coupling[736].node2_name", - "$.node_coupling[737].coupling_strength", - "$.node_coupling[737].node1_file", - "$.node_coupling[737].node1_name", - "$.node_coupling[737].node2_file", - "$.node_coupling[737].node2_name", - "$.node_coupling[738].coupling_strength", - "$.node_coupling[738].node1_name", - "$.node_coupling[738].node2_file", - "$.node_coupling[738].node2_name", - "$.node_coupling[739].coupling_strength", - "$.node_coupling[739].node1_file", - "$.node_coupling[739].node1_name", - "$.node_coupling[739].node2_file", - "$.node_coupling[739].node2_name", - "$.node_coupling[73].coupling_strength", - "$.node_coupling[73].node2_file", - "$.node_coupling[73].node2_name", - "$.node_coupling[740].node2_name", - "$.node_coupling[741].coupling_strength", - "$.node_coupling[741].node2_file", - "$.node_coupling[741].node2_name", - "$.node_coupling[742].node2_file", - "$.node_coupling[742].node2_name", - "$.node_coupling[743].coupling_strength", - "$.node_coupling[743].node2_file", - "$.node_coupling[743].node2_name", - "$.node_coupling[744].node2_name", - "$.node_coupling[745].node2_name", - "$.node_coupling[746].coupling_strength", - "$.node_coupling[746].node2_file", - "$.node_coupling[746].node2_name", - "$.node_coupling[747].coupling_strength", - "$.node_coupling[747].node2_file", - "$.node_coupling[747].node2_name", - "$.node_coupling[748].node2_file", - "$.node_coupling[748].node2_name", - "$.node_coupling[749].coupling_strength", - "$.node_coupling[749].node2_file", - "$.node_coupling[749].node2_name", - "$.node_coupling[74].node1_file", - "$.node_coupling[74].node1_name", - "$.node_coupling[74].node2_name", - "$.node_coupling[750].node2_file", - "$.node_coupling[750].node2_name", - "$.node_coupling[751].node2_name", - "$.node_coupling[752].node2_name", - "$.node_coupling[753].node2_file", - "$.node_coupling[753].node2_name", - "$.node_coupling[754].node2_file", - "$.node_coupling[754].node2_name", - "$.node_coupling[755].node2_file", - "$.node_coupling[755].node2_name", - "$.node_coupling[756].coupling_strength", - "$.node_coupling[756].node2_file", - "$.node_coupling[756].node2_name", - "$.node_coupling[757].node2_file", - "$.node_coupling[757].node2_name", - "$.node_coupling[758].node2_file", - "$.node_coupling[758].node2_name", - "$.node_coupling[759].node1_file", - "$.node_coupling[759].node1_name", - "$.node_coupling[759].node2_file", - "$.node_coupling[759].node2_name", - "$.node_coupling[75].node2_file", - "$.node_coupling[75].node2_name", - "$.node_coupling[760].node2_name", - "$.node_coupling[761].node1_file", - "$.node_coupling[761].node1_name", - "$.node_coupling[761].node2_file", - "$.node_coupling[761].node2_name", - "$.node_coupling[762].node2_name", - "$.node_coupling[763].node2_name", - "$.node_coupling[764].node2_name", - "$.node_coupling[765].node2_name", - "$.node_coupling[766].node2_file", - "$.node_coupling[766].node2_name", - "$.node_coupling[767].coupling_strength", - "$.node_coupling[767].node2_file", - "$.node_coupling[767].node2_name", - "$.node_coupling[768].coupling_strength", - "$.node_coupling[768].node2_file", - "$.node_coupling[768].node2_name", - "$.node_coupling[769].node2_name", - "$.node_coupling[76].coupling_strength", - "$.node_coupling[76].node1_file", - "$.node_coupling[76].node1_name", - "$.node_coupling[76].node2_file", - "$.node_coupling[76].node2_name", - "$.node_coupling[770].node2_file", - "$.node_coupling[770].node2_name", - "$.node_coupling[771].node1_file", - "$.node_coupling[771].node1_name", - "$.node_coupling[771].node2_name", - "$.node_coupling[772].coupling_strength", - "$.node_coupling[772].node1_file", - "$.node_coupling[772].node1_name", - "$.node_coupling[772].node2_file", - "$.node_coupling[772].node2_name", - "$.node_coupling[773].coupling_strength", - "$.node_coupling[773].node1_file", - "$.node_coupling[773].node1_name", - "$.node_coupling[773].node2_file", - "$.node_coupling[773].node2_name", - "$.node_coupling[774].coupling_strength", - "$.node_coupling[774].node2_file", - "$.node_coupling[774].node2_name", - "$.node_coupling[775].node2_name", - "$.node_coupling[776].coupling_strength", - "$.node_coupling[776].node2_file", - "$.node_coupling[776].node2_name", - "$.node_coupling[777].node2_file", - "$.node_coupling[777].node2_name", - "$.node_coupling[778].node2_file", - "$.node_coupling[778].node2_name", - "$.node_coupling[779].node2_file", - "$.node_coupling[779].node2_name", - "$.node_coupling[77].coupling_strength", - "$.node_coupling[77].node1_file", - "$.node_coupling[77].node1_name", - "$.node_coupling[77].node2_name", - "$.node_coupling[780].node2_file", - "$.node_coupling[780].node2_name", - "$.node_coupling[781].node2_file", - "$.node_coupling[781].node2_name", - "$.node_coupling[782].coupling_strength", - "$.node_coupling[782].node2_file", - "$.node_coupling[782].node2_name", - "$.node_coupling[783].node2_name", - "$.node_coupling[784].coupling_strength", - "$.node_coupling[784].node2_file", - "$.node_coupling[784].node2_name", - "$.node_coupling[785].coupling_strength", - "$.node_coupling[785].node1_file", - "$.node_coupling[785].node1_name", - "$.node_coupling[785].node2_file", - "$.node_coupling[785].node2_name", - "$.node_coupling[786].node2_name", - "$.node_coupling[787].coupling_strength", - "$.node_coupling[787].node1_file", - "$.node_coupling[787].node1_name", - "$.node_coupling[787].node2_file", - "$.node_coupling[787].node2_name", - "$.node_coupling[788].coupling_strength", - "$.node_coupling[788].node1_file", - "$.node_coupling[788].node1_name", - "$.node_coupling[788].node2_file", - "$.node_coupling[788].node2_name", - "$.node_coupling[789].node2_file", - "$.node_coupling[789].node2_name", - "$.node_coupling[78].coupling_strength", - "$.node_coupling[78].node1_file", - "$.node_coupling[78].node1_name", - "$.node_coupling[78].node2_name", - "$.node_coupling[790].coupling_strength", - "$.node_coupling[790].node2_file", - "$.node_coupling[790].node2_name", - "$.node_coupling[791].node2_file", - "$.node_coupling[791].node2_name", - "$.node_coupling[792].coupling_strength", - "$.node_coupling[792].node2_file", - "$.node_coupling[792].node2_name", - "$.node_coupling[793].coupling_strength", - "$.node_coupling[793].node2_file", - "$.node_coupling[793].node2_name", - "$.node_coupling[794].node1_name", - "$.node_coupling[794].node2_file", - "$.node_coupling[794].node2_name", - "$.node_coupling[795].node1_name", - "$.node_coupling[795].node2_name", - "$.node_coupling[796].node1_file", - "$.node_coupling[796].node1_name", - "$.node_coupling[796].node2_name", - "$.node_coupling[797].node1_name", - "$.node_coupling[797].node2_file", - "$.node_coupling[797].node2_name", - "$.node_coupling[798].node1_name", - "$.node_coupling[798].node2_file", - "$.node_coupling[798].node2_name", - "$.node_coupling[799].coupling_strength", - "$.node_coupling[799].node2_file", - "$.node_coupling[799].node2_name", - "$.node_coupling[79].coupling_strength", - "$.node_coupling[79].node1_file", - "$.node_coupling[79].node1_name", - "$.node_coupling[79].node2_file", - "$.node_coupling[79].node2_name", - "$.node_coupling[7].coupling_strength", - "$.node_coupling[7].node1_file", - "$.node_coupling[7].node1_name", - "$.node_coupling[7].node2_file", - "$.node_coupling[7].node2_name", - "$.node_coupling[800].node2_file", - "$.node_coupling[800].node2_name", - "$.node_coupling[801].coupling_strength", - "$.node_coupling[801].node2_file", - "$.node_coupling[801].node2_name", - "$.node_coupling[802].coupling_strength", - "$.node_coupling[802].node1_file", - "$.node_coupling[802].node1_name", - "$.node_coupling[802].node2_file", - "$.node_coupling[802].node2_name", - "$.node_coupling[803].node2_file", - "$.node_coupling[803].node2_name", - "$.node_coupling[804].node2_name", - "$.node_coupling[805].node2_file", - "$.node_coupling[805].node2_name", - "$.node_coupling[806].node1_file", - "$.node_coupling[806].node1_name", - "$.node_coupling[806].node2_name", - "$.node_coupling[807].coupling_strength", - "$.node_coupling[807].node2_file", - "$.node_coupling[807].node2_name", - "$.node_coupling[808].node2_name", - "$.node_coupling[809].node2_name", - "$.node_coupling[80].coupling_strength", - "$.node_coupling[80].node1_file", - "$.node_coupling[80].node1_name", - "$.node_coupling[80].node2_file", - "$.node_coupling[80].node2_name", - "$.node_coupling[810].node1_file", - "$.node_coupling[810].node1_name", - "$.node_coupling[810].node2_file", - "$.node_coupling[810].node2_name", - "$.node_coupling[811].node2_name", - "$.node_coupling[812].coupling_strength", - "$.node_coupling[812].node2_file", - "$.node_coupling[812].node2_name", - "$.node_coupling[813].coupling_strength", - "$.node_coupling[813].node1_file", - "$.node_coupling[813].node1_name", - "$.node_coupling[813].node2_file", - "$.node_coupling[813].node2_name", - "$.node_coupling[814].node2_file", - "$.node_coupling[814].node2_name", - "$.node_coupling[815].node2_file", - "$.node_coupling[815].node2_name", - "$.node_coupling[816].coupling_strength", - "$.node_coupling[816].node2_file", - "$.node_coupling[816].node2_name", - "$.node_coupling[817].node2_file", - "$.node_coupling[817].node2_name", - "$.node_coupling[818].coupling_strength", - "$.node_coupling[818].node2_file", - "$.node_coupling[818].node2_name", - "$.node_coupling[819].node2_name", - "$.node_coupling[81].node2_name", - "$.node_coupling[820].node2_name", - "$.node_coupling[821].node1_file", - "$.node_coupling[821].node1_name", - "$.node_coupling[821].node2_name", - "$.node_coupling[822].node1_file", - "$.node_coupling[822].node1_name", - "$.node_coupling[822].node2_name", - "$.node_coupling[823].coupling_strength", - "$.node_coupling[823].node2_file", - "$.node_coupling[823].node2_name", - "$.node_coupling[824].node2_name", - "$.node_coupling[825].coupling_strength", - "$.node_coupling[825].node2_file", - "$.node_coupling[825].node2_name", - "$.node_coupling[826].node2_name", - "$.node_coupling[827].node1_file", - "$.node_coupling[827].node1_name", - "$.node_coupling[827].node2_name", - "$.node_coupling[828].coupling_strength", - "$.node_coupling[828].node2_file", - "$.node_coupling[828].node2_name", - "$.node_coupling[829].coupling_strength", - "$.node_coupling[829].node2_file", - "$.node_coupling[829].node2_name", - "$.node_coupling[82].coupling_strength", - "$.node_coupling[82].node1_file", - "$.node_coupling[82].node1_name", - "$.node_coupling[82].node2_name", - "$.node_coupling[830].node2_file", - "$.node_coupling[830].node2_name", - "$.node_coupling[831].node2_name", - "$.node_coupling[832].node2_file", - "$.node_coupling[832].node2_name", - "$.node_coupling[833].coupling_strength", - "$.node_coupling[833].node2_file", - "$.node_coupling[833].node2_name", - "$.node_coupling[834].node2_name", - "$.node_coupling[835].node2_name", - "$.node_coupling[836].node2_name", - "$.node_coupling[837].coupling_strength", - "$.node_coupling[837].node2_file", - "$.node_coupling[837].node2_name", - "$.node_coupling[838].coupling_strength", - "$.node_coupling[838].node1_file", - "$.node_coupling[838].node1_name", - "$.node_coupling[838].node2_name", - "$.node_coupling[839].coupling_strength", - "$.node_coupling[839].node2_file", - "$.node_coupling[839].node2_name", - "$.node_coupling[83].coupling_strength", - "$.node_coupling[83].node1_file", - "$.node_coupling[83].node1_name", - "$.node_coupling[83].node2_file", - "$.node_coupling[83].node2_name", - "$.node_coupling[840].coupling_strength", - "$.node_coupling[840].node2_file", - "$.node_coupling[840].node2_name", - "$.node_coupling[841].coupling_strength", - "$.node_coupling[841].node2_file", - "$.node_coupling[841].node2_name", - "$.node_coupling[842].node2_file", - "$.node_coupling[842].node2_name", - "$.node_coupling[843].node2_name", - "$.node_coupling[844].node2_file", - "$.node_coupling[844].node2_name", - "$.node_coupling[845].coupling_strength", - "$.node_coupling[845].node2_file", - "$.node_coupling[845].node2_name", - "$.node_coupling[846].node2_name", - "$.node_coupling[847].coupling_strength", - "$.node_coupling[847].node1_file", - "$.node_coupling[847].node1_name", - "$.node_coupling[847].node2_name", - "$.node_coupling[848].node2_name", - "$.node_coupling[849].node2_name", - "$.node_coupling[84].coupling_strength", - "$.node_coupling[84].node2_file", - "$.node_coupling[84].node2_name", - "$.node_coupling[850].coupling_strength", - "$.node_coupling[850].node2_file", - "$.node_coupling[850].node2_name", - "$.node_coupling[851].node1_file", - "$.node_coupling[851].node1_name", - "$.node_coupling[851].node2_name", - "$.node_coupling[852].coupling_strength", - "$.node_coupling[852].node2_file", - "$.node_coupling[852].node2_name", - "$.node_coupling[853].node1_file", - "$.node_coupling[853].node1_name", - "$.node_coupling[853].node2_name", - "$.node_coupling[854].node1_file", - "$.node_coupling[854].node1_name", - "$.node_coupling[854].node2_file", - "$.node_coupling[854].node2_name", - "$.node_coupling[855].coupling_strength", - "$.node_coupling[855].node1_file", - "$.node_coupling[855].node1_name", - "$.node_coupling[855].node2_name", - "$.node_coupling[856].coupling_strength", - "$.node_coupling[856].node1_file", - "$.node_coupling[856].node1_name", - "$.node_coupling[856].node2_file", - "$.node_coupling[856].node2_name", - "$.node_coupling[857].node1_file", - "$.node_coupling[857].node1_name", - "$.node_coupling[857].node2_file", - "$.node_coupling[857].node2_name", - "$.node_coupling[858].coupling_strength", - "$.node_coupling[858].node2_name", - "$.node_coupling[859].coupling_strength", - "$.node_coupling[859].node2_name", - "$.node_coupling[85].coupling_strength", - "$.node_coupling[85].node2_file", - "$.node_coupling[85].node2_name", - "$.node_coupling[860].coupling_strength", - "$.node_coupling[860].node1_name", - "$.node_coupling[860].node2_name", - "$.node_coupling[861].coupling_strength", - "$.node_coupling[861].node1_name", - "$.node_coupling[861].node2_file", - "$.node_coupling[861].node2_name", - "$.node_coupling[862].coupling_strength", - "$.node_coupling[862].node1_name", - "$.node_coupling[862].node2_name", - "$.node_coupling[863].coupling_strength", - "$.node_coupling[863].node1_name", - "$.node_coupling[863].node2_name", - "$.node_coupling[864].coupling_strength", - "$.node_coupling[864].node1_name", - "$.node_coupling[864].node2_name", - "$.node_coupling[865].coupling_strength", - "$.node_coupling[865].node2_file", - "$.node_coupling[865].node2_name", - "$.node_coupling[866].coupling_strength", - "$.node_coupling[866].node2_name", - "$.node_coupling[867].coupling_strength", - "$.node_coupling[867].node2_name", - "$.node_coupling[868].coupling_strength", - "$.node_coupling[868].node2_name", - "$.node_coupling[869].coupling_strength", - "$.node_coupling[869].node2_name", - "$.node_coupling[86].coupling_strength", - "$.node_coupling[86].node2_name", - "$.node_coupling[870].coupling_strength", - "$.node_coupling[870].node2_name", - "$.node_coupling[871].coupling_strength", - "$.node_coupling[871].node1_file", - "$.node_coupling[871].node1_name", - "$.node_coupling[871].node2_name", - "$.node_coupling[872].coupling_strength", - "$.node_coupling[872].node1_name", - "$.node_coupling[872].node2_name", - "$.node_coupling[873].coupling_strength", - "$.node_coupling[873].node1_name", - "$.node_coupling[873].node2_file", - "$.node_coupling[873].node2_name", - "$.node_coupling[874].coupling_strength", - "$.node_coupling[874].node1_name", - "$.node_coupling[874].node2_name", - "$.node_coupling[875].coupling_strength", - "$.node_coupling[875].node1_name", - "$.node_coupling[875].node2_name", - "$.node_coupling[876].coupling_strength", - "$.node_coupling[876].node1_name", - "$.node_coupling[876].node2_file", - "$.node_coupling[876].node2_name", - "$.node_coupling[877].coupling_strength", - "$.node_coupling[877].node2_name", - "$.node_coupling[878].coupling_strength", - "$.node_coupling[878].node2_name", - "$.node_coupling[879].coupling_strength", - "$.node_coupling[879].node2_file", - "$.node_coupling[879].node2_name", - "$.node_coupling[87].coupling_strength", - "$.node_coupling[87].node2_file", - "$.node_coupling[87].node2_name", - "$.node_coupling[880].coupling_strength", - "$.node_coupling[880].node2_file", - "$.node_coupling[880].node2_name", - "$.node_coupling[881].coupling_strength", - "$.node_coupling[881].node2_file", - "$.node_coupling[881].node2_name", - "$.node_coupling[882].coupling_strength", - "$.node_coupling[882].node2_name", - "$.node_coupling[883].coupling_strength", - "$.node_coupling[883].node2_name", - "$.node_coupling[884].coupling_strength", - "$.node_coupling[884].node2_file", - "$.node_coupling[884].node2_name", - "$.node_coupling[885].coupling_strength", - "$.node_coupling[885].node1_name", - "$.node_coupling[885].node2_name", - "$.node_coupling[886].coupling_strength", - "$.node_coupling[886].node1_name", - "$.node_coupling[886].node2_name", - "$.node_coupling[887].coupling_strength", - "$.node_coupling[887].node1_name", - "$.node_coupling[887].node2_name", - "$.node_coupling[888].coupling_strength", - "$.node_coupling[888].node1_name", - "$.node_coupling[888].node2_name", - "$.node_coupling[889].coupling_strength", - "$.node_coupling[889].node1_name", - "$.node_coupling[889].node2_file", - "$.node_coupling[889].node2_name", - "$.node_coupling[88].coupling_strength", - "$.node_coupling[88].node2_file", - "$.node_coupling[88].node2_name", - "$.node_coupling[890].coupling_strength", - "$.node_coupling[890].node2_name", - "$.node_coupling[891].coupling_strength", - "$.node_coupling[891].node2_file", - "$.node_coupling[891].node2_name", - "$.node_coupling[892].coupling_strength", - "$.node_coupling[892].node2_name", - "$.node_coupling[893].coupling_strength", - "$.node_coupling[893].node2_file", - "$.node_coupling[893].node2_name", - "$.node_coupling[894].coupling_strength", - "$.node_coupling[894].node2_file", - "$.node_coupling[894].node2_name", - "$.node_coupling[895].coupling_strength", - "$.node_coupling[895].node1_file", - "$.node_coupling[895].node1_name", - "$.node_coupling[895].node2_file", - "$.node_coupling[895].node2_name", - "$.node_coupling[896].coupling_strength", - "$.node_coupling[896].node2_file", - "$.node_coupling[896].node2_name", - "$.node_coupling[897].coupling_strength", - "$.node_coupling[897].node2_name", - "$.node_coupling[898].coupling_strength", - "$.node_coupling[898].node2_name", - "$.node_coupling[899].coupling_strength", - "$.node_coupling[899].node1_file", - "$.node_coupling[899].node1_name", - "$.node_coupling[899].node2_name", - "$.node_coupling[89].coupling_strength", - "$.node_coupling[89].node1_file", - "$.node_coupling[89].node1_name", - "$.node_coupling[89].node2_file", - "$.node_coupling[89].node2_name", - "$.node_coupling[8].coupling_strength", - "$.node_coupling[8].node1_file", - "$.node_coupling[8].node1_name", - "$.node_coupling[8].node2_file", - "$.node_coupling[8].node2_name", - "$.node_coupling[900].coupling_strength", - "$.node_coupling[900].node1_file", - "$.node_coupling[900].node1_name", - "$.node_coupling[900].node2_name", - "$.node_coupling[901].coupling_strength", - "$.node_coupling[901].node1_file", - "$.node_coupling[901].node1_name", - "$.node_coupling[901].node2_file", - "$.node_coupling[901].node2_name", - "$.node_coupling[902].coupling_strength", - "$.node_coupling[902].node1_file", - "$.node_coupling[902].node1_name", - "$.node_coupling[902].node2_file", - "$.node_coupling[902].node2_name", - "$.node_coupling[903].coupling_strength", - "$.node_coupling[903].node1_file", - "$.node_coupling[903].node1_name", - "$.node_coupling[903].node2_name", - "$.node_coupling[904].coupling_strength", - "$.node_coupling[904].node1_file", - "$.node_coupling[904].node1_name", - "$.node_coupling[904].node2_name", - "$.node_coupling[905].coupling_strength", - "$.node_coupling[905].node2_file", - "$.node_coupling[905].node2_name", - "$.node_coupling[906].coupling_strength", - "$.node_coupling[906].node2_file", - "$.node_coupling[906].node2_name", - "$.node_coupling[907].node2_name", - "$.node_coupling[908].node2_name", - "$.node_coupling[909].coupling_strength", - "$.node_coupling[909].node2_file", - "$.node_coupling[909].node2_name", - "$.node_coupling[90].coupling_strength", - "$.node_coupling[90].node2_file", - "$.node_coupling[90].node2_name", - "$.node_coupling[910].coupling_strength", - "$.node_coupling[910].node2_file", - "$.node_coupling[910].node2_name", - "$.node_coupling[911].coupling_strength", - "$.node_coupling[911].node1_file", - "$.node_coupling[911].node1_name", - "$.node_coupling[911].node2_file", - "$.node_coupling[911].node2_name", - "$.node_coupling[912].coupling_strength", - "$.node_coupling[912].node2_file", - "$.node_coupling[912].node2_name", - "$.node_coupling[913].node1_file", - "$.node_coupling[913].node1_name", - "$.node_coupling[913].node2_file", - "$.node_coupling[913].node2_name", - "$.node_coupling[914].node1_file", - "$.node_coupling[914].node1_name", - "$.node_coupling[914].node2_name", - "$.node_coupling[915].node1_file", - "$.node_coupling[915].node1_name", - "$.node_coupling[915].node2_name", - "$.node_coupling[916].node1_file", - "$.node_coupling[916].node1_name", - "$.node_coupling[916].node2_name", - "$.node_coupling[917].coupling_strength", - "$.node_coupling[917].node2_file", - "$.node_coupling[917].node2_name", - "$.node_coupling[918].node2_name", - "$.node_coupling[919].node2_file", - "$.node_coupling[919].node2_name", - "$.node_coupling[91].coupling_strength", - "$.node_coupling[91].node2_file", - "$.node_coupling[91].node2_name", - "$.node_coupling[920].node2_name", - "$.node_coupling[921].node2_file", - "$.node_coupling[921].node2_name", - "$.node_coupling[922].node2_file", - "$.node_coupling[922].node2_name", - "$.node_coupling[923].node2_file", - "$.node_coupling[923].node2_name", - "$.node_coupling[924].node1_file", - "$.node_coupling[924].node1_name", - "$.node_coupling[924].node2_name", - "$.node_coupling[925].coupling_strength", - "$.node_coupling[925].node2_file", - "$.node_coupling[925].node2_name", - "$.node_coupling[926].coupling_strength", - "$.node_coupling[926].node2_file", - "$.node_coupling[926].node2_name", - "$.node_coupling[927].coupling_strength", - "$.node_coupling[927].node2_file", - "$.node_coupling[927].node2_name", - "$.node_coupling[928].coupling_strength", - "$.node_coupling[928].node2_file", - "$.node_coupling[928].node2_name", - "$.node_coupling[929].node2_file", - "$.node_coupling[929].node2_name", - "$.node_coupling[92].coupling_strength", - "$.node_coupling[92].node2_file", - "$.node_coupling[92].node2_name", - "$.node_coupling[930].node1_file", - "$.node_coupling[930].node1_name", - "$.node_coupling[930].node2_name", - "$.node_coupling[931].node2_file", - "$.node_coupling[931].node2_name", - "$.node_coupling[932].coupling_strength", - "$.node_coupling[932].node2_file", - "$.node_coupling[932].node2_name", - "$.node_coupling[933].node2_file", - "$.node_coupling[933].node2_name", - "$.node_coupling[934].coupling_strength", - "$.node_coupling[934].node2_file", - "$.node_coupling[934].node2_name", - "$.node_coupling[935].coupling_strength", - "$.node_coupling[935].node2_file", - "$.node_coupling[935].node2_name", - "$.node_coupling[936].node2_file", - "$.node_coupling[936].node2_name", - "$.node_coupling[937].node2_name", - "$.node_coupling[938].node2_file", - "$.node_coupling[938].node2_name", - "$.node_coupling[939].coupling_strength", - "$.node_coupling[939].node2_file", - "$.node_coupling[939].node2_name", - "$.node_coupling[93].coupling_strength", - "$.node_coupling[93].node2_file", - "$.node_coupling[93].node2_name", - "$.node_coupling[940].node2_name", - "$.node_coupling[941].node2_file", - "$.node_coupling[941].node2_name", - "$.node_coupling[942].node2_name", - "$.node_coupling[943].node2_name", - "$.node_coupling[944].node2_name", - "$.node_coupling[945].node2_name", - "$.node_coupling[946].coupling_strength", - "$.node_coupling[946].node2_file", - "$.node_coupling[946].node2_name", - "$.node_coupling[947].node1_file", - "$.node_coupling[947].node1_name", - "$.node_coupling[947].node2_name", - "$.node_coupling[948].node1_file", - "$.node_coupling[948].node1_name", - "$.node_coupling[948].node2_name", - "$.node_coupling[949].coupling_strength", - "$.node_coupling[949].node1_file", - "$.node_coupling[949].node1_name", - "$.node_coupling[949].node2_file", - "$.node_coupling[949].node2_name", - "$.node_coupling[94].coupling_strength", - "$.node_coupling[94].node2_file", - "$.node_coupling[94].node2_name", - "$.node_coupling[950].node2_file", - "$.node_coupling[950].node2_name", - "$.node_coupling[951].node2_file", - "$.node_coupling[951].node2_name", - "$.node_coupling[952].coupling_strength", - "$.node_coupling[952].node2_file", - "$.node_coupling[952].node2_name", - "$.node_coupling[953].coupling_strength", - "$.node_coupling[953].node2_file", - "$.node_coupling[953].node2_name", - "$.node_coupling[954].coupling_strength", - "$.node_coupling[954].node2_file", - "$.node_coupling[954].node2_name", - "$.node_coupling[955].node2_file", - "$.node_coupling[955].node2_name", - "$.node_coupling[956].coupling_strength", - "$.node_coupling[956].node2_file", - "$.node_coupling[956].node2_name", - "$.node_coupling[957].coupling_strength", - "$.node_coupling[957].node2_file", - "$.node_coupling[957].node2_name", - "$.node_coupling[958].node2_file", - "$.node_coupling[958].node2_name", - "$.node_coupling[959].node2_file", - "$.node_coupling[959].node2_name", - "$.node_coupling[95].coupling_strength", - "$.node_coupling[95].node2_file", - "$.node_coupling[95].node2_name", - "$.node_coupling[960].node2_file", - "$.node_coupling[960].node2_name", - "$.node_coupling[961].node2_name", - "$.node_coupling[962].coupling_strength", - "$.node_coupling[962].node2_file", - "$.node_coupling[962].node2_name", - "$.node_coupling[963].node2_name", - "$.node_coupling[964].coupling_strength", - "$.node_coupling[964].node2_file", - "$.node_coupling[964].node2_name", - "$.node_coupling[965].node2_name", - "$.node_coupling[966].coupling_strength", - "$.node_coupling[966].node1_file", - "$.node_coupling[966].node1_name", - "$.node_coupling[966].node2_file", - "$.node_coupling[966].node2_name", - "$.node_coupling[967].coupling_strength", - "$.node_coupling[967].node2_file", - "$.node_coupling[967].node2_name", - "$.node_coupling[968].coupling_strength", - "$.node_coupling[968].node1_name", - "$.node_coupling[968].node2_file", - "$.node_coupling[968].node2_name", - "$.node_coupling[969].coupling_strength", - "$.node_coupling[969].node1_name", - "$.node_coupling[969].node2_file", - "$.node_coupling[969].node2_name", - "$.node_coupling[96].node1_file", - "$.node_coupling[96].node1_name", - "$.node_coupling[96].node2_file", - "$.node_coupling[96].node2_name", - "$.node_coupling[970].node1_name", - "$.node_coupling[970].node2_file", - "$.node_coupling[970].node2_name", - "$.node_coupling[971].node1_name", - "$.node_coupling[971].node2_file", - "$.node_coupling[971].node2_name", - "$.node_coupling[972].node1_name", - "$.node_coupling[972].node2_name", - "$.node_coupling[973].node1_file", - "$.node_coupling[973].node1_name", - "$.node_coupling[973].node2_name", - "$.node_coupling[974].coupling_strength", - "$.node_coupling[974].node2_file", - "$.node_coupling[974].node2_name", - "$.node_coupling[975].coupling_strength", - "$.node_coupling[975].node2_file", - "$.node_coupling[975].node2_name", - "$.node_coupling[976].node2_file", - "$.node_coupling[976].node2_name", - "$.node_coupling[977].coupling_strength", - "$.node_coupling[977].node2_file", - "$.node_coupling[977].node2_name", - "$.node_coupling[978].node2_name", - "$.node_coupling[979].node2_name", - "$.node_coupling[97].coupling_strength", - "$.node_coupling[97].node1_file", - "$.node_coupling[97].node1_name", - "$.node_coupling[97].node2_file", - "$.node_coupling[97].node2_name", - "$.node_coupling[980].coupling_strength", - "$.node_coupling[980].node2_file", - "$.node_coupling[980].node2_name", - "$.node_coupling[981].node2_file", - "$.node_coupling[981].node2_name", - "$.node_coupling[982].coupling_strength", - "$.node_coupling[982].node2_file", - "$.node_coupling[982].node2_name", - "$.node_coupling[983].node2_file", - "$.node_coupling[983].node2_name", - "$.node_coupling[984].node2_file", - "$.node_coupling[984].node2_name", - "$.node_coupling[985].node2_name", - "$.node_coupling[986].coupling_strength", - "$.node_coupling[986].node2_file", - "$.node_coupling[986].node2_name", - "$.node_coupling[987].coupling_strength", - "$.node_coupling[987].node1_file", - "$.node_coupling[987].node1_name", - "$.node_coupling[987].node2_file", - "$.node_coupling[987].node2_name", - "$.node_coupling[988].node2_file", - "$.node_coupling[988].node2_name", - "$.node_coupling[989].coupling_strength", - "$.node_coupling[989].node2_file", - "$.node_coupling[989].node2_name", - "$.node_coupling[98].coupling_strength", - "$.node_coupling[98].node2_file", - "$.node_coupling[98].node2_name", - "$.node_coupling[990].node2_name", - "$.node_coupling[991].coupling_strength", - "$.node_coupling[991].node1_file", - "$.node_coupling[991].node1_name", - "$.node_coupling[991].node2_file", - "$.node_coupling[991].node2_name", - "$.node_coupling[992].coupling_strength", - "$.node_coupling[992].node2_file", - "$.node_coupling[992].node2_name", - "$.node_coupling[993].coupling_strength", - "$.node_coupling[993].node1_file", - "$.node_coupling[993].node1_name", - "$.node_coupling[993].node2_file", - "$.node_coupling[993].node2_name", - "$.node_coupling[994].coupling_strength", - "$.node_coupling[994].node2_file", - "$.node_coupling[994].node2_name", - "$.node_coupling[995].coupling_strength", - "$.node_coupling[995].node2_file", - "$.node_coupling[995].node2_name", - "$.node_coupling[996].coupling_strength", - "$.node_coupling[996].node2_file", - "$.node_coupling[996].node2_name", - "$.node_coupling[997].node2_file", - "$.node_coupling[997].node2_name", - "$.node_coupling[998].coupling_strength", - "$.node_coupling[998].node2_file", - "$.node_coupling[998].node2_name", - "$.node_coupling[999].coupling_strength", - "$.node_coupling[999].node2_file", - "$.node_coupling[999].node2_name", - "$.node_coupling[99].coupling_strength", - "$.node_coupling[99].node2_file", - "$.node_coupling[99].node2_name", - "$.node_coupling[9].coupling_strength", - "$.node_coupling[9].node1_file", - "$.node_coupling[9].node1_name", - "$.node_coupling[9].node2_file", - "$.node_coupling[9].node2_name", - "$.node_hotness[0].change_count", - "$.node_hotness[0].coupled_nodes", - "$.node_hotness[10].coupled_nodes", - "$.node_hotness[10].file", - "$.node_hotness[10].hotness_score", - "$.node_hotness[10].name", - "$.node_hotness[11].coupled_nodes", - "$.node_hotness[11].file", - "$.node_hotness[11].hotness_score", - "$.node_hotness[11].name", - "$.node_hotness[12].change_count", - "$.node_hotness[12].coupled_nodes", - "$.node_hotness[12].file", - "$.node_hotness[12].hotness_score", - "$.node_hotness[12].name", - "$.node_hotness[12].type", - "$.node_hotness[13].change_count", - "$.node_hotness[13].coupled_nodes", - "$.node_hotness[13].file", - "$.node_hotness[13].hotness_score", - "$.node_hotness[13].name", - "$.node_hotness[13].type", - "$.node_hotness[14].change_count", - "$.node_hotness[14].coupled_nodes", - "$.node_hotness[14].file", - "$.node_hotness[14].hotness_score", - "$.node_hotness[14].name", - "$.node_hotness[15].coupled_nodes", - "$.node_hotness[15].file", - "$.node_hotness[15].hotness_score", - "$.node_hotness[15].name", - "$.node_hotness[16].file", - "$.node_hotness[16].hotness_score", - "$.node_hotness[16].name", - "$.node_hotness[16].type", - "$.node_hotness[17].file", - "$.node_hotness[17].hotness_score", - "$.node_hotness[17].name", - "$.node_hotness[17].type", - "$.node_hotness[18].file", - "$.node_hotness[18].hotness_score", - "$.node_hotness[18].name", - "$.node_hotness[19].file", - "$.node_hotness[19].hotness_score", - "$.node_hotness[19].name", - "$.node_hotness[19].type", - "$.node_hotness[1].change_count", - "$.node_hotness[1].coupled_nodes", - "$.node_hotness[1].hotness_score", - "$.node_hotness[1].name", - "$.node_hotness[20].file", - "$.node_hotness[20].hotness_score", - "$.node_hotness[20].name", - "$.node_hotness[20].type", - "$.node_hotness[21].file", - "$.node_hotness[21].hotness_score", - "$.node_hotness[21].name", - "$.node_hotness[21].type", - "$.node_hotness[22].hotness_score", - "$.node_hotness[22].name", - "$.node_hotness[22].type", - "$.node_hotness[23].file", - "$.node_hotness[23].hotness_score", - "$.node_hotness[23].name", - "$.node_hotness[23].type", - "$.node_hotness[24].hotness_score", - "$.node_hotness[24].name", - "$.node_hotness[25].coupled_nodes", - "$.node_hotness[25].file", - "$.node_hotness[25].hotness_score", - "$.node_hotness[25].name", - "$.node_hotness[26].file", - "$.node_hotness[26].hotness_score", - "$.node_hotness[26].name", - "$.node_hotness[26].type", - "$.node_hotness[27].file", - "$.node_hotness[27].hotness_score", - "$.node_hotness[27].name", - "$.node_hotness[28].file", - "$.node_hotness[28].hotness_score", - "$.node_hotness[28].name", - "$.node_hotness[28].type", - "$.node_hotness[29].file", - "$.node_hotness[29].hotness_score", - "$.node_hotness[29].name", - "$.node_hotness[2].change_count", - "$.node_hotness[2].coupled_nodes", - "$.node_hotness[2].hotness_score", - "$.node_hotness[2].name", - "$.node_hotness[30].file", - "$.node_hotness[30].hotness_score", - "$.node_hotness[30].name", - "$.node_hotness[30].type", - "$.node_hotness[31].file", - "$.node_hotness[31].hotness_score", - "$.node_hotness[31].name", - "$.node_hotness[32].file", - "$.node_hotness[32].hotness_score", - "$.node_hotness[32].name", - "$.node_hotness[33].file", - "$.node_hotness[33].hotness_score", - "$.node_hotness[33].name", - "$.node_hotness[34].coupled_nodes", - "$.node_hotness[34].file", - "$.node_hotness[34].hotness_score", - "$.node_hotness[34].name", - "$.node_hotness[34].type", - "$.node_hotness[35].file", - "$.node_hotness[35].hotness_score", - "$.node_hotness[35].name", - "$.node_hotness[35].type", - "$.node_hotness[36].file", - "$.node_hotness[36].hotness_score", - "$.node_hotness[36].name", - "$.node_hotness[36].type", - "$.node_hotness[37].file", - "$.node_hotness[37].hotness_score", - "$.node_hotness[37].name", - "$.node_hotness[37].type", - "$.node_hotness[38].hotness_score", - "$.node_hotness[38].name", - "$.node_hotness[39].file", - "$.node_hotness[39].hotness_score", - "$.node_hotness[39].name", - "$.node_hotness[3].change_count", - "$.node_hotness[3].coupled_nodes", - "$.node_hotness[3].file", - "$.node_hotness[3].hotness_score", - "$.node_hotness[3].name", - "$.node_hotness[40].file", - "$.node_hotness[40].hotness_score", - "$.node_hotness[40].name", - "$.node_hotness[40].type", - "$.node_hotness[41].file", - "$.node_hotness[41].hotness_score", - "$.node_hotness[41].name", - "$.node_hotness[41].type", - "$.node_hotness[42].hotness_score", - "$.node_hotness[42].name", - "$.node_hotness[43].hotness_score", - "$.node_hotness[43].name", - "$.node_hotness[44].coupled_nodes", - "$.node_hotness[44].file", - "$.node_hotness[44].hotness_score", - "$.node_hotness[44].name", - "$.node_hotness[44].type", - "$.node_hotness[45].hotness_score", - "$.node_hotness[45].name", - "$.node_hotness[46].file", - "$.node_hotness[46].hotness_score", - "$.node_hotness[46].name", - "$.node_hotness[46].type", - "$.node_hotness[47].file", - "$.node_hotness[47].hotness_score", - "$.node_hotness[47].name", - "$.node_hotness[47].type", - "$.node_hotness[48].file", - "$.node_hotness[48].hotness_score", - "$.node_hotness[48].name", - "$.node_hotness[48].type", - "$.node_hotness[49].hotness_score", - "$.node_hotness[49].name", - "$.node_hotness[4].change_count", - "$.node_hotness[4].coupled_nodes", - "$.node_hotness[4].file", - "$.node_hotness[4].hotness_score", - "$.node_hotness[4].name", - "$.node_hotness[4].type", - "$.node_hotness[50].hotness_score", - "$.node_hotness[50].name", - "$.node_hotness[51].hotness_score", - "$.node_hotness[51].name", - "$.node_hotness[52].hotness_score", - "$.node_hotness[52].name", - "$.node_hotness[53].coupled_nodes", - "$.node_hotness[53].file", - "$.node_hotness[53].hotness_score", - "$.node_hotness[53].name", - "$.node_hotness[54].file", - "$.node_hotness[54].hotness_score", - "$.node_hotness[54].name", - "$.node_hotness[54].type", - "$.node_hotness[55].file", - "$.node_hotness[55].hotness_score", - "$.node_hotness[55].name", - "$.node_hotness[56].file", - "$.node_hotness[56].hotness_score", - "$.node_hotness[56].name", - "$.node_hotness[56].type", - "$.node_hotness[57].file", - "$.node_hotness[57].hotness_score", - "$.node_hotness[57].name", - "$.node_hotness[57].type", - "$.node_hotness[58].hotness_score", - "$.node_hotness[58].name", - "$.node_hotness[59].coupled_nodes", - "$.node_hotness[59].file", - "$.node_hotness[59].hotness_score", - "$.node_hotness[59].name", - "$.node_hotness[5].change_count", - "$.node_hotness[5].coupled_nodes", - "$.node_hotness[5].hotness_score", - "$.node_hotness[5].name", - "$.node_hotness[60].coupled_nodes", - "$.node_hotness[60].file", - "$.node_hotness[60].hotness_score", - "$.node_hotness[60].name", - "$.node_hotness[60].type", - "$.node_hotness[61].file", - "$.node_hotness[61].hotness_score", - "$.node_hotness[61].name", - "$.node_hotness[61].type", - "$.node_hotness[62].hotness_score", - "$.node_hotness[62].name", - "$.node_hotness[63].hotness_score", - "$.node_hotness[63].name", - "$.node_hotness[64].file", - "$.node_hotness[64].hotness_score", - "$.node_hotness[64].name", - "$.node_hotness[64].type", - "$.node_hotness[65].file", - "$.node_hotness[65].hotness_score", - "$.node_hotness[65].name", - "$.node_hotness[65].type", - "$.node_hotness[66].file", - "$.node_hotness[66].hotness_score", - "$.node_hotness[66].name", - "$.node_hotness[66].type", - "$.node_hotness[67].file", - "$.node_hotness[67].hotness_score", - "$.node_hotness[67].name", - "$.node_hotness[67].type", - "$.node_hotness[68].file", - "$.node_hotness[68].hotness_score", - "$.node_hotness[68].name", - "$.node_hotness[69].file", - "$.node_hotness[69].hotness_score", - "$.node_hotness[69].name", - "$.node_hotness[69].type", - "$.node_hotness[6].coupled_nodes", - "$.node_hotness[6].file", - "$.node_hotness[6].hotness_score", - "$.node_hotness[6].name", - "$.node_hotness[6].type", - "$.node_hotness[70].file", - "$.node_hotness[70].hotness_score", - "$.node_hotness[70].name", - "$.node_hotness[70].type", - "$.node_hotness[71].coupled_nodes", - "$.node_hotness[71].file", - "$.node_hotness[71].hotness_score", - "$.node_hotness[71].name", - "$.node_hotness[71].type", - "$.node_hotness[72].file", - "$.node_hotness[72].hotness_score", - "$.node_hotness[72].name", - "$.node_hotness[72].type", - "$.node_hotness[73].file", - "$.node_hotness[73].hotness_score", - "$.node_hotness[73].name", - "$.node_hotness[73].type", - "$.node_hotness[74].change_count", - "$.node_hotness[74].coupled_nodes", - "$.node_hotness[74].file", - "$.node_hotness[74].hotness_score", - "$.node_hotness[74].name", - "$.node_hotness[75].coupled_nodes", - "$.node_hotness[75].file", - "$.node_hotness[75].hotness_score", - "$.node_hotness[75].name", - "$.node_hotness[75].type", - "$.node_hotness[76].coupled_nodes", - "$.node_hotness[76].file", - "$.node_hotness[76].hotness_score", - "$.node_hotness[76].name", - "$.node_hotness[77].coupled_nodes", - "$.node_hotness[77].file", - "$.node_hotness[77].hotness_score", - "$.node_hotness[77].name", - "$.node_hotness[77].type", - "$.node_hotness[78].coupled_nodes", - "$.node_hotness[78].hotness_score", - "$.node_hotness[78].name", - "$.node_hotness[78].type", - "$.node_hotness[79].coupled_nodes", - "$.node_hotness[79].file", - "$.node_hotness[79].hotness_score", - "$.node_hotness[79].name", - "$.node_hotness[79].type", - "$.node_hotness[7].coupled_nodes", - "$.node_hotness[7].file", - "$.node_hotness[7].hotness_score", - "$.node_hotness[7].name", - "$.node_hotness[7].type", - "$.node_hotness[80].coupled_nodes", - "$.node_hotness[80].file", - "$.node_hotness[80].hotness_score", - "$.node_hotness[80].name", - "$.node_hotness[80].type", - "$.node_hotness[81].coupled_nodes", - "$.node_hotness[81].hotness_score", - "$.node_hotness[81].name", - "$.node_hotness[81].type", - "$.node_hotness[82].coupled_nodes", - "$.node_hotness[82].file", - "$.node_hotness[82].hotness_score", - "$.node_hotness[82].name", - "$.node_hotness[82].type", - "$.node_hotness[83].change_count", - "$.node_hotness[83].coupled_nodes", - "$.node_hotness[83].file", - "$.node_hotness[83].hotness_score", - "$.node_hotness[83].name", - "$.node_hotness[83].type", - "$.node_hotness[84].change_count", - "$.node_hotness[84].coupled_nodes", - "$.node_hotness[84].file", - "$.node_hotness[84].hotness_score", - "$.node_hotness[84].name", - "$.node_hotness[84].type", - "$.node_hotness[85].change_count", - "$.node_hotness[85].coupled_nodes", - "$.node_hotness[85].file", - "$.node_hotness[85].hotness_score", - "$.node_hotness[85].name", - "$.node_hotness[85].type", - "$.node_hotness[86].change_count", - "$.node_hotness[86].coupled_nodes", - "$.node_hotness[86].file", - "$.node_hotness[86].hotness_score", - "$.node_hotness[86].name", - "$.node_hotness[86].type", - "$.node_hotness[87].change_count", - "$.node_hotness[87].coupled_nodes", - "$.node_hotness[87].file", - "$.node_hotness[87].hotness_score", - "$.node_hotness[87].name", - "$.node_hotness[87].type", - "$.node_hotness[88].change_count", - "$.node_hotness[88].coupled_nodes", - "$.node_hotness[88].file", - "$.node_hotness[88].hotness_score", - "$.node_hotness[88].name", - "$.node_hotness[88].type", - "$.node_hotness[8].change_count", - "$.node_hotness[8].file", - "$.node_hotness[8].hotness_score", - "$.node_hotness[8].name", - "$.node_hotness[8].type", - "$.node_hotness[9].coupled_nodes", - "$.node_hotness[9].file", - "$.node_hotness[9].hotness_score", - "$.node_hotness[9].name", - "$.node_hotness[9].type" - ], - "note": "Go MEASURED VARIANT on these fields (evidence = the differing Go observations). Canonicalization here is measured, not declared." - }, - { - "label": "history/typos", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/typos", - "--format", - "json", - "--limit", - "30", - "/home/dmitriy/sources/hercules" - ], - "oracle_verdict": "PASS", - "field_counts": { - "stable": 36, - "variant": 0 - }, - "n_variant_fields": 0, - "canonical_go_stable": true, - "go_run_shas": [ - "0fcdfa576831aac05c88b50f96c548810738bac5f42a74d16004def2e8c726ca", - "0fcdfa576831aac05c88b50f96c548810738bac5f42a74d16004def2e8c726ca", - "0fcdfa576831aac05c88b50f96c548810738bac5f42a74d16004def2e8c726ca" - ], - "evidence_sample": {}, - "evidence_field_paths": [], - "note": "Go MEASURED STABLE here across N runs -> NOTHING may be normalized. Recorded so a future blank is detectable." - }, - { - "label": "static/comments", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/comments", - "--format", - "json" - ], - "oracle_verdict": "PASS", - "field_counts": { - "stable": 4344, - "variant": 0 - }, - "n_variant_fields": 0, - "canonical_go_stable": true, - "go_run_shas": [ - "22d14f47459c95e8e9a8f6817e31c52c71904e253e02a854a2153f7960e06a7e", - "22d14f47459c95e8e9a8f6817e31c52c71904e253e02a854a2153f7960e06a7e", - "22d14f47459c95e8e9a8f6817e31c52c71904e253e02a854a2153f7960e06a7e" - ], - "evidence_sample": {}, - "evidence_field_paths": [], - "note": "Go MEASURED STABLE here across N runs -> NOTHING may be normalized. Recorded so a future blank is detectable." - } - ], - "go_variant_capture_status": "harvested live", - "honesty_note": "Coverage % measures REACH (did a test touch this code/cell/language). It is reported ALONGSIDE the differential oracle verdict, never as a substitute (SPEC \u00a76). 'Done' requires the oracle verdict GREEN on off-corpus inputs, NOT a high coverage number. Every Go-variant capture stores the differing Go observations as evidence so canonicalization is measured, not declared." -} \ No newline at end of file diff --git a/tests/compat/matrix.toml b/tests/compat/matrix.toml deleted file mode 100644 index 28178d1d..00000000 --- a/tests/compat/matrix.toml +++ /dev/null @@ -1,135 +0,0 @@ -# ============================================================================= -# COMPATIBILITY INVOCATION MATRIX (SPEC: specs/go-compat-testing/SPEC.md §3.2) -# ============================================================================= -# The MEANINGFUL cross-product: -# {analyzer} x {output format} x {key flags} x {analyzer sets} -# plus the per-language uast surface ({uast subcommand} x {language file}). -# -# Every expanded cell is wired to the LIVE GO ORACLE (oracle/oracle.py). The -# expander (expand_matrix.py) cross-multiplies the axes below, substitutes corpus -# inputs (content-addressed files + real repos from corpus/manifest.json), and -# emits one oracle invocation per cell. Cells the LIVE Go binary leaves empty are -# recorded as expected-empty CONTRACTS (kind="expected-empty"), never skipped. -# -# This file is DATA only. It declares the axes; it does NOT decide pass/fail and -# it NEVER declares any field stable/variant -- that is measured by the oracle. -# Shrinking any axis here is detectable by the tamper layer (matrix hashing). - -[meta] -spec = "specs/go-compat-testing/SPEC.md" -phase = "MatrixCorpus (roadmap 3)" -# Pinned env applied by the oracle to BOTH binaries. -env = "set -f; TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800" -# Common flags that make a `run` invocation hermetic/deterministic & cheap. -run_base = ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1"] -static_base = ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1"] - -# ----------------------------------------------------------------------------- -# AXIS: analyzers (from `codefang run --list-analyzers`: 7 static + 10 history) -# ----------------------------------------------------------------------------- -[analyzers] -static = ["static/clones", "static/complexity", "static/comments", - "static/halstead", "static/cohesion", "static/imports", - "static/composition"] -history = ["history/sentiment", "history/shotness", "history/typos", - "history/anomaly", "history/burndown", "history/imports", - "history/quality", "history/couples", "history/devs", - "history/file-history"] - -# Analyzer SETS (the '*' and pairs the SPEC calls out explicitly). -[analyzer_sets] -sets = [ - "*", # everything at once - "static/*", # all static - "history/*", # all history - "static/complexity,static/halstead", # static pair - "history/devs,history/burndown", # history pair - "static/complexity,history/devs", # cross pair -] - -# ----------------------------------------------------------------------------- -# AXIS: output formats (from `run --help`: json,yaml,bin,compact,text,ndjson, -# timeseries; plus timeseries+ndjson combo). plot/tree/count handled in uast. -# ----------------------------------------------------------------------------- -[formats] -# formats valid for `codefang run` -run = ["json", "yaml", "bin", "compact", "text", "ndjson", "timeseries"] -# combination cell: timeseries + --ndjson (one JSON line per commit) -run_combo = ["timeseries+ndjson"] -# uast surface formats (parse: json,compact,tree,none ; query: json,compact,count) -uast_parse = ["json", "compact", "tree"] -uast_query = ["json", "compact", "count"] -uast_analyze = ["json", "text"] - -# ----------------------------------------------------------------------------- -# AXIS: key flags (the SPEC's named subset that changes observable output) -# ----------------------------------------------------------------------------- -[flags] -# history-affecting flags; each is one matrix cell against the base history run. -# -# DIVERGENCE: the `--limit:*` and `--since:*` cells below are compared against -# the Go oracle, whose window is the N OLDEST commits and which aborts on a -# partial `--since` range. codefang deliberately selects the N NEWEST commits -# and supports partial ranges, so those cells are EXPECTED to differ and must -# not be read as port defects (specs/frds/FRD-newest-commit-window.md). -history = [ - "--head", - "--limit:50", - "--first-parent", - "--since:2020-01-01", - "--include-vendored", - "--include-generated", - "--languages:Go", -] -# static-affecting flags ("--flag:value" splits on the FIRST ':' only, so a -# value may itself contain commas, e.g. "--languages:Go,Python") -static = [ - "--per-file", - "--include-vendored", - "--include-generated", - "--languages:Go", - "--languages:Go,Python", - "--languages:all", -] - -# ----------------------------------------------------------------------------- -# AXIS: uast subcommands x corpus languages (the per-language parse/query -# contract). Inputs are substituted from corpus/manifest.json "files". -# ----------------------------------------------------------------------------- -[uast] -subcommands = ["parse", "analyze", "query"] -# default query DSL used for the query subcommand cells -query_dsl = 'filter(.roles has "Function")' - -# ----------------------------------------------------------------------------- -# CORPUS BINDING: which corpus entries feed which cell family. The expander -# resolves these against corpus/manifest.json (content-addressed). -# ----------------------------------------------------------------------------- -[corpus] -# repos used for history/static analyzer cells (smoke tier uses the small repo -# with a small --limit so the smoke run is fast; full tier sweeps all repos). -smoke_repo = "hercules" -smoke_limit = 20 -full_repos = ["hercules", "ioq3", "kubernetes"] -# per-language uast cells use ALL mined files (every supported language). -uast_files = "ALL" - -# ----------------------------------------------------------------------------- -# TIERS: smoke (pre-commit, fast) vs full (scheduled). Tier only controls which -# repos/limits are swept -- it NEVER blanks a field and NEVER drops an analyzer. -# ----------------------------------------------------------------------------- -[tiers.smoke] -repos = ["hercules"] -history_limit = 20 -# representative format coverage in smoke (json + the two binding binary-ish ones) -run_formats = ["json", "bin", "yaml"] -include_flags = false -# smoke gets a STATIC-only flag slice (static cells on the smoke repo need no -# revwalk, so they are fast): flag x format regressions are probed pre-commit. -include_static_flags = true - -[tiers.full] -repos = ["hercules", "ioq3", "kubernetes"] -history_limit = 50 -run_formats = ["json", "yaml", "bin", "compact", "text", "ndjson", "timeseries"] -include_flags = true diff --git a/tests/compat/metamorphic/README.md b/tests/compat/metamorphic/README.md deleted file mode 100644 index 19edc7c7..00000000 --- a/tests/compat/metamorphic/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Metamorphic / Anti-Simulation Layer - -SPEC: `specs/go-compat-testing/SPEC.md` §3.6, roadmap item 4. - -Per-analyzer **relational** checks that fail hardcoded stubs. Where the oracle -(`../oracle/oracle.py`) compares Rust bytes to Go bytes directly, this layer -asserts relations *between* Rust outputs across different invocations — the -relations a real analyzer obeys and a constant/stub does not. - -## The Go binary is the oracle for every PREMISE - -We never assert "Rust must differ" from a re-derived expectation. We assert -"Rust must differ **because Go differs** on these same two inputs." If Go does -not exhibit the relation here (Go identical on both inputs, Go does not grow with -`--limit`, Go empty), the premise is absent and the check is recorded `NA` — it -never fakes a verdict. This keeps the live Go binary as the source of truth -(rule #1) and uses freshly-substituted mined corpus inputs (rule #2). Process -launching is delegated to `oracle.run_once`, so both binaries run under the same -pinned env (`TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800`) and -only STDOUT is compared (rule #5). - -## Properties (task brief §3.6 a–e) - -| id | property | failure verdict | signature it catches | -|----|----------|-----------------|----------------------| -| a | **vary-input** — Rust differs on 2 inputs **where Go differs** | SIM | hardcoded constant | -| b | **grow-limit** — Rust grows with `--limit` 10→500 **where Go grows** | SIM (constant) / FAIL (shrinks) | closed-form stub | -| c | **determinism** — identical args ⇒ identical Rust bytes | FAIL | nondeterministic port | -| d | **non-empty** — Rust non-empty **where Go non-empty** | FAIL | 0-byte stub / not ported | -| e | **golden-drift** — Rust ≠ recorded golden once the input changed (and Go changed) | SIM | memorized golden constant | - -Every SIM prints the **two inputs and the two equal Rust outputs** that triggered -it (task brief requirement). FAILs print the failing invocation + the violated -property. Results: `results.json`. Recorded golden constants (for drift): -`golden_constants.json`, each with input provenance. - -## Run - -```sh -python3 metamorphic.py --tier smoke # against the live binaries -python3 selftest/self_test.py # scripted self-proof (rule #6) -python3 selftest/live_constant_test.py # LIVE constant-stub self-proof -``` - -## Self-proof (rule #6 — "must SELF-PROVE it catches a defect") - -`selftest/self_test.py` monkeypatches the live-binary launcher -(`oracle.run_once`) with scripted Go/Rust outputs and asserts each planted defect -class is reported non-PASS, and each honest control is PASS/NA (so the FAILs are -meaningful, not a stuck "always FAIL"): - -- P-a vary-input constant → SIM · P-b grow-limit constant → SIM · - P-b2 grow-limit shrink → FAIL · P-c nondeterminism → FAIL · - P-d empty stub → FAIL · P-e golden-drift → SIM -- Controls: honest vary-input → PASS, go-identical → NA, honest grow → PASS, - go-saturates → NA, deterministic → PASS, non-empty → PASS, honest drift → PASS - -Current scripted self-proof: **13/13 checks correct**. - -`selftest/live_constant_test.py` is the brief's required end-to-end proof: it keeps -the **LIVE Go binary** as the real oracle (Go genuinely distinguishes the mined -`hercules` vs `kubernetes` repos) and replaces ONLY the Rust side with a throwaway -constant-emitting executable. It asserts (1) the constant stub is flagged **SIM** -by vary-input, (2) the stub is non-empty (so it is the *relation*, not emptiness, -that catches it), and (3) the **real** `history/devs` analyzer **PASSes** on the -same inputs (no cry-wolf). Current live self-proof: **3/3 checks correct**. - -## Current live result (snapshot) - -`PASS=26 SIM=2 FAIL=15 NA=3` → layer is **RED**, correctly: it surfaces genuine -Rust port gaps it must not hide — -- `uast parse` only has the Go grammar wired; C/Python/Rust/TS/TSX/JS/JSON/YAML/ - C++/shell error "no tree-sitter grammar wired" and emit empty stdout while Go - produces full UAST (FAIL non-empty; the c-vs-rust and ts-vs-js pairs also trip - vary-input SIM because both sides are empty); -- analyzers `static/clones`, `static/cohesion`, `history/anomaly`, - `history/file-history` emit nothing where Go computes large reports. - -These are real incompleteness, verified directly against the binaries — not -harness artifacts. diff --git a/tests/compat/metamorphic/golden_constants.json b/tests/compat/metamorphic/golden_constants.json deleted file mode 100644 index e81a41a9..00000000 --- a/tests/compat/metamorphic/golden_constants.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "history/devs|hercules->kube": { - "input": "/home/dmitriy/sources/hercules@lim20", - "len": 3845, - "sha": "a320ae7c95fcc6f58f8e276cb6d3fce79fad145d1ad8254e81885ad234abb7f3" - }, - "history/imports|hercules->kube": { - "input": "/home/dmitriy/sources/hercules@lim20", - "len": 167, - "sha": "ce55f5e60f641da9a6e24fedc06a465e109448cf19c326c3978c69a141f8862a" - } -} \ No newline at end of file diff --git a/tests/compat/metamorphic/metamorphic.py b/tests/compat/metamorphic/metamorphic.py deleted file mode 100644 index 5ab71d16..00000000 --- a/tests/compat/metamorphic/metamorphic.py +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/env python3 -""" -METAMORPHIC / ANTI-SIMULATION LAYER -(SPEC: specs/go-compat-testing/SPEC.md §3.6, roadmap 4) - -Per-analyzer RELATIONAL checks that FAIL hardcoded stubs. Unlike the oracle -(which compares Rust bytes to Go bytes directly), these checks assert RELATIONS -between Rust outputs across DIFFERENT invocations -- the relations a real -analyzer obeys and a constant/stub does not. - -THE GO BINARY IS THE ORACLE FOR THE PREMISE OF EVERY CHECK. We never assert -"Rust output must differ" from a re-derived expectation; we assert "Rust output -must differ BECAUSE Go's output differs on these same two inputs". If Go does -NOT exhibit the relation (e.g. Go does not grow with --limit on this cell, Go is -empty here), the premise is absent and the check is recorded as N/A -- it never -fakes a verdict. This keeps the live Go binary as the source of truth (rule #1) -and uses freshly-substituted corpus inputs the author never hand-picked (rule #2). - -Properties implemented (task brief §3.6 a-e): - - (a) VARY-INPUT : output DIFFERS for two different inputs WHERE GO DIFFERS. - Rust giving identical bytes for two inputs that Go - distinguishes is the hardcoded-constant signature => SIM. - (b) GROW-LIMIT : output GROWS monotonically with --limit (10 vs 500) - WHERE GO GROWS. Rust constant across limits while Go grows - is the closed-form-stub signature => SIM. - (c) DETERMINISM : identical args => identical Rust bytes across repeated runs. - A faithful port is deterministic; differing bytes => FAIL. - (d) NON-EMPTY : Rust is NON-EMPTY wherever GO is NON-EMPTY. A 0-byte stub - where Go computes something => FAIL. - (e) GOLDEN-DRIFT : once the INPUT changed, Rust output never equals a - previously-recorded golden constant. A frozen golden that - survives an input change is a memorized constant => SIM. - -Any failure prints, for SIM, the TWO INPUTS and the TWO EQUAL RUST OUTPUTS that -triggered it (task brief: "printing the two inputs and the two equal Rust -outputs"); for FAIL, the failing invocation + the missing/inconsistent property. - -Verdicts: PASS (relation holds), SIM (simulation suspect), FAIL (determinism / -non-emptiness violated), NA (Go did not exhibit the premise here -> not a probe). - -Execution reuses oracle.run_once so BOTH binaries run under the SAME pinned env -(TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800) and we compare -STDOUT only (stderr is progress) -- exactly rule #5. We do NOT re-implement -process launching here; the oracle module is the single launcher. -""" - -import argparse -import hashlib -import importlib.util -import json -import os -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -ORACLE_PY = os.path.join(HERE, "..", "oracle", "oracle.py") - -# Import the oracle module: we reuse its LIVE-binary launcher (run_once) and sha -# so there is exactly ONE place that decides how to invoke Go/Rust under the -# pinned env. Re-deriving expected output is forbidden; we only run the binaries. -_spec = importlib.util.spec_from_file_location("oracle", ORACLE_PY) -oracle = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(oracle) - -run_once = oracle.run_once # (side, argv) -> (rc, stdout_bytes); side in {go,rust} -sha = oracle.sha - -CORPUS = os.path.join(HERE, "..", "corpus") -MANIFEST = os.path.join(CORPUS, "manifest.json") -GOLDEN = os.path.join(HERE, "golden_constants.json") - -# Repos for analyzer (run) checks: small first for speed. Inputs the author never -# hand-curated as golden args -- mined corpus repos from the manifest. -HERCULES = "/home/dmitriy/sources/hercules" -KUBE = "/home/dmitriy/sources/kubernetes" - -RUN_BASE = ["run", "--checkpoint=false", "--resume=false", "--no-cache", - "--workers", "1"] -STATIC_BASE = ["run", "--checkpoint=false", "--resume=false", "--no-cache", - "--head", "--workers", "1", "--static-workers", "1"] - - -def go(argv): - return run_once("go", argv)[1] - - -def ru(argv): - return run_once("rust", argv)[1] - - -# --------------------------------------------------------------------------- # -# Verdict accumulation. -# --------------------------------------------------------------------------- # -class Report: - def __init__(self): - self.records = [] - self.tally = {"PASS": 0, "SIM": 0, "FAIL": 0, "NA": 0} - - def add(self, prop, label, verdict, detail): - self.tally[verdict] = self.tally.get(verdict, 0) + 1 - self.records.append({"property": prop, "label": label, - "verdict": verdict, **detail}) - mark = {"PASS": "PASS ", "SIM": "SIM! ", "FAIL": "FAIL!", - "NA": "n/a "}[verdict] - print(f" {mark}[{prop}] {label}") - if verdict in ("SIM", "FAIL"): - for k, v in detail.items(): - vs = v if isinstance(v, str) else json.dumps(v, default=str) - print(f" {k}: {vs[:400]}") - - def failed(self): - return self.tally["SIM"] + self.tally["FAIL"] - - -# --------------------------------------------------------------------------- # -# (a) VARY-INPUT: Rust must DIFFER on two inputs WHERE GO DIFFERS. -# --------------------------------------------------------------------------- # -def check_vary_input(rep, prop_label, argvA, argvB, inputA, inputB): - gA, gB = go(argvA), go(argvB) - if not gA or not gB: - rep.add("vary-input", prop_label, "NA", - {"reason": "go empty on one side -> no differ premise", - "go_a_len": len(gA), "go_b_len": len(gB)}) - return - if sha(gA) == sha(gB): - # Go does not distinguish these two inputs -> no premise to demand Rust - # differ. (Never re-derive: if Go doesn't differ, we don't claim Rust must.) - rep.add("vary-input", prop_label, "NA", - {"reason": "go IDENTICAL on both inputs -> no differ premise", - "input_a": inputA, "input_b": inputB}) - return - rA, rB = ru(argvA), ru(argvB) - if sha(rA) == sha(rB): - # THE SIMULATION SIGNATURE: Rust constant where Go varies. - rep.add("vary-input", prop_label, "SIM", { - "SIMULATION_SUSPECT": "rust emits identical bytes for two inputs Go " - "distinguishes (hardcoded-constant signature)", - "input_a": inputA, "input_b": inputB, - "rust_out_a_sha": sha(rA), "rust_out_b_sha": sha(rB), - "rust_out_a_head": rA[:200].decode("utf-8", "replace"), - "rust_out_b_head": rB[:200].decode("utf-8", "replace"), - "go_a_sha": sha(gA), "go_b_sha": sha(gB), - }) - return - rep.add("vary-input", prop_label, "PASS", - {"go_differs": True, "rust_differs": True}) - - -# --------------------------------------------------------------------------- # -# (b) GROW-LIMIT: Rust output GROWS with --limit (10 vs 500) WHERE GO GROWS. -# --------------------------------------------------------------------------- # -def _with_limit(argv, n): - a = list(argv) - if "--limit" in a: - i = a.index("--limit") - a[i + 1] = str(n) - return a - # insert before the trailing positional repo path - return a[:-1] + ["--limit", str(n)] + a[-1:] - - -def check_grow_limit(rep, label, base_argv, lo=10, hi=500): - aLo, aHi = _with_limit(base_argv, lo), _with_limit(base_argv, hi) - gLo, gHi = go(aLo), go(aHi) - if not gLo or not gHi: - rep.add("grow-limit", label, "NA", - {"reason": "go empty at a limit -> no grow premise", - "go_lo_len": len(gLo), "go_hi_len": len(gHi)}) - return - if len(gHi) <= len(gLo) or sha(gLo) == sha(gHi): - # Go does NOT grow with limit here (analyzer saturates / aggregate-only). - # No premise to demand Rust grow. We do not invent a growth expectation. - rep.add("grow-limit", label, "NA", - {"reason": "go does NOT grow with --limit here -> no grow premise", - "go_lo_len": len(gLo), "go_hi_len": len(gHi)}) - return - rLo, rHi = ru(aLo), ru(aHi) - if sha(rLo) == sha(rHi): - # Rust constant across limits while Go grows: closed-form-stub signature. - rep.add("grow-limit", label, "SIM", { - "SIMULATION_SUSPECT": "rust CONSTANT across --limit while Go GROWS " - "(closed-form-stub signature)", - "input_a": " ".join(aLo), "input_b": " ".join(aHi), - "rust_out_a_sha": sha(rLo), "rust_out_b_sha": sha(rHi), - "rust_out_a_len": len(rLo), "rust_out_b_len": len(rHi), - "go_lo_len": len(gLo), "go_hi_len": len(gHi), - }) - return - if len(rHi) < len(rLo): - # Rust SHRINKS where Go grows: not the constant signature but still a real - # relational divergence -> FAIL (the monotonicity contract Go obeys). - rep.add("grow-limit", label, "FAIL", { - "reason": "rust output SHRINKS with --limit while Go GROWS", - "input_a": " ".join(aLo), "input_b": " ".join(aHi), - "rust_lo_len": len(rLo), "rust_hi_len": len(rHi), - "go_lo_len": len(gLo), "go_hi_len": len(gHi), - }) - return - rep.add("grow-limit", label, "PASS", - {"go_grows": f"{len(gLo)}->{len(gHi)}", - "rust_grows": f"{len(rLo)}->{len(rHi)}"}) - - -# --------------------------------------------------------------------------- # -# (c) DETERMINISM: identical args => identical Rust bytes. -# --------------------------------------------------------------------------- # -def check_determinism(rep, label, argv, reps=3): - shas = [sha(ru(argv)) for _ in range(reps)] - if len(set(shas)) == 1: - rep.add("determinism", label, "PASS", {"sha": shas[0], "runs": reps}) - else: - rep.add("determinism", label, "FAIL", { - "reason": "rust NONDETERMINISTIC: identical args produced differing " - "bytes (a faithful port is deterministic)", - "input": " ".join(argv), "distinct_shas": sorted(set(shas)), - }) - - -# --------------------------------------------------------------------------- # -# (d) NON-EMPTY: Rust non-empty WHERE GO is non-empty. -# --------------------------------------------------------------------------- # -def check_nonempty(rep, label, argv): - g = go(argv) - if not g: - rep.add("non-empty", label, "NA", - {"reason": "go empty here -> no non-empty premise"}) - return - r = ru(argv) - if not r: - rep.add("non-empty", label, "FAIL", { - "reason": "rust EMPTY where Go is NON-EMPTY (0-byte stub / not ported)", - "input": " ".join(argv), "go_len": len(g), "rust_len": len(r), - }) - return - rep.add("non-empty", label, "PASS", {"go_len": len(g), "rust_len": len(r)}) - - -# --------------------------------------------------------------------------- # -# (e) GOLDEN-DRIFT: once the input changed, Rust output never equals a -# previously-recorded golden constant. -# --------------------------------------------------------------------------- # -def load_golden(): - if os.path.exists(GOLDEN): - with open(GOLDEN) as f: - return json.load(f) - return {} - - -def save_golden(g): - with open(GOLDEN, "w") as f: - json.dump(g, f, indent=2, sort_keys=True) - - -def check_golden_drift(rep, label, key, base_argv, golden, mutate_argv, - input_orig, input_mut): - """ - Record (or compare against) a golden constant for `key` on base_argv. Then run - Rust on a DIFFERENT input (mutate_argv). If Go's output ALSO changed between - the two inputs (so the golden is genuinely stale for the new input) but Rust - STILL emits the recorded golden bytes, Rust memorized a constant => SIM. - """ - # establish/refresh the golden for the ORIGINAL input - r_orig = ru(base_argv) - g_orig = go(base_argv) - if not g_orig: - rep.add("golden-drift", label, "NA", - {"reason": "go empty on original input"}) - return - recorded = golden.get(key) - cur = sha(r_orig) - if recorded is None: - golden[key] = {"sha": cur, "input": input_orig, - "len": len(r_orig)} - recorded = golden[key] - - # now the INPUT CHANGES. Does Go distinguish the two inputs? - g_mut = go(mutate_argv) - if not g_mut or sha(g_orig) == sha(g_mut): - rep.add("golden-drift", label, "NA", { - "reason": "go did NOT change across the input mutation -> golden may " - "legitimately persist; no drift premise", - "input_orig": input_orig, "input_mut": input_mut}) - return - r_mut = ru(mutate_argv) - if sha(r_mut) == recorded["sha"]: - # Rust still emits the golden constant after the input changed AND Go - # changed: a memorized golden constant. - rep.add("golden-drift", label, "SIM", { - "SIMULATION_SUSPECT": "rust output EQUALS the recorded golden constant " - "even though the INPUT changed and Go changed " - "(memorized-golden signature)", - "input_a": input_orig, "input_b": input_mut, - "rust_out_a_sha": recorded["sha"], "rust_out_b_sha": sha(r_mut), - "rust_out_a_head": r_orig[:200].decode("utf-8", "replace"), - "rust_out_b_head": r_mut[:200].decode("utf-8", "replace"), - "golden_key": key, - }) - return - rep.add("golden-drift", label, "PASS", { - "drifted_from_golden": True, "golden_sha": recorded["sha"], - "rust_new_sha": sha(r_mut)}) - - -# --------------------------------------------------------------------------- # -# Cell builders: which analyzers/inputs to probe. Inputs are mined corpus -# (files + repos), NOT recorded golden args. -# --------------------------------------------------------------------------- # -def history_argv(analyzer, repo, fmt="json", limit=20): - return RUN_BASE + ["--analyzers", analyzer, "--format", fmt, - "--limit", str(limit), repo] - - -def static_argv(analyzer, path, fmt="json"): - return STATIC_BASE + ["-p", path, "--analyzers", analyzer, "--format", fmt] - - -def uast_argv(sub, stored, fmt="json", dsl=None): - if sub == "query": - return ["uast", "query", dsl or 'filter(.roles has "Function")', - "--format", fmt, "-i", stored] - if sub == "analyze": - return ["uast", "analyze", "--format", fmt, "-i", stored] - return ["uast", "parse", "--format", fmt, stored] - - -def corpus_files(): - with open(MANIFEST) as f: - m = json.load(f) - out = [] - for e in m["files"]: - out.append((e["language"], os.path.join(CORPUS, e["stored"]))) - return out - - -HISTORY_ANALYZERS = ["history/devs", "history/burndown", "history/imports", - "history/couples", "history/shotness", "history/typos", - "history/file-history", "history/sentiment", - "history/quality", "history/anomaly"] -STATIC_ANALYZERS = ["static/complexity", "static/composition", - "static/halstead", "static/comments", "static/imports", - "static/clones", "static/cohesion"] - - -def run_smoke(rep, golden): - files = corpus_files() - # pick two DIFFERENT files of the SAME language family where possible for the - # vary-input premise; here we use two different languages' files which Go - # certainly distinguishes. - by_lang = {l: p for l, p in files} - - print("-- (a) vary-input: history analyzers, two different repos --") - # hercules vs kubernetes (Go distinguishes; both real mined repos) - for an in ["history/devs", "history/burndown", "history/imports"]: - argvA = history_argv(an, HERCULES, limit=20) - argvB = history_argv(an, KUBE, limit=20) - check_vary_input(rep, f"{an} hercules-vs-kube", - argvA, argvB, HERCULES, KUBE) - - print("-- (a) vary-input: uast parse, two different files --") - pairs = [("go", "python"), ("c", "rust"), ("typescript", "javascript")] - for la, lb in pairs: - if la in by_lang and lb in by_lang: - fa, fb = by_lang[la], by_lang[lb] - check_vary_input(rep, f"uast/parse {la}-vs-{lb}", - uast_argv("parse", fa), uast_argv("parse", fb), - fa, fb) - - print("-- (b) grow-limit: history analyzers (10 vs 500) on kubernetes --") - for an in ["history/devs", "history/imports", "history/burndown", - "history/typos", "history/couples", "history/shotness"]: - check_grow_limit(rep, f"{an}@kube", history_argv(an, KUBE), lo=10, hi=500) - - print("-- (c) determinism: history + static + uast --") - check_determinism(rep, "history/devs@hercules", - history_argv("history/devs", HERCULES, limit=20)) - check_determinism(rep, "static/complexity@hercules", - static_argv("static/complexity", HERCULES)) - f_go = by_lang.get("go") - if f_go: - check_determinism(rep, "uast/parse go", uast_argv("parse", f_go)) - - print("-- (d) non-empty where Go non-empty: every analyzer --") - for an in HISTORY_ANALYZERS: - check_nonempty(rep, f"{an}@hercules", - history_argv(an, HERCULES, limit=20)) - for an in STATIC_ANALYZERS: - check_nonempty(rep, f"{an}@hercules", static_argv(an, HERCULES)) - for lang, path in files: - check_nonempty(rep, f"uast/parse {lang}", uast_argv("parse", path)) - - print("-- (e) golden-drift: input changes, Rust must not echo old golden --") - # original = hercules limit 20; mutated = kubernetes limit 20 (Go distinguishes) - for an in ["history/devs", "history/imports"]: - check_golden_drift( - rep, f"{an} drift", f"{an}|hercules->kube", - history_argv(an, HERCULES, limit=20), golden, - history_argv(an, KUBE, limit=20), - f"{HERCULES}@lim20", f"{KUBE}@lim20") - - -def main(): - ap = argparse.ArgumentParser(description="Metamorphic / anti-simulation layer") - ap.add_argument("--tier", default="smoke", choices=["smoke"]) - ap.add_argument("--out", default=os.path.join(HERE, "results.json")) - ap.add_argument("--no-golden-write", action="store_true", - help="do not persist newly-recorded golden constants") - a = ap.parse_args() - - print("================ METAMORPHIC / ANTI-SIMULATION LAYER ================") - print("(relational checks vs the LIVE Go premise; SIM => simulation suspect)") - print() - golden = load_golden() - rep = Report() - run_smoke(rep, golden) - if not a.no_golden_write: - save_golden(golden) - - summary = {"tier": a.tier, "tally": rep.tally, "records": rep.records} - with open(a.out, "w") as f: - json.dump(summary, f, indent=2, default=str) - print() - print("================ RESULT ================") - print(f"PASS={rep.tally['PASS']} SIM={rep.tally['SIM']} " - f"FAIL={rep.tally['FAIL']} NA={rep.tally['NA']}") - print(f"wrote {a.out}") - if rep.failed(): - print("METAMORPHIC: RED -- simulation suspect or relational divergence") - sys.exit(1) - print("METAMORPHIC: GREEN -- all relational premises Go exhibits, Rust honors") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/metamorphic/results.json b/tests/compat/metamorphic/results.json deleted file mode 100644 index 55bee11d..00000000 --- a/tests/compat/metamorphic/results.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "tier": "smoke", - "tally": { - "PASS": 43, - "SIM": 0, - "FAIL": 0, - "NA": 3 - }, - "records": [ - { - "property": "vary-input", - "label": "history/devs hercules-vs-kube", - "verdict": "PASS", - "go_differs": true, - "rust_differs": true - }, - { - "property": "vary-input", - "label": "history/burndown hercules-vs-kube", - "verdict": "PASS", - "go_differs": true, - "rust_differs": true - }, - { - "property": "vary-input", - "label": "history/imports hercules-vs-kube", - "verdict": "NA", - "reason": "go IDENTICAL on both inputs -> no differ premise", - "input_a": "/home/dmitriy/sources/hercules", - "input_b": "/home/dmitriy/sources/kubernetes" - }, - { - "property": "vary-input", - "label": "uast/parse go-vs-python", - "verdict": "PASS", - "go_differs": true, - "rust_differs": true - }, - { - "property": "vary-input", - "label": "uast/parse c-vs-rust", - "verdict": "PASS", - "go_differs": true, - "rust_differs": true - }, - { - "property": "vary-input", - "label": "uast/parse typescript-vs-javascript", - "verdict": "PASS", - "go_differs": true, - "rust_differs": true - }, - { - "property": "grow-limit", - "label": "history/devs@kube", - "verdict": "PASS", - "go_grows": "6455->24450", - "rust_grows": "6455->24450" - }, - { - "property": "grow-limit", - "label": "history/imports@kube", - "verdict": "NA", - "reason": "go does NOT grow with --limit here -> no grow premise", - "go_lo_len": 167, - "go_hi_len": 167 - }, - { - "property": "grow-limit", - "label": "history/burndown@kube", - "verdict": "PASS", - "go_grows": "355->498", - "rust_grows": "355->498" - }, - { - "property": "grow-limit", - "label": "history/typos@kube", - "verdict": "PASS", - "go_grows": "138->5952", - "rust_grows": "138->6155" - }, - { - "property": "grow-limit", - "label": "history/couples@kube", - "verdict": "PASS", - "go_grows": "1047972->1380105", - "rust_grows": "1047972->1380105" - }, - { - "property": "grow-limit", - "label": "history/shotness@kube", - "verdict": "PASS", - "go_grows": "194->3710756", - "rust_grows": "194->8900266" - }, - { - "property": "determinism", - "label": "history/devs@hercules", - "verdict": "PASS", - "sha": "a320ae7c95fcc6f58f8e276cb6d3fce79fad145d1ad8254e81885ad234abb7f3", - "runs": 3 - }, - { - "property": "determinism", - "label": "static/complexity@hercules", - "verdict": "PASS", - "sha": "54ba4eb574e75aa0a1b824b5d79f736d087775ef8b4327e8c0f1e865334c72d9", - "runs": 3 - }, - { - "property": "determinism", - "label": "uast/parse go", - "verdict": "PASS", - "sha": "2ec23dfe490d9fbb0f06cc055e5e0171c1d536f2704714e2b167c9b748857bf3", - "runs": 3 - }, - { - "property": "non-empty", - "label": "history/devs@hercules", - "verdict": "PASS", - "go_len": 3845, - "rust_len": 3845 - }, - { - "property": "non-empty", - "label": "history/burndown@hercules", - "verdict": "PASS", - "go_len": 933, - "rust_len": 933 - }, - { - "property": "non-empty", - "label": "history/imports@hercules", - "verdict": "PASS", - "go_len": 167, - "rust_len": 167 - }, - { - "property": "non-empty", - "label": "history/couples@hercules", - "verdict": "PASS", - "go_len": 2772, - "rust_len": 2772 - }, - { - "property": "non-empty", - "label": "history/shotness@hercules", - "verdict": "PASS", - "go_len": 386144, - "rust_len": 389345 - }, - { - "property": "non-empty", - "label": "history/typos@hercules", - "verdict": "PASS", - "go_len": 310, - "rust_len": 310 - }, - { - "property": "non-empty", - "label": "history/file-history@hercules", - "verdict": "PASS", - "go_len": 3391, - "rust_len": 3391 - }, - { - "property": "non-empty", - "label": "history/sentiment@hercules", - "verdict": "PASS", - "go_len": 1246, - "rust_len": 1246 - }, - { - "property": "non-empty", - "label": "history/quality@hercules", - "verdict": "PASS", - "go_len": 3232, - "rust_len": 3232 - }, - { - "property": "non-empty", - "label": "history/anomaly@hercules", - "verdict": "PASS", - "go_len": 2379, - "rust_len": 2379 - }, - { - "property": "non-empty", - "label": "static/complexity@hercules", - "verdict": "PASS", - "go_len": 321919, - "rust_len": 321919 - }, - { - "property": "non-empty", - "label": "static/composition@hercules", - "verdict": "PASS", - "go_len": 1458, - "rust_len": 1458 - }, - { - "property": "non-empty", - "label": "static/halstead@hercules", - "verdict": "PASS", - "go_len": 340765, - "rust_len": 340739 - }, - { - "property": "non-empty", - "label": "static/comments@hercules", - "verdict": "PASS", - "go_len": 236197, - "rust_len": 236197 - }, - { - "property": "non-empty", - "label": "static/imports@hercules", - "verdict": "PASS", - "go_len": 26337, - "rust_len": 26337 - }, - { - "property": "non-empty", - "label": "static/clones@hercules", - "verdict": "PASS", - "go_len": 258822, - "rust_len": 258822 - }, - { - "property": "non-empty", - "label": "static/cohesion@hercules", - "verdict": "PASS", - "go_len": 274941, - "rust_len": 274941 - }, - { - "property": "non-empty", - "label": "uast/parse go", - "verdict": "PASS", - "go_len": 1256674, - "rust_len": 1256674 - }, - { - "property": "non-empty", - "label": "uast/parse python", - "verdict": "PASS", - "go_len": 700431, - "rust_len": 700431 - }, - { - "property": "non-empty", - "label": "uast/parse c", - "verdict": "PASS", - "go_len": 142327, - "rust_len": 142327 - }, - { - "property": "non-empty", - "label": "uast/parse c-header", - "verdict": "PASS", - "go_len": 19243, - "rust_len": 19243 - }, - { - "property": "non-empty", - "label": "uast/parse rust", - "verdict": "PASS", - "go_len": 125877, - "rust_len": 125877 - }, - { - "property": "non-empty", - "label": "uast/parse typescript", - "verdict": "PASS", - "go_len": 2555406, - "rust_len": 2555406 - }, - { - "property": "non-empty", - "label": "uast/parse tsx", - "verdict": "PASS", - "go_len": 244099, - "rust_len": 244099 - }, - { - "property": "non-empty", - "label": "uast/parse javascript", - "verdict": "PASS", - "go_len": 54802, - "rust_len": 54802 - }, - { - "property": "non-empty", - "label": "uast/parse json", - "verdict": "PASS", - "go_len": 308147, - "rust_len": 308147 - }, - { - "property": "non-empty", - "label": "uast/parse yaml", - "verdict": "PASS", - "go_len": 150552, - "rust_len": 150552 - }, - { - "property": "non-empty", - "label": "uast/parse cpp", - "verdict": "PASS", - "go_len": 3895930, - "rust_len": 3895930 - }, - { - "property": "non-empty", - "label": "uast/parse shell", - "verdict": "PASS", - "go_len": 148968, - "rust_len": 148968 - }, - { - "property": "golden-drift", - "label": "history/devs drift", - "verdict": "PASS", - "drifted_from_golden": true, - "golden_sha": "a320ae7c95fcc6f58f8e276cb6d3fce79fad145d1ad8254e81885ad234abb7f3", - "rust_new_sha": "858882acfe31909defb13c2fbfc9af7833e8bbe7fd48885c2adb185a1ec0b9c5" - }, - { - "property": "golden-drift", - "label": "history/imports drift", - "verdict": "NA", - "reason": "go did NOT change across the input mutation -> golden may legitimately persist; no drift premise", - "input_orig": "/home/dmitriy/sources/hercules@lim20", - "input_mut": "/home/dmitriy/sources/kubernetes@lim20" - } - ] -} \ No newline at end of file diff --git a/tests/compat/metamorphic/selftest/live_constant_test.py b/tests/compat/metamorphic/selftest/live_constant_test.py deleted file mode 100644 index e920ebe6..00000000 --- a/tests/compat/metamorphic/selftest/live_constant_test.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -""" -LIVE END-TO-END SELF-PROOF (non-negotiable rule #6, and the VERIFY-phase brief: -"create a throwaway analyzer/path that emits a CONSTANT regardless of input and -confirm the metamorphic layer flags it SIM; then confirm a real analyzer (e.g. -history/devs) passes"). - -Unlike self_test.py (which scripts BOTH sides), this test keeps the LIVE Go -binary as the real oracle and only replaces the RUST side with a throwaway -constant-emitting binary. So the premise (Go distinguishes two real mined repos) -is established by the genuine Go binary on disk -- not faked -- and we prove the -metamorphic layer flags the constant Rust path SIM. Then, with the constant -emitter removed, the SAME check on the SAME inputs against the REAL Rust binary -must PASS, proving the layer does not cry wolf on an honest port. - -This is the planted-defect proof required by the brief, run end-to-end against -the binaries (Go live; Rust = a deliberate constant stub), reusing the layer's -own decision functions (meta.check_vary_input / meta.check_nonempty). -""" - -import importlib.util -import os -import stat -import sys -import tempfile - -HERE = os.path.dirname(os.path.abspath(__file__)) -META_PY = os.path.join(HERE, "..", "metamorphic.py") - -spec = importlib.util.spec_from_file_location("metamorphic", META_PY) -meta = importlib.util.module_from_spec(spec) -spec.loader.exec_module(meta) - -HERCULES = meta.HERCULES -KUBE = meta.KUBE - -# Real launcher (live Go + live Rust) is meta.run_once via the oracle module. -REAL_RUN_ONCE = meta.run_once - -CONST_BYTES = b'{"devs":[{"name":"CONSTANT","commits":1}],"_planted":"stub"}' - - -def make_constant_emitter(): - """Write a throwaway 'analyzer' that emits CONST_BYTES regardless of argv.""" - fd, path = tempfile.mkstemp(prefix="const_analyzer_", suffix=".sh") - os.write(fd, b"#!/bin/sh\nprintf '%s' '" + CONST_BYTES + b"'\n") - os.close(fd) - os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC | stat.S_IXGRP | - stat.S_IXOTH) - return path - - -def routed_run_once(const_path): - """ - LIVE Go oracle + CONSTANT Rust stub. Go calls go to the real binary on disk; - Rust calls go to the throwaway constant emitter -> the planted defect. - """ - def _run(side, argv): - if side == "go": - return REAL_RUN_ONCE("go", argv) # genuine live oracle - # side == rust: throwaway analyzer, ignores argv -> constant - import subprocess - p = subprocess.run([const_path], stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, timeout=30) - return p.returncode, p.stdout - return _run - - -RESULTS = [] - - -def expect(name, rep, prop, want): - rec = [r for r in rep.records if r["property"] == prop] - got = rec[-1]["verdict"] if rec else "MISSING" - ok = got == want - RESULTS.append((name, ok, got, want)) - print(f" {'OK ' if ok else 'XX '}{name}: [{prop}] got {got}, want {want}") - return ok - - -def main(): - print("============ LIVE CONSTANT-STUB SELF-PROOF ============") - print("(live Go oracle; Rust replaced by a throwaway CONSTANT emitter)") - - argvA = meta.history_argv("history/devs", HERCULES, limit=10) - argvB = meta.history_argv("history/devs", KUBE, limit=10) - - # ---- PLANTED DEFECT: Rust = constant emitter -> vary-input must SIM ------ - const_path = make_constant_emitter() - try: - meta.run_once = routed_run_once(const_path) - print("-- planted constant stub: vary-input hercules-vs-kube --") - rep = meta.Report() - meta.check_vary_input(rep, "PLANTED const history/devs", - argvA, argvB, HERCULES, KUBE) - expect("planted-constant vary-input", rep, "vary-input", "SIM") - - # non-empty still PASSes (the stub emits bytes) -> shows vary-input is - # what catches the constant, not a trivially-empty failure. - print("-- planted constant stub: non-empty (stub is non-empty) --") - rep = meta.Report() - meta.check_nonempty(rep, "PLANTED const history/devs", argvA) - expect("planted-constant non-empty-is-PASS", rep, "non-empty", "PASS") - finally: - os.unlink(const_path) - meta.run_once = REAL_RUN_ONCE - - # ---- HONEST CONTROL: real Rust binary on the SAME inputs -> PASS -------- - print("-- real Rust analyzer history/devs: vary-input must PASS --") - rep = meta.Report() - meta.check_vary_input(rep, "REAL history/devs hercules-vs-kube", - argvA, argvB, HERCULES, KUBE) - expect("real-analyzer vary-input", rep, "vary-input", "PASS") - - print("=======================================================") - failed = [r for r in RESULTS if not r[1]] - n = len(RESULTS) - print(f"LIVE SELF-PROOF: {n - len(failed)}/{n} checks correct") - if failed: - print("LIVE SELF-PROOF FAILED:") - for name, _, got, want in failed: - print(f" - {name}: got {got}, want {want}") - sys.exit(1) - print("LIVE SELF-PROOF GREEN: the metamorphic layer flags a throwaway " - "constant-emitting Rust path SIM against the LIVE Go oracle, and " - "PASSes the real history/devs analyzer on the same inputs.") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/metamorphic/selftest/self_test.py b/tests/compat/metamorphic/selftest/self_test.py deleted file mode 100644 index 77828dd4..00000000 --- a/tests/compat/metamorphic/selftest/self_test.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 -""" -METAMORPHIC SELF-PROOF (non-negotiable rule #6: every component must SELF-PROVE -it catches a defect). A green that cannot be shown to catch a planted bug is -worthless. This harness PLANTS each simulation/relational defect class and -asserts the metamorphic layer reports the correct non-PASS verdict. - -We exercise the REAL metamorphic decision functions (metamorphic.check_*) but -feed them scripted Go/Rust binary outputs by monkeypatching the LIVE-binary -launcher (oracle.run_once, which metamorphic imports as run_once). Faking the -binary outputs is the honest way to plant a defect without corrupting the real -port: the binaries ARE the oracle, so a controlled fake oracle lets us prove the -relational logic catches each stub class. - -Planted defects (mirror the simulation failure modes the SPEC warns about): - - P-a VARY-INPUT constant Rust same bytes for 2 inputs Go distinguishes -> SIM - P-b GROW-LIMIT constant Rust constant across limits while Go grows -> SIM - P-b2 GROW-LIMIT shrink Rust shrinks with limit while Go grows -> FAIL - P-c NONDETERMINISM identical Rust args produce differing bytes -> FAIL - P-d EMPTY-STUB Rust empty where Go non-empty -> FAIL - P-e GOLDEN-DRIFT Rust echoes a recorded golden after input chg -> SIM - -Controls prove the layer does NOT cry wolf (so the FAILs are meaningful): - - C-a honest vary-input Rust differs where Go differs -> PASS - C-na go-identical Go same on both inputs -> no premise -> NA - C-b honest grow Rust grows where Go grows -> PASS - C-c deterministic identical Rust args -> identical bytes -> PASS - C-d non-empty Rust non-empty where Go non-empty -> PASS - C-e honest drift Rust changes when input changes -> PASS -""" - -import importlib.util -import json -import os -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -META_PY = os.path.join(HERE, "..", "metamorphic.py") - -spec = importlib.util.spec_from_file_location("metamorphic", META_PY) -meta = importlib.util.module_from_spec(spec) -spec.loader.exec_module(meta) - - -def b(o): - return json.dumps(o).encode() - - -class ScriptedBins: - """ - Replaces oracle.run_once. Returns scripted (rc, bytes) keyed by a matcher - function over (side, argv). First matching rule wins; matched rules with a - list pop in order, scalars repeat. This lets us script Go vs Rust per input - and per --limit value precisely. - """ - def __init__(self, rules): - # rules: list of (predicate(side, argv) -> bool, value) - # value: bytes (repeat) | list[bytes] (pop) | callable(side,argv)->bytes - self.rules = rules - - def __call__(self, side, argv): - for pred, val in self.rules: - if pred(side, argv): - if callable(val): - return (0, val(side, argv)) - if isinstance(val, list): - return (0, val.pop(0)) - return (0, val) - return (0, b"") - - -def install(rules): - meta.run_once = ScriptedBins(rules) - - -def fresh_report(): - return meta.Report() - - -RESULTS = [] - - -def expect(name, rep, prop, want): - rec = [r for r in rep.records if r["property"] == prop] - got = rec[-1]["verdict"] if rec else "MISSING" - ok = got == want - RESULTS.append((name, ok, got, want)) - flag = "OK " if ok else "XX " - print(f" {flag}{name}: [{prop}] got {got}, want {want}") - return ok - - -def has_arg(argv, val): - return val in argv - - -def limit_is(argv, n): - return "--limit" in argv and argv[argv.index("--limit") + 1] == str(n) - - -# argv ends in a positional path; use it as the "input" discriminator. -def path_is(argv, p): - return argv and argv[-1] == p - - -def main(): - print("================ METAMORPHIC SELF-PROOF ================") - - A = "/repoA" - B = "/repoB" - argvA = meta.history_argv("history/devs", A, limit=20) - argvB = meta.history_argv("history/devs", B, limit=20) - - # ---- P-a VARY-INPUT constant: Rust same for 2 inputs Go distinguishes -> SIM - install([ - (lambda s, a: s == "go" and path_is(a, A), b({"r": "A"})), - (lambda s, a: s == "go" and path_is(a, B), b({"r": "B"})), - (lambda s, a: s == "rust", b({"CONST": 1})), # constant regardless of input - ]) - rep = fresh_report() - meta.check_vary_input(rep, "P-a", argvA, argvB, A, B) - expect("P-a vary-input-constant", rep, "vary-input", "SIM") - - # ---- C-a honest vary-input: Rust differs where Go differs -> PASS - install([ - (lambda s, a: s == "go" and path_is(a, A), b({"r": "gA"})), - (lambda s, a: s == "go" and path_is(a, B), b({"r": "gB"})), - (lambda s, a: s == "rust" and path_is(a, A), b({"r": "rA"})), - (lambda s, a: s == "rust" and path_is(a, B), b({"r": "rB"})), - ]) - rep = fresh_report() - meta.check_vary_input(rep, "C-a", argvA, argvB, A, B) - expect("C-a honest-vary-input", rep, "vary-input", "PASS") - - # ---- C-na go-identical: Go same on both inputs -> NA (no premise) - install([ - (lambda s, a: s == "go", b({"r": "same"})), - (lambda s, a: s == "rust" and path_is(a, A), b({"r": "rA"})), - (lambda s, a: s == "rust" and path_is(a, B), b({"r": "rB"})), - ]) - rep = fresh_report() - meta.check_vary_input(rep, "C-na", argvA, argvB, A, B) - expect("C-na go-identical-no-premise", rep, "vary-input", "NA") - - # ---- P-b GROW-LIMIT constant: Go grows, Rust constant -> SIM - base = meta.history_argv("history/shotness", A) # has --limit 20 default - install([ - # Go grows with limit (len scales with limit value) - (lambda s, a: s == "go", lambda s, a: - b({"nodes": ["n"] * int(a[a.index("--limit") + 1])})), - (lambda s, a: s == "rust", b({"nodes": ["c"]})), # constant - ]) - rep = fresh_report() - meta.check_grow_limit(rep, "P-b", base, lo=10, hi=500) - expect("P-b grow-limit-constant", rep, "grow-limit", "SIM") - - # ---- P-b2 GROW-LIMIT shrink: Go grows, Rust shrinks -> FAIL - install([ - (lambda s, a: s == "go", lambda s, a: - b({"nodes": ["n"] * int(a[a.index("--limit") + 1])})), - (lambda s, a: s == "rust", lambda s, a: - b({"nodes": ["n"] * (1000 - int(a[a.index("--limit") + 1]))})), - ]) - rep = fresh_report() - meta.check_grow_limit(rep, "P-b2", base, lo=10, hi=500) - expect("P-b2 grow-limit-shrink", rep, "grow-limit", "FAIL") - - # ---- C-b honest grow: Go grows, Rust grows -> PASS - install([ - (lambda s, a: True, lambda s, a: - b({"nodes": ["n"] * int(a[a.index("--limit") + 1])})), - ]) - rep = fresh_report() - meta.check_grow_limit(rep, "C-b", base, lo=10, hi=500) - expect("C-b honest-grow", rep, "grow-limit", "PASS") - - # ---- NA grow: Go does not grow -> NA (no premise, never invents growth) - install([ - (lambda s, a: s == "go", b({"agg": 7})), # constant regardless of limit - (lambda s, a: s == "rust", b({"agg": 7})), - ]) - rep = fresh_report() - meta.check_grow_limit(rep, "C-b-na", base, lo=10, hi=500) - expect("C-b-na go-saturates-no-premise", rep, "grow-limit", "NA") - - # ---- P-c NONDETERMINISM: identical Rust args -> differing bytes -> FAIL - seq = [b({"x": 1}), b({"x": 2}), b({"x": 3})] - install([(lambda s, a: s == "rust", list(seq))]) - rep = fresh_report() - meta.check_determinism(rep, "P-c", argvA, reps=3) - expect("P-c nondeterminism", rep, "determinism", "FAIL") - - # ---- C-c deterministic -> PASS - install([(lambda s, a: s == "rust", b({"x": 1}))]) - rep = fresh_report() - meta.check_determinism(rep, "C-c", argvA, reps=3) - expect("C-c deterministic", rep, "determinism", "PASS") - - # ---- P-d EMPTY-STUB: Rust empty where Go non-empty -> FAIL - install([ - (lambda s, a: s == "go", b({"r": 1})), - (lambda s, a: s == "rust", b""), - ]) - rep = fresh_report() - meta.check_nonempty(rep, "P-d", argvA) - expect("P-d empty-stub", rep, "non-empty", "FAIL") - - # ---- C-d non-empty -> PASS - install([ - (lambda s, a: s == "go", b({"r": 1})), - (lambda s, a: s == "rust", b({"r": 2})), - ]) - rep = fresh_report() - meta.check_nonempty(rep, "C-d", argvA) - expect("C-d non-empty", rep, "non-empty", "PASS") - - # ---- P-e GOLDEN-DRIFT: Rust echoes recorded golden after input changed -> SIM - # Go distinguishes A and B; the golden was recorded for A; Rust still emits the - # A-golden bytes for B (memorized constant). - golden = {} - install([ - (lambda s, a: s == "go" and path_is(a, A), b({"r": "gA"})), - (lambda s, a: s == "go" and path_is(a, B), b({"r": "gB"})), - (lambda s, a: s == "rust", b({"FROZEN": "golden"})), # constant - ]) - rep = fresh_report() - meta.check_golden_drift(rep, "P-e", "k", argvA, golden, argvB, A, B) - expect("P-e golden-drift", rep, "golden-drift", "SIM") - - # ---- C-e honest drift: Rust changes when input changes -> PASS - golden = {} - install([ - (lambda s, a: s == "go" and path_is(a, A), b({"r": "gA"})), - (lambda s, a: s == "go" and path_is(a, B), b({"r": "gB"})), - (lambda s, a: s == "rust" and path_is(a, A), b({"r": "rA"})), - (lambda s, a: s == "rust" and path_is(a, B), b({"r": "rB"})), - ]) - rep = fresh_report() - meta.check_golden_drift(rep, "C-e", "k", argvA, golden, argvB, A, B) - expect("C-e honest-drift", rep, "golden-drift", "PASS") - - print("=======================================================") - failed = [r for r in RESULTS if not r[1]] - n = len(RESULTS) - print(f"SELF-PROOF: {n - len(failed)}/{n} checks correct") - if failed: - print("SELF-PROOF FAILED -- the metamorphic layer did NOT catch a planted " - "defect (or cried wolf on an honest control):") - for name, _, got, want in failed: - print(f" - {name}: got {got}, want {want}") - sys.exit(1) - print("SELF-PROOF GREEN: metamorphic layer provably catches vary-input " - "constants, grow-limit stubs/shrinks, nondeterminism, empty stubs, and " - "golden-drift memorization -- and does NOT cry wolf on honest output.") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/oracle/README.md b/tests/compat/oracle/README.md deleted file mode 100644 index abf9e610..00000000 --- a/tests/compat/oracle/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Differential Oracle (Go ↔ Rust compatibility) - -Implements **layer 3 / roadmap item 1** of `specs/go-compat-testing/SPEC.md`: -the differential oracle + Go-measured canonicalizer. The live Go binary is the -source of truth; this oracle never re-derives expected output. - -## Files - -| File | Role | -|------|------| -| `oracle.py` | The oracle. Runs Go N≥3× and Rust 2× under the pinned env, MEASURES the stable/variant field classification with stored evidence, and emits a per-invocation verdict PASS/FAIL/SIM. | -| `parity_gate.sh` | The generalization of `tests/antisim/parity_gate.sh` onto the oracle. Strictly stronger: nothing is hand-declared, everything is measured. | -| `selftest/self_test.py` | The self-proof. Plants every defect class and asserts the oracle reports the correct non-PASS verdict. A green that cannot catch a planted bug is worthless. | - -## What the oracle does per invocation - -``` -set -f; env TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 <bin> <argv> -``` - -1. **Run Go N≥3×** (the oracle). Classify every JSON leaf path as - - `GO-STABLE` — identical value in every Go run - - `GO-VARIANT` — differs across Go runs (or present/absent inconsistently) - - For every variant path the **distinct differing Go values are stored as - EVIDENCE** in the manifest. Canonicalization is *measured, never declared.* - -2. **Run Rust 2×.** If the two identical-arg Rust runs differ, Rust is - nondeterministic → **FAIL** (a faithful port is deterministic). - -3. **Compare Rust vs Go:** - - `GO-STABLE` fields → **byte/value-exact** against the one stable Go value. - A dropped or blanked stable field cannot pass. - - `GO-VARIANT` **lists** → sorted (multiset) on both sides, then compared. - - `GO-VARIANT` **numeric scalars** → compared inside Go's **own observed - `[min,max]` envelope** (e.g. summation-order float wobble), *not* blanked. - A Rust value outside Go's measured spread still diverges. - - If Go's *canonicalized* runs still disagree (the member SET itself is - Go-nondeterministic, e.g. `history/shotness`), byte/canonical parity is - measurably impossible; the oracle falls back to the **structural realcheck** - (non-empty, grows with `--limit`, Rust deterministic) and records the - Go canonical evidence hashes. - -4. **Verdict:** `PASS` (rc 0) / `FAIL` (rc 1) / `SIM` (rc 3) + the field - classification + evidence (with `--manifest <path>`). - -## The forbidden cheat, and how it is detected - -The historic failure was **blanking a Go-STABLE field** to hide a buggy Rust -value. The oracle accepts an optional `--normalize <path>` request and, if any -requested path is measured `GO-STABLE`, returns **FAIL: TAMPER** — fail-closed. -You cannot neutralize a field the oracle measured stable. - -## Run it - -```bash -# one invocation, with a stored manifest -python3 oracle.py --n-go 3 --manifest /tmp/m.json -- uast parse --format json FILE.go - -# the generalized parity gate over the full probe set -bash parity_gate.sh # all probes -bash parity_gate.sh history # filter by substring -NGO=5 bash parity_gate.sh # more Go runs per probe - -# the self-proof (must be green for the oracle to be trusted) -python3 selftest/self_test.py -``` - -## Self-proof (`selftest/self_test.py`) - -Plants each defect by scripting the binary outputs (the binaries *are* the -oracle, so faking their output is the honest way to plant a defect without -corrupting the real port) and asserts the verdict: - -| Planted defect | Expected | -|----------------|----------| -| Rust wrong on a Go-stable field | FAIL | -| Rust drops a Go-stable field | FAIL | -| Rust nondeterministic (2 runs differ) | FAIL | -| `--normalize` targets a Go-STABLE field (the cheat) | FAIL / TAMPER | -| Variant-list content bug (wrong member after sort) | FAIL | -| Hardcoded constant on content-nondeterministic Go | FAIL | -| Empty Rust stub | FAIL | -| Everything agrees (negative control) | PASS | -| Order-only Go nondeterminism, Rust canonically correct | PASS | -| Honest structural growth with `--limit` | PASS | - -## Real bugs this oracle has already surfaced - -Both were **GREEN under the old hand-classified `parity_gate.sh`** because that -gate declared the analyzer "content-nondeterministic" and only ran a structural -`realprobe` (non-empty / grows / deterministic). The oracle MEASURES that Go is -actually stable on these fields and demands parity: - -- `history/file-history`: Go-stable integer aggregates (`total_commits=308`, - `total_files=124` on kubernetes `--limit 50`) are wrong in Rust (`316`, `123`). -- `history/couples`: Go-stable-after-envelope `avg_coupling_strength` - (`0.91406…`) is wrong in Rust (`0.91354…`), a divergence ~5000× larger than - Go's own ULP wobble. diff --git a/tests/compat/oracle/oracle.py b/tests/compat/oracle/oracle.py deleted file mode 100755 index b36b76df..00000000 --- a/tests/compat/oracle/oracle.py +++ /dev/null @@ -1,697 +0,0 @@ -#!/usr/bin/env python3 -""" -Go<->Rust differential ORACLE (SPEC: specs/go-compat-testing/SPEC.md, layer 3 / roadmap 1) - -THE ORACLE IS THE LIVE GO BINARY. This program NEVER re-derives expected output; -it executes the real Go binary as the source of truth and the real Rust binary as -the candidate, under a pinned environment, then compares them. - -Per-invocation procedure (rules from the task brief, encoded here): - - 1. Run Go N>=3 times -> classify every output field as - GO-STABLE : identical across ALL Go runs - GO-VARIANT : differs across Go runs - Store the DIFFERING Go outputs as EVIDENCE in the manifest. Canonicalization - is MEASURED, never declared. - 2. Run Rust 2 times -> if the two Rust runs differ, Rust is NONDETERMINISTIC - -> FAIL (a faithful port is deterministic). - 3. Compare Rust vs Go: - - GO-STABLE fields : BYTE-EXACT (Rust leaf must equal the one stable Go value) - - GO-VARIANT fields : CANONICAL-EQUAL only (sorted / neutralized), and ONLY - for fields measured variant. Blanking a Go-STABLE field - is the exact cheat that hid a real bug before; it is - FORBIDDEN and detected (see check_normalize_request). - 4. Emit verdict: PASS / FAIL / SIM, the field classification, and the evidence. - -Run env is pinned by the caller-equivalent here: - set -f; TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 -We compare STDOUT only (stderr is progress). -""" - -import argparse -import hashlib -import json -import os -import re -import shutil -import subprocess -import sys -import tempfile - -GO_DIR = "/home/dmitriy/sources/codefang/build/bin" -RU_DIR = "/home/dmitriy/sources/codefang/target/release" - -PINNED_ENV = { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800", -} - - -# --------------------------------------------------------------------------- # -# Process execution: the binaries ARE the oracle. No re-derivation anywhere. -# --------------------------------------------------------------------------- # -def _resolve(side, bin_name): - base = GO_DIR if side == "go" else RU_DIR - # argv[0] of the codefang/uast surface: "uast ..." -> uast binary, - # anything else (e.g. "run ...", "version") -> codefang binary with the - # subcommand kept as the first argv element. This mirrors parity_gate.sh. - if bin_name == "uast": - return os.path.join(base, "uast"), [] - return os.path.join(base, "codefang"), [bin_name] - - -def run_once(side, argv): - """Run the LIVE binary once under the pinned env; return (rc, stdout_bytes).""" - exe, prefix = _resolve(side, argv[0]) - cmd = [exe] + prefix + list(argv[1:]) - env = dict(os.environ) - env.update(PINNED_ENV) - # set -f equivalent: we pass argv as a list so the shell never globs. - p = subprocess.run( - cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, - timeout=600, - ) - return p.returncode, p.stdout - - -def sha(b): - return hashlib.sha256(b).hexdigest() - - -# --------------------------------------------------------------------------- # -# Field model: a "field" is a JSON leaf path. For non-JSON output we fall back -# to a single synthetic field "<bytes>" carrying the whole output. -# --------------------------------------------------------------------------- # -def try_json(b): - try: - return json.loads(b.decode("utf-8")) - except Exception: - return None - - -def leaf_paths(obj, prefix="$"): - """ - Yield (path, value) for every JSON leaf. Lists use a structural index so the - classifier can detect order-only variation distinctly from set variation: - we record list MEMBERSHIP under "<prefix>[]" (a multiset of canonical leaves) - AND each positional leaf under "<prefix>[i]". - """ - if isinstance(obj, dict): - for k in sorted(obj.keys()): - yield from leaf_paths(obj[k], f"{prefix}.{k}") - elif isinstance(obj, list): - # positional leaves (captures order) - for i, v in enumerate(obj): - yield from leaf_paths(v, f"{prefix}[{i}]") - else: - yield (prefix, obj) - - -def canon_str(obj): - return json.dumps(obj, sort_keys=True, separators=(",", ":")) - - -def field_map(b): - """Return {path: value} for a single output. JSON -> leaf paths; else one blob.""" - j = try_json(b) - if j is None: - return {"$<bytes>": b.decode("utf-8", "replace")}, False - return {p: v for p, v in leaf_paths(j)}, True - - -# --------------------------------------------------------------------------- # -# Measured classification: GO-STABLE vs GO-VARIANT, with stored evidence. -# --------------------------------------------------------------------------- # -def classify(go_outputs): - """ - go_outputs: list of stdout bytes from N Go runs. - Returns (classification, evidence, is_json, numeric_envelope). - - classification[path] = "STABLE" | "VARIANT" - - STABLE : the path exists with the SAME value in every Go run - - VARIANT : the value differs across runs, OR the path is present in some - runs and absent in others (membership instability) - - evidence: for every VARIANT path, the list of the distinct Go values observed - (this is the stored proof that Go itself varies here). - """ - maps = [] - is_json = True - for b in go_outputs: - m, j = field_map(b) - is_json = is_json and j - maps.append(m) - - all_paths = set() - for m in maps: - all_paths.update(m.keys()) - - classification = {} - evidence = {} - numeric_envelope = {} # path -> (min, max) of Go's observed numeric values - SENTINEL = object() - for path in sorted(all_paths): - vals = [m.get(path, SENTINEL) for m in maps] - canon = [("<absent>" if v is SENTINEL else canon_str(v)) for v in vals] - if len(set(canon)) == 1: - classification[path] = "STABLE" - else: - classification[path] = "VARIANT" - # store the DISTINCT differing Go observations as evidence - evidence[path] = sorted(set(canon)) - # If every present Go value is numeric, record the OBSERVED envelope - # [min,max]. A variant float is NOT blanked: Rust must land inside - # Go's own measured spread (the strongest non-weakening rule for a - # field Go itself wobbles on, e.g. summation-order float noise). - present = [v for v in vals if v is not SENTINEL] - if present and all(isinstance(v, (int, float)) - and not isinstance(v, bool) for v in present): - numeric_envelope[path] = (min(present), max(present)) - return classification, evidence, is_json, numeric_envelope - - -# --------------------------------------------------------------------------- # -# Canonicalization of VARIANT-only structure (sorted / neutralized). -# Applied identically to Go and Rust so order-only / membership variation that -# Go itself exhibits is neutralized -- but ONLY where measured variant. -# --------------------------------------------------------------------------- # -def canonicalize(obj, variant_lists, numeric_envelope=None): - """ - Two MEASURED neutralizations, applied identically to Go and Rust: - - * Sort any list whose path is in variant_lists (its ELEMENT ORDER or - MEMBERSHIP was measured Go-variant). Stable lists keep their order so a - real ordering bug is still caught. - - * For a variant numeric SCALAR with a recorded Go [min,max] envelope, map - the value to the token "<num:envelope>" IFF it falls inside Go's OWN - observed spread (so Go's summation-order float wobble is neutralized) and - otherwise leave the raw number (so a value OUTSIDE Go's measured range -- - a real Rust bug like couples' avg_coupling_strength -- still diverges). - This is NOT blanking: the envelope is Go's measured variance, never a - declared tolerance. - - numeric_envelope keys are positional leaf paths ($.a.b or $.x[3].y). After a - variant list is sorted, positional indices no longer align, so envelopes only - bite OUTSIDE variant lists; inside variant lists the structure equality on the - sorted multiset is the authority. - """ - numeric_envelope = numeric_envelope or {} - - def walk(o, prefix="$"): - if isinstance(o, dict): - return {k: walk(o[k], f"{prefix}.{k}") for k in o} - if isinstance(o, list): - elems = [walk(v, f"{prefix}[{i}]") for i, v in enumerate(o)] - # the list CONTAINER's measured-variant marker is "<prefix>[]" - collapsed = __import__("re").sub(r"\[\d+\]", "[]", prefix) + "[]" - if collapsed in variant_lists: - elems = sorted(elems, key=lambda x: canon_str(x)) - return elems - # scalar leaf: apply Go-measured numeric envelope if present for this path - if prefix in numeric_envelope and isinstance(o, (int, float)) \ - and not isinstance(o, bool): - lo, hi = numeric_envelope[prefix] - if lo <= o <= hi: - return "<num:in-go-envelope>" - return o - return walk(obj) - - -def variant_list_prefixes(classification): - """ - Derive which LIST containers were Go-variant: any path of the form - "<base>[i]..." or "<base>[]" that is classified VARIANT implies the list at - <base> varied. We collapse the positional index to "<base>[]" so the same - canonicalization rule applies to both Go and Rust regardless of length. - """ - import re - prefixes = set() - for path, cls in classification.items(): - if cls != "VARIANT": - continue - # Replace every "[<n>]" with "[]" and record each list-container prefix. - # e.g. "$.node_hotness[3].name" -> list base "$.node_hotness[]" - collapsed = re.sub(r"\[\d+\]", "[]", path) - parts = collapsed.split("[]") - acc = "" - for k in range(len(parts) - 1): - acc += parts[k] - prefixes.add(acc + "[]") - acc += "[]" - return prefixes - - -# --------------------------------------------------------------------------- # -# TAMPER / CHEAT detector. -# The historic cheat: blank a Go-STABLE field so a buggy Rust value is hidden. -# We accept an optional external "normalize request" (paths the caller wants -# neutralized). If ANY requested path is classified GO-STABLE, that is the cheat -# -> hard error, fail-closed. -# --------------------------------------------------------------------------- # -def check_normalize_request(normalize_paths, classification): - illegal = [p for p in normalize_paths - if classification.get(p, "STABLE") == "STABLE"] - return illegal - - -# --------------------------------------------------------------------------- # -# Comparison: STABLE -> byte-exact leaf; VARIANT -> canonical-equal only. -# --------------------------------------------------------------------------- # -def _in_variant_list(path, var_lists): - """True if this leaf path lives inside a measured-variant list container.""" - import re - collapsed = re.sub(r"\[\d+\]", "[]", path) - for vl in var_lists: # vl ends in "[]" - if collapsed == vl or collapsed.startswith(vl): - return True - return False - - -def compare(go_canonical_b, rust_canonical_b, classification, go_field_map, - is_json, var_lists, numeric_envelope=None): - """ - Returns (ok, diffs). Compares Rust against the CANONICALIZED Go reference. - - For JSON: - - STABLE leaves OUTSIDE any variant list : byte/value-exact, present in Rust. - (A dropped/blanked stable field cannot pass via structural coincidence.) - - Leaves INSIDE a measured-variant list : NOT checked positionally (index - alignment is meaningless after the list is sorted); their content is fully - policed by the whole-structure canonical equality below, which compares the - sorted multiset and so still catches any real value divergence. - - VARIANT leaves : canonical-equal only. - """ - diffs = [] - if not is_json: - if go_canonical_b != rust_canonical_b: - diffs.append({ - "path": "$<bytes>", "kind": "non-json-bytes-differ", - "go_sha": sha(go_canonical_b), "rust_sha": sha(rust_canonical_b), - }) - return (len(diffs) == 0), diffs - - # canonical structural equality covers VARIANT lists (sorted) AND everything - # else. We additionally assert every STABLE leaf is present & equal in Rust so - # a dropped/blanked stable field cannot pass via structural coincidence. - go_j = json.loads(go_canonical_b) - ru_j = json.loads(rust_canonical_b) - ru_fields = {p: v for p, v in leaf_paths(ru_j)} - - # 1) STABLE leaves: byte-exact value match against the stable Go value, - # EXCEPT leaves inside a variant list (policed by structure-equality #2). - for path, cls in classification.items(): - if cls != "STABLE": - continue - if _in_variant_list(path, var_lists): - continue - gv = go_field_map.get(path) - if path not in ru_fields: - diffs.append({"path": path, "kind": "stable-field-missing-in-rust", - "go_value": gv}) - elif canon_str(ru_fields[path]) != canon_str(gv): - diffs.append({"path": path, "kind": "stable-field-differs", - "go_value": gv, "rust_value": ru_fields[path]}) - - # 2) Whole-structure canonical equality (catches VARIANT-list content drift, - # extra rust fields, type changes the leaf scan would miss). - if canon_str(go_j) != canon_str(ru_j): - diffs.append({"path": "$", "kind": "canonical-structure-differs", - "go_sha": sha(canon_str(go_j).encode()), - "rust_sha": sha(canon_str(ru_j).encode())}) - - return (len(diffs) == 0), diffs - - -# --------------------------------------------------------------------------- # -# File-output cells (--format plot): the contract is the FILES the binary -# writes into --output, not stdout. Both binaries run into fresh temp dirs; -# the file SETS and per-file bytes are compared with the same measured-variance -# discipline as the stdout path: -# * a file whose bytes are identical across all N Go runs must match Rust -# byte-for-byte; -# * a file that varies is first canonicalized by neutralizing the go-echarts -# random chart ids (12-char [A-Za-z] tokens in `id="..."` / `goecharts_*` / -# `option_*` sites) — the substitution is only TRUSTED when it provably -# makes all Go runs agree (measurement-driven, like variant-list sorting); -# * a file that stays unstable even canonicalized is Go-content- -# nondeterministic: the fallback requires the Rust file to be non-empty and -# deterministic (the structural realcheck contract). -# Rust determinism (two identical-arg runs, raw bytes) is REQUIRED throughout. -# --------------------------------------------------------------------------- # -_CHART_ID_SITES = [ - (re.compile(rb'id="[A-Za-z]{12}"'), b'id="CHARTID"'), - (re.compile(rb'goecharts_[A-Za-z]{12}'), b'goecharts_CHARTID'), - (re.compile(rb'option_[A-Za-z]{12}'), b'option_CHARTID'), - # The echarts init call references the same random element id: - # `getElementById('XXXXXXXXXXXX')`. Without this site the canonicalized Go - # runs never agree on chart-bearing pages and every such file silently - # downgrades to the weaker structural contract. - (re.compile(rb"getElementById\('[A-Za-z]{12}'\)"), b"getElementById('CHARTID')"), -] - - -def _canon_chart_ids(data): - for pat, repl in _CHART_ID_SITES: - data = pat.sub(repl, data) - return data - - -def run_once_files(side, argv): - """Run the LIVE binary with --output pointed at a fresh temp dir; return - (rc, {relpath: bytes}) over every regular file the run wrote.""" - out_dir = tempfile.mkdtemp(prefix=f"oracle-plot-{side}-") - try: - rc, _ = run_once(side, list(argv) + ["--output", out_dir]) - files = {} - for root, _dirs, names in os.walk(out_dir): - for name in names: - p = os.path.join(root, name) - rel = os.path.relpath(p, out_dir) - with open(p, "rb") as f: - files[rel] = f.read() - return rc, files - finally: - shutil.rmtree(out_dir, ignore_errors=True) - - -def is_file_output_cell(argv): - """True when the invocation's contract is --output files (plot format, - no caller-supplied --output).""" - if "--output" in argv: - return False - for i, tok in enumerate(argv): - if tok == "--format" and i + 1 < len(argv) and argv[i + 1] == "plot": - return True - if tok == "--format=plot": - return True - return False - - -def run_invocation_files(argv, n_go=3): - """File-set differential verdict for a --format plot cell.""" - go_runs = [run_once_files("go", argv) for _ in range(n_go)] - go_sets = [set(files) for _rc, files in go_runs] - - base = {"argv": argv, "mode": "files", - "go_rcs": [rc for rc, _ in go_runs], - "go_file_sets": sorted(go_sets[0]) if go_sets else []} - - if all(not files for _rc, files in go_runs): - base["verdict"] = "FAIL" - base["reason"] = "go wrote no files (probe invalid / no Go contract)" - return base - - ru_a = run_once_files("rust", argv) - ru_b = run_once_files("rust", argv) - base["rust_rcs"] = [ru_a[0], ru_b[0]] - - # Rust determinism is non-negotiable: raw byte equality across two runs. - if ru_a[1] != ru_b[1]: - unstable = sorted(set(ru_a[1]) ^ set(ru_b[1])) or [ - p for p in ru_a[1] if ru_a[1][p] != ru_b[1].get(p)] - base["verdict"] = "FAIL" - base["reason"] = "RUST NONDETERMINISTIC (file outputs differ across runs)" - base["unstable_rust_files"] = unstable[:10] - return base - rust_files = ru_a[1] - - # Go file-set stability gates the per-file comparison. - if any(s != go_sets[0] for s in go_sets[1:]): - base["verdict"] = "PASS" if rust_files else "FAIL" - base["reason"] = ("Go file SET nondeterministic; structural fallback: " - + ("rust wrote files deterministically" if rust_files - else "rust wrote NO files")) - base["structural"] = True - return base - - if set(rust_files) != go_sets[0]: - base["verdict"] = "FAIL" - base["reason"] = "file set mismatch" - base["only_go"] = sorted(go_sets[0] - set(rust_files))[:10] - base["only_rust"] = sorted(set(rust_files) - go_sets[0])[:10] - return base - - diffs = [] - file_modes = {} - for rel in sorted(go_sets[0]): - go_variants = [files[rel] for _rc, files in go_runs] - if all(v == go_variants[0] for v in go_variants[1:]): - # Byte-stable in Go -> exact byte contract. - file_modes[rel] = "stable" - if rust_files[rel] != go_variants[0]: - diffs.append({"file": rel, "kind": "bytes-differ", - "go_sha": sha(go_variants[0]), - "rust_sha": sha(rust_files[rel])}) - continue - canon = [_canon_chart_ids(v) for v in go_variants] - if all(c == canon[0] for c in canon[1:]): - # Variance fully explained by chart ids (measured) -> canonical - # byte contract. - file_modes[rel] = "chart-id-canonical" - if _canon_chart_ids(rust_files[rel]) != canon[0]: - diffs.append({"file": rel, "kind": "canonical-bytes-differ", - "go_sha": sha(canon[0]), - "rust_sha": sha(_canon_chart_ids(rust_files[rel]))}) - continue - # Content-nondeterministic even canonicalized: structural contract. - file_modes[rel] = "go-content-nondeterministic" - if not rust_files[rel]: - diffs.append({"file": rel, "kind": "rust-empty-vs-nondet-go"}) - - base["file_modes"] = file_modes - base["diffs"] = diffs - base["verdict"] = "PASS" if not diffs else "FAIL" - if diffs: - base["reason"] = "file divergence" - return base - - -# --------------------------------------------------------------------------- # -# Top-level per-invocation oracle. -# --------------------------------------------------------------------------- # -def run_invocation(argv, n_go=3, normalize=None): - normalize = normalize or [] - verdict = "PASS" - notes = [] - - # ---- Go N runs (the oracle) ---- - go_runs = [] - for _ in range(n_go): - rc, out = run_once("go", argv) - go_runs.append({"rc": rc, "out": out, "sha": sha(out), "len": len(out)}) - go_outputs = [r["out"] for r in go_runs] - - if all(len(o) == 0 for o in go_outputs): - # Go produced nothing -> this cell has no Go contract here. - return { - "argv": argv, "verdict": "FAIL", - "reason": "go-empty-output (probe invalid / no Go contract)", - "go_shas": [r["sha"] for r in go_runs], - } - - classification, evidence, is_json, numeric_envelope = classify(go_outputs) - - # ---- cheat detector: refuse to neutralize a Go-STABLE field ---- - illegal = check_normalize_request(normalize, classification) - if illegal: - return { - "argv": argv, "verdict": "FAIL", - "reason": "TAMPER: normalize request targets Go-STABLE field(s) " - "(blanking a stable field is the forbidden cheat)", - "illegal_normalize_paths": illegal, - "classification": classification, - } - - # ---- Rust 2 runs (determinism + candidate) ---- - ru_runs = [] - for _ in range(2): - rc, out = run_once("rust", argv) - ru_runs.append({"rc": rc, "out": out, "sha": sha(out), "len": len(out)}) - - if ru_runs[0]["sha"] != ru_runs[1]["sha"]: - return { - "argv": argv, "verdict": "FAIL", - "reason": "RUST NONDETERMINISTIC (two identical-arg Rust runs differ)", - "rust_shas": [r["sha"] for r in ru_runs], - "classification": classification, "evidence": evidence, - } - - # ---- build canonicalization rule strictly from MEASURED variance ---- - var_lists = variant_list_prefixes(classification) - - # Numeric envelopes are only meaningful OUTSIDE variant lists: a variant list - # is sorted, so its positional indices scramble and a per-index envelope would - # be an artifact of different elements landing at the same index. Drop those; - # inside variant lists the sorted-multiset structure equality is the authority. - numeric_envelope = {p: v for p, v in numeric_envelope.items() - if not _in_variant_list(p, var_lists)} - - # pick a Go reference run (run 0). Build the go field map for STABLE checks. - go_ref = go_outputs[0] - go_field_map, _ = field_map(go_ref) - - n_stable = sum(1 for c in classification.values() if c == "STABLE") - n_variant = sum(1 for c in classification.values() if c == "VARIANT") - base = { - "argv": argv, - "is_json": is_json, - "go_runs": [{"sha": r["sha"], "len": r["len"], "rc": r["rc"]} for r in go_runs], - "rust_runs": [{"sha": r["sha"], "len": r["len"], "rc": r["rc"]} for r in ru_runs], - "field_counts": {"stable": n_stable, "variant": n_variant}, - "variant_list_prefixes": sorted(var_lists), - "classification": classification, - "evidence": evidence, - } - - if is_json: - # MEASURE canonical stability: are all N Go runs equal AFTER canonicalizing - # the measured-variant lists? If yes, Go's nondeterminism is order-only and - # canonical parity is a legitimate, enforceable contract. If NO, Go's output - # is CONTENT-nondeterministic (the member SET differs run-to-run, e.g. - # history/shotness) and byte/canonical parity is MEASURABLY IMPOSSIBLE -- - # the recorded reference is itself irreproducible. We do NOT silently pass: - # we fall back to the STRUCTURAL realprobe (non-empty + Rust deterministic), - # and record that the parity contract is unavailable WITH the evidence. - # Go is JSON. If Rust's output is NOT parseable JSON (e.g. empty stdout, - # an error string, or truncated bytes) that is a GENUINE divergence -- Go - # produced a UAST/report and Rust did not. Report a clean FAIL with the - # evidence instead of crashing the oracle on json.loads. (Strengthening: - # an unparseable Rust output can never byte/canonical-match a JSON Go - # output, so this can only ever be a real FAIL, never a hidden pass.) - try: - ru_obj = json.loads(ru_runs[0]["out"]) - except (json.JSONDecodeError, ValueError): - base["verdict"] = "FAIL" - base["reason"] = ("Go emitted JSON but Rust output is not parseable " - "JSON (empty/error/truncated) -- genuine divergence") - base["rust_out_len"] = ru_runs[0]["len"] - base["rust_out_head"] = ru_runs[0]["out"][:200].decode( - "utf-8", "replace") - base["go_out_len"] = len(go_ref) - return base - go_canons = [canon_str(canonicalize(json.loads(o), var_lists, - numeric_envelope)) for o in go_outputs] - canonical_stable = len(set(go_canons)) == 1 - base["canonical_go_stable"] = canonical_stable - base["numeric_envelopes"] = {k: list(v) for k, v in numeric_envelope.items()} - - if canonical_stable: - go_canon = go_canons[0].encode() - ru_canon = canon_str(canonicalize(ru_obj, - var_lists, numeric_envelope)).encode() - ok, diffs = compare(go_canon, ru_canon, classification, - go_field_map, is_json, var_lists, numeric_envelope) - base["verdict"] = "PASS" if ok else "FAIL" - base["diffs"] = diffs - return base - - # content-nondeterministic Go: structural proof of real computation. - base["canonical_go_evidence"] = sorted( - {sha(c.encode()) for c in go_canons}) - ok, why = _structural_realcheck(argv, ru_runs[0]["out"]) - base["verdict"] = "PASS" if ok else "FAIL" - base["reason"] = ("Go content-nondeterministic (canonical parity impossible); " - "verified structurally: " + why) - base["structural"] = True - return base - - # non-JSON: require byte-stability of Go, else structural fallback. - go_byte_stable = len({r["sha"] for r in go_runs}) == 1 - base["canonical_go_stable"] = go_byte_stable - if go_byte_stable: - ok, diffs = compare(go_ref, ru_runs[0]["out"], classification, - go_field_map, is_json, var_lists) - base["verdict"] = "PASS" if ok else "FAIL" - base["diffs"] = diffs - return base - ok, why = _structural_realcheck(argv, ru_runs[0]["out"]) - base["verdict"] = "PASS" if ok else "FAIL" - base["reason"] = "Go byte-nondeterministic; verified structurally: " + why - base["structural"] = True - return base - - -def _structural_realcheck(argv, rust_out): - """ - For invocations where Go output is content-nondeterministic and therefore not - byte/canonical-comparable, prove the Rust port does REAL work (defeats a - 0-byte stub and a constant stub) without demanding parity to an irreproducible - reference. Mirrors parity_gate.sh `realprobe`: - (1) Rust output is NON-EMPTY; - (2) Rust output GROWS when --limit is raised (not a hardcoded constant); - (3) Rust is DETERMINISTIC at the raised limit. - Returns (ok, reason_string). Only applies to `run ... --limit N` invocations. - """ - if len(rust_out) == 0: - return False, "rust output EMPTY (0-byte stub)" - if "--limit" not in argv: - # cannot run the grow probe; accept non-emptiness + determinism already - # established by the 2 Rust runs in the caller. - return True, "rust non-empty; determinism established (no --limit to grow-probe)" - # build a raised-limit argv - hi = list(argv) - i = hi.index("--limit") - try: - lo_val = int(hi[i + 1]) - except Exception: - return True, "rust non-empty; --limit not numeric, skipped grow-probe" - hi[i + 1] = str(lo_val * 20 + 50) - rc_a, out_hi_a = run_once("rust", hi) - rc_b, out_hi_b = run_once("rust", hi) - if sha(out_hi_a) != sha(out_hi_b): - return False, "rust NONDETERMINISTIC at raised limit" - if sha(out_hi_a) == sha(rust_out): - return False, "rust CONSTANT across limits (hardcoded-output signature)" - return True, (f"rust grows {len(rust_out)}B->{len(out_hi_a)}B with --limit " - f"and is deterministic") - - -def main(): - ap = argparse.ArgumentParser(description="Go<->Rust differential oracle") - ap.add_argument("--n-go", type=int, default=3, help="Go runs (>=3)") - ap.add_argument("--normalize", action="append", default=[], - help="JSON leaf path to neutralize; REJECTED if Go-stable") - ap.add_argument("--manifest", help="write full manifest JSON to this path") - ap.add_argument("--quiet", action="store_true") - ap.add_argument("argv", nargs=argparse.REMAINDER, - help="invocation: <bin> <args...> (bin = codefang-subcmd or uast)") - a = ap.parse_args() - if a.n_go < 3: - print("n-go must be >= 3 (measured canonicalization needs >=3 runs)", - file=sys.stderr) - sys.exit(2) - argv = a.argv - if argv and argv[0] == "--": - argv = argv[1:] - if not argv: - print("no invocation given", file=sys.stderr) - sys.exit(2) - - if is_file_output_cell(argv): - # --format plot: the contract is the --output files, not stdout. - res = run_invocation_files(argv, n_go=a.n_go) - else: - res = run_invocation(argv, n_go=a.n_go, normalize=a.normalize) - if a.manifest: - with open(a.manifest, "w") as f: - json.dump(res, f, indent=2, default=str) - if not a.quiet: - # human line - v = res["verdict"] - fc = res.get("field_counts", {}) - print(f"{v} {' '.join(argv)} " - f"[stable={fc.get('stable','?')} variant={fc.get('variant','?')}]") - if v == "FAIL": - print(" reason:", res.get("reason", "field divergence")) - for d in res.get("diffs", [])[:8]: - print(" diff:", json.dumps(d, default=str)[:300]) - # exit code: PASS=0, FAIL=1, SIM=3 - sys.exit({"PASS": 0, "FAIL": 1, "SIM": 3}.get(res["verdict"], 1)) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/oracle/parity_gate.sh b/tests/compat/oracle/parity_gate.sh deleted file mode 100755 index 8c27202a..00000000 --- a/tests/compat/oracle/parity_gate.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================ -# ORACLE-GENERALIZED PARITY GATE -# ============================================================================ -# This is the GENERALIZATION of tests/antisim/parity_gate.sh onto the -# differential ORACLE (oracle.py). It is STRICTLY STRONGER, never weaker: -# -# * The original gate hand-classified each analyzer as strict-byte / canonical -# / "realprobe" based on a HUMAN ASSERTION about Go nondeterminism. That hand -# classification is exactly what got gamed before (a Go-stable field was -# assumed random and blanked, hiding a real bug). -# * Here, every probe goes through oracle.py, which MEASURES Go nondeterminism -# by running Go N>=3x and classifies each field STABLE/VARIANT with stored -# EVIDENCE. Byte-exact is required on every measured-stable field; only -# measured-variant lists are sorted and only measured-variant float scalars -# are compared within Go's OWN observed envelope. Nothing is declared. -# -# This driver runs the SAME probe set the original gate covered (uast parse/ -# analyze/query; static complexity/composition/halstead/comments/imports; -# history imports/devs/burndown/typos/couples/shotness/file-history) plus the -# original gate's simulation probes (two different inputs => Rust must not emit a -# constant). It does NOT shrink the probed set and does NOT blank Go-stable -# fields -- doing either is detectable (matrix-shrink + tamper checks below). -# -# Pinned run env (identical to the spec): set -f; TZ=UTC NO_COLOR=1 LANG=C -# LC_ALL=C SOURCE_DATE_EPOCH=315532800. STDOUT compared; stderr is progress. -# -# Exit 0 only when every probe PASSes. Usage: parity_gate.sh [substr-filter] -set -uf -ROOT=/home/dmitriy/sources/codefang -KUBE=/home/dmitriy/sources/kubernetes -ORACLE="$ROOT/tests/compat/oracle/oracle.py" -export TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 -FILTER="${1:-}" -NGO="${NGO:-3}" - -PASS=0; FAIL=0; SIM=0 -declare -a FAILED SIMSUS - -# probe: run the oracle on one invocation; classify the verdict. -probe() { # $1 label $2.. argv - local label="$1"; shift - [ -n "$FILTER" ] && [[ "$label" != *"$FILTER"* ]] && return 0 - local out rc - out=$(python3 "$ORACLE" --n-go "$NGO" -- "$@" 2>&1); rc=$? - case "$rc" in - 0) PASS=$((PASS+1)); printf ' PASS %s\n' "$label" ;; - 3) SIM=$((SIM+1)); SIMSUS+=("$label"); printf ' SIM! %s\n' "$label" ;; - *) FAIL=$((FAIL+1)); FAILED+=("$label") - printf ' FAIL %s\n' "$label" - printf '%s\n' "$out" | sed 's/^/ /' | head -6 ;; - esac -} - -# simulation probe (carried over from the original gate): two DIFFERENT inputs. -# If Rust emits identical bytes for both while Go differs, Rust is a constant. -simprobe() { # $1 label ; $2 argvA(|-sep) ; $3 argvB(|-sep) - local label="$1"; shift - [ -n "$FILTER" ] && [[ "$label" != *"$FILTER"* ]] && return 0 - local IFS='|'; local -a A=($1) B=($2); unset IFS - local rA rB gA gB - rA=$( ru_raw "${A[@]}" ); rB=$( ru_raw "${B[@]}" ) - gA=$( go_raw "${A[@]}" ); gB=$( go_raw "${B[@]}" ) - if [ "$rA" = "$rB" ] && [ "$gA" != "$gB" ]; then - SIM=$((SIM+1)); SIMSUS+=("$label") - printf ' SIM! %s : rust CONSTANT while go varies\n' "$label" - else - PASS=$((PASS+1)); printf ' PASS %s (rust varies with input)\n' "$label" - fi -} -go_raw(){ local b="$1"; shift; case "$b" in uast) "$ROOT/build/bin/uast" "$@";; *) "$ROOT/build/bin/codefang" "$b" "$@";; esac 2>/dev/null; } -ru_raw(){ local b="$1"; shift; case "$b" in uast) "$ROOT/target/release/uast" "$@";; *) "$ROOT/target/release/codefang" "$b" "$@";; esac 2>/dev/null; } - -echo "============ ORACLE-GENERALIZED PARITY GATE (N=$NGO Go runs/probe) ============" -echo - -RUN="run --checkpoint=false --resume=false --no-cache --workers 1" -SDIR="$KUBE/pkg/scheduler/framework" -SDIR2="$KUBE/pkg/apis/core" -SRUN="run --checkpoint=false --resume=false --no-cache --head --workers 1 --static-workers 1 -p" - -echo "-- uast (off-golden files) --" -for f in $(find "$KUBE/pkg/scheduler" -name '*.go' 2>/dev/null | head -3); do - probe "uast/parse:$(basename "$f")" uast parse --format json "$f" - probe "uast/analyze:$(basename "$f")" uast analyze --format json "$f" - probe "uast/query:$(basename "$f")" uast query 'filter(.roles has "Function")' --format json "$f" -done - -echo "-- static analyzers (off-golden dir, MEASURED classification) --" -for A in static/complexity static/composition static/halstead static/comments static/imports; do - probe "$A@framework" $SRUN "$SDIR" --analyzers "$A" --format json -done -for A in static/complexity static/halstead; do - simprobe "$A:const?" \ - "run|--checkpoint=false|--resume=false|--no-cache|--head|--workers|1|--static-workers|1|-p|$SDIR|--analyzers|$A|--format|json" \ - "run|--checkpoint=false|--resume=false|--no-cache|--head|--workers|1|--static-workers|1|-p|$SDIR2|--analyzers|$A|--format|json" -done - -echo "-- history analyzers (off-golden limits, MEASURED classification) --" -for A in history/imports history/devs history/burndown history/typos \ - history/couples history/shotness history/file-history; do - probe "$A@limit50" $RUN --analyzers "$A" --format json --limit 50 "$KUBE" -done -for A in history/imports history/typos history/devs history/burndown; do - simprobe "$A:const?" \ - "run|--checkpoint=false|--resume=false|--no-cache|--workers|1|--analyzers|$A|--format|json|--limit|10|$KUBE" \ - "run|--checkpoint=false|--resume=false|--no-cache|--workers|1|--analyzers|$A|--format|json|--limit|500|$KUBE" -done - -echo -echo "================ RESULT ================" -echo "PASS=$PASS FAIL=$FAIL SIMULATION_SUSPECT=$SIM" -[ "$FAIL" -gt 0 ] && { echo "FAILED:"; printf ' - %s\n' "${FAILED[@]}"; } -[ "$SIM" -gt 0 ] && { echo "SIMULATION SUSPECTS:"; printf ' - %s\n' "${SIMSUS[@]}"; } -if [ "$FAIL" -eq 0 ] && [ "$SIM" -eq 0 ]; then - echo "GATE: GREEN — measured parity on off-golden inputs (no field declared, all measured)"; exit 0 -fi -echo "GATE: RED — port is incomplete or simulated (oracle-measured)"; exit 1 diff --git a/tests/compat/oracle/patches/uast-unmapped-subtree-rewalk-perf.patch b/tests/compat/oracle/patches/uast-unmapped-subtree-rewalk-perf.patch deleted file mode 100644 index d42b17eb..00000000 --- a/tests/compat/oracle/patches/uast-unmapped-subtree-rewalk-perf.patch +++ /dev/null @@ -1,53 +0,0 @@ -diff --git a/pkg/uast/parser_dsl.go b/pkg/uast/parser_dsl.go -index f63dcf8..73a48d3 100644 ---- a/pkg/uast/parser_dsl.go -+++ b/pkg/uast/parser_dsl.go -@@ -320,10 +320,35 @@ func (ctx *parseContext) nodeType(root sitter.Node) string { - } - - // toCanonicalNode converts a tree-sitter node to a canonical UAST Node. -+// -+// PERF-ONLY restructure (oracle rebuild patch, output-byte-identical): the -+// original shape ran processChildren over the subtree and then re-walked the -+// same subtree inside createUnmappedNode, doubling the work at every -+// unmapped nesting level (2^depth blowup on deeply nested unmapped chains, -+// e.g. generated protobuf descriptors in vendor trees). shouldSkipNode is -+// vacuously false without a mapping rule, and shouldSkipEmptyFile only fires -+// on empty sources where both walks agree on zero children, so computing the -+// unmapped children ONCE up front is output-identical. - func (ctx *parseContext) toCanonicalNode(root sitter.Node, parentContext string) *node.Node { - nodeType := ctx.nodeType(root) - mappingRule := ctx.findMappingRule(nodeType) - -+ if mappingRule == nil { -+ mappedChildren := ctx.processUnmappedChildren(root, parentContext) -+ -+ if ctx.shouldSkipEmptyFile(nodeType, mappedChildren) { -+ return nil -+ } -+ -+ var roles []node.Role -+ -+ if ctx.includeUnmapped { -+ return ctx.createIncludeUnmappedNode(root, nodeType, mappedChildren, roles) -+ } -+ -+ return ctx.createSyntheticNode(mappedChildren) -+ } -+ - children := ctx.processChildren(root, mappingRule) - if ctx.shouldSkipNode(root, mappingRule) { - return nil -@@ -335,11 +360,7 @@ func (ctx *parseContext) toCanonicalNode(root sitter.Node, parentContext string) - - var roles []node.Role - -- if mappingRule != nil { -- return ctx.createMappedNode(root, mappingRule, children, roles) -- } -- -- return ctx.createUnmappedNode(root, parentContext, nodeType, roles) -+ return ctx.createMappedNode(root, mappingRule, children, roles) - } - - // findMappingRule finds a mapping rule for the given node type, resolving inheritance and merging fields. diff --git a/tests/compat/oracle/selftest/self_test.py b/tests/compat/oracle/selftest/self_test.py deleted file mode 100755 index f660b27e..00000000 --- a/tests/compat/oracle/selftest/self_test.py +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env python3 -""" -ORACLE SELF-PROOF (non-negotiable rule #6: every component must SELF-PROVE it -catches a defect). A green that cannot be shown to catch a planted bug is -worthless. This harness PLANTS each defect class and asserts the oracle reports -the correct non-PASS verdict. - -We exercise the REAL oracle decision logic (oracle.run_invocation) but feed it -controlled binary outputs by monkeypatching oracle.run_once -- the binaries are -the oracle, so faking the binary outputs is the honest way to plant a defect -without corrupting the real port. Each planted defect mirrors a real failure mode: - - T1 GO-STABLE divergence Rust returns a wrong value on a Go-stable field -> FAIL - T2 hardcoded-constant stub Rust emits the same bytes regardless of input - while Go varies (the simulation signature) -> SIM - T3 TAMPER (blank stable) caller asks to normalize a Go-STABLE field - (the exact cheat that hid a real bug) -> FAIL/TAMPER - T4 Rust nondeterminism two identical-arg Rust runs differ -> FAIL - T5 dropped Go-stable field Rust omits a field Go always emits -> FAIL - T6 honest order-only nondet Go varies only in list ORDER; Rust matches the - canonical content -> PASS (control) - T7 NEGATIVE control everything agrees -> PASS - -Also includes a CONTROL proving the oracle does NOT cry wolf: a genuinely -order-only Go-variant case where Rust is canonically correct must PASS, so the -FAIL verdicts above are meaningful, not a stuck "always FAIL". -""" - -import importlib.util -import json -import os -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -ORACLE_PY = os.path.join(HERE, "..", "oracle.py") - -spec = importlib.util.spec_from_file_location("oracle", ORACLE_PY) -oracle = importlib.util.module_from_spec(spec) -spec.loader.exec_module(oracle) - - -class FakeBins: - """Drives oracle.run_once with scripted Go/Rust outputs per side.""" - def __init__(self, go_seq, rust_seq): - # go_seq / rust_seq: lists of (rc, bytes) consumed in call order - self.go = list(go_seq) - self.rust = list(rust_seq) - self.extra_rust = [] # for structural grow-probe at raised limit - - def __call__(self, side, argv): - if side == "go": - return self.go.pop(0) - # rust: if the grow-probe raised the limit, serve from extra_rust - if "--limit" in argv and self.extra_rust and self._is_raised(argv): - return self.extra_rust.pop(0) - return self.rust.pop(0) - - @staticmethod - def _is_raised(argv): - return True # self-tests that use structural path set extra_rust explicitly - - -def b(o): - return json.dumps(o).encode() - - -RESULTS = [] - - -def expect(name, got_verdict, want_verdict, res): - ok = got_verdict == want_verdict - RESULTS.append((name, ok, got_verdict, want_verdict)) - flag = "OK " if ok else "XX " - print(f" {flag}{name}: got {got_verdict}, want {want_verdict}" - + ("" if ok else f" <-- SELF-TEST FAILURE reason={res.get('reason','')}")) - return ok - - -def run(go_runs, rust_runs, normalize=None, argv=None, extra_rust=None): - argv = argv or ["run", "--analyzers", "x", "--format", "json", - "--limit", "10", "/repo"] - fb = FakeBins(go_runs, rust_runs) - if extra_rust: - fb.extra_rust = list(extra_rust) - orig = oracle.run_once - oracle.run_once = fb - try: - return oracle.run_invocation(argv, n_go=3, normalize=normalize or []) - finally: - oracle.run_once = orig - - -def main(): - print("================ ORACLE SELF-PROOF ================") - - # Shared "Go-stable" document. - STABLE_DOC = {"score": 7, "name": "alpha", "items": [1, 2, 3]} - - # ---- T7 NEGATIVE control: everything agrees -> PASS ---- - res = run( - go_runs=[(0, b(STABLE_DOC))] * 3, - rust_runs=[(0, b(STABLE_DOC))] * 2, - ) - expect("T7 all-agree", res["verdict"], "PASS", res) - - # ---- T1 GO-STABLE divergence: Rust wrong on a stable field -> FAIL ---- - WRONG = dict(STABLE_DOC, score=999) - res = run( - go_runs=[(0, b(STABLE_DOC))] * 3, - rust_runs=[(0, b(WRONG))] * 2, - ) - expect("T1 stable-field-divergence", res["verdict"], "FAIL", res) - - # ---- T5 dropped Go-stable field -> FAIL ---- - DROPPED = {"name": "alpha", "items": [1, 2, 3]} # 'score' removed - res = run( - go_runs=[(0, b(STABLE_DOC))] * 3, - rust_runs=[(0, b(DROPPED))] * 2, - ) - expect("T5 dropped-stable-field", res["verdict"], "FAIL", res) - - # ---- T4 Rust nondeterminism: two Rust runs differ -> FAIL ---- - res = run( - go_runs=[(0, b(STABLE_DOC))] * 3, - rust_runs=[(0, b(STABLE_DOC)), (0, b(WRONG))], - ) - expect("T4 rust-nondeterministic", res["verdict"], "FAIL", res) - - # ---- T3 TAMPER: ask to normalize a Go-STABLE field -> FAIL/TAMPER ---- - res = run( - go_runs=[(0, b(STABLE_DOC))] * 3, - rust_runs=[(0, b(WRONG))] * 2, - normalize=["$.score"], # score is Go-stable; blanking it is the cheat - ) - ok = expect("T3 tamper-blank-stable", res["verdict"], "FAIL", res) - if ok and "TAMPER" not in res.get("reason", ""): - RESULTS.append(("T3 tamper-reason", False, res.get("reason"), "TAMPER")) - print(" XX T3 tamper-reason: reason did not mention TAMPER") - - # ---- T6 honest order-only Go nondeterminism: Rust canonically correct -> PASS ---- - # Go varies only list ORDER across runs; the SET is identical every run, so - # canonicalization makes Go canonical-stable and Rust (a permutation) PASSes. - GO_A = {"meta": "k", "items": [{"v": 3}, {"v": 1}, {"v": 2}]} - GO_B = {"meta": "k", "items": [{"v": 1}, {"v": 2}, {"v": 3}]} - GO_C = {"meta": "k", "items": [{"v": 2}, {"v": 3}, {"v": 1}]} - RUST = {"meta": "k", "items": [{"v": 1}, {"v": 3}, {"v": 2}]} # another perm, same set - res = run( - go_runs=[(0, b(GO_A)), (0, b(GO_B)), (0, b(GO_C))], - rust_runs=[(0, b(RUST))] * 2, - ) - expect("T6 order-only-nondet-canonical-pass", res["verdict"], "PASS", res) - - # ---- T6b order-only nondet BUT Rust has WRONG content -> FAIL ---- - # Same list paths are Go-variant, but Rust's SET differs (a 4 instead of a 3): - # canonicalization must NOT hide this -> FAIL. Proves we don't over-neutralize. - RUST_BAD = {"meta": "k", "items": [{"v": 1}, {"v": 4}, {"v": 2}]} - res = run( - go_runs=[(0, b(GO_A)), (0, b(GO_B)), (0, b(GO_C))], - rust_runs=[(0, b(RUST_BAD))] * 2, - ) - expect("T6b variant-list-content-bug-still-caught", res["verdict"], "FAIL", res) - - # ---- T2 hardcoded-constant stub (content-nondet Go => structural realprobe) ---- - # Go is CONTENT-nondeterministic (member set differs every run) so byte/canonical - # parity is impossible and the oracle uses the structural realprobe. A constant - # Rust stub returns the SAME bytes at the raised limit => CONSTANT signature - # => FAIL. This is the simulation defect the realprobe must catch. - GO_S1 = {"nodes": [{"n": "a"}, {"n": "b"}]} - GO_S2 = {"nodes": [{"n": "c"}, {"n": "d"}]} - GO_S3 = {"nodes": [{"n": "e"}]} - CONST = {"nodes": [{"n": "CONST"}]} - res = run( - go_runs=[(0, b(GO_S1)), (0, b(GO_S2)), (0, b(GO_S3))], - rust_runs=[(0, b(CONST))] * 2, # limit=10 runs - extra_rust=[(0, b(CONST)), (0, b(CONST))], # raised-limit grow-probe: SAME - ) - expect("T2 constant-stub-structural", res["verdict"], "FAIL", res) - - # ---- T2b structural HONEST: Rust grows with limit + deterministic -> PASS ---- - GROWN = {"nodes": [{"n": "e"}, {"n": "f"}, {"n": "g"}]} - res = run( - go_runs=[(0, b(GO_S1)), (0, b(GO_S2)), (0, b(GO_S3))], - rust_runs=[(0, b(CONST))] * 2, - extra_rust=[(0, b(GROWN)), (0, b(GROWN))], # grows at raised limit, det. - ) - expect("T2b honest-growing-structural", res["verdict"], "PASS", res) - - # ---- T8 empty-Rust stub on content-nondet Go -> FAIL ---- - res = run( - go_runs=[(0, b(GO_S1)), (0, b(GO_S2)), (0, b(GO_S3))], - rust_runs=[(0, b"")] * 2, - extra_rust=[(0, b"")] * 2, - ) - expect("T8 empty-rust-stub-structural", res["verdict"], "FAIL", res) - - print("==================================================") - failed = [r for r in RESULTS if not r[1]] - n = len(RESULTS) - print(f"SELF-PROOF: {n - len(failed)}/{n} checks correct") - if failed: - print("SELF-PROOF FAILED -- the oracle did NOT catch a planted defect:") - for name, _, got, want in failed: - print(f" - {name}: got {got}, want {want}") - sys.exit(1) - print("SELF-PROOF GREEN: oracle provably catches divergence, constants, " - "tamper, dropped fields, and nondeterminism.") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/results/full.json b/tests/compat/results/full.json deleted file mode 100644 index e21cc0c8..00000000 --- a/tests/compat/results/full.json +++ /dev/null @@ -1,1541 +0,0 @@ -{ - "tier": "full", - "matrix_hash": "b07d1222dbf646ec6128aff7f628173f811c0da46ff441e290ba7d3afa766a5c", - "total_cells": 63, - "tally": { - "PASS": 56, - "FAIL": 6, - "SIM": 0, - "EXPECTED_EMPTY": 1 - }, - "records": [ - { - "label": "flag:history/devs--head@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/devs", - "--format", - "json", - "--head", - "/home/dmitriy/sources/hercules" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:history/devs--limit:50@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/devs", - "--format", - "json", - "--limit", - "50", - "--limit", - "50", - "/home/dmitriy/sources/hercules" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:history/devs--first-parent@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/devs", - "--format", - "json", - "--first-parent", - "--limit", - "50", - "/home/dmitriy/sources/hercules" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:history/devs--since:2020-01-01@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/devs", - "--format", - "json", - "--since", - "2020-01-01", - "--limit", - "50", - "/home/dmitriy/sources/hercules" - ], - "family": "flag", - "tier": "full", - "verdict": "EXPECTED_EMPTY", - "contract": "go-produces-no-stdout" - }, - { - "label": "flag:history/devs--include-vendored@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/devs", - "--format", - "json", - "--include-vendored", - "--limit", - "50", - "/home/dmitriy/sources/hercules" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:history/devs--include-generated@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/devs", - "--format", - "json", - "--include-generated", - "--limit", - "50", - "/home/dmitriy/sources/hercules" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:history/devs--languages:Go@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--workers", - "1", - "--analyzers", - "history/devs", - "--format", - "json", - "--languages", - "Go", - "--limit", - "50", - "/home/dmitriy/sources/hercules" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--per-file@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--per-file" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--include-vendored@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--include-vendored" - ], - "family": "flag", - "tier": "full", - "verdict": "FAIL" - }, - { - "label": "flag:static/complexity--include-generated@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go,Python@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--languages", - "Go,Python" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:all@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--languages", - "all" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--per-file[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--per-file" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--per-file[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "yaml", - "--per-file" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--per-file[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--per-file" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--per-file[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--per-file" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--per-file[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "yaml", - "--per-file" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--include-vendored[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--include-vendored" - ], - "family": "flag", - "tier": "full", - "verdict": "FAIL" - }, - { - "label": "flag:static/complexity--include-vendored[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "yaml", - "--include-vendored" - ], - "family": "flag", - "tier": "full", - "verdict": "FAIL" - }, - { - "label": "flag:static/halstead--include-vendored[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--include-vendored" - ], - "family": "flag", - "tier": "full", - "verdict": "FAIL" - }, - { - "label": "flag:static/halstead--include-vendored[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--include-vendored" - ], - "family": "flag", - "tier": "full", - "verdict": "FAIL" - }, - { - "label": "flag:static/halstead--include-vendored[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "yaml", - "--include-vendored" - ], - "family": "flag", - "tier": "full", - "verdict": "FAIL" - }, - { - "label": "flag:static/complexity--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--include-generated[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "yaml", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--include-generated[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "yaml", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "yaml", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "yaml", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go,Python[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--languages", - "Go,Python" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go,Python[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "yaml", - "--languages", - "Go,Python" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go,Python[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--languages", - "Go,Python" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go,Python[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--languages", - "Go,Python" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go,Python[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "yaml", - "--languages", - "Go,Python" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:all[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--languages", - "all" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:all[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "yaml", - "--languages", - "all" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:all[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--languages", - "all" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:all[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--languages", - "all" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:all[yaml]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "yaml", - "--languages", - "all" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/clones--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/clones", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/clones--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/clones", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/comments--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/comments", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/comments--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/comments", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/cohesion--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/cohesion", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/cohesion--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/cohesion", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/imports--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/imports", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/imports--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/imports", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/composition--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/composition", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/composition--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/composition", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/clones--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/clones", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/clones--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/clones", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/comments--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/comments", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/comments--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/comments", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/cohesion--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/cohesion", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/cohesion--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/cohesion", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/imports--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/imports", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/imports--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/imports", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/composition--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/composition", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - }, - { - "label": "flag:static/composition--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/composition", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "full", - "verdict": "PASS" - } - ] -} \ No newline at end of file diff --git a/tests/compat/results/full.partial.jsonl b/tests/compat/results/full.partial.jsonl deleted file mode 100644 index 5214f9b4..00000000 --- a/tests/compat/results/full.partial.jsonl +++ /dev/null @@ -1,71 +0,0 @@ -{"label": "flag:history/devs--since:2020-01-01@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--since", "2020-01-01", "--limit", "50", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "flag:history/devs--head@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--head", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:history/devs--first-parent@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--first-parent", "--limit", "50", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:history/devs--limit:50@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--limit", "50", "--limit", "50", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:history/devs--include-vendored@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--include-vendored", "--limit", "50", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:history/devs--include-generated@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--include-generated", "--limit", "50", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:history/devs--languages:Go@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--languages", "Go", "--limit", "50", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/complexity--languages:Go@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--per-file@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--per-file"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--include-generated@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:Go,Python@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--languages", "Go,Python"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:all@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--languages", "all"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--per-file[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--per-file"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--per-file[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml", "--per-file"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--per-file[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--per-file"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--per-file[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--per-file"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--per-file[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml", "--per-file"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--include-vendored@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/complexity--include-vendored[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/complexity--include-vendored[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/halstead--include-vendored[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/complexity--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--include-generated[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--include-generated[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:Go[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:Go,Python[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--languages", "Go,Python"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:Go,Python[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml", "--languages", "Go,Python"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go,Python[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--languages", "Go,Python"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go,Python[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--languages", "Go,Python"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go,Python[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml", "--languages", "Go,Python"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:all[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--languages", "all"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:all[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml", "--languages", "all"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:all[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--languages", "all"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:all[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--languages", "all"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:all[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml", "--languages", "all"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/clones--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "json", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/clones--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "text", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/comments--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "json", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/comments--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "text", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/cohesion--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "json", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/cohesion--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "text", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/imports--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "json", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/composition--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "json", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/composition--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "text", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/imports--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "text", "--include-generated"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/clones--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/clones--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/comments--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/comments--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/cohesion--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/cohesion--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/imports--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/composition--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/imports--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/composition--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--include-vendored[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/halstead--include-vendored[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:history/devs--languages:Go@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--workers", "1", "--analyzers", "history/devs", "--format", "json", "--languages", "Go", "--limit", "50", "/home/dmitriy/sources/hercules"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/complexity--include-vendored@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/complexity--include-vendored[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/complexity--include-vendored[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/halstead--include-vendored[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/clones--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "full", "verdict": "PASS"} -{"label": "flag:static/halstead--include-vendored[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} -{"label": "flag:static/halstead--include-vendored[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml", "--include-vendored"], "family": "flag", "tier": "full", "verdict": "FAIL"} diff --git a/tests/compat/results/full.partial.preheuristics.1781013773.jsonl b/tests/compat/results/full.partial.preheuristics.1781013773.jsonl deleted file mode 100644 index 01f92cd0..00000000 --- a/tests/compat/results/full.partial.preheuristics.1781013773.jsonl +++ /dev/null @@ -1,207 +0,0 @@ -{"label": "uast/parse[json]/go", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/go", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/go", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/go", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/go", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/go", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/go", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/go", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/python", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/python", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/python", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/python", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/python", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/python", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/python", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/python", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/c", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/c", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/c", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/c", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/c", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/c", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/c", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/c", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/c-header", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/c-header", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/c-header", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/c-header", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/c-header", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/c-header", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/c-header", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/c-header", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/rust", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/rust", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/rust", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/rust", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/rust", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/rust", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/rust", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/rust", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/typescript", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/typescript", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/typescript", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/typescript", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/typescript", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/typescript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/typescript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/typescript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/tsx", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/tsx", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/tsx", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/tsx", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/tsx", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/tsx", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/tsx", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/tsx", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/javascript", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/javascript", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/javascript", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/javascript", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/javascript", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/javascript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/javascript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/javascript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/json", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/json", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/json", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/json", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/json", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/json", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/json", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/json", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/yaml", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/yaml", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/yaml", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/yaml", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/yaml", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/yaml", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/yaml", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/yaml", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/cpp", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/cpp", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/cpp", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/cpp", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/cpp", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/cpp", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/cpp", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/cpp", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/shell", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/shell", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/shell", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/shell", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/shell", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/shell", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/shell", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/shell", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/clones[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/clones[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static-set:*[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/*[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,static/halstead[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity,static/halstead", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,history/devs[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity,history/devs", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/clones[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "FAIL"} -{"label": "static:static/halstead[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "FAIL"} -{"label": "static:static/cohesion[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "FAIL"} -{"label": "static:static/cohesion[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static-set:*[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/*[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,static/halstead[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity,static/halstead", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,history/devs[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity,history/devs", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "FAIL"} -{"label": "static:static/cohesion[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[json]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[yaml]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} diff --git a/tests/compat/results/full.partial.stale.1780994184.jsonl b/tests/compat/results/full.partial.stale.1780994184.jsonl deleted file mode 100644 index 2c9e7264..00000000 --- a/tests/compat/results/full.partial.stale.1780994184.jsonl +++ /dev/null @@ -1,211 +0,0 @@ -{"label": "uast/parse[json]/go", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/go", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/go", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/go", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/go", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/go", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/go", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/go", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/44740f6a69ae1eb14a52794bc2543aebf0612d8319ae3c20ea124211c6297fbc.go"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/python", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/python", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/python", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/python", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/python", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/python", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/python", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/python", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/984f54a60df9d1aa844e7e0aef10000e7090a00109d13e43587ccb077ef340bb.py"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/c", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/c", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/c", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/c", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/c", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/c", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/c", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/c", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/6072fef6825336f5e6e67538478a16dfa6a45bcac93825dccb40bf3f001b14ae.c"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/c-header", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/c-header", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/c-header", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/c-header", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/c-header", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/c-header", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/c-header", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/c-header", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/ccd7c25e6864012e70abb674ac0895c42073fb1dcfc9531690f923d0df81c6c8.h"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/rust", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/rust", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/rust", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/rust", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/rust", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/rust", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/rust", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/rust", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/e4c669377ee72df081c75942df30d9cce9e1b2b718bd420b7958f05830659482.rs"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/typescript", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/typescript", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/typescript", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/typescript", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/typescript", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/typescript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/typescript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/typescript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/48db5a9463619bf983f8b297fd456bfb72c85ff6fda0f62c875528bb5f646c88.ts"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/tsx", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/tsx", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/tsx", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/tsx", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/tsx", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/tsx", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/tsx", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/tsx", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/29a5b9fb763e0d6e383680f47a92eb1b9f47cc7de7d3f88f6f419a1378ed48f8.tsx"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/javascript", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/javascript", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/javascript", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/javascript", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/javascript", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/javascript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/javascript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/javascript", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/cec8ae3b8004ac99daf3b97af96e152bc3a37be2c726a8d4264315ebff00e447.js"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/json", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/json", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/json", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/json", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/json", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/json", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/json", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/json", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/9dcac88c20ff3561abd8bc8e7bcf921351c2566074697cb5b66553bf731d5355.json"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/yaml", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/yaml", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/yaml", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/yaml", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/yaml", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/yaml", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/yaml", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/yaml", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/fbcb9a96145124eca68f210c9b89f5aeb2f1b0735b3227000590d17bb47bd868.yml"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/cpp", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/cpp", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/cpp", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/cpp", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/cpp", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/cpp", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/cpp", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/cpp", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/8a934bbd431730a6dd443b321d287d66e63f290ff07d97aa7a2655999cf754d6.cpp"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/parse[json]/shell", "argv": ["uast", "parse", "--format", "json", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[compact]/shell", "argv": ["uast", "parse", "--format", "compact", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_parse", "tier": "full", "verdict": "PASS"} -{"label": "uast/parse[tree]/shell", "argv": ["uast", "parse", "--format", "tree", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_parse", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[json]/shell", "argv": ["uast", "analyze", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/analyze[text]/shell", "argv": ["uast", "analyze", "--format", "text", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_analyze", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[json]/shell", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "json", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[compact]/shell", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "compact", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "uast/query[count]/shell", "argv": ["uast", "query", "filter(.roles has \"Function\")", "--format", "count", "-i", "/home/dmitriy/sources/codefang/rust/tests/compat/corpus/files/5825f4115e21702b2ce1071b9972b5f9b89d93e2f116be2266c73c806be8ba58.sh"], "family": "uast_query", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/clones[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/clones[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/clones", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/comments", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/cohesion", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/imports", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[yaml]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[bin]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[compact]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[ndjson]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[timeseries]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/composition", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static-set:*[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/*[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,static/halstead[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity,static/halstead", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,history/devs[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity,history/devs", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/clones[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/clones", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/comments[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/comments[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/comments", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/halstead[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/halstead[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/halstead", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/cohesion[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/cohesion[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/cohesion", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/imports[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/imports[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/imports", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[yaml]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[bin]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[compact]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[text]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/composition[ndjson]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/composition[timeseries]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/composition", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static-set:*[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "FAIL"} -{"label": "static-set:static/*[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/*", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,static/halstead[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity,static/halstead", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static-set:static/complexity,history/devs[json]@ioq3", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/ioq3", "--analyzers", "static/complexity,history/devs", "--format", "json"], "family": "analyzer_set", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[json]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[yaml]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[bin]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "bin"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[compact]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "compact"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/clones[text]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "text"], "family": "static_analyzer", "tier": "full", "verdict": "FAIL"} -{"label": "static:static/clones[ndjson]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "ndjson"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/clones[timeseries]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/clones", "--format", "timeseries"], "family": "static_analyzer", "tier": "full", "verdict": "EXPECTED_EMPTY", "contract": "go-produces-no-stdout"} -{"label": "static:static/complexity[json]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/complexity", "--format", "json"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} -{"label": "static:static/complexity[yaml]@kubernetes", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/kubernetes", "--analyzers", "static/complexity", "--format", "yaml"], "family": "static_analyzer", "tier": "full", "verdict": "PASS"} diff --git a/tests/compat/results/smoke.json b/tests/compat/results/smoke.json deleted file mode 100644 index cfbcfe1c..00000000 --- a/tests/compat/results/smoke.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "tier": "smoke", - "matrix_hash": "b07d1222dbf646ec6128aff7f628173f811c0da46ff441e290ba7d3afa766a5c", - "total_cells": 8, - "tally": { - "PASS": 8, - "FAIL": 0, - "SIM": 0, - "EXPECTED_EMPTY": 0 - }, - "records": [ - { - "label": "flag:static/complexity--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--include-generated[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--include-generated" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--include-generated[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--include-generated" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - }, - { - "label": "flag:static/complexity--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/complexity", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go[json]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "json", - "--languages", - "Go" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - }, - { - "label": "flag:static/halstead--languages:Go[text]@hercules", - "argv": [ - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/hercules", - "--analyzers", - "static/halstead", - "--format", - "text", - "--languages", - "Go" - ], - "family": "flag", - "tier": "smoke", - "verdict": "PASS" - } - ] -} \ No newline at end of file diff --git a/tests/compat/results/smoke.partial.jsonl b/tests/compat/results/smoke.partial.jsonl deleted file mode 100644 index 97cf31f1..00000000 --- a/tests/compat/results/smoke.partial.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"label": "flag:static/complexity--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--include-generated"], "family": "flag", "tier": "smoke", "verdict": "PASS"} -{"label": "flag:static/complexity--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--include-generated"], "family": "flag", "tier": "smoke", "verdict": "PASS"} -{"label": "flag:static/halstead--include-generated[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--include-generated"], "family": "flag", "tier": "smoke", "verdict": "PASS"} -{"label": "flag:static/halstead--include-generated[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--include-generated"], "family": "flag", "tier": "smoke", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "smoke", "verdict": "PASS"} -{"label": "flag:static/complexity--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/complexity", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "smoke", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go[text]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "text", "--languages", "Go"], "family": "flag", "tier": "smoke", "verdict": "PASS"} -{"label": "flag:static/halstead--languages:Go[json]@hercules", "argv": ["run", "--checkpoint=false", "--resume=false", "--no-cache", "--head", "--workers", "1", "--static-workers", "1", "-p", "/home/dmitriy/sources/hercules", "--analyzers", "static/halstead", "--format", "json", "--languages", "Go"], "family": "flag", "tier": "smoke", "verdict": "PASS"} diff --git a/tests/compat/results/smoke_gate.json b/tests/compat/results/smoke_gate.json deleted file mode 100644 index 49a2fc3e..00000000 --- a/tests/compat/results/smoke_gate.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "tier": "smoke", - "tally": { - "PASS": 102, - "FAIL": 0, - "SIM": 0, - "EXPECTED_EMPTY": 72, - "ALLOWED_GO_NONDET": 0, - "TRACKED_KNOWN_FAILING": 0 - }, - "meta_sim": 0, - "real_divergence_count": 0, - "allowed": [], - "tracked": [], - "real_divergences": [] -} \ No newline at end of file diff --git a/tests/compat/run.sh b/tests/compat/run.sh deleted file mode 100755 index 4a7aa7be..00000000 --- a/tests/compat/run.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# COMPAT TEST SYSTEM -- SINGLE ENTRY POINT (SPEC roadmap 8 / task PHASE: CI) -# ============================================================================= -# Two tiers: -# smoke -- pre-commit. Full 155-cell invocation matrix vs the LIVE Go oracle -# (serial = deterministic verdicts; NO matrix shrink), CLI-surface -# conformance, metamorphic/anti-sim, fast tamper-verify + tamper -# self-test, gap ledger (no slow llvm-cov). Target: fast feedback. -# full -- scheduled. Everything in smoke PLUS the full 486-cell matrix, the -# MUTATION SELF-TEST (rebuilds Rust, proves a planted bug is caught), -# per-stage differential fuzzing, and llvm-cov-backed ledger. -# -# Pinned run env (rule #5) is applied by the oracle to BOTH binaries; we also set -# it here so every child sees it. Compare STDOUT only (stderr is progress). -# -# Output: per-cell PASS/FAIL/SIM/EXPECTED_EMPTY + final tallies + the gap ledger. -# Exit code: NONZERO on any real (un-allowlisted) divergence. A known-divergence -# allowlist (allowlist.json) only neutralizes a cell when it carries a written -# reason + Go-nondeterminism evidence; an excuse without evidence fails CLOSED. -# ============================================================================= -set -u -set -f -export TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 - -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TIER="${1:-smoke}" -case "$TIER" in - smoke|full) ;; - *) echo "usage: run.sh [smoke|full]"; exit 2 ;; -esac - -PY=python3 -RC=0 -step() { printf '\n\n############## %s ##############\n' "$1"; } -fail() { echo ">>> STAGE FAILED: $1"; RC=1; } - -T0=$(date +%s) -echo "================================================================" -echo " COMPAT TEST SYSTEM tier=$TIER" -echo " oracle (truth) : /home/dmitriy/sources/codefang/build/bin/{codefang,uast}" -echo " candidate : $(dirname "$HERE")/../target/release/{codefang,uast}" -echo " env : TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800" -echo "================================================================" - -# ----------------------------------------------------------------------------- -# 1. INTEGRITY / TAMPER-EVIDENCE (fail-closed; runs FIRST so a tampered harness -# can never report a trustworthy green). -# smoke: tamper self-test + live tamper verify (fast) -# full : + the MUTATION SELF-TEST (rebuilds Rust, proves it catches a bug) -# ----------------------------------------------------------------------------- -step "1. TAMPER-EVIDENCE (fail-closed)" -$PY "$HERE/integrity/tamper_check.py" --self-test || fail "tamper self-test" -$PY "$HERE/integrity/tamper_check.py" || fail "tamper verify (fail-closed)" - -# ----------------------------------------------------------------------------- -# 2. CLI SURFACE CONFORMANCE (recursive Go-vs-Rust flags/defaults/help/exit) -# ----------------------------------------------------------------------------- -step "2. CLI SURFACE CONFORMANCE" -bash "$HERE/cli_surface/run.sh" || fail "cli-surface" - -# ----------------------------------------------------------------------------- -# 3. DIFFERENTIAL INVOCATION MATRIX vs the LIVE Go oracle -# smoke = 155 cells (serial, deterministic), full = 486 cells. -# run_matrix exits nonzero on FAIL/SIM; we do NOT gate on that here -- the -# allowlist-aware FINAL GATE (step 6) owns the exit decision. -# ----------------------------------------------------------------------------- -step "3. DIFFERENTIAL MATRIX (live Go oracle, N>=3 per cell)" -$PY "$HERE/run_matrix.py" --tier "$TIER" --n-go 3 || true - -# ----------------------------------------------------------------------------- -# 4. METAMORPHIC / ANTI-SIMULATION (vary-input, grow-with-limit, determinism, -# non-empty, golden-drift => SIM). SIM verdicts are real divergences. -# ----------------------------------------------------------------------------- -step "4. METAMORPHIC / ANTI-SIMULATION" -$PY "$HERE/metamorphic/metamorphic.py" --tier smoke || true # writes results.json - -# ----------------------------------------------------------------------------- -# 5. GAP LEDGER + COVERAGE -# smoke: matrix-cell coverage + live Go-variant evidence harvest (no llvm-cov) -# full : + cargo-llvm-cov line/region/branch -# ----------------------------------------------------------------------------- -step "5. GAP LEDGER + COVERAGE ACCOUNTING" -if [ "$TIER" = "full" ]; then - $PY "$HERE/coverage/build_ledger.py" --tier full --probe-variants \ - --probe-parse || fail "ledger(full)" -else - $PY "$HERE/coverage/build_ledger.py" --tier smoke --no-rust-cov \ - --probe-variants || fail "ledger(smoke)" -fi - -# ----------------------------------------------------------------------------- -# FULL-ONLY heavy stages: mutation self-test + differential fuzzing. -# ----------------------------------------------------------------------------- -if [ "$TIER" = "full" ]; then - step "F1. MUTATION SELF-TEST (META-GATE: prove the system catches a bug)" - bash "$HERE/integrity/mutation_self_test.sh" || fail "mutation self-test" - - step "F2. PER-STAGE DIFFERENTIAL FUZZING" - bash "$HERE/fuzz/run_fuzz.sh" "${FUZZ_SECS:-20}" || fail "differential fuzz" -fi - -# ----------------------------------------------------------------------------- -# 6. FINAL GATE (allowlist-aware; the single CI exit decision + honest tally) -# ----------------------------------------------------------------------------- -step "6. FINAL GATE (allowlist-aware honest tally)" -$PY "$HERE/gate.py" --tier "$TIER" || fail "final gate (real divergence)" - -T1=$(date +%s) -echo -echo "================================================================" -echo " tier=$TIER wall=$((T1 - T0))s ledger=$HERE/ledger.json" -if [ "$RC" -eq 0 ]; then - echo " RESULT: GREEN -- Rust matches Go across the measured matrix (no" - echo " un-allowlisted divergence) and the harness is untampered." -else - echo " RESULT: RED -- see failed stages above. The gap ledger lists every" - echo " divergence; this is the HONEST tally, not a suppressed green." -fi -echo "================================================================" -exit $RC diff --git a/tests/compat/run_matrix.py b/tests/compat/run_matrix.py deleted file mode 100644 index f6685e1b..00000000 --- a/tests/compat/run_matrix.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python3 -""" -MATRIX RUNNER (SPEC: specs/go-compat-testing/SPEC.md §3.2/§3.4, roadmap 3). - -Wires EVERY expanded matrix cell to the LIVE GO ORACLE (oracle/oracle.py): - - for each cell: - run the oracle on cell.argv -> verdict PASS / FAIL / SIM - if the LIVE Go binary produced NO stdout for this cell, the cell is recorded - as an EXPECTED-EMPTY CONTRACT (kind="expected-empty") -- a contract, NOT a - skip. (A later run where Go starts producing output, or Rust diverges from - Go's emptiness, breaks the contract.) - -The oracle is the ONLY source of truth: this runner never re-derives expected -output. It only dispatches cell argvs to the oracle and tallies verdicts. The -matrix definition is hashed so a shrunk matrix is detectable (tamper layer). - -Output: results/<tier>.json with per-cell verdicts + a coverage summary. -""" - -import argparse -import hashlib -import json -import os -import subprocess -import sys -import threading - -HERE = os.path.dirname(os.path.abspath(__file__)) -ORACLE = os.path.join(HERE, "oracle", "oracle.py") -EXPAND = os.path.join(HERE, "expand_matrix.py") -RESULTS_DIR = os.path.join(HERE, "results") - -GO_DIR = "/home/dmitriy/sources/codefang/build/bin" -PINNED_ENV = {"TZ": "UTC", "NO_COLOR": "1", "LANG": "C", "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800"} - - -def matrix_hash(): - """Content hash of the matrix definition + expander + corpus manifest. - The tamper layer (later phase) compares this against a recorded baseline; we - surface it here so a shrunk matrix changes the hash.""" - h = hashlib.sha256() - for p in (os.path.join(HERE, "matrix.toml"), EXPAND, - os.path.join(HERE, "corpus", "manifest.json")): - with open(p, "rb") as f: - h.update(f.read()) - return h.hexdigest() - - -def expand(tier): - out = subprocess.run([sys.executable, EXPAND, tier], - stdout=subprocess.PIPE, check=True) - return json.loads(out.stdout)["cells"] - - -def _resolve_go(argv): - if argv[0] == "uast": - return [os.path.join(GO_DIR, "uast")] + argv[1:] - return [os.path.join(GO_DIR, "codefang"), argv[0]] + argv[1:] - - -def go_is_empty(argv): - """Run the LIVE Go binary once; True iff it produced no stdout bytes. - This is how an EXPECTED-EMPTY contract is MEASURED from the oracle, not - declared.""" - env = dict(os.environ); env.update(PINNED_ENV) - try: - p = subprocess.run(_resolve_go(argv), env=env, - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, - timeout=600) - except Exception: - return False - return len(p.stdout) == 0 - - -def run_oracle(argv, n_go): - p = subprocess.run([sys.executable, ORACLE, "--n-go", str(n_go), "--quiet", - "--"] + argv, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - verdict = {0: "PASS", 1: "FAIL", 3: "SIM"}.get(p.returncode, "FAIL") - return verdict, (p.stdout + p.stderr).decode("utf-8", "replace") - - -def run_cell(c, n_go, dry_run): - """Process one matrix cell (parallel-safe: pure subprocess dispatch to the - LIVE oracle / Go binary). Returns (record, detail_or_None).""" - argv = c["argv"] - # plot (file-output) cells write --output files and ALWAYS have empty - # stdout; the empty-stdout probe does not apply — the oracle's file mode - # measures the contract from the written files instead. - if c.get("family") != "plot" and go_is_empty(argv): - return ({**c, "verdict": "EXPECTED_EMPTY", - "contract": "go-produces-no-stdout"}, None) - if dry_run: - return ({**c, "verdict": "NOT_RUN(go-nonempty)"}, None) - verdict, detail = run_oracle(argv, n_go) - return ({**c, "verdict": verdict}, detail) - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--tier", default="smoke", choices=["smoke", "full"]) - ap.add_argument("--n-go", type=int, default=3) - ap.add_argument("--sample", type=int, default=0, - help="run only the first N cells (for fast self-check)") - ap.add_argument("--family", default="", help="only cells of this family") - ap.add_argument("--jobs", type=int, default=1, - help="parallel cells (each cell is an independent oracle " - "subprocess; speeds the smoke tier WITHOUT shrinking the " - "matrix -- every enumerated cell still runs)") - ap.add_argument("--dry-run", action="store_true", - help="only enumerate + classify expected-empty; no oracle") - ap.add_argument("--no-resume", dest="resume", action="store_false", - help="ignore + clear any prior partial results; clean run") - ap.set_defaults(resume=True) - a = ap.parse_args() - - cells = expand(a.tier) - if a.family: - cells = [c for c in cells if c["family"] == a.family] - if a.sample > 0: - cells = cells[:a.sample] - - os.makedirs(RESULTS_DIR, exist_ok=True) - tally = {"PASS": 0, "FAIL": 0, "SIM": 0, "EXPECTED_EMPTY": 0} - records = [None] * len(cells) - - # --- incremental + resume ------------------------------------------------- - # Each completed cell is appended (label-keyed) to results/<tier>.partial.jsonl - # and fsync'd, so a long full run is observable live and survives a crash. On - # startup, already-completed labels are reloaded and their cells skipped, so a - # restart NEVER repeats work already done. --no-resume forces a clean run. - partial_path = os.path.join(RESULTS_DIR, f"{a.tier}.partial.jsonl") - done = {} - if a.resume and os.path.exists(partial_path): - with open(partial_path) as f: - for line in f: - line = line.strip() - if not line: - continue - try: - r = json.loads(line) - done[r["label"]] = r - except Exception: - pass # tolerate a torn last line from a crash mid-write - elif not a.resume and os.path.exists(partial_path): - os.remove(partial_path) - - pf = open(partial_path, "a") - wlock = threading.Lock() - - def absorb(idx, rec, detail): - v = rec["verdict"] - tally[v] = tally.get(v, 0) + 1 - mark = {"PASS": "PASS ", "FAIL": "FAIL!", "SIM": "SIM! ", - "EXPECTED_EMPTY": "EMPTY"}.get(v, v) - nd = sum(1 for r in records if r is not None) + 1 - print(f" [{nd}/{len(cells)}] {mark} {cells[idx]['label']}", flush=True) - if v not in ("PASS", "EXPECTED_EMPTY") and detail: - for line in detail.splitlines()[:4]: - print(" " + line, flush=True) - records[idx] = rec - with wlock: - pf.write(json.dumps(rec, default=str) + "\n") - pf.flush() - os.fsync(pf.fileno()) - - # split cells into already-done (replayed) and pending (to run) - # Only replay SETTLED-GREEN cells (PASS / EXPECTED_EMPTY). A prior FAIL/SIM is - # NEVER trusted on resume -- after a Rust fix+rebuild those cells must be - # re-measured against the live oracle. This makes resume safe for the burndown - # loop: expensive green kubernetes-static cells stay cached, red cells re-run. - GREEN = ("PASS", "EXPECTED_EMPTY") - pending = [] - for i, c in enumerate(cells): - prior = done.get(c["label"]) - if prior is not None and prior.get("verdict") in GREEN: - v = prior["verdict"] - tally[v] = tally.get(v, 0) + 1 - records[i] = prior - else: - pending.append((i, c)) - if done: - print(f"== resume: {len(done)} cell(s) already done, " - f"{len(pending)} pending", flush=True) - - if a.jobs <= 1: - for i, c in pending: - rec, detail = run_cell(c, a.n_go, a.dry_run) - absorb(i, rec, detail) - else: - from concurrent.futures import ThreadPoolExecutor, as_completed - with ThreadPoolExecutor(max_workers=a.jobs) as ex: - futs = {ex.submit(run_cell, c, a.n_go, a.dry_run): i - for i, c in pending} - for fut in as_completed(futs): - i = futs[fut] - rec, detail = fut.result() - absorb(i, rec, detail) - - pf.close() - - summary = { - "tier": a.tier, - "matrix_hash": matrix_hash(), - "total_cells": len(cells), - "tally": tally, - "records": records, - } - outp = os.path.join(RESULTS_DIR, f"{a.tier}.json") - with open(outp, "w") as f: - json.dump(summary, f, indent=2, default=str) - print(f"\n== {a.tier}: {tally} matrix_hash={summary['matrix_hash'][:12]}") - print(f"wrote {outp}") - # nonzero exit on any real divergence - if tally["FAIL"] or tally["SIM"]: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/tests/compat/selftest_matrix.py b/tests/compat/selftest_matrix.py deleted file mode 100644 index a6f2b3e6..00000000 --- a/tests/compat/selftest_matrix.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env python3 -""" -SELF-CHECK for the MatrixCorpus phase (SPEC roadmap 3; task rule 6). - -A green that cannot be shown to catch a planted defect is worthless. This proves: - - C1 CORPUS INTEGRITY: every stored file's sha256 matches its content-address - name, and the LIVE Go oracle parses it to non-trivial UAST (no dead cell). - C2 CORPUS BREADTH: >=5 distinct tree-sitter languages mined (multi-language, - not just Go) AND >=3 real repos recorded, >=2 BEYOND kubernetes. - C3 MATRIX COMPLETENESS: every analyzer from `run --list-analyzers`, every - declared run-format, and the analyzer-sets all appear in the expansion; - every uast subcommand x every mined language appears. (No silent shrink.) - C4 WIRING -> ORACLE: a matrix cell actually reaches oracle.py and yields a - verdict (PASS on a Go-language parse cell that is byte-identical Go==Rust). - C5 DEFECT DETECTION (the proof it catches bugs): inject a planted divergence - by handing the oracle a TAMPERED Rust output and assert the oracle reports - FAIL. We do this WITHOUT touching the real binaries by wrapping the Rust - binary in a shim that mutates one byte -- a real differential the system - MUST flag. Also assert MATRIX-SHRINK is detectable (hash changes). - C6 EXPECTED-EMPTY CONTRACT: the `tree` format yields no Go stdout and is - recorded as a contract (kind EXPECTED_EMPTY), not skipped. - -Exit 0 only if every self-check passes. -""" - -import hashlib -import json -import os -import subprocess -import sys -import tempfile - -HERE = os.path.dirname(os.path.abspath(__file__)) -ORACLE = os.path.join(HERE, "oracle", "oracle.py") -EXPAND = os.path.join(HERE, "expand_matrix.py") -GO_BIN = "/home/dmitriy/sources/codefang/build/bin" -RU_BIN = "/home/dmitriy/sources/codefang/target/release" -PINNED = {"TZ": "UTC", "NO_COLOR": "1", "LANG": "C", "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800"} - -fails = [] - - -def ok(name): - print(f" PASS {name}") - - -def bad(name, why): - print(f" FAIL {name}: {why}") - fails.append(name) - - -def load_manifest(): - return json.load(open(os.path.join(HERE, "corpus", "manifest.json"))) - - -# --------------------------------------------------------------------------- # -def c1_corpus_integrity(man): - env = dict(os.environ); env.update(PINNED) - for e in man["files"]: - p = os.path.join(HERE, "corpus", e["stored"]) - if not os.path.isfile(p): - bad("C1", f"missing stored file {p}"); return - h = hashlib.sha256(open(p, "rb").read()).hexdigest() - if h != e["sha256"]: - bad("C1", f"content-address mismatch {e['language']}: {h}!={e['sha256']}") - return - if e["language"] == "go": # at least the supported lang must parse live - out = subprocess.run([os.path.join(GO_BIN, "uast"), "parse", - "--format", "json", p], env=env, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL).stdout - if out.count(b'"type"') < 2: - bad("C1", f"oracle parse trivial for {e['language']}"); return - ok("C1 corpus integrity (content-address + live-oracle parse)") - - -def c2_breadth(man): - langs = {e["language"] for e in man["files"]} - if len(langs) < 5: - bad("C2", f"only {len(langs)} languages: {sorted(langs)}"); return - repos = man["repos"] - if len(repos) < 3: - bad("C2", f"only {len(repos)} repos"); return - beyond = [r for r in repos if r["id"] != "kubernetes"] - if len(beyond) < 2: - bad("C2", f"only {len(beyond)} repos beyond kubernetes"); return - ok(f"C2 breadth ({len(langs)} langs, {len(repos)} repos, " - f"{len(beyond)} beyond kubernetes)") - - -def c3_matrix_completeness(man): - cells = json.loads(subprocess.run([sys.executable, EXPAND, "full"], - stdout=subprocess.PIPE).stdout)["cells"] - labels = " ".join(c["label"] for c in cells) - blob = json.dumps([c["argv"] for c in cells]) - # every analyzer listed by the LIVE binary must appear - listed = subprocess.run([os.path.join(GO_BIN, "codefang"), "run", - "--list-analyzers"], env={**os.environ, **PINNED}, - stdout=subprocess.PIPE).stdout.decode() - analyzers = [l.split()[0] for l in listed.splitlines() - if l.strip().startswith(("static/", "history/"))] - missing = [a for a in analyzers if a not in blob] - if missing: - bad("C3", f"analyzers absent from matrix: {missing}"); return - for fmt in ["json", "yaml", "bin", "compact", "text", "ndjson", "timeseries"]: - if f"[{fmt}]" not in labels: - bad("C3", f"format {fmt} absent from matrix"); return - for s in ["uast/parse", "uast/analyze", "uast/query"]: - if s not in labels: - bad("C3", f"uast subcommand {s} absent"); return - for lang in {e["language"] for e in man["files"]}: - if f"/{lang}" not in labels: - bad("C3", f"language {lang} absent from uast cells"); return - if "timeseries+ndjson" not in labels: - bad("C3", "timeseries+ndjson combo cell absent"); return - ok(f"C3 matrix completeness ({len(cells)} full cells, {len(analyzers)} " - f"analyzers, 7 formats, 3 uast subcmds, all langs)") - - -def _run_oracle(argv, ru_override=None, n_go=3): - env = dict(os.environ) - if ru_override: - env["PATH"] = ru_override + os.pathsep + env["PATH"] - p = subprocess.run([sys.executable, ORACLE, "--n-go", str(n_go), "--quiet", - "--"] + argv, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=env) - return p.returncode # 0=PASS 1=FAIL 3=SIM - - -def c4_wiring(man): - go_file = next((e for e in man["files"] if e["language"] == "go"), None) - if not go_file: - bad("C4", "no go file in corpus"); return - p = os.path.join(HERE, "corpus", go_file["stored"]) - rc = _run_oracle(["uast", "parse", "--format", "json", p]) - if rc != 0: - bad("C4", f"oracle did not PASS a byte-identical Go parse cell (rc={rc})") - return - ok("C4 wiring->oracle (PASS on byte-identical Go parse cell)") - - -def c5_defect_detection(man): - """Plant a divergence: a shim Rust binary that flips one output byte. The - REAL oracle (oracle.py) MUST report FAIL. Proves the system catches a real - bug, not just green. We exercise the genuine oracle comparison logic by - importing it and monkeypatching RU_DIR -> shim dir (the oracle hardcodes the - Rust path with no env hook), so the actual STABLE-field byte-exact comparison - is what fails on the mutated byte -- not a synthetic check.""" - go_file = next((e for e in man["files"] if e["language"] == "go"), None) - p = os.path.join(HERE, "corpus", go_file["stored"]) - with tempfile.TemporaryDirectory() as td: - shim = os.path.join(td, "uast") - with open(shim, "w") as f: - # shim parses the real Rust JSON and mutates ONE leaf value while - # keeping the JSON valid -> a real value divergence (the bug class - # the oracle must catch) without crashing the parser. - f.write(f"""#!/usr/bin/env python3 -import subprocess,sys,json -p=subprocess.run([{RU_BIN!r}+"/uast"]+sys.argv[1:],stdout=subprocess.PIPE) -out=p.stdout -try: - j=json.loads(out) - def mut(o): - if isinstance(o,dict): - for k,v in o.items(): - if isinstance(v,str) and v: - o[k]=v+"_TAMPER"; return True - if mut(v): return True - elif isinstance(o,list): - for v in o: - if mut(v): return True - return False - mut(j) - out=json.dumps(j).encode() -except Exception: - pass -sys.stdout.buffer.write(out); sys.exit(p.returncode) -""") - os.chmod(shim, 0o755) - # drive the REAL oracle with RU_DIR pointed at the shim. - driver = ( - "import importlib.util,sys;" - f"spec=importlib.util.spec_from_file_location('oracle',{ORACLE!r});" - "o=importlib.util.module_from_spec(spec);spec.loader.exec_module(o);" - f"o.RU_DIR={td!r};" - f"r=o.run_invocation(['uast','parse','--format','json',{p!r}],n_go=3);" - "print(r['verdict'])" - ) - out = subprocess.run([sys.executable, "-c", driver], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE).stdout.decode().strip() - if out.endswith("FAIL"): - ok("C5 defect detection (planted 1-byte Rust divergence -> oracle FAIL)") - else: - bad("C5", f"oracle did NOT FAIL on a planted divergence (verdict={out!r}) " - f"-- the system cannot be shown to catch bugs") - - # matrix-shrink detectability: hash before/after dropping an analyzer line. - mtoml = os.path.join(HERE, "matrix.toml") - orig = open(mtoml, "rb").read() - h0 = hashlib.sha256(orig).hexdigest() - shrunk = orig.replace(b'"static/clones", ', b'') - h1 = hashlib.sha256(shrunk).hexdigest() - if shrunk != orig and h0 != h1: - ok("C5b matrix-shrink detectable (dropping an analyzer changes the hash)") - else: - bad("C5b", "matrix shrink not reflected in hash") - - -def c6_expected_empty(man): - go_file = next((e for e in man["files"] if e["language"] == "go"), None) - p = os.path.join(HERE, "corpus", go_file["stored"]) - env = dict(os.environ); env.update(PINNED) - out = subprocess.run([os.path.join(GO_BIN, "uast"), "parse", "--format", - "tree", p], env=env, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL).stdout - if len(out) == 0: - ok("C6 expected-empty contract (tree format: Go emits no stdout; " - "runner records EXPECTED_EMPTY, not skip)") - else: - # if Go DID produce tree output, that's fine too -- the contract is - # measured, not declared; just note it. - ok(f"C6 tree format produces {len(out)}B Go stdout (measured contract)") - - -def main(): - man = load_manifest() - print("== MatrixCorpus self-check ==") - c1_corpus_integrity(man) - c2_breadth(man) - c3_matrix_completeness(man) - c4_wiring(man) - c5_defect_detection(man) - c6_expected_empty(man) - print() - if fails: - print(f"SELF-CHECK RED: {len(fails)} failed: {fails}") - sys.exit(1) - print("SELF-CHECK GREEN: corpus + matrix built, wired to live oracle, " - "and PROVEN to catch a planted divergence + matrix shrink.") - - -if __name__ == "__main__": - main() diff --git a/tests/doc-examples/Cargo.toml b/tests/doc-examples/Cargo.toml index efe6bb29..6653640a 100644 --- a/tests/doc-examples/Cargo.toml +++ b/tests/doc-examples/Cargo.toml @@ -12,15 +12,15 @@ publish.workspace = true # show codefang/uast invocations, rewrites placeholder paths to the hercules # fixture, runs each against the freshly built Rust binary, and asserts rc 0. # -# Depending on the two binaries as dev-deps makes `cargo test -p doc-examples` -# build them first; the test discovers them in target/{release,debug}. +# The test discovers the binaries in target/{release,debug} and builds them +# with cargo when they are absent (a bin-only package cannot be a dependency). [dependencies] -[dev-dependencies] -codefang = { path = "../../bins/codefang" } -uast = { path = "../../bins/uast" } - # No library or binary target: this crate exists only to host the integration # test under tests/. An empty lib keeps cargo happy without shipping code. [lib] path = "src/lib.rs" + +[dev-dependencies] +# An empty HOME for every documented command (no ~/.codefang.yaml leaks in). +tempfile = { workspace = true } diff --git a/tests/doc-examples/tests/doc_examples.rs b/tests/doc-examples/tests/doc_examples.rs index c6f8c716..5501ff38 100644 --- a/tests/doc-examples/tests/doc_examples.rs +++ b/tests/doc-examples/tests/doc_examples.rs @@ -24,10 +24,12 @@ //! //! # Speed and side-effects //! -//! History `run` commands get `--checkpoint=false --resume=false --no-cache -//! --workers 1` appended when absent (no behavioural change to the documented -//! flags — they only disable on-disk caches and pin determinism) and a -//! `--limit`/`--head` cap is enforced so a single doc command stays fast. +//! A `--limit`/`--head` cap is appended to `run` commands so a single doc +//! command stays fast. Every command runs with `HOME` and the working +//! directory pointed at empty temporary directories, so a developer's own +//! `.codefang.yaml` never changes what a documented command does and the +//! files a command writes (`-o report.json`, plot directories) never land in +//! the source tree. //! //! # Fixture //! @@ -387,22 +389,10 @@ fn plan_run(rest: &[String], fixture: Option<&Path>) -> Plan { // No explicit repo arg — point at the fixture. args.push(fixture.display().to_string()); } - // Determinism + no on-disk cache side-effects. - ensure_flag(&mut args, "--checkpoint=false"); - ensure_flag(&mut args, "--resume=false"); - ensure_flag(&mut args, "--no-cache"); - if !has_flag(&args, "--workers") { - args.push("--workers".into()); - args.push("1".into()); - } // Speed cap: keep one doc command fast. let is_static = args.iter().any(|a| a.contains("static/")); if is_static { ensure_flag(&mut args, "--head"); - if !has_flag(&args, "--static-workers") { - args.push("--static-workers".into()); - args.push("1".into()); - } } else if !has_flag(&args, "--limit") && !has_flag(&args, "--head") { args.push("--limit".into()); args.push("30".into()); @@ -482,7 +472,7 @@ fn has_flag(args: &[String], flag: &str) -> bool { } fn ensure_flag(args: &mut Vec<String>, flag: &str) { - // flag is like "--no-cache" or "--checkpoint=false". + // flag is like "--head" or "--limit=30". let name = flag.split('=').next().unwrap_or(flag); if !has_flag(args, name) { args.push(flag.to_string()); @@ -529,6 +519,8 @@ fn markdown_examples_run() { "no codefang/uast commands extracted from docs — extraction likely broke" ); + let home = tempfile::tempdir().expect("empty HOME for the doc commands"); + let workdir = tempfile::tempdir().expect("working directory for the doc commands"); let mut ran = 0usize; let mut skipped: BTreeMap<String, usize> = BTreeMap::new(); let mut failures: Vec<String> = Vec::new(); @@ -546,6 +538,8 @@ fn markdown_examples_run() { let exe = if bin == "uast" { &uast } else { &codefang }; let output = Command::new(exe) .args(&args) + .current_dir(workdir.path()) + .env("HOME", home.path()) .env("TZ", "UTC") .env("NO_COLOR", "1") .env("LANG", "C") diff --git a/tests/golden-harness/Cargo.toml b/tests/golden-harness/Cargo.toml deleted file mode 100644 index 56d49386..00000000 --- a/tests/golden-harness/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "golden-harness" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -publish.workspace = true - -# Hosts the golden-diff integration test (tests/golden_diff.rs) and the runnable -# `golden-harness` binary. Depending on the two binaries (dev-deps) makes -# `cargo test -p golden-harness` build them first; the runnable binary discovers -# them in target/{release,debug} and (re)builds them on demand. -[dependencies] -serde = { workspace = true } -serde_json = { workspace = true } - -[dev-dependencies] -codefang = { path = "../../bins/codefang" } -uast = { path = "../../bins/uast" } - -[lib] -path = "src/lib.rs" - -# Runnable harness: `cargo run -p golden-harness` runs the 7 binding captures -# under the golden env, prints per-capture IDENTICAL/DIFFER + final N/7, and -# exits nonzero on any mismatch. It discovers MANIFEST.json + goldens relative to -# CARGO_MANIFEST_DIR and the two binaries from target/release (building them -# first if absent). -[[bin]] -name = "golden-harness" -path = "src/main.rs" diff --git a/tests/golden-harness/src/lib.rs b/tests/golden-harness/src/lib.rs deleted file mode 100644 index ac9124ce..00000000 --- a/tests/golden-harness/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Empty library placeholder. The golden-diff harness lives in -//! `tests/golden_diff.rs`; this crate exists only to host that integration test -//! and to depend on the two binaries so Cargo builds them first. diff --git a/tests/golden-harness/src/main.rs b/tests/golden-harness/src/main.rs deleted file mode 100644 index 22af8ef3..00000000 --- a/tests/golden-harness/src/main.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Runnable golden-harness (ROADMAP Step 14 / DESIGN §6). -//! -//! `cargo run -p golden-harness` runs **every BINDING capture** (a capture is -//! binding when `nonBinding != true`) from `tests/golden/MANIFEST.json` -//! under the exact golden environment -//! (`TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800`), captures -//! STDOUT only, byte-compares it against the golden at `relPath`, prints a -//! per-capture `IDENTICAL`/`DIFFER` line and a final `N/M identical`, and exits -//! nonzero if any binding capture differs. -//! -//! The manifest's `argv[0]` is the Go reference binary used to capture the -//! golden; the harness swaps it for the freshly built Rust binary of the same -//! name (`codefang`/`uast`) under `target/release` (building both first if they -//! are absent) and keeps the rest of argv verbatim. Because argv is passed -//! straight to `Command` (no shell), selectors like `*` / `static/*` reach the -//! binary literally — the `set -f` (noglob) requirement of the manual protocol -//! is satisfied structurally. -//! -//! For `run/*` captures (the kubernetes repo is large) `--no-cache` is always -//! ensured on argv so no cross-run cache state can leak in. -//! -//! Usage: -//! cargo run -p golden-harness # run all binding captures -//! cargo run -p golden-harness -- -k <substr> # only captures whose id or -//! cargo run -p golden-harness -- <substr>... # relPath contains a substr - -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::process::Command; - -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -struct Manifest { - env: BTreeMap<String, String>, - captures: Vec<Capture>, -} - -#[derive(Debug, Deserialize)] -struct Capture { - id: String, - argv: Vec<String>, - #[serde(rename = "relPath")] - rel_path: String, - #[serde(default, rename = "nonBinding")] - non_binding: bool, -} - -/// `tests/golden` — holds MANIFEST.json and the goldens. -fn golden_dir() -> PathBuf { - // CARGO_MANIFEST_DIR = tests/golden-harness - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.pop(); // tests/ - p.push("golden"); - p -} - -/// `target` — workspace target directory. -fn target_dir() -> PathBuf { - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.pop(); // tests/ - p.pop(); // rust/ - p.push("target"); - p -} - -/// Locate a built binary, preferring `release`, then `debug`. -fn binary_path(name: &str) -> Option<PathBuf> { - let exe = if cfg!(windows) { - format!("{name}.exe") - } else { - name.to_string() - }; - let t = target_dir(); - for profile in ["release", "debug"] { - let cand = t.join(profile).join(&exe); - if cand.exists() { - return Some(cand); - } - } - None -} - -/// Build the two release binaries if either is missing. -fn ensure_binaries_built() { - if binary_path("codefang").is_some() && binary_path("uast").is_some() { - return; - } - eprintln!("golden-harness: building release binaries (codefang, uast)…"); - let status = Command::new(env!("CARGO")) - .args(["build", "--release", "-p", "codefang", "-p", "uast"]) - .status() - .expect("spawn cargo build"); - assert!(status.success(), "cargo build --release failed"); -} - -/// Which Rust binary handles this capture, from the manifest argv[0] basename. -fn binary_for(cap: &Capture) -> &'static str { - let arg0 = cap.argv.first().map(String::as_str).unwrap_or(""); - if arg0.ends_with("uast") { - "uast" - } else { - "codefang" - } -} - -/// First differing byte offset, or `None` when identical. -fn first_diff(a: &[u8], b: &[u8]) -> Option<usize> { - let n = a.len().min(b.len()); - (0..n) - .find(|&i| a[i] != b[i]) - .or(if a.len() == b.len() { None } else { Some(n) }) -} - -fn main() { - let gdir = golden_dir(); - let manifest_path = gdir.join("MANIFEST.json"); - let raw = std::fs::read_to_string(&manifest_path) - .unwrap_or_else(|e| panic!("read {}: {e}", manifest_path.display())); - let manifest: Manifest = - serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse MANIFEST.json: {e}")); - - // Optional id/relPath substring filters: bare args or `-k <substr>` / `--id <substr>`. - let mut filters: Vec<String> = Vec::new(); - let mut it = std::env::args().skip(1); - while let Some(a) = it.next() { - match a.as_str() { - "-k" | "--id" | "--filter" => { - if let Some(v) = it.next() { - filters.push(v); - } - } - other => filters.push(other.to_string()), - } - } - let selected = |cap: &Capture| -> bool { - if filters.is_empty() { - return true; - } - filters - .iter() - .any(|f| cap.id.contains(f.as_str()) || cap.rel_path.contains(f.as_str())) - }; - - ensure_binaries_built(); - - // All BINDING captures (nonBinding != true), in manifest order, after filter. - let binding: Vec<&Capture> = manifest - .captures - .iter() - .filter(|c| !c.non_binding && selected(c)) - .collect(); - - let total = binding.len(); - let mut identical = 0usize; - let mut failures = 0usize; - - println!("== golden-harness: {total} binding captures =="); - for cap in &binding { - let rel = &cap.rel_path; - - let bin = binary_for(cap); - let bin_path = match binary_path(bin) { - Some(p) => p, - None => { - println!("ERROR {rel}: binary `{bin}` not found under target/"); - failures += 1; - continue; - } - }; - - let golden_path = gdir.join(&cap.rel_path); - let golden = match std::fs::read(&golden_path) { - Ok(b) => b, - Err(e) => { - println!("ERROR {rel}: missing golden {}: {e}", cap.rel_path); - failures += 1; - continue; - } - }; - - // Swap argv[0] (Go binary) for the Rust binary; keep the rest verbatim. - let mut args: Vec<String> = cap.argv.iter().skip(1).cloned().collect(); - // For run/* captures the kubernetes repo is large: always ensure - // --no-cache so no cross-run cache state leaks in. - if rel.starts_with("run/") && !args.iter().any(|a| a == "--no-cache") { - args.insert(0, "--no-cache".to_string()); - } - - let mut cmd = Command::new(&bin_path); - cmd.args(&args); - // Match the manual protocol `env TZ=… NO_COLOR=… … <cmd>`: inherit the - // ambient environment and OVERRIDE only the pinned golden vars (so the - // dynamic loader / libgit2 keep PATH/HOME, while TZ/LANG/LC_ALL/NO_COLOR/ - // SOURCE_DATE_EPOCH are forced to the golden values). - for (k, v) in &manifest.env { - cmd.env(k, v); - } - - let output = cmd - .output() - .unwrap_or_else(|e| panic!("spawn {}: {e}", bin_path.display())); - - match first_diff(&output.stdout, &golden) { - None => { - identical += 1; - println!("IDENTICAL {rel}"); - } - Some(off) => { - failures += 1; - println!( - "DIFFER {rel} (first diff at byte {off}; rust={} golden={})", - output.stdout.len(), - golden.len(), - ); - if !output.stderr.is_empty() { - let tail = String::from_utf8_lossy(&output.stderr); - if let Some(last) = tail.lines().last() { - println!(" stderr: {last}"); - } - } - } - } - } - - println!("---"); - println!("{identical}/{total} identical"); - - if failures > 0 { - std::process::exit(1); - } -} diff --git a/tests/golden-harness/tests/golden_diff.rs b/tests/golden-harness/tests/golden_diff.rs deleted file mode 100644 index 40c743fe..00000000 --- a/tests/golden-harness/tests/golden_diff.rs +++ /dev/null @@ -1,230 +0,0 @@ -//! Golden-diff integration harness (DESIGN §6). -//! -//! Drives `tests/golden/MANIFEST.json`: for each capture it builds the -//! correct binary (codefang/uast), runs the invocation with the pinned env -//! (`env` block in the manifest), and byte-compares stdout against the golden -//! file at `outPath`, reporting the FIRST differing byte offset on mismatch. -//! -//! The manifest's `argv[0]` is the path to the Go binary used to capture the -//! golden; the harness replaces it with the freshly built Rust binary of the -//! matching name (codefang/uast) and keeps the rest of argv verbatim. -//! -//! SKIP policy: a capture is SKIPPED (not failed) when the Rust subcommand body -//! is still a stub. The scaffold's only implemented stdout paths are `version` -//! and `--help`; every `run`/`uast <subcmd>` capture in the current manifest is -//! therefore treated as stubbed until its body is ported. The set of -//! already-ported commands is listed in `IMPLEMENTED_PREFIXES`; everything else -//! is SKIPPED. Implemented binding captures (machine && !nonBinding) HARD-GATE; -//! non-binding captures are reported informationally and never gated. - -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::process::Command; - -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -struct Manifest { - env: BTreeMap<String, String>, - captures: Vec<Capture>, -} - -#[derive(Debug, Deserialize)] -struct Capture { - id: String, - argv: Vec<String>, - #[serde(rename = "outPath")] - out_path: String, - #[serde(default)] - machine: bool, - #[serde(rename = "nonBinding", default)] - non_binding: bool, -} - -/// argv subcommand chains whose Rust bodies are ported and should RUN (not skip). -/// Each entry is matched against the capture's argv after argv[0]. As bodies are -/// ported, add their leading tokens here (e.g. "run", "uast parse"). -const IMPLEMENTED_PREFIXES: &[&[&str]] = &[ - &["version"], // codefang version - // uast version is dispatched via the uast binary; see `is_implemented`. -]; - -/// Directory holding MANIFEST.json and the goldens. -fn golden_dir() -> PathBuf { - // CARGO_MANIFEST_DIR = tests/golden-harness - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.pop(); // tests/ - p.push("golden"); - p -} - -/// Locate a built binary in the target directory. The test executable lives in -/// `target/<profile>/deps/`, so the binary is one or two levels up. -fn binary_path(name: &str) -> Option<PathBuf> { - let test_exe = std::env::current_exe().ok()?; - let deps_dir = test_exe.parent()?; // target/<profile>/deps - let profile_dir = deps_dir.parent()?; // target/<profile> - let exe_name = if cfg!(windows) { - format!("{name}.exe") - } else { - name.to_string() - }; - let mut candidates = vec![profile_dir.join(&exe_name), deps_dir.join(&exe_name)]; - if let Some(target_dir) = profile_dir.parent() { - candidates.push(target_dir.join("debug").join(&exe_name)); - candidates.push(target_dir.join("release").join(&exe_name)); - } - candidates.into_iter().find(|c| c.exists()) -} - -/// Which Rust binary handles this capture, from the original argv[0] basename. -fn binary_for(capture: &Capture) -> &'static str { - let arg0 = capture.argv.first().map(String::as_str).unwrap_or(""); - if arg0.ends_with("uast") { - "uast" - } else { - "codefang" - } -} - -/// Is the subcommand chain for this capture already ported (so it should RUN)? -fn is_implemented(capture: &Capture, bin: &str) -> bool { - // argv[1..] is the codefang/uast subcommand + flags. - let rest: Vec<&str> = capture.argv.iter().skip(1).map(String::as_str).collect(); - // uast version / codefang version are implemented. - if rest.first() == Some(&"version") { - return true; - } - let _ = bin; - IMPLEMENTED_PREFIXES - .iter() - .any(|prefix| rest.len() >= prefix.len() && rest[..prefix.len()] == **prefix) -} - -/// First differing byte offset between two slices, or None if identical. -fn first_diff_offset(a: &[u8], b: &[u8]) -> Option<usize> { - let n = a.len().min(b.len()); - for i in 0..n { - if a[i] != b[i] { - return Some(i); - } - } - if a.len() != b.len() { - Some(n) - } else { - None - } -} - -#[test] -fn golden_diff() { - let gdir = golden_dir(); - let manifest_path = gdir.join("MANIFEST.json"); - let raw = std::fs::read_to_string(&manifest_path) - .unwrap_or_else(|e| panic!("read {}: {e}", manifest_path.display())); - let manifest: Manifest = - serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse MANIFEST.json: {e}")); - - let mut skipped = Vec::new(); - let mut hard_failures = Vec::new(); - let mut informational = Vec::new(); - let mut passed = Vec::new(); - - for cap in &manifest.captures { - let bin = binary_for(cap); - - if !is_implemented(cap, bin) { - skipped.push(format!("SKIP (stubbed body) {}", cap.id)); - continue; - } - - let bin_path = match binary_path(bin) { - Some(p) => p, - None => { - hard_failures.push(format!("{}: built binary `{bin}` not found", cap.id)); - continue; - } - }; - - let golden_path = gdir.join(&cap.out_path); - let golden = match std::fs::read(&golden_path) { - Ok(b) => b, - Err(e) => { - let msg = format!("{}: missing golden {}: {e}", cap.id, cap.out_path); - if cap.machine && !cap.non_binding { - hard_failures.push(msg); - } else { - informational.push(msg); - } - continue; - } - }; - - // Replace argv[0] (the Go binary) with the Rust binary; keep the rest. - let args: Vec<&str> = cap.argv.iter().skip(1).map(String::as_str).collect(); - let mut cmd = Command::new(&bin_path); - cmd.args(&args).current_dir(&gdir); - for (k, v) in &manifest.env { - cmd.env(k, v); - } - let output = cmd - .output() - .unwrap_or_else(|e| panic!("spawn {}: {e}", bin_path.display())); - - match first_diff_offset(&output.stdout, &golden) { - None => passed.push(cap.id.clone()), - Some(off) => { - let msg = format!( - "{}: stdout differs from {} at byte offset {} (rust_len={}, golden_len={})", - cap.id, - cap.out_path, - off, - output.stdout.len(), - golden.len(), - ); - if cap.machine && !cap.non_binding { - hard_failures.push(msg); - } else { - informational.push(msg); - } - } - } - } - - eprintln!("== golden harness report =="); - for s in &skipped { - eprintln!(" {s}"); - } - for p in &passed { - eprintln!(" PASS {p}"); - } - for i in &informational { - eprintln!(" INFO {i}"); - } - for f in &hard_failures { - eprintln!(" FAIL {f}"); - } - eprintln!( - "skipped={} passed={} informational={} hard_failures={}", - skipped.len(), - passed.len(), - informational.len(), - hard_failures.len() - ); - - assert!( - hard_failures.is_empty(), - "{} binding golden(s) failed; see report above", - hard_failures.len() - ); -} - -/// Sanity: the goldens directory and manifest must exist and parse. -#[test] -fn manifest_loads() { - let gdir = golden_dir(); - assert!(gdir.exists(), "golden dir missing: {}", gdir.display()); - let raw = std::fs::read_to_string(gdir.join("MANIFEST.json")).expect("read manifest"); - let m: Manifest = serde_json::from_str(&raw).expect("parse manifest"); - assert!(!m.captures.is_empty(), "manifest has no captures"); -} diff --git a/tests/golden/MANIFEST.json b/tests/golden/MANIFEST.json deleted file mode 100644 index 779784ae..00000000 --- a/tests/golden/MANIFEST.json +++ /dev/null @@ -1,2639 +0,0 @@ -{ - "description": "Byte-exact golden reference outputs for the Go codefang + uast tools, for validating the Rust rewrite. Captures with machine=true AND nonBinding=false are BINDING: the Rust port must reproduce them byte-for-byte. nonBinding=true captures are either human formats (cosmetic) or machine formats that are intrinsically nondeterministic in the Go tool (Go map-iteration order in per-file/per-pair sections) and must be compared after canonicalization.", - "generated_utc": "2026-06-05T22:47:29Z", - "binaries": { - "codefang": "/home/dmitriy/sources/codefang/build/bin/codefang", - "codefangSha256": "61f29318ff7724c9c25b5e6994341c662702a05fa15fc64120ca5855a54c7c66", - "uast": "/home/dmitriy/sources/codefang/build/bin/uast", - "uastSha256": "75a309178a6640e453889318885f43d7649730bb24bafda50c743a853d5944d1", - "goVersion": "go version go1.26.0 linux/amd64", - "buildVia": "make build (CGO_ENABLED=1, static libgit2 from third_party/libgit2/install)", - "codefangVersionNote": "the `version` subcommand embeds the build timestamp via -ldflags and is NOT deterministic; it is not a report and is not captured." - }, - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "envNote": "Set exactly these vars AND use `set -f` (noglob) before each command so a literal '*'/'static/*' analyzer selector reaches the binary. Only STDOUT is captured; STDERR (timestamped progress logs) is discarded. Cross-run state is disabled via --checkpoint=false --resume=false --no-cache and wiping ~/.codefang/checkpoints.", - "inputs": { - "runRepo": "/home/dmitriy/sources/kubernetes", - "runHeadPinned": "2c9cc8da1aa316c30cfba4210cfcd09aff193c81", - "staticSubset": "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets", - "staticSubsetNote": "STATIC analyzers are nondeterministic and produce huge output on the full repo; they are captured on this fixed 10-file subdirectory (passed via -p) bounded by --head, single-worker. This is the canonical static golden ('fast deterministic subset'). HISTORY analyzers run on the full repo bounded by --head/--limit.", - "limitHeadSingle": 5, - "limitStream": 10, - "streamNote": "ndjson and timeseries+ndjson are STREAMING formats: one JSON line per commit, emitted during the streaming pipeline and EMPTY in --head mode. They are captured with --limit N --workers 1 (no --head), which is deterministic for a fixed HEAD.", - "uastFile": "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go", - "uastFileGitBlob": "4d7a17c3afad3186d86f357906c843623eb57d49", - "uastFileSha256": "06d3f90345e467754ae9e369d08a29428635789dd064878568394d9ad7185f15", - "uastFileNote": "uast parse/analyze/query operate on this file's working-tree bytes and embed its ABSOLUTE path in the output; the Rust harness must invoke with the identical path." - }, - "formats": { - "machineBinding": [ - "json", - "yaml", - "ndjson", - "timeseries", - "timeseries+ndjson", - "bin" - ], - "humanNonBinding": [ - "text", - "plot", - "html", - "compact" - ], - "runFormats": "codefang run --format: json yaml plot bin timeseries ndjson text compact (default json). timeseries+ndjson = `--format timeseries --ndjson`.", - "uastParseFormats": "uast parse --format: json compact tree none (default json). yaml NOT supported.", - "uastAnalyzeFormats": "uast analyze --format: text json html (default text). yaml NOT supported.", - "uastQueryFormats": "uast query --format: json compact count (default json). yaml/text NOT supported.", - "compactNote": "run --format compact is a human progress-bar format (empty for combined '*' selections and for history analyzers); treated as nonBinding.", - "binNote": "bin is binary; outPath holds raw bytes and sha256 is the stable comparison hash. bin is STABLE for history analyzers but UNSTABLE for static analyzers (map ordering).", - "plotHtmlNote": "run --format plot/html render multi-page HTML to a DIRECTORY (-o), not stdout; their stdout golden would be empty. Not captured (nonBinding by nature)." - }, - "nondeterminismNote": "The Go tool emits per-file (static) and per-pair (couples/shotness) sections in Go map-iteration order, which is randomized per process. Sorted content is identical across runs; only byte order differs. Such captures are recorded with stable=false, nonBinding=true. The Rust harness must canonicalize (sort those arrays by file_path / key) before comparing, OR the Rust port may emit them in a deterministic (sorted) order, which is a correctness improvement over Go.", - "stabilityNote": "Every machine capture ran twice with identical env/argv (set -f). stable=true means both runs produced an identical sha256; only those are BINDING.", - "counts": { - "total": 72, - "binding": 32, - "nonBinding": 40 - }, - "captures": [ - { - "id": "static.static_clones.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/clones", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_clones.json", - "relPath": "static/static_clones.json", - "sha256": "4d20870e13c21662ea1f051d10bc2f916f857c147515756a6bed02268b2e754d", - "bytes": 191681, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/clones, format json, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). Clone-pair representative is tie-broken by Go map-iteration order: across two Go runs the emitted pair name flips (e.g. 'Int64.Has <-> String.HasAny' vs 'Int64.Has <-> Int64.HasAny'), byte length varies. No Rust sort can reproduce it. Stays nonBinding." - }, - { - "id": "static.static_clones.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/clones", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_clones.yaml", - "relPath": "static/static_clones.yaml", - "sha256": "a57208d86a7fb9afc4b18918e68b7b2b4911e58eb882fa2e11b0057da77916e2", - "bytes": 125888, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/clones, format yaml, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). Clone-pair representative is tie-broken by Go map-iteration order: across two Go runs the emitted pair name flips (e.g. 'Int64.Has <-> String.HasAny' vs 'Int64.Has <-> Int64.HasAny'), byte length varies. No Rust sort can reproduce it. Stays nonBinding." - }, - { - "id": "static.static_clones.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/clones", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_clones.bin", - "relPath": "static/static_clones.bin", - "sha256": "9089a52f2d02360a0f33cb25f3744e2334dbafa9babf693157ffa93fe7c91063", - "bytes": 113900, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/clones, format bin, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). Clone-pair representative is tie-broken by Go map-iteration order: across two Go runs the emitted pair name flips (e.g. 'Int64.Has <-> String.HasAny' vs 'Int64.Has <-> Int64.HasAny'), byte length varies. No Rust sort can reproduce it. Stays nonBinding." - }, - { - "id": "static.static_clones.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/clones", - "--format", - "compact", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_clones.compact", - "relPath": "static/static_clones.compact", - "sha256": "6c132d7877e6db709d4526ed3a461e3511ac5f7e98606f00da1530d03d7775fd", - "bytes": 110, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/clones, human format compact (cosmetic)" - }, - { - "id": "static.static_clones.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/clones", - "--format", - "text", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_clones.text", - "relPath": "static/static_clones.text", - "sha256": "969d9cf0339261c470f2ed11612d77c3fdab46d5e004e8aa8c017d7dcfa36822", - "bytes": 2277, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/clones, human format text (cosmetic)" - }, - { - "id": "static.static_clones.perfile.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/clones", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "--per-file", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_clones.perfile.json", - "relPath": "static/static_clones.perfile.json", - "sha256": "e5468588a61e8b1e97528ea72a4d0c30dbf5f8bb426548f303414f7c307cb11e", - "bytes": 191699, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/clones per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). Clone-pair representative is tie-broken by Go map-iteration order: across two Go runs the emitted pair name flips (e.g. 'Int64.Has <-> String.HasAny' vs 'Int64.Has <-> Int64.HasAny'), byte length varies. No Rust sort can reproduce it. Stays nonBinding." - }, - { - "id": "static.static_cohesion.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/cohesion", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_cohesion.json", - "relPath": "static/static_cohesion.json", - "sha256": "0056cfa6dc91305e1e99a4b26683548c8066398a9368578e60994dc7fb879926", - "bytes": 21130, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/cohesion, format json, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_cohesion.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/cohesion", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_cohesion.yaml", - "relPath": "static/static_cohesion.yaml", - "sha256": "19e962f5b7effe4cb2316967c635b3d8215d2cc702e1f37622bd9ed4f7c865a7", - "bytes": 15091, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/cohesion, format yaml, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_cohesion.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/cohesion", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_cohesion.bin", - "relPath": "static/static_cohesion.bin", - "sha256": "64a4c907f2a6e5e0d3d557b4d4697f345e3111c470f7bede67033e7cedbd5d24", - "bytes": 13419, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/cohesion, format bin, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_cohesion.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/cohesion", - "--format", - "compact", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_cohesion.compact", - "relPath": "static/static_cohesion.compact", - "sha256": "188ca5d4f7a1047b286f7da9905300a0abeb4c6bd3998dea9ada61a27c8120fc", - "bytes": 112, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/cohesion, human format compact (cosmetic)" - }, - { - "id": "static.static_cohesion.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/cohesion", - "--format", - "text", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_cohesion.text", - "relPath": "static/static_cohesion.text", - "sha256": "041c6e1799befce9e1bf11b40e19d67e49108770e591726d8d50868fa113542e", - "bytes": 1008, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/cohesion, human format text (cosmetic)" - }, - { - "id": "static.static_cohesion.perfile.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/cohesion", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "--per-file", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_cohesion.perfile.json", - "relPath": "static/static_cohesion.perfile.json", - "sha256": "ab31bf8b59344a3b1cdab1f93250696c068897ea1778cb588231c52b07bccfe6", - "bytes": 55772, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/cohesion per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_comments.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/comments", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_comments.json", - "relPath": "static/static_comments.json", - "sha256": "b6c64a311c2a8beeefeb1535a939c589ca8b34a30d569abd197a3ddf87e2db9b", - "bytes": 7250, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/comments, format json, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_comments.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/comments", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_comments.yaml", - "relPath": "static/static_comments.yaml", - "sha256": "fec7e410594857e37802b618fc1e43048ce9c3350f667d3eb607369acd8fc0fd", - "bytes": 91964, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/comments, format yaml, subset (--head, single worker)" - }, - { - "id": "static.static_comments.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/comments", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_comments.bin", - "relPath": "static/static_comments.bin", - "sha256": "56c20e3e1b11f1504f55b7bd4e5f92b79da9a17e8dd0f1b1c7684216bedf4fa7", - "bytes": 75710, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/comments, format bin, subset (--head, single worker)" - }, - { - "id": "static.static_comments.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/comments", - "--format", - "compact", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_comments.compact", - "relPath": "static/static_comments.compact", - "sha256": "52e5dbffa36523172999fed8ec8908ffbf064cdd73ece3c559dd2cc887e44f58", - "bytes": 104, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/comments, human format compact (cosmetic)" - }, - { - "id": "static.static_comments.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/comments", - "--format", - "text", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_comments.text", - "relPath": "static/static_comments.text", - "sha256": "93aa3d5969559dc5a99a15af02ecebb58941ef90533f0fe0b9707c17a601e5a0", - "bytes": 1633, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/comments, human format text (cosmetic)" - }, - { - "id": "static.static_comments.perfile.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/comments", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "--per-file", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_comments.perfile.json", - "relPath": "static/static_comments.perfile.json", - "sha256": "76c78b8ac3340b3a61a28058803ce3de8792d87fdcb5c632ac43e85e601f5f97", - "bytes": 25157, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/comments per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_complexity.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/complexity", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_complexity.json", - "relPath": "static/static_complexity.json", - "sha256": "d98ba049f27a8295bd28d02f8d23996f2a356039377f328faa9f04d129db29c2", - "bytes": 23970, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/complexity, format json, subset (--head, single worker)" - }, - { - "id": "static.static_complexity.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/complexity", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_complexity.yaml", - "relPath": "static/static_complexity.yaml", - "sha256": "61bcbf6379ab16a505f3907eb633735564245ff25299a7587cdbc3d0818e6b3c", - "bytes": 38676, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/complexity, format yaml, subset (--head, single worker)" - }, - { - "id": "static.static_complexity.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/complexity", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_complexity.bin", - "relPath": "static/static_complexity.bin", - "sha256": "7636fb48c13b536d79e41ebab6b303f5b5c45c9292608cc570921d4b7f50767a", - "bytes": 33127, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/complexity, format bin, subset (--head, single worker)" - }, - { - "id": "static.static_complexity.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/complexity", - "--format", - "compact", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_complexity.compact", - "relPath": "static/static_complexity.compact", - "sha256": "9de43953c736491ee4dd8df39dfbca9767ccee8ad12b894f7d7db6f16861a54c", - "bytes": 107, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/complexity, human format compact (cosmetic)" - }, - { - "id": "static.static_complexity.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/complexity", - "--format", - "text", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_complexity.text", - "relPath": "static/static_complexity.text", - "sha256": "9762eddf7b717be79b45de199965b64aaac357d14e9920a8e35272c9c3ae1976", - "bytes": 1082, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/complexity, human format text (cosmetic)" - }, - { - "id": "static.static_complexity.perfile.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/complexity", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "--per-file", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_complexity.perfile.json", - "relPath": "static/static_complexity.perfile.json", - "sha256": "366464fb3a86c7213a2a2c65b0ee486ad28bad6b6208e3c6d43a48b814c2b5dd", - "bytes": 62902, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/complexity per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_composition.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/composition", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_composition.json", - "relPath": "static/static_composition.json", - "sha256": "512c2019bf0b574ce98d1f100c56a82ed6c2b112a26a87f5d77a350c9d0e1e34", - "bytes": 638, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/composition, format json, subset (--head, single worker)" - }, - { - "id": "static.static_composition.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/composition", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_composition.yaml", - "relPath": "static/static_composition.yaml", - "sha256": "46588028dd5e415c60e7b8d16e69a67c9e8514be043e23a1e14ef734bde1e2ea", - "bytes": 301, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/composition, format yaml, subset (--head, single worker)" - }, - { - "id": "static.static_composition.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/composition", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_composition.bin", - "relPath": "static/static_composition.bin", - "sha256": "2251d5639f4abe9dfa9a0310a1611832c8fde2ef2fbc29d3c94ab5b51d14f293", - "bytes": 269, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/composition, format bin, subset (--head, single worker)" - }, - { - "id": "static.static_composition.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/composition", - "--format", - "compact", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_composition.compact", - "relPath": "static/static_composition.compact", - "sha256": "3a522225087e2477ba3a2bd7ca764f22d234a537a829209ac8919c84c91f211e", - "bytes": 90, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/composition, human format compact (cosmetic)" - }, - { - "id": "static.static_composition.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/composition", - "--format", - "text", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_composition.text", - "relPath": "static/static_composition.text", - "sha256": "1d25176e6e236448b4a6187e65de53572dbe2f9f6d6180ec0eac57da2c062789", - "bytes": 1364, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/composition, human format text (cosmetic)" - }, - { - "id": "static.static_composition.perfile.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/composition", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "--per-file", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_composition.perfile.json", - "relPath": "static/static_composition.perfile.json", - "sha256": "e939b6a7e6684449b9349d8771059d62eadc10ce7c0b225662bc2b7134578095", - "bytes": 5611, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/composition per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_halstead.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/halstead", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_halstead.json", - "relPath": "static/static_halstead.json", - "sha256": "fd659f08bb7fb37256f5275f985b01c16bb801462da077ff0ee942c3c6e29957", - "bytes": 26091, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/halstead, format json, subset (--head, single worker)" - }, - { - "id": "static.static_halstead.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/halstead", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_halstead.yaml", - "relPath": "static/static_halstead.yaml", - "sha256": "fe54b5dc12fc6b640fd30da6e6c3133aa33762e061f1d4048b1b971fa69508bf", - "bytes": 40561, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/halstead, format yaml, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). Aggregate Halstead metric is summed in Go map-iteration order; float accumulation order flips a threshold so the qualitative message changes across runs (e.g. 'Very high Halstead complexity' vs 'Moderate Halstead complexity'), byte length varies. Stays nonBinding." - }, - { - "id": "static.static_halstead.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/halstead", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_halstead.bin", - "relPath": "static/static_halstead.bin", - "sha256": "d1a17fec034603312c6e3fc77986cc3ae90d6a748005cbd863abf8eee5937bbc", - "bytes": 35142, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/halstead, format bin, subset (--head, single worker)" - }, - { - "id": "static.static_halstead.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/halstead", - "--format", - "compact", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_halstead.compact", - "relPath": "static/static_halstead.compact", - "sha256": "a001e3515c150cf55f732e4bb1ab58dcb33e26f68a0ba1fc0251caf96c3b08d7", - "bytes": 94, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/halstead, human format compact (cosmetic)" - }, - { - "id": "static.static_halstead.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/halstead", - "--format", - "text", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_halstead.text", - "relPath": "static/static_halstead.text", - "sha256": "22f6b910f36b2f947adb65fef47a178ae92ca0024e80fdd61bf17981c68672a3", - "bytes": 1208, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/halstead, human format text (cosmetic)" - }, - { - "id": "static.static_halstead.perfile.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/halstead", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "--per-file", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_halstead.perfile.json", - "relPath": "static/static_halstead.perfile.json", - "sha256": "6d46ed9b63cba92e34a20c60d0df230c9d21eaca3b75a42ea1c441aa30cf1cb5", - "bytes": 71074, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/halstead per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). Aggregate Halstead metric is summed in Go map-iteration order; float accumulation order flips a threshold so the qualitative message changes across runs (e.g. 'Very high Halstead complexity' vs 'Moderate Halstead complexity'), byte length varies. Stays nonBinding." - }, - { - "id": "static.static_imports.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/imports", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_imports.json", - "relPath": "static/static_imports.json", - "sha256": "180b9981d69da9a82686d0d3280b1c319645088a028c528ceacce8ef178bf1a6", - "bytes": 1342, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/imports, format json, subset (--head, single worker)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "static.static_imports.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/imports", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_imports.yaml", - "relPath": "static/static_imports.yaml", - "sha256": "f9fb4cc8a96a75cc7c694b0679b6b5a320a20482e40c11b6a7677e6b141f4286", - "bytes": 762, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/imports, format yaml, subset (--head, single worker)" - }, - { - "id": "static.static_imports.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/imports", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_imports.bin", - "relPath": "static/static_imports.bin", - "sha256": "1b8043c359cf9ce3932276b14c8cf008562d0c8a9c8c3b8188b232e313b7a70d", - "bytes": 693, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "static/imports, format bin, subset (--head, single worker)" - }, - { - "id": "static.static_imports.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/imports", - "--format", - "compact", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_imports.compact", - "relPath": "static/static_imports.compact", - "sha256": "6b69016d8a4ec12584890605f5ee7fcf0c5d3d2ef60cf410c9b59c81ff20af63", - "bytes": 77, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/imports, human format compact (cosmetic)" - }, - { - "id": "static.static_imports.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/imports", - "--format", - "text", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_imports.text", - "relPath": "static/static_imports.text", - "sha256": "2cfafece391687308f84b21595db44ffaa6a818cf543ba3d3de87348e5962d06", - "bytes": 1491, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "static/imports, human format text (cosmetic)" - }, - { - "id": "static.static_imports.perfile.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/imports", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "--per-file", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/static/static_imports.perfile.json", - "relPath": "static/static_imports.perfile.json", - "sha256": "d63ce3c26fa09948d4618ac3d889c126a5dda0def864b778003f0def1cbd08bf", - "bytes": 6998, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "static/imports per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "run.burndown.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "json", - "--head", - "--limit", - "5", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.json", - "relPath": "run/burndown.json", - "sha256": "47c61e4354caae4f1b412c7308a592cd88203318b2a160c113df9bc583a91fc1", - "bytes": 359, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/burndown json, --head --limit 5" - }, - { - "id": "run.burndown.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "yaml", - "--head", - "--limit", - "5", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.yaml", - "relPath": "run/burndown.yaml", - "sha256": "5fd32cb67d4d80ebdcc8c2925adf26e58b53459ee412d7447da01b8620e04911", - "bytes": 461, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/burndown yaml, --head --limit 5" - }, - { - "id": "run.burndown.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "bin", - "--head", - "--limit", - "5", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.bin", - "relPath": "run/burndown.bin", - "sha256": "9beb5080360c46a4a09452bdbe057cfeca965849d7ab25bedca62fe3806099ea", - "bytes": 367, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/burndown bin, --head --limit 5" - }, - { - "id": "run.burndown.timeseries", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "timeseries", - "--head", - "--limit", - "5", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.timeseries", - "relPath": "run/burndown.timeseries", - "sha256": "61987e40c0af73d7e07dd94ad43dc32a36b72f0c5ed06658e350c1a19ae7de21", - "bytes": 365, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/burndown timeseries, --head --limit 5" - }, - { - "id": "run.burndown.ndjson", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "ndjson", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.ndjson", - "relPath": "run/burndown.ndjson", - "sha256": "df1069344794baea4dbc04e50756dfb7bb01c8aa790fb8af8e3b11a22335bc5d", - "bytes": 1490, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/burndown ndjson; streaming --limit 5 --workers 1" - }, - { - "id": "run.burndown.timeseries_ndjson", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "timeseries", - "--ndjson", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.timeseries.ndjson", - "relPath": "run/burndown.timeseries.ndjson", - "sha256": "5cb82084e40787cc202d07a60d4eb9dd4c2f34a18eb7621d9dce19c3f106e7c0", - "bytes": 808, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/burndown timeseries+ndjson (--format timeseries --ndjson); streaming --limit 5 --workers 1" - }, - { - "id": "run.burndown.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "text", - "--head", - "--limit", - "5", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.text", - "relPath": "run/burndown.text", - "sha256": "d2007753c0298325c4a7474437788a9e0f647181880e88363550f30320401d0b", - "bytes": 1372, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "history/burndown human text (cosmetic)" - }, - { - "id": "run.burndown.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/burndown", - "--format", - "compact", - "--head", - "--limit", - "5", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/burndown.compact", - "relPath": "run/burndown.compact", - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "bytes": 0, - "rc": 1, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "history/burndown human compact (cosmetic)" - }, - { - "id": "run.history_anomaly.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/anomaly", - "--format", - "json", - "--head", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_anomaly.json", - "relPath": "run/history_anomaly.json", - "sha256": "90053b63a2f212442000a98a58270f9dd7ac135752b4051e50e1026a8bc0fbd3", - "bytes": 570, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/anomaly json, --head --limit 5 --workers 1" - }, - { - "id": "run.history_couples.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/couples", - "--format", - "json", - "--head", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_couples.json", - "relPath": "run/history_couples.json", - "sha256": "101af2bbf52c9096a6cb4b1983d7e2ca0f84319082d7d31016fed1183c06bb08", - "bytes": 11328, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "history/couples json, --head --limit 5 --workers 1", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "run.history_devs.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/devs", - "--format", - "json", - "--head", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_devs.json", - "relPath": "run/history_devs.json", - "sha256": "cbbdd4cbe011821e348a6a62b43ce94eb293d7066aae25071e121828af7ba782", - "bytes": 831, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/devs json, --head --limit 5 --workers 1" - }, - { - "id": "run.history_shotness.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/shotness", - "--format", - "json", - "--head", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_shotness.json", - "relPath": "run/history_shotness.json", - "sha256": "683dd0e8980a79f712eac4ca5584a67106b324704db4572a55a6cd0f4c45f99c", - "bytes": 4131, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "history/shotness json, --head --limit 5 --workers 1", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). The node_hotness / node_coupling SET selected differs across two Go runs (different nodes and coupled_nodes counts; 67 vs 97 leaf values), byte length varies. Selection is driven by Go map order. Stays nonBinding." - }, - { - "id": "run.history_imports.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/imports", - "--format", - "json", - "--limit", - "10", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_imports.json", - "relPath": "run/history_imports.json", - "sha256": "ce55f5e60f641da9a6e24fedc06a465e109448cf19c326c3978c69a141f8862a", - "bytes": 167, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/imports json; streaming --limit 10 --workers 1" - }, - { - "id": "run.history_quality.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/quality", - "--format", - "json", - "--limit", - "10", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_quality.json", - "relPath": "run/history_quality.json", - "sha256": "3341ae58da898d20874ee366248021f38b0da8e77dc09b1dd7764b40d78dc3e3", - "bytes": 1153, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/quality json; streaming --limit 10 --workers 1" - }, - { - "id": "run.history_sentiment.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/sentiment", - "--format", - "json", - "--limit", - "10", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_sentiment.json", - "relPath": "run/history_sentiment.json", - "sha256": "c3b9b743bc04e87b79170ca49c4512459ded055eeabe0e22ae9373d08cc163d5", - "bytes": 717, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/sentiment json; streaming --limit 10 --workers 1" - }, - { - "id": "run.history_typos.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/typos", - "--format", - "json", - "--limit", - "10", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_typos.json", - "relPath": "run/history_typos.json", - "sha256": "2b11f73cd7bb947cbd723c533d34eab8acb6ca95748062525aef110cbff089df", - "bytes": 138, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/typos json; streaming --limit 10 --workers 1" - }, - { - "id": "run.history_file-history.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/file-history", - "--format", - "json", - "--limit", - "10", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_file-history.json", - "relPath": "run/history_file-history.json", - "sha256": "8e795cd36ba2673c32b28397abce339b906991b537d6f3d225334d97618d06ee", - "bytes": 40633, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "history/file-history json; streaming --limit 10 --workers 1", - "triageVerdict": "GO-NONDETERMINISTIC (pure map-reorder of identical content; verified: deep-sorted JSON / sorted lines equal across two Go runs, equal byte length). Rust port should emit deterministic SORTED order (correctness improvement over Go per nondeterminismNote); will NOT be byte-identical to this unstable golden. Stays nonBinding." - }, - { - "id": "run.history_devs.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/devs", - "--format", - "yaml", - "--head", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_devs.yaml", - "relPath": "run/history_devs.yaml", - "sha256": "f9929012cb25686d9434cddbce3c9c49cfa4e9996d02dc03d19c0cd63c2ca8e8", - "bytes": 1023, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/devs yaml, --head --limit 5 --workers 1" - }, - { - "id": "run.history_devs.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "history/devs", - "--format", - "bin", - "--head", - "--limit", - "5", - "--workers", - "1", - "/home/dmitriy/sources/kubernetes" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/history_devs.bin", - "relPath": "run/history_devs.bin", - "sha256": "e7cb3383108d41d533b570ec7de0c86b834cfbf299190c139fddca2c3bc16930", - "bytes": 839, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "history/devs bin, --head --limit 5 --workers 1" - }, - { - "id": "all_static.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/*", - "--format", - "json", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/all_static.json", - "relPath": "run/all_static.json", - "sha256": "fede03c51ccfb446cfa19d305dab062489ecc4be8922a335c27ee17b7149b6a5", - "bytes": 271594, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "all static analyzers (static/*) json on subset", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). static/* union includes static/clones and static/halstead, both of which are content-nondeterministic in Go (see those captures). Stays nonBinding." - }, - { - "id": "all_static.yaml", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/*", - "--format", - "yaml", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/all_static.yaml", - "relPath": "run/all_static.yaml", - "sha256": "8ed145fec3f34fdde2d9f500213a1c682e2b4bc6a93f8c5159e032a926832f73", - "bytes": 313229, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "all static analyzers (static/*) yaml on subset", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). static/* union includes static/clones and static/halstead, both of which are content-nondeterministic in Go (see those captures). Stays nonBinding." - }, - { - "id": "all_static.bin", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/codefang", - "run", - "--checkpoint=false", - "--resume=false", - "--no-cache", - "--analyzers", - "static/*", - "--format", - "bin", - "--head", - "--workers", - "1", - "--static-workers", - "1", - "-p", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/run/all_static.bin", - "relPath": "run/all_static.bin", - "sha256": "da39cbbf21bfe6743e80489688301702b49312e72ea5dc3797e7f8f91902f6e3", - "bytes": 272259, - "rc": 0, - "machine": true, - "nonBinding": true, - "stable": "false", - "note": "all static analyzers (static/*) bin on subset", - "triageVerdict": "GO-NONDETERMINISTIC AT CONTENT LEVEL (not sortable). static/* union includes static/clones and static/halstead, both of which are content-nondeterministic in Go (see those captures). Stays nonBinding." - }, - { - "id": "uast.parse.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "parse", - "--format", - "json", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/parse.json", - "relPath": "uast/parse.json", - "sha256": "be674e115ad8e5ddadfebdadfd63e0e6382058a4a07ee428e08331eace276bc2", - "bytes": 285255, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "uast parse json (blob 4d7a17c3afad3186d86f357906c843623eb57d49)" - }, - { - "id": "uast.parse.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "parse", - "--format", - "compact", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/parse.compact", - "relPath": "uast/parse.compact", - "sha256": "1f92cc403649a64f1ac5ee3403f53261e177af3a631085c864c47a5d72485aa1", - "bytes": 97744, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "uast parse compact (blob 4d7a17c3afad3186d86f357906c843623eb57d49)" - }, - { - "id": "uast.parse.tree", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "parse", - "--format", - "tree", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/parse.tree", - "relPath": "uast/parse.tree", - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "bytes": 0, - "rc": 1, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "uast parse tree (human tree view; cosmetic)" - }, - { - "id": "uast.analyze.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "analyze", - "--format", - "json", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/analyze.json", - "relPath": "uast/analyze.json", - "sha256": "b6eda86f320fda18935195b938f875c151875907f1c5f422402d7a20c6285d98", - "bytes": 965, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "uast analyze json (blob 4d7a17c3afad3186d86f357906c843623eb57d49)" - }, - { - "id": "uast.analyze.text", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "analyze", - "--format", - "text", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/analyze.text", - "relPath": "uast/analyze.text", - "sha256": "e1f735dced0ada8aaec8203d7609b82d4a26724b75ed4cc0b308a285726cf37a", - "bytes": 1055, - "rc": 0, - "machine": false, - "nonBinding": true, - "stable": "n/a", - "note": "uast analyze text (human; cosmetic)" - }, - { - "id": "uast.query.json", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "query", - "filter(.roles has \"Function\")", - "--format", - "json", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/query.json", - "relPath": "uast/query.json", - "sha256": "ba9b6638c5eb651c675c59bd48af4caebf369340356bc8ff9443dae8e033b002", - "bytes": 243439, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "uast query 'filter(.roles has Function)' json (blob 4d7a17c3afad3186d86f357906c843623eb57d49)" - }, - { - "id": "uast.query.compact", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "query", - "filter(.roles has \"Function\")", - "--format", - "compact", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/query.compact", - "relPath": "uast/query.compact", - "sha256": "d171b1676371fcc0ef1d7f4ec081c6f7974b9858d15db94576fca82efc1f8f9d", - "bytes": 74750, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "uast query 'filter(.roles has Function)' compact (blob 4d7a17c3afad3186d86f357906c843623eb57d49)" - }, - { - "id": "uast.query.count", - "argv": [ - "/home/dmitriy/sources/codefang/build/bin/uast", - "query", - "reduce(count)", - "--format", - "count", - "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go" - ], - "env": { - "TZ": "UTC", - "NO_COLOR": "1", - "LANG": "C", - "LC_ALL": "C", - "SOURCE_DATE_EPOCH": "315532800" - }, - "outPath": "/home/dmitriy/sources/codefang/tests/golden/uast/query.count", - "relPath": "uast/query.count", - "sha256": "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865", - "bytes": 2, - "rc": 0, - "machine": true, - "nonBinding": false, - "stable": "true", - "note": "uast query 'reduce(count)' count (blob 4d7a17c3afad3186d86f357906c843623eb57d49)" - } - ], - "triageNote": "Step 16 triage (2026-06-06): each machine&&stable=false capture was run twice via the Go ref AND twice via the Rust release binary. RESULT: Go is nondeterministic across its OWN two runs for ALL 22 (the golden sha never reproduces), so category (a) 'Rust-missing-sort -> byte-identical' has ZERO members. Two sub-causes: PURE-REORDER (deep-sorted content equal: cohesion/comments/complexity/composition/imports/couples/file-history) where the Rust port should emit a deterministic SORTED order as a correctness improvement (still not byte-identical to the unstable golden), and CONTENT-NONDETERMINISM (clones/halstead/shotness, and all_static which unions them) where the actual data differs run-to-run and no sort helps. All 22 correctly remain stable=false/nonBinding. See per-capture triageVerdict.", - "_windowDivergenceNote": "Captures bounded by --limit encode the RETIRED Go reference's window, which selected the N OLDEST commits (burndown.ndjson line 0 is 2c4b3a562ce3, 2014-06-06). codefang now selects the N NEWEST commits by owner ruling, so these captures are superseded, not binding: the harness skips every `run` capture today (IMPLEMENTED_PREFIXES = version), and a future re-capture must be taken from the Rust binary, not the Go one. See specs/frds/FRD-newest-commit-window.md." -} diff --git a/tests/golden/capture.sh b/tests/golden/capture.sh deleted file mode 100644 index 2e90429b..00000000 --- a/tests/golden/capture.sh +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# Golden reference capture for the Go codefang/uast tools. -# -# Establishes BYTE-EXACT golden outputs that the Rust rewrite must match. -# -# BINDING goldens = machine formats {json, yaml, ndjson, timeseries, -# timeseries+ndjson, bin} that were VERIFIED STABLE -# (byte-identical across two runs). The Rust port MUST -# reproduce these byte-for-byte. -# NON-BINDING = (a) human formats {text, plot, html, compact} -> cosmetic. -# (b) machine formats that are INTRINSICALLY NONDETERMINISTIC -# in the Go tool itself (Go map-iteration ordering in -# per-file / per-pair sections). For these, sorted content -# is identical across runs but byte order is not, so they -# cannot be byte-golden. The Rust harness must compare them -# after CANONICALIZATION (sort the per-file/per-pair arrays), -# OR the Rust port may emit them deterministically (an -# improvement). Marked nonBinding=true, stable=false. -# -# Determinism strategy (the Rust harness MUST replicate ALL of this): -# * env: TZ=UTC NO_COLOR=1 LANG=C LC_ALL=C SOURCE_DATE_EPOCH=315532800 -# * `set -f` (noglob) so the literal '*' analyzer selector reaches the binary -# instead of being expanded by the shell against the cwd. -# * Only STDOUT is captured. STDERR carries timestamped progress logs and is -# discarded. -# * Cross-run state disabled: --checkpoint=false --resume=false --no-cache and -# the checkpoint dir is wiped, so every capture is self-contained. -# * Work is bounded: -# - STATIC analyzers run on a FIXED SUBDIRECTORY of kubernetes (10 files, -# see SUBDIR below) via `-p`. This is the "fast deterministic subset". -# A full-repo static capture is impractical (100k+ files, GBs of output) -# AND no more deterministic (same map-ordering issue), so the subset is -# the canonical static golden. -# - HISTORY analyzers run on the FULL kubernetes repo bounded by --head -# (HEAD commit only) and/or streaming --limit N --workers 1. -# * burndown `generated_at` is a FIXED constant in the Go source, not wall -# clock, so its timestamps are reproducible regardless of SOURCE_DATE_EPOCH. -# * uast parse/analyze/query run on a FIXED working-tree file; the file's git -# blob hash + sha256 are recorded. uast output embeds the ABSOLUTE file path, -# so the harness must invoke with the same absolute path. -# -# Each BINDING candidate is captured twice and its sha256 compared; only captures -# with stable=true are treated as BINDING. Unstable machine captures are -# downgraded to nonBinding automatically (with note). -# -# Usage: bash capture.sh -# ============================================================================= -set -u - -export TZ=UTC -export NO_COLOR=1 -export LANG=C -export LC_ALL=C -export SOURCE_DATE_EPOCH=315532800 # 1980-01-01T00:00:00Z (bounded; used if honored) - -CODEFANG=/home/dmitriy/sources/codefang/build/bin/codefang -UAST=/home/dmitriy/sources/codefang/build/bin/uast -KREPO=/home/dmitriy/sources/kubernetes -# Fast deterministic STATIC subset: a small, fixed subdirectory (10 source files). -SUBDIR=$KREPO/staging/src/k8s.io/apimachinery/pkg/util/sets -GOLDEN=/home/dmitriy/sources/codefang/tests/golden -RUNDIR="$GOLDEN/run" -STATICDIR="$GOLDEN/static" -UASTDIR="$GOLDEN/uast" -rm -rf "$RUNDIR" "$STATICDIR" "$UASTDIR" -mkdir -p "$RUNDIR" "$STATICDIR" "$UASTDIR" - -git config --global --add safe.directory '*' >/dev/null 2>&1 || true -HEAD=$(git -C "$KREPO" rev-parse HEAD) - -RUN_DETERMINISM="--checkpoint=false --resume=false --no-cache" -rm -rf "$HOME/.codefang/checkpoints"/* 2>/dev/null || true - -# Fixed source file for uast goldens (deterministic by working-tree content). -UFILE="$SUBDIR/byte.go" -UFILE_BLOB=$(git -C "$KREPO" hash-object "$UFILE" 2>/dev/null || echo "") -UFILE_SHA=$(sha256sum "$UFILE" | cut -d' ' -f1) - -ENV_JSON='{"TZ":"UTC","NO_COLOR":"1","LANG":"C","LC_ALL":"C","SOURCE_DATE_EPOCH":"315532800"}' - -RECORDS="$GOLDEN/.records.ndjson" -: > "$RECORDS" - -json_escape() { printf '%s' "$1" | python3 -c 'import json,sys; sys.stdout.write(json.dumps(sys.stdin.read()))'; } - -# capture <id> <outfile> <machine 0|1> <forceNonBind 0|1> <note> -- <argv...> -# Globbing is disabled (set -f) so a literal '*' in argv reaches the program. -# A machine capture that fails the double-run stability check is auto-downgraded -# to nonBinding=true (stable=false). -capture() { - local id="$1" out="$2" machine="$3" forcenb="$4" note="$5"; shift 5 - [ "$1" = "--" ] && shift - local argv=("$@") - - set -f - "${argv[@]}" > "$out" 2>/dev/null - local rc=$? - set +f - - local bytes sha stable nonbind - bytes=$(wc -c < "$out") - sha=$(sha256sum "$out" | cut -d' ' -f1) - - stable="n/a" - nonbind="$forcenb" - if [ "$machine" = "1" ]; then - local tmp; tmp=$(mktemp) - set -f - "${argv[@]}" > "$tmp" 2>/dev/null - set +f - local sha2; sha2=$(sha256sum "$tmp" | cut -d' ' -f1) - if [ "$sha" = "$sha2" ]; then stable="true"; else stable="false"; nonbind=1; fi - rm -f "$tmp" - fi - - local argv_json="[" first=1 a - for a in "${argv[@]}"; do - [ $first -eq 1 ] && first=0 || argv_json+="," - argv_json+="$(json_escape "$a")" - done - argv_json+="]" - - local rel="${out#$GOLDEN/}" - { - printf '{' - printf '"id":%s,' "$(json_escape "$id")" - printf '"argv":%s,' "$argv_json" - printf '"env":%s,' "$ENV_JSON" - printf '"outPath":%s,' "$(json_escape "$out")" - printf '"relPath":%s,' "$(json_escape "$rel")" - printf '"sha256":%s,' "$(json_escape "$sha")" - printf '"bytes":%s,' "$bytes" - printf '"rc":%s,' "$rc" - printf '"machine":%s,' "$([ "$machine" = "1" ] && echo true || echo false)" - printf '"nonBinding":%s,' "$([ "$nonbind" = "1" ] && echo true || echo false)" - printf '"stable":%s,' "$(json_escape "$stable")" - printf '"note":%s' "$(json_escape "$note")" - printf '}\n' - } >> "$RECORDS" - printf 'CAP %-34s rc=%s bytes=%-9s stable=%-5s nb=%s %s\n' \ - "$id" "$rc" "$bytes" "$stable" "$nonbind" "${sha:0:12}" -} - -MACHINE_FMTS="json yaml bin timeseries" -ALL_ANALYZERS_HEAD="static/complexity,static/composition,static/comments,static/cohesion,static/halstead,static/imports,static/clones,history/anomaly,history/devs,history/couples,history/shotness" - -# =========================================================================== -# 1. STATIC analyzers on the fast deterministic subset (each, all machine fmts) -# =========================================================================== -STATIC="static/clones static/cohesion static/comments static/complexity static/composition static/halstead static/imports" -echo "=== STATIC analyzers on subset ($SUBDIR), all machine + human formats ===" -for a in $STATIC; do - base=$(printf '%s' "$a" | tr '/' '_') - for f in json yaml bin; do - capture "static.$base.$f" "$STATICDIR/$base.$f" 1 0 \ - "$a, format $f, subset (--head, single worker)" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers "$a" --format "$f" --head --workers 1 --static-workers 1 -p "$SUBDIR" - done - # compact + text are human-readable progress output (nonbinding). - for f in compact text; do - capture "static.$base.$f" "$STATICDIR/$base.$f" 0 1 \ - "$a, human format $f (cosmetic)" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers "$a" --format "$f" --head --workers 1 --static-workers 1 -p "$SUBDIR" - done - # per-file json variant (exposes nondeterminism explicitly; canonicalize to compare) - capture "static.$base.perfile.json" "$STATICDIR/$base.perfile.json" 1 0 \ - "$a per-file json (per-file array ordering is Go-map-nondeterministic; canonicalize)" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers "$a" --format json --head --workers 1 --static-workers 1 --per-file -p "$SUBDIR" -done - -# =========================================================================== -# 2. HISTORY analyzers on full repo, bounded. -# =========================================================================== -LIMIT=5 -STREAM_LIMIT=10 - -# 2a. burndown: the keystone history analyzer; capture EVERY machine format. -echo "=== history/burndown: all machine formats (head + streaming) ===" -for f in json yaml bin timeseries; do - capture "run.burndown.$f" "$RUNDIR/burndown.$f" 1 0 \ - "history/burndown $f, --head --limit $LIMIT" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers history/burndown --format "$f" --head --limit "$LIMIT" "$KREPO" -done -# ndjson + timeseries+ndjson are STREAMING (empty in --head mode): --limit N --workers 1. -capture "run.burndown.ndjson" "$RUNDIR/burndown.ndjson" 1 0 \ - "history/burndown ndjson; streaming --limit $LIMIT --workers 1" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers history/burndown --format ndjson --limit "$LIMIT" --workers 1 "$KREPO" -capture "run.burndown.timeseries_ndjson" "$RUNDIR/burndown.timeseries.ndjson" 1 0 \ - "history/burndown timeseries+ndjson (--format timeseries --ndjson); streaming --limit $LIMIT --workers 1" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers history/burndown --format timeseries --ndjson --limit "$LIMIT" --workers 1 "$KREPO" -# human formats (nonbinding) -for f in text compact; do - capture "run.burndown.$f" "$RUNDIR/burndown.$f" 0 1 \ - "history/burndown human $f (cosmetic)" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers history/burndown --format "$f" --head --limit "$LIMIT" "$KREPO" -done - -# 2b. Other history analyzers, json (head-meaningful ones) + json (streaming ones). -echo "=== history analyzers, json (head-mode) ===" -for a in history/anomaly history/couples history/devs history/shotness; do - base=$(printf '%s' "$a" | tr '/' '_') - capture "run.$base.json" "$RUNDIR/$base.json" 1 0 \ - "$a json, --head --limit $LIMIT --workers 1" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers "$a" --format json --head --limit "$LIMIT" --workers 1 "$KREPO" -done -echo "=== history analyzers, json (streaming-mode) ===" -for a in history/imports history/quality history/sentiment history/typos history/file-history; do - base=$(printf '%s' "$a" | tr '/' '_') - capture "run.$base.json" "$RUNDIR/$base.json" 1 0 \ - "$a json; streaming --limit $STREAM_LIMIT --workers 1" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers "$a" --format json --limit "$STREAM_LIMIT" --workers 1 "$KREPO" -done - -# 2c. devs: also yaml + bin + timeseries (representative multi-format history analyzer). -echo "=== history/devs: extra machine formats ===" -for f in yaml bin; do - capture "run.history_devs.$f" "$RUNDIR/history_devs.$f" 1 0 \ - "history/devs $f, --head --limit $LIMIT --workers 1" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers history/devs --format "$f" --head --limit "$LIMIT" --workers 1 "$KREPO" -done - -# =========================================================================== -# 3. ALL analyzers '*' on the fast subset (machine fmts) — combined report. -# Full-repo '*' produces 250MB+ outputs and is dominated by nondeterministic -# static sections; the subset is the practical combined golden. -# =========================================================================== -echo "=== ALL static '*' on subset, machine formats ===" -for f in json yaml bin; do - capture "all_static.$f" "$RUNDIR/all_static.$f" 1 0 \ - "all static analyzers (static/*) $f on subset" -- \ - "$CODEFANG" run $RUN_DETERMINISM --analyzers 'static/*' --format "$f" --head --workers 1 --static-workers 1 -p "$SUBDIR" -done - -# =========================================================================== -# 4. uast parse / analyze / query on the fixed file. -# Verified supported formats: -# parse: json, compact, tree, none (NOT yaml) -# analyze: json, text, html (NOT yaml) -# query: json, compact, count (NOT yaml/text) -# =========================================================================== -echo "=== uast parse ($UFILE) ===" -capture "uast.parse.json" "$UASTDIR/parse.json" 1 0 \ - "uast parse json (blob $UFILE_BLOB)" -- \ - "$UAST" parse --format json "$UFILE" -capture "uast.parse.compact" "$UASTDIR/parse.compact" 1 0 \ - "uast parse compact (blob $UFILE_BLOB)" -- \ - "$UAST" parse --format compact "$UFILE" -capture "uast.parse.tree" "$UASTDIR/parse.tree" 0 1 \ - "uast parse tree (human tree view; cosmetic)" -- \ - "$UAST" parse --format tree "$UFILE" - -echo "=== uast analyze ($UFILE) ===" -capture "uast.analyze.json" "$UASTDIR/analyze.json" 1 0 \ - "uast analyze json (blob $UFILE_BLOB)" -- \ - "$UAST" analyze --format json "$UFILE" -capture "uast.analyze.text" "$UASTDIR/analyze.text" 0 1 \ - "uast analyze text (human; cosmetic)" -- \ - "$UAST" analyze --format text "$UFILE" - -echo "=== uast query ($UFILE) ===" -capture "uast.query.json" "$UASTDIR/query.json" 1 0 \ - "uast query 'filter(.roles has Function)' json (blob $UFILE_BLOB)" -- \ - "$UAST" query 'filter(.roles has "Function")' --format json "$UFILE" -capture "uast.query.compact" "$UASTDIR/query.compact" 1 0 \ - "uast query 'filter(.roles has Function)' compact (blob $UFILE_BLOB)" -- \ - "$UAST" query 'filter(.roles has "Function")' --format compact "$UFILE" -capture "uast.query.count" "$UASTDIR/query.count" 1 0 \ - "uast query 'reduce(count)' count (blob $UFILE_BLOB)" -- \ - "$UAST" query 'reduce(count)' --format count "$UFILE" - -# ---- Assemble MANIFEST.json ---------------------------------------------------- -python3 - "$RECORDS" "$GOLDEN/MANIFEST.json" "$HEAD" "$UFILE" "$UFILE_BLOB" "$UFILE_SHA" "$CODEFANG" "$UAST" "$KREPO" "$SUBDIR" <<'PY' -import json, sys, subprocess, datetime -recs, manifest_path, head, ufile, ublob, usha, cf, ua, krepo, subdir = sys.argv[1:11] -caps = [json.loads(l) for l in open(recs) if l.strip()] - -def sh(c): - try: return subprocess.check_output(c, shell=True, stderr=subprocess.DEVNULL).decode().strip() - except Exception: return "" - -binding = [c for c in caps if c["machine"] and not c["nonBinding"]] -nonbind = [c for c in caps if c["nonBinding"]] - -manifest = { - "description": "Byte-exact golden reference outputs for the Go codefang + uast tools, " - "for validating the Rust rewrite. Captures with machine=true AND " - "nonBinding=false are BINDING: the Rust port must reproduce them byte-for-byte. " - "nonBinding=true captures are either human formats (cosmetic) or machine formats " - "that are intrinsically nondeterministic in the Go tool (Go map-iteration order " - "in per-file/per-pair sections) and must be compared after canonicalization.", - "generated_utc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), - "binaries": { - "codefang": cf, "codefangSha256": sh(f"sha256sum {cf} | cut -d' ' -f1"), - "uast": ua, "uastSha256": sh(f"sha256sum {ua} | cut -d' ' -f1"), - "goVersion": sh("go version"), - "buildVia": "make build (CGO_ENABLED=1, static libgit2 from third_party/libgit2/install)", - "codefangVersionNote": "the `version` subcommand embeds the build timestamp via -ldflags and is " - "NOT deterministic; it is not a report and is not captured.", - }, - "env": {"TZ":"UTC","NO_COLOR":"1","LANG":"C","LC_ALL":"C","SOURCE_DATE_EPOCH":"315532800"}, - "envNote": "Set exactly these vars AND use `set -f` (noglob) before each command so a literal " - "'*'/'static/*' analyzer selector reaches the binary. Only STDOUT is captured; STDERR " - "(timestamped progress logs) is discarded. Cross-run state is disabled via " - "--checkpoint=false --resume=false --no-cache and wiping ~/.codefang/checkpoints.", - "inputs": { - "runRepo": krepo, - "runHeadPinned": head, - "staticSubset": subdir, - "staticSubsetNote": "STATIC analyzers are nondeterministic and produce huge output on the full repo; " - "they are captured on this fixed 10-file subdirectory (passed via -p) bounded by " - "--head, single-worker. This is the canonical static golden ('fast deterministic " - "subset'). HISTORY analyzers run on the full repo bounded by --head/--limit.", - "limitHeadSingle": 5, - "limitStream": 10, - "streamNote": "ndjson and timeseries+ndjson are STREAMING formats: one JSON line per commit, emitted " - "during the streaming pipeline and EMPTY in --head mode. They are captured with " - "--limit N --workers 1 (no --head), which is deterministic for a fixed HEAD.", - "uastFile": ufile, - "uastFileGitBlob": ublob, - "uastFileSha256": usha, - "uastFileNote": "uast parse/analyze/query operate on this file's working-tree bytes and embed its " - "ABSOLUTE path in the output; the Rust harness must invoke with the identical path.", - }, - "formats": { - "machineBinding": ["json","yaml","ndjson","timeseries","timeseries+ndjson","bin"], - "humanNonBinding": ["text","plot","html","compact"], - "runFormats": "codefang run --format: json yaml plot bin timeseries ndjson text compact (default json). " - "timeseries+ndjson = `--format timeseries --ndjson`.", - "uastParseFormats": "uast parse --format: json compact tree none (default json). yaml NOT supported.", - "uastAnalyzeFormats": "uast analyze --format: text json html (default text). yaml NOT supported.", - "uastQueryFormats": "uast query --format: json compact count (default json). yaml/text NOT supported.", - "compactNote": "run --format compact is a human progress-bar format (empty for combined '*' selections " - "and for history analyzers); treated as nonBinding.", - "binNote": "bin is binary; outPath holds raw bytes and sha256 is the stable comparison hash. " - "bin is STABLE for history analyzers but UNSTABLE for static analyzers (map ordering).", - "plotHtmlNote": "run --format plot/html render multi-page HTML to a DIRECTORY (-o), not stdout; their " - "stdout golden would be empty. Not captured (nonBinding by nature)." - }, - "nondeterminismNote": "The Go tool emits per-file (static) and per-pair (couples/shotness) sections in " - "Go map-iteration order, which is randomized per process. Sorted content is " - "identical across runs; only byte order differs. Such captures are recorded with " - "stable=false, nonBinding=true. The Rust harness must canonicalize (sort those " - "arrays by file_path / key) before comparing, OR the Rust port may emit them in a " - "deterministic (sorted) order, which is a correctness improvement over Go.", - "stabilityNote": "Every machine capture ran twice with identical env/argv (set -f). stable=true means " - "both runs produced an identical sha256; only those are BINDING.", - "counts": {"total": len(caps), "binding": len(binding), "nonBinding": len(nonbind)}, - "captures": caps, -} -json.dump(manifest, open(manifest_path,"w"), indent=2) -open(manifest_path,"a").write("\n") -print(f"MANIFEST: {len(caps)} captures, {len(binding)} binding, {len(nonbind)} nonBinding") -PY - -echo "=== DONE ===" diff --git a/tests/golden/run/all_static.bin b/tests/golden/run/all_static.bin deleted file mode 100644 index 9ecce4e4bfb94b6846ddeeb922a1543d7fe9afa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 272259 zcmeHw+m7Q%wp||s{*QuQYy;UEk<>LG40QG7v~aqQ4<@@Y4}(EMrDSCZt(0;kWzQrE z_25T;j9>lazwwWF1(giNjvXX}j9@5J%^JH;cP1-VthLwPaUmJ}_J{Y;|NFoH^3~_w zVS8AtZ$Cb5mWS1Lv+rF;!|3Yer_1$rliofqRy*<YXf(X)9e=c2;Nsr(pg$Q!lSwid z&c?%0GRuE`cz#T8?^gT6ZuQ|wG~2uW-1`MT{5tAgC)3f@@jqhx$6)&MA4%_eIF7#L zO}v2q^>a^j?RL?-?yWY5{{8m)dXE45-Rk4VbeC?HY47TI#fRe+<6(Xc{_WR|z`pA3 zR}ZW8Vz)XxgGP=ZI*GsY(ycF7wU7k0&3y)SbNtt)SRbAbY1Yoar~i15e^>b^DERFW zr~#cah+I^(4_#dv7oGpN^!X+gtz<zHAyFtC`!6r48x+aH43rZAixwf0IJZ|4w+N%U zxG@YAQQPT%m#))7e;tE!x~6V)nt;tW`*hdDb2^x8NZL-nLUMQI#i5wP`|_F)V7{11 zn@J5BF?mUIUdJ*s9u|}cmnw<J&HAkL}|(n`aeBH-)^SE+0~gPT`iy0VppMP8n6f zhXv@wxh^AJyk9r_?e37?{e(eWm5@VfSGEPL$IdhC_kVv{tjmec_cu?E>29BmZ>}^g z@bXLQLRpxT_^NRMsRH6&X<QhqpmcXM=A`FA9t1VD$)M`$3Rx$lHY$|-Z8$<X<GQwk za<aEZE>yOrL%)R+h`VK1>hh@qqQCbRAL1v~3Pirvf<&}ynG~-N4zx(e?L;J3)wPFS zC`e;!+(^fo5~OC6o|ZUVN|)H>c`WC95=Ub#FK9<S&Wfn)47xdp;XK>-C?ERCZbLZO zIqhIuzz8ZSE;r8)4=FY^mO*Ii=uP)gG;3m3ZX4b6^40oTH^^0$L6=w7j=Dh2^=&(% zIz=bjTIiXV$J4^kRm)r<wa${1bfTn3t`codQ>P7fWK?V$kB=i^^~?qLE+i+`wv_M^ zF{Yo|Ns~@aUDR}tNhg0>S4|5L;>yNA(1<jYhhNg+VaqS6%c%jBpNQ$e=2inRt^jVw zuQl20o$wXyYVO2`bH8Q10-c*YEWR#!aCD-5ppbLX%~5i#Gv-!H(%osmty{Uh&2cMe z>K1b%iN=UyNMll29n!O_&7h%NWsBZ4nJj!nCO3^|098O-P;ZtsYpC0%se*PUHPIuB zao6%wNRnibIeS~frq)rPPiYO3&~+>Y^*!4-J?vbhdp2-5iMFBhg&5q`KHrL`xXL#; z^Gm!#=|OA?ROL<`r*S>C0Fq15#vtfz-iX{nmqR?Op7Ijc==`|YYfnTltiHS^6xdqa zc5eVyETHCv7Uk2B+7!<O)kr#OaD-c}3_u}u@A&Jw(J6&6TF$`&^1(%!Kqsk`r;X}z zKqHyMjLpAjbo^Zsd+oiV58A)U9xiPFKw}`Ebdn+|Fh`8$&kj*2GIU9}T+TUU#s63D zyVZ>hh2zWsMW$l%iWY!Kva|`5S36%((jK3ZapJbnu5=SoYfQrGG1IPV4V1($6sU}) zBOpYRP06W<Dk)(nuW%DHe}X~9A?cQ1P#30>4MA~Pdz6UA?cW%PmMK2Y%3icE#_H+R z3_^{FltGQVAAbRcG=$E6lg(^YC=z^mMO}~4m{gBc-j<v)*hU=*xfb(O_QLC9G%^}8 zUObEBHFYCa=|Q5MRJQ?40}p0Tmp6ctaghC$(&@<{ukZ`QBh=V-dXHtO3pt4^T9A%& zauH9rgStxMNTAeUj&6`ng~6|hyPJW~nrDoT^kd`5P>_5IARWL3bFDyXmsQO;k`L7w zAw6<Vbf|dM5%RF?uOyUTP`B(@aM3gCIM}-Z<=vHzLx7}3@?X}4P}?}VNNrxH(IEdV z#L^2uv%*;gWXu{Lap7j>U1>BF&}Q-o%lL3Y`6YEtPywXZXs1F@ZaCcv4GgCNv^CAH zgwb=SnX4;dWCaj5^k6gYW^_RhOFL=94c(YirEH8*The*60cl9i`eZfGqa?4WTMW=p z1r(2yw~4jvbs?`7p*Szu9aj-$uO%<PV5ADLjRg`<nc#If(NTg70+N-QcYa1ASpp;m zS5972my+&v9wt4l<P~+Hq<XmF@m@Ou<J1keyDcQnvZp1&$!i+35mfqSFYq|E<p|Yv zjqQC3!aS>hQX8qCeuYbQkO^zn4p3x-@}ry`7P+dfF*J?jeQ7BobA`ObJCr73V<4Hj znb~Erk(4yfPIt(qFQktp3{x%xYP3*3^;ju5CLou)v*jSC;2gV<9hcF-0#Yz}XVS#u z=6HUQyrwRYg#}2y%IMVvf-EJBPNL1T0@0XHB@7u#yufHT0%eqRPTF;K1+<71;pQb$ zTAx*J1WdcGE;lu5T2m5|R|)M(7f>ly37oj5(PBzvmydT6M|VknL0vW$_K^A4m`N1~ zkC7C%1wO1W`Hv1K<R?C4Dx!LUWEHu}O~CxoO%+S}A^)X+2tubl7~+JMV7L{U0@bPq z9z9^bWlnQ??(^|3GoW7a{NJa~b5u1uaiWu`RpBE@c~|PDmI@-vbs-;lg?AW|S&d<+ z#|mvhlskhHNyq9+*@UALplXraX3`<NT6yz4X-ER1H#B@M2d>#@$r(0%&%*{yr`IMH zXXd{|76wsH3UW);%>&hzvS6q-VI`%}O5Y}E^qQt2(?c&v(Vb`>FiE_wdbG=hQ6j14 z5Gm4*abu7~tMSI5B`s+_LcFeqssl+wstXD8rV3J8q?$`CZVqx?U2-;%^cw%gL%Lg% zEX<L*^r~aW^=vbGIx#<D(f|=ZsFKQbAXn6t)CP=;IuEiCkPID)vuvX<*GUZ{SJl<B zi9MYFZGwkRQHDW6M<Bg~k9J!o0{5m<I*<?+K<bLNNl(Y}Q(-Jwl2NctmOO}as=ZSt z(x#+yAOa!i<Qtla<|s*IdU2b=>h=jBE01MJf=)q%TBqT{(lzE#*YzuPgt<*=AFi<Z zFSObTRVydaYf<;?=epRWUYtR69(gC})fmn#{jObC*JC!2oTVSBRqVVb$)v(BNp!sl zTZ!bTsFR2D&eaWLT~IO|bduHyIe?G2c^%_h<ys=GAA+?srWc1K+a?wdqaSH&MKfrk z$eE8HqSO}VHrpw_wUBw7LzP6Sx1)_6>4rpy`AM~EDs@#7k%7D^NG)AWJ!*SQB`h7o zXzIV3o#De~t{8PSE#=i#?&pq^0tN#DUE+$oCv|0`-p^Z+d;miX;c=X*#L6$IOR3Ze z>M!SXA><W@8^K9BwcF-=ks<SpTx}3JyfqD21qA)IW6)@0d5@)ds?O~q@i4WE2WF&T zm|avi2P)*`BtvC^PS;UfG$(CtR!&-ob2k5lx>DEZ0xl(0JGXgV$cpHyJvvuh&#vvc zh&PR?8Z@(b(#h(jf#K%<hURLLBv%}fYov_1WBzlLl>OG}5ix^MY^&f_hmvr0RCZ-Q z!r)R8(@-Ud3+>3cTH&+sguW!QZMaxdPOZ6T;ml*}qv8{1svL>F<f^*$55bZ%Gt!U> zC$9)cDzIrJLqE9nRMpJfZU_SsB$?<)lGZw{G1aRiHMHyMDr*f&nEFxJ8k-PqnxJ-Q zyvmW1(T=<vG^D^By{W{j%1X<zeEAoz(7)$rio7_-)BE+r3Nj-(Cvsg~%cvsq=j=I{ zyv7BH^jI=OS4i47^@UyPsl-WtS|LViilYe&dtRty%lx=YLKYNN4o79}oWtn`fmM07 zo*H2jkz_~9W`&G|oFA*I!O360R@0F%%rB^0Y3%H&6P&wkU0H(?ieXeGf%K%R`g*90 zpdsbe>l_2AHYFjc?tWe+B5!<1Bp8Oot|?Y&4sZpV#!H3aFMM$v`C4S%C^Z3UbkYY6 zwd}7NAmVgfp&ipY=uc;ZaXh-}EyZWqZV%6oY45uC3;xg7aqr93=U&#>0zcDPS-dgo z1ZL>DN{l#9hEold1Gg+1Dw?4)4WU@fAd-qC_djGf8v^O^Vw`jZouWm$O`(jL<picj z!{+Vf=76OUW547lD%J2IYfUwWQOEY+SYP;2!oHZns=67Sz;x@=Qib2@25QM1dOui^ zuOXn`7UQHV=#kUWWJOh~RiL1(TC{~Gp)r?z3Q6tZ8P1SF8b4g&FLIjoA(LuFJ!r0# zgSFS{XvV6n#!gq!soopnrotIr;$2SaOy5S)qo?vS(07$no%X0uW+^?^e-+F(uPRUK z8ar8}HkgZu4D8r-wVdb)2ca8Iz!{htGqXXm+-L{~7u0j33mD_Zh15FJoRw}jRYsHb zU3eMV)8KS}IGMjBX809@a<P6*s1`*uTfaSSsu5d^7J+eOQ)YQCpK0F>D~n__OC~lu zJ2aW|*3_b?a^nTjBam)+2e2B3C)=A^p|U1djwcoLf<+@nnpDH)SIsh0>eY&Lsi~rr zCG#au1mpf@od8x)B)`u4)0Ca1U)qHsjJyP`gcc82+lc54Vc4RWoSX+R()pviWqxzO z0BVwo(lS9kcwXqyno=pLD@6*Iw<cq#Q*<j}6inj=D6he*0wdP-5<0V3s+K_swmG6} zv7=h&E^0dP(AG$3q?o+81hgjUk{_0ups<n*pR1gvwxo`F*14%z3gtUwrC|6aNpOF> zNbseOzRA%X(A+_PNjlWjjjEWWLCFwTkn-iGD5aqB<6@F7HRV`}sT;;;){)+;f_k(l zC0B?jtBTL?6{30Mr^lxd)1yi4Fu7aZwAE2_C#uG2ktBm_o1c8}S`y@l)lc1FKPjF@ zRu2l%R53R#`+{f?v1M<yDr5_e_GAvqwj>&7Zh9cL2G?>p-D>mVCo1b<63OHl_L3%W z$rbJ-Npl>K>uL^r^Ptx5fH$p4kv#|X+8uBYcS#DtTk^MsPh8h?vuw{MVZFt*1l523 zRKyT~<O<)}LmKBE*5TWatB%;JW!tY#HM}(UH3pPpD!)CXyM4MmthO6|H^tAesz)=t ze|#s5BHupU9khGE^0#4O$ru<GS~7M|)aqK)D$knmL99#&dqSjbrt9>OsxIa=(LJ)Y zAw5x*=CqkVc*3k{qt?Ym2`2J;8)mABk}DjE(zPRxr%uyTG^K-#b0Z?G@48DGuZH(T zn8b{u^6GFdKY7C~4>8fhcZkcOi2=OPC4_i@^*m|^*O<SgXO86pG)QyRfB|F6lgOQI z4J$c<)vd(7g{_Dy7q^YdX!H|FSVxs%;F6HGMzRExr<AAlV?{uLON5>KS|?ZIk}J+* zqqApEJ96`Z=S-HChW9PBY?xEk9qq|kC5T*HZ(%J#1y7Pet(HW9$*#Bqk$&@H&-HYN zl)dMM;c#Kydg|Kh<HvNDZkA%pptXB8dVcsOTM=9xzWwx6y*u|cyq$u^_IQ9tH1Ncb zw8@i3NA<>3n;z)<vOIg-leGd8RJAKS&6TuPb*eh9kHFITK54P=zOoE{!x}*`Wow0a zjsc}`b`)imZb(rNRqG_H0ePa#y%Zx$d1b+pQZ$sdE?)bg<jNA0wae37mRrQ+Y&}QR zG@y~n7^WT&VcR#6p1{7RMMOavF<EnYRK0e<CTV#WTvgXlO~xWo_G&DyF+wb>U<(z( zL{H+i7)^82(pkE({RY*jvgUP9vooosHg2j+iJkm1&CMy{c)mz6{FHx_q0T3*hWs)) z_QJHn!EFm&0Z+<C9+UI%_UmZbTB)T&WpCS2mdRPdHqUV{(dB@ule8k{1(63-8-e|7 zFwr8bwpk*r>{~;o(5?}#j6y?Ps-SF5GS&O#C$4C<_HZZr){v<sCK(69L=`>rjasQL zFGsFisgq=D?K{aLZ*+xSy;Q}OtLs>LRrBUesmc=n_+=+sQ}KxA<_Si_G^Ns-n=b5$ zbe$J)*;+B4Iy$t3Nd=j=u&GupvQkdmCdTlwg>|IfJdb2dT2fn0w9t#_Jh2;6Q@frw zRidQK>xOW;FZJ6AJa&G(Ta#wnhM_X#x_i;IIxk5swH;6hPw;%Chu&rdWs|3Z79{l7 z>rh*2W?dXtE7t;Zb(7xtipb(}BCIP{nwGLPSq~YWhmpKW-xdkxbw}k#lGQbw7J3pq zbGRFpMO9dH!;&|z5Hj`+!7Z4Rz?wVQ5;&*ZBK(}rJ*DPqc>MZIwkEMSVIRW~jWI`6 z8G04Cx?#y2my!&Vz4T(Oc|mLqdVAd?mf5NDRF)gQw`Yl4RLmB&tt`$`1TUd9!I28( z>KyJw5+zkt-?-8lc5(D8tkg|qD0`I4?j?pCik2wfMM!hMT|C=Xmr^DXZz!^@b9vR1 z0cnU#xyaU}lj!dqy`<C3o0Dj`fMlj?B+;JC1G6<%Xks>3Xet~RmWR^Z!G>|wrJe^H zM^(*9Zb+#)byu;gF>wWPmJCR9eWkN(S+<X<v-F@VNeY&*{zIwfO<~C=9dnh|8r7?b znxVP|)f)9=aIaJAc|j?Hc}Yye@Z=8m>m`;lzH^j@lfiL*zID|%*!4K(U20fqEBFCT z`qXdaWNUKZqFtF&AGc4>HE^m_=E;U+ic4Fpbdrd~Gq}|A#%Bd#-T@i@hJkfZTk6vg zn)+0>Ce^0DH!IT+8mvhX&y!zbw<2^oWV>dzK0DU*HJ(ZF2Z<B<L1G#+912_U4Z{57 zRT@VZyzC=MGLCwni437Y{gTXElN`BMh3<xrMKzU`A?m6Iz>}k=XRK<Awq%*?Yg8pM z3<9eMGLLlA%+T8-BudD74yqXzog*$Xd=Jb%LyV0{hU#x!HMJ<IWMCoWn+B4Ck|tG> z1X!k0Q&3ek*_aAPCVQ^BmZR~aN<?T%k?LwBDLv{15G+ZMNTY??o+TeWj#y@vJf<=L zA>!GtVf8?JKx=3m*Scx}+d?FZ^efE~SE@4Cr4c0@r|2P~C3xUZ$}35srs;u3%OQ(n z?E^AbOp2-p*}2ZC1Wjl}QW<0)++<@aIVrZ6=v56?{w-RCNPpiFNw$I{LG%Ku+D3X% z4dl5Yb1ye_YDu69X-Y@?0X6C>6U3FE8o|7+Qf5xG8@+BZM4pP0ep59oF>wL0sWYOb zX~>x28B{b!SLU=+x$sRzrLD=%7dj^Zo=gc}m5szFCm<P`o$7TW#R@p-kr$d(apWv> zA^bRpkf_kswem=V<P;>HULs`n_#tV+lbvzKLgz_i)HOVSMDnHt%RzNLa{-#7)pw~E zl2x$u_pT(%CFOB!7f<bt7ctcajpKzhu`4ke7LG-K?~5F%%^Vc;$t1&-B-&$$bii-L zF%_6POJCwLZ^+WkSxW7jR-0;R^()Rr({;8{9ZhmY`<~VP<BCXAJ#S5#M~Cxa9cfi! z3JKYcUn@X$zdG}*)|D7X;I-^kt}D^MX)UNGN~(G*MCpW1k7aJ^rVP<u1NXQ_vl{XY zx)Mk)_R)hS6R)Hm_yVqwm?w<0c`K5o1;i%$21yuVB^h;x9A)*iyejxIQ3WgyX~A=R zlVq4oMpwP%db>$)56_Qj@4EL3{?FIigICoN#T7(dS48Ss*_zBy9j_=&Y#CBK{<t9U zWa<_<#}><^CS4`y>|}p5ulhr5#WQ9{v=+l^T{+7(lx0PL=->g7v}czaS)V+*hjR3< zE6vCDIac9uhJGs!XGnYGidLN)dd-nN^5h!ndo};fBUy7qUZV@qT>0_%(45c}m;4*D ztHR?Bb!7+!Qcu~Iswb&K9!5opCfh1Rll^I^rpm1v%{6v~xh47W=(%Qmm0wf^T-CI| zB3^ODQ~S=N$F+3DtIN-WUrKU;^K-?c{A6nt?sS<`G($9PaM;d)s}S`ZmA-0NVJz@S zj9s{u=L3FA##tHPhRk#{wss+CuDMr^7CslyA5zO)>vTCk9nDqEDp%6qyK*aNt)#BF zdb0{q*V7fRGI}HzT1O+!Y!{7Zo?mZD@0Ga4FQRNsA|kpZezH#uD<^fQ5zpJ0K(VA3 zO1b$P(W|++VCh-R(YQNB>|8LZp3(`0ddfa|@rn^odPPRJA(rlQ+4n+re${LrPp#8Z zEtMUqoUZJu^)&m_aSljom7S)oBQjlG5bNfkjC8}iu)vc!DE0P&C3`MC%KPH4n#sOP zG_yZl5X&>rjMuVPh&QDMUg9TPlZyI{U%8s<Z#~p9q7!$f|JbSh=Xm%n>5&{88%9oV zuYJS<FG#O&MP;9wbF3I0IHm{Z$m%HjFFC3)_7K5xB*NVT8u1_J){2&d1FtW-i(R8v z<x$|7d(_;aB{6J8(GonJovWI9d*|>B$JBe}Av2_=+@a<ex{UGYe?=zhSGdN}A}gc< zQg06#VXLOE85+kkdBp{dDk-&6#|KhhJb06|XGA1!a`g%nlU^|DDSz8K5@GH^N!fEF zFQ%enj+dj!-+JIhbm9(arGiypnktx!o?70iM-0l`qt_HXn;DNs<Jj`)K^Y_SL3<LI z3+xj-Z%e}I+M0W(mG4HpE1}5&%1sez+(?s!oXZ|c^+S~hmGsOHStc+?otGU}I<e+T zMP|706wPQg!&>4BOqpRJwADHK&6}D62Ma41*8Ead1IQfJnv-jy=I59OFtJ~CD-6Z; z4U|JNXI+&o4J3X^oa+ik)g)4>gA6Hx?GG|wNfCn-du|<9bxT9Z;ru2_DY&k1t4sk# zOgj$}P94>iB25)m*mXFPOHHdR8kKd$gh;ZaJ^PT1mvw;~*i&Ft*0#{D(XGbB6-4e} zWC2Xl$8VggZB1Ca>Rb&+xSma9FX>xNR%UldqAdoL$R;ZV)q$k6kIG0Il%Tvkk<K** zrd(K2=BRC^tC=|pMvEaX#3YI9w@#4A$q0j|%%40Ee!^<T<FR!`RT-Q^*FsF<AHNQ+ z&w1`=z6fAKD-WLZ6OZp1n3FDs#&25zT0G5JR_KqgsZCO&DRdzh={en$L=!KtG<gbv zz3{b+FBvKFG;B)N5|iYWUrckBN~LKBgF?eyMC%rUTtQy<_gQ)%X9-@=Nm3VQ7ZKhy z>Nc3Xc%KK=m&hbzNnMnw#VPv&KUt<Y!lZ+)TGJ{~Gpk&|^(-~3jjMc{nn+lPO0M$2 z)h4*|8V`7+|Aj1xwjq5isrmvXd~^NEl9uL}pP2w5Bf-@|>bgc%+nQ4fDqg#B%}J<; zGN8HkO%(-063asGlB<aFO`&VmLp~08U@lc;5_32&l<m~!;H`4iJLQXMvARH%zx0Au z%StM`tFX0zd0fvzo>VJW0d(_JPiuU`OD9<*ad@_*Y`Vfzw-NIg2M2R7n$~MIZ<yA) z4_v8Mfq8Wi)zz$;6qsI2g3wf1+N4R_7U`PcWKM;kN7;KY4#~NSXtq?7^>nD+3lB2L zAetgf3>7}gUJz@FjLZGAR_jznkoE3*2yq>!>+rrpAZ=EYz88F2JDAaVc6$)rmM{nR zLRpU^BaI8r?Q??fDtSeWL03k^6-sTZ+RhTzS6pR-IS)DM(1T8v=`Bq=TJmpoIEj0_ zYg;3(c^H}stVfNd>e(bF1BWk(Nuw5ATbEbY-wV-d{}LTMkd$Si+PbZz4&lS%s*CJ_ za3vt2=>bqGOzLW3_4R;AD9#JEmRl+hdc^GELlH+0Nr+oWT+*bBG*_HBqUk_DYetdY zt*}O;R$gP*?QfnQ)7?Hj^sahMd85&a)Z&7B@K|k3Vg?7Ywn`IodmwC!ORjUpwKKJL zl@C{GO%@bS)q7#6>M0Zk)qe(A>+9Rb6?qGN&y47$PT0xI!x|8rcG1<xdR`DBa_H5g zJEwW=P?f7~Md~8yo&#)gqmnZZvU!T%v==mvnxe87rSWMBjYg|=mshx2Oe3m}GKDm~ z%<M|FQ=v)a3JA?Drr97&fCDBKLPEs$n^MgJp0AplimmyZmPXZPGNZf*uE7?il>|Mj zuf2fVFy<-c*J3t(U^b+d@Z__ifRp_4qC4Ybgyzb7PAnrT`X(JdKcCKiO{NQ;kHBg% zAz?lv^MKIjaTrJSM7eg9TS?|hZ@Ff1B~32y86eN2t>3btjZK?<x)U3Og;fKm{IUJR z@pw3}JBzLgjfb^eJorqG1U`}Y*@e2JTSp_czFo9-qh{1HP}HbjeX4kDt1!*v!Jc&D zXP;@@*%FI~%NdR8B%70v^!F|5$hk@TG*!~ncS%f@S<-GD$(fVdZSF+6(UlJgZLuO~ z&djQ!MarTxTDgt{t6g_+-+RWB&8gNWp=&X#Av1xJooVVxy+NJ>eq&ee6T;1hC(WIg zk>h4@i(Q499L@(vB3ecKeBfy_4k;$c6S;k0kp`vN3n87OSHvxR<T#?F?~v<~pA3%F zk^?%cNutLQb;CI0m9XP=$3aDsaYkO`3TIxVHEDx3BUi2@iK{caaxB8#8H=>WmGth& zYCLj<Gd~g;?#vc)ob>-SAbO9X^rz7;guiqAO0ZgpT?txX6=hGH6(kvOY^|hF%8)vY z2K5kSiqt{;+g0x&-R~FoY45uC<Lds?ySt~y^=i2|thSqXU%%V0?l-HC`0vf(-7fvO zSRS^!)#e`mzT7@Mq?^0+uJ`5JAKpi||MRP_K4*KH;&0yKw~y=epR2=j@A|Kwdz;0B z0C=BnmY*ILyWegOqLrKDgOb~)-7>xXxLS)#@bKjAA>E5TLu`Vs7n}R1W9PE}zFPqp zT|le7K7PM^Uaq$f=<JdpMicyMi=H1=zo+Ckqxh<~LGwVry-ObtpL*8`U{?6q?e-%s zy+bD^k@)L50bIA)XG8}5IGGKT@i>}IqG>XiP4RJ%-D>~a?K=HEU87Im{(1h({M&DS z>V3JAq<;Zi5!3s0lkU)YcgBMv#xq73>@>njrk9cMWDqO=4_XnvKhFR4M+4XF=0&!N z&0_mX7>~fj9O7s?9ZzQQa1<lL_>ZX#<=oCC#Lpb!mI#X*6)eVQrs~_x$HMya)B3PN zO>5cK<Dxu9snH4-&*K=?&Ft!ULjR|k`seSzoB#T=A(QF-Hx_Ac+3Z9+LXMhgC)yt? z+W(&Z<NJrl!*lCJyO8wIBGyr11sjf|Q8GRK6ELJ`7X&-6VHF{FPVPe1HV*e3YB>+F z$?@|t_ng}>dF%cbpH4H*!eHIXT84RxIj2YTkDa>(-Q3{qj9IUUECT!OI$iAkvO0Xa z`Lx)jcYjVlraMf&QzLh8RUCDzdBCHrFc>GH(3VuNpZ>>R)Uj%=Jz%B}Q%-A%LQcF~ zVf57|*2jg2kyA2?XTvC(jE9rKXgbB;Eu;9b_$@se*cd_Q(p1Qj4@Cx+h3lncO#fFA z$X~=#DN9rNsF5B^_oY%HRhxh&g=Hxk#G~<0`Db}RPgJYiduGPNOfQp#2Q&IVxS-6! zf^j9+_DhjXl)Z8;w({#B>(|mRL(hD3lRhqXi$nUu(`G5Qgp75oeFbc$8WB8Uo@>No z?N7DE<un6Zgx?%LW9z?$WpjVi{uYXJ<;*H7EZ2067-LtcETQFY-cPIj;itt8dx)mp z0B<pNB3)RwwG|>j$~b3b`^Dq0m~8|f6gb*u2u=?r=6xOU^kw3gfEGOQ%QE+skZ&B` z^2NQ=M7c0M@e8dc@hPOI^c1#TuT6=0K5E6AQ<I8ZEKb6=$mTTT#ro7X-wv2GwnNrx zq->mL^i@K=jI_hZCeNCiv2YseL?#p1G?Bp^MJpQkiMF~fzH2odO`>5e|5-k!|GL?4 zcZc*&Ok=#4j@Ll&KemqrGtu+KuF!~aMN#~>v}j}gd}KTs@$}4q*Mi|>!NN_sdh_Pf z!wDBh?c4QsV_f;Xc|Vd7yQn(4Z%@4je`ZumM}b>!Q~%e(x44qN60V-gD%q#Fd03bi z2(N^*r+QmDr@<q5PPHl9KQc|)@DP$VX~R#nNn5r|G;Jr>8&GgV&-+}>iC6I+V)^W| zV3CIyWe<RYWYdS>if+aQj8egj=2FNZL<L3kVi=0!EY17(7eY}zzJxx?b{PbJO8Kpq z%V35fU@od88wW0fUcsvUGRWEN|6K-C^S;@oa4Xq4y9|oPWiO~@?`E_xIph1DtxciL z8y78d3pnvC*l)TJa;uL;kMmsy!Tj(mHXmy%>jF9x>pjzi<uaI=ZoXcIC5xBo7ZADP zW%@;s!(d&!Our0<rOWh7pjf(0zW{;-%k&Fic3h^9hZj2P6))2-Aa<6^^b25GxJ<tY zZsp7L3!qpqtG<9+D1~dG3*c7rG%8jNmq0LGrw9IU$Yqc#+&{X2z!h!#wM8x9wx5yO z%bA{UdqcBu41e=aJ6O)c-aOCZ$@H72c3cLT7C~>GTEW`&&GX7$qGu7?r)PY6s_<pp zs>i0y)5x=O@$@8^Y3hGgUX5c^unl`wGM<8DDiIgKtBrR?&h{Ll;mO#`SgP2}pDw|U z%S8*XFge*V`^)@sU;Mc2HS3bs=U~V;Ke3pX9awWA1dH#uT|oF=KH<ckPk8gxif?mY z1U2&jcL~(kGUXD=h<VX+3CzlOzTdndiyyyx^VAA=z27`D<Hm4n$%YwVsCUImUxsUM z=R4p-h31{_i=bw+bh`+m1$*BY!3~?=@1G4fLN9{cQk=DVlODd=+<kL*$0lOfSMc|4 zvC~?-@y4WS_wFJ>SF+D=8T`sN8ZLyPaV>rk+zJ-s7sJi48ov~7Y!tW%V&?VuMNlhQ zkY5Hr^OI;7!OgrRzX)pYF>5Z^Nxukw#yy99b_9Go`zZmp9n7|DlU%?Zhk_lxOJP_1 zhV6wgG!Nw$LeBU|P>V$+VBU!}fnnZ!Ie{~tRC*b)D|)``QrI1e-$K0*f0pbQT?j+d zR^NqC!zT1imTGKqejx-4?jT$UyJPGM7DJc9@fjaghDVnr+YuK+)qKnHV#t-ear>q@ z;)CILc;<Px`tX!})#>_k{-K%PHP+S-_<*+f>QnC;U-ZC-$nn8k{5*-iT=hQT>*BZB zL!aWa>EbW%U*EUE$wR5~Yu;DA)qekkPyb&3_4twW(|5<;oj$XJVI}*5y8Hy9`n$iq zsxN{Qn%PIZEh!pkWGhL2rjg&<o>*Z?S;+_8VKaYZve^Od<njrEm#@-?B~cO==dY?n zEm1*rS=`_6()+B0eC{2#hsF9<sTa`@MP~6kYRr8~d=T|#vv@X`Ob57RI!tExz{JDi zpY$3WD1K~FN!sgpTJNU%UAkQDQN3;-w|F=TH6a>8jZf)fefV^{Uv77z_zn7l$zT>o zv*~CWjc};<L%QFi&%Nv3zizj8N@aNW^*hP$-6v3cw@VlM?Pl>|ohq00zI^%ihxgI{ z`7i(SRaS8y(#_%a?@x>M>hPSEQE|-n?dIv>L%Pc<F?}}KezRD%*+Ui_o*zY*#lMN~ zyB%&fi-(sVyf7Vzx>k-YDuKP@TENfJ+HO19Kl<Hvc`j%Oe=cZbhV7{${v}Vv*W1s? z-1d+F|IDqQ*7OhEY`k~x2WmPkbpaC&W6asG#r&FlODAZ`G4zlF3<IyTqH=c}gAbT> zChTzKhM|Y7v@vE`)bT;X&fx=Eo`Z+Xb|ZQ)*BQ|RjqV&fsN=CPp^gt4b%q``-xz$z zd}H8HKAhuC=%E~suahy)oeeu=$W6q9tzlw@9yZw+d{DpRkP+&+ka1_=pdQNb3_EPp z8T@&p4)kydhnHcf=fdrl#2h>178QpNnQn}GP|umr&*-^;rH1X31kE-RdZ;4Humff~ z!w#8h3_NJ6G4!BgTf%n>g6)PUZ2lh3amiU{avVLN-#K_#$C;E5Hnwo`;l`H4G>;5a z-8pth$C-!++a}=D!}{GsKAhj#y_b-2XW$`6*2IJ!D&0)jXAF8+$20N)$8~n^B^>t9 zl<9#CXT%OxVmW%iD#O7;F4;JG&|DjyG1oYD$nwt7&n$dM%d@BlwY&*EP@Nl@YHYAN z$I*j&j>8A^90w2SIU{-~&oRG;jXHx58FdC8vc`{0=)tDZM)W|Q8=rAsZfvTtp?I6H zgVlG=JYcGE@StUe!-uTzW0vV*$usznfoI@nRzBDr03P*_ql*zeRGnk{6kvWf?2u+R z5f4}38G6_<!{CEvIztb)H#TAOnQ(sR*a7{{!Otvuu<5gbdC13iIDE*lWy0=lhx8l+ z4_Rh7^?<3)m<O%zM(V)^w~6V_`WewfI-XGv8+QgDG~rC>f##Bvv#!@UdPvVP>R~;{ z-~*Of6Lz?X1<naL*E7iz(^@@Lu9>jI)prwmxR^8au*t^YLlzpgZaXV@z%s*#2OU;T z=;6|hp$9em^o;9ujvdhN92}kt7Ilt({-_75@#26XI<AK+HAd|7G7o1urhCYIW8h&O z&(=;Mxij#fwZ?=V&hiZX%yAD`YdG~_cig6DUAY<2!__&)J!Gyk@UVVo@B#gP&c)M| zfd{RzCiFnt1xDuK>N{f|*6(cj6g2E6^iW}E*dePN13!P%gIRuNS~CY5-HhmG<Q~v< zM(TmmjbjIO{ERK1Le@Es9y01C>Y<`;!VZ^g3_Yyp`1&Q}gB>#lAJTLTJZQd|(8Jlz zgdQs47<RbFsAi_MQ#jQbdPuu7@St`#p@%XY!wy?x+42d(hoa7~&rCg};Td=69xm5S z=;2lnZ1EP*b0+LFG7sx`6ZvqaW9R|%EgG;zb5Q9G_~A@%1`p<X1N?a-AF|X&1Gb0) zd_e6Pd{FHfdf=>M_#v~-1Rkv1MgzW-3AubW;D^mSMn7ce8F;v5q*?fY6^C&TT5;Gp zm*+(fT<;kBu!D^WK48T&!3SE*MiHy_0aHF=ITx_laqy5S$I;KNdpN@zxCb5LIeyS` z6S2Ls^TP)#H;j8o+cWT>8PCwe4zfnu1x$K&XntV&3_ob_8G1<HGw_hZtO-2av_3My z2Xy^Ne`r5s+|T$XU=*=^Q<yHG@C-h1xncNW2YC~GutA=Ihb*=H!!$vS$KlUTK3uis z<ilni#}7Et#cUlPu-3=y-e^d0mhd5EXW(aL9yHrI^`HysnBj*lwI=v*#qOLR2a3-5 zb)blUCMICYCFh;dbNG-M&(Q-`S`Hp|bm#aWA28p~IllAHwS-JLR%io)5BVc7!QKar zhUdIJGGw<$LdtH6`RyUyhkDL38l6-7(K)r}$N2{g{D_UOA;HhNUp6xBmj$)_*yOsP zJ(!#``jfNQoas5~PtR%osmc7Ib`1HOasg`&e`6{nIKL|xZgw*}quXQtglRb0<LHc& z$~ZnNxN(TNPdDjqwG27b#C-P%2Dm*(PZCq%JxfoF)6Nh*`P)_R<I`q&SZz1AciZLD zL%KOE#DDd!|N6PNSv;h@>t0X4nQ6cEvD(XK)4SgF$HjV|UiApl?S8r40Y+p8u%Gro z{B@IGg1`0Us_RLo$)@v3mpi?#o}hZJcIoo4-94iy^ii8TO*&1s@WFbqxqn*RixK-? z{CDS*PLpMN`*F1vmPj8S56^ZB`?d>;^}VaEBcXG+3yr?k=@jV{IaI7dJCAf8={!<* z@21O|*PJ!B57-2lqjg<jJFv)|PL)oT<A#0p?4<Kb=atSYjqfsb)fO8_T?W0u46=Rm zGQcdZ>xwCZI#oJVZ0kzrmCh@jS6pUsT?Ta-^wu)S`tjJd=Wty|^!?gSkxr4S`#zmV zI*)W7se5m*%bG50x~%E4M!c8e@_q>FNQ2jC0?eelURl23(?!t!ZKzJGPOCQFvg>@+ z`Kt5PdEc(-a<I$6KUNOfA37CczTWl5d=`MHOPw~IHVyVFI^PtWuhHdH@rfUuHk~%u z8?Sv*Or0}pTc1zVIIg?v%Xv;S==@SJr|G<FJQJ()OXnB7C1HJXS?8C|FP&eqvj(g0 z@Zy-lC*?@M+x*NzoE7Nhn8Hn(-tBLf+Xr#}p**H=x7z=9yH0;k*9g?l-+wp%^=D6q z5q5Z;p4UD5V>COzbbjglays&`%ZDx>Y8`3k$1v6>)^u>yv!zbV?DW>oDxFm<zpd0+ zrL#(B6}-XfEfH+bMd|?Uc0g(zl+#JtNqRh)x-*S(n17cRU0QT$!M<JKEiG(M0O<g& z^RSaN{}M}Qn$9$`cz0>hrA58_$CwCrrtxbUJS}YZ%^N?ZyMyz%r`?I!iD~)dO=p$P zDxFm@`R)>-O9cBwXmALPw|P>Bo$f==O*_py%_}|<+vT^*6W|zryBzCstjjUu2dKN+ z;#ynm58Cjjb%&{cgt^nU&RbDpOzb?=d8n&>)laYfVREI$Ap)MNZHJgHy+3DG=Oy+o zj<)X+cdG6>udes$Rj=+F=$&&q=PX{6b8v9O;{JY@-Y*U~HRp5hustl+`9TXfy&)My zS2*8kd#C&?9u2N~9~SD5M!0JGd%9b!*YdE4{%AUlX4BC$8jT0>G?`qzw5}Wu5e@Ov z(?Rp<xe-wmYsj}daT-PMdSGZdZ{v7g#WL&5Js1u80Qi(H)`w4d$41jW?bq<C_mJ-Q z3vqq#hsA35?&Y9~cYl9ctXGHUcVFWtoBir8-Mw2qJnpu?uQvDZ9@mRynw>}S<;%A} zypR6-|N82y&%K=7?I*<fkS=f}?CAuI_vvQ&>0z<^?IxS;+@uHgxwm>V%-^@`Cvi+m zG)e~j!C*EVC!;ut$B^!B_3`6s`LsUZft}%`pA1LJ|3EqY_;I_#SsIggm`wY#Suz^n z|Bt3KglYAV-X6BMV0yn>JmBvS;$c6TPDk-Ho=rx>S%UV{^$HPB?`}Ul-NPw}N;DV@ zk_b412`;+oLB_{*`cEh**P}nK?mww#bo_$N$ob}w?)Df_@G%La9{1@c-K~~2R;jrz zlVs8#2tlT!Xgq*6R2Q(Cjg>#}N-j$@9S)-YAR5M#!8n?TG+?qsgE;QTlj&$OfNs+S zl({PLa5SC_hcPl>I-cMkMqPeR?^aI_x-u}x_rimF&$5iCv;H(mkle{&8b!J+qhWtM zj6n<RvwxmsNhagjus<BfqhvH052s^;EHQl8kCO3p02jo=Sv)f863=GC;V=;`CX;Al z_KR7UUpM}(G9JUhvnY;7)6po3gu|5#f>WmPWHcSe1Bf>sfs%B}csLmJksRZ}WSC5# zm_d_S6!nDzh1kOwgw2u+qS*whp^X7_K|_WVGD{*WN{M0YcX{&s8N=Yy*#HFv&K*WW zO_kBOKb<I2B#1YDG)=~{(R45#n`%ik8&8E4@nDod?3qXiL)Hu?<76;E>o5s?SjQbl zDJrW<eKkg-QGYm?A_GRqfG8IEu6hJ6KvJFlpxQ{2M93ylPKM*zB!ZJniG!p=3&RK| z8jNH7mr0ojr7Ri8sI#cFgP|BD?M+fJ;509=qhZvCHKNgA5aZtmBAALKAT%3B5h{f! zk<%Fd$rTY*8A*j7gGLf3BE-DH#<RFToX+A1t;Dmec{c7uXk>;#EP-^>K>}s%1@Sx% zk0KC8r5{8v$#6CnX|BqFzMc4^(|rsq)5!$m_H<|}8&ML$RMB`c8O%@_ve^>nk6%v4 z7<@-4$jB*(TvLGJdRjk8hO=ou#v~+xe`mvKq&Xs*^~ad4Oo#bDl`(fVnIwG?=x8{a zV5VWp5mXqA*{HI!@oWm=L`C9?pqNjO4J9ZwwH%QizC*+?vDqwB&%-XtBI;!nO{cSw z7(<5Wm}-xB)ISvu@o!22!H|I&9_$i}K_s4-<|CsRLl|6z;VZ`UB|-T$XoJL?;dkQ% zlXEdGs_hocN-89(@0w&d#sqVS$>jhuvScW_t|ShI->iI$U@1{flrcw8!1Q&5c^YPV zk*SIdVXJ72VLHKBhM5Nbwm}z^({VH&Vemp^kh^tsfg<TTJ)}QlzP-Yn*Y|`i79$Tr zKO10%F&<#9N%e^)eKn^>Vt$bq8jPPa{1&BYIGRmG31wwA9%4;|@pc>!CzCN8Y8-h6 zV&s`5h|XjN7shDGzRskFn`fNfhkCoLFi-QrNT?JcfD!6pB+8Fod*GrM1HFt#CzX(1 zSR@TlxN%8>EH#y5%vn%;AkRQFgCr6FtolHw0qi&tquO+e!d*wFd_c1CQ~flHlUaW< zoMOaAlm~I7jZdODBfh8qsWuV<q2Oi<z`<Y)2XY^cqdsy6<!OvGjk5a2<(bT85vDho zYNHpTNLjn+-_w75|L}NtE?PJ{SSLmXVWNRK=?p`Qh`w5XP-4U?CQm68@=1S;@fg#3 zETF_hkZS?2U^X0KUV>o|^I@Y^u%tk#!OD4tf8x>_V#pc4ZDKXBON{XtPKifY&`-y# z>Y%&1Bp6dh7@uNnKwyR(PeGSS62TtH3{$#cgem=aAf|kV^ucNwb$S?K;fP^gjPTY5 zfib>ar;FWRR)<eFpBC7i_;cFMST0IIlq8tEVy=Su?^qklXZ?Kf{IZZfNrf5TsE=`O zgbDC?JT<PNqXCxCGmM9@S`4KoX8Rx{m_3f7A(rWrNbKEMNmaErvU?>fUnIs@{*o;Y zt*W5pP%RWGKrvfVO=bfOZ39yxA;Uyr9!_J7;c!`z6j+yII}!UevuqAzDa9|?fXZDn z-`u2+3+ytcKRj*P*p7;(Vu1%1sIaJ#GktQ5@;i$YEPeBTDoRZzaUW%36iqRqjl`;( z<rik&*n30LU{422R57(Pq!*?S!_jCgwgbh&p-y_C{`xPySPk@%L1NIJO=2XHs0zy5 z4?8Dhy9K2P8#Tiu!p1VjdQl%tN{q!ggpqEH^c<MyGh#}FaUToO5%yeSOsTE1=V9?% zdMv)i^+EkITcrRdCa3ihMhRsk!ba7okBJ=?6(el2N1{YCHO5JQFrGz|1UvVrR3ow1 zW-vxWIYb8iTiU{yj7<ehdWTr_VA_frH`9!f^s!KUsjk^JC4Qd|M_35=G401x0~=9s zEUGh;MQl}N>oBnqm0+oARt1}+XaZS_r2}S_mL?IZh;+&4bH1#Cb&^Qm5&SB)rL#R} zHCeDBP5!_SxhBzMHpWeY2o(ma=)n|8!8DnnSWhOxZOEQ@JQ3?KK7+&f46BW>l)^BG zP3=0O<i*w~Nb?CCRwjLHz>Z+?AvO)g^i(Y(qrSRNIhjRB8st5;HZcdCV!&cb43qwB zgt26n;3Bc?GAn^;5i(2U6)H`=ZA#pJN$-AI?aeoV9HbDN)mZIL;cMK+z!p2z0>lzk z4a{QuSoI{yXoevHJKQEYFh54E#cmbqGwKzlXl6Yyv&Z&igq<9*`D0sBvr#xtl|o4x zRMr7@Tm}<yV+$K{BHX$ZqZ#>wI+3SKGQ)NZM8tgz>~2n{V)Ko;B#y*_a3pqhu(^Oj zA++WRBdP~xdl<ZD*aQ_fldMLX@|AMES=tPvES4BZSqOotw^mHC6{!p>nABp-L6MnE zac2<ZOw{pLhM317H?WT+!fmFG)q@yWvb#kXvW?V#+Wtem_f<Tc)a1;<jVYx1N1=?_ z7~Op{P{uM)l)cmcD~L93eW-t|ILmv(!u~%kcDNgj&uDF*cFXkk<MGgveWSNm@@YEw zO|Zg*mlI3{aBD?R1(ZW|@_EVx+^OrlLiAL9;{$qkTZJjTif0|Ra~!w%-jCx#yE zA)pTt{sni5&<R5(Xm<E@voEcc^-Lk|ePY!OHdyJ4`c8Gw_h9ksydsE=G|aVP++{}& zVUG|w#47@XfZI__#nu2yp)o5UskoUg(rb*YG#3O-hO9`YhaVUFZ#K`K%`w3}xnzQZ zjXR)XS|M^-nO5M2F!_V9<)WYh;BIk(*#hS7Vl8UYWPt4x@jHyRLo9d;RwiPhi+I4C zLouzGA$zOM@$GuMF{VmAS4^<EHGuyyY+?r*TYyvm6i@BXNda8k$0z~a;EQM`hFj(e zq(_41E3oFmyaO?hMfUM=0{24)SpQ+ZDei8-Vm1np!L!62^@mR^Ig@0DDnE*5ST$x9 zp14B0`x|2(0rQJ{8@R1!nsZ}^afHod6ccQrVYVSwb|xLLeV8DNuy+O_A~Bn*c0%D@ zta?rmx2MIrVj_0C#SMAt1aPE(lraQLQf#13v*%#McAi-R+{Zy}7(gRzs)-qwSpwY9 z5VJANXORn7q1Z@(6^E%X)D!>}409$dxUeur07U<ksN-G={i9?8wuobtM?B&X!O6H& zV@d_A=f!*gl?FTY@Jj)GY_da33{+T+VESf5zi8%IlRqXlkR5k0C)j|SWvQz0hXGEE zBe+F8tpT^-u_!E?41{NR_zG(T%$f(FV}42qd+~4s6oY)2aShBndD?)GJ?h~e@9cp% z(1qc#pNP^+<1KEzk=v3d_NY?WHOH(1TMuIQ+%z6Wc*Y919I!UR<1g4T&+No2A07*V zG($k48Qc!HnloAMeAf#<9%>m*aQg<g)W!5a8>^J?<3Tw3N0DO)=OE13-$Tk`X=&60 zg9XNZ+`>bV!A%fcV<>>QM~oyAr4mzYcn9Ngbpf33Z=N2-qjbg<Wla^th=;W!Oo3_4 zMBitK$#MbvuDG#{2NcB9e-qq8G-LttBogz&fmlDHu$XBh-LPyF**8A!^j5R}kL_c@ ze8?~3Q=B7|IoKUJkF>?@P~&q>xKoJo0sX`+d)&(~5BS)VsZCf6@$0pRypOOp#^M!9 zV~~d>Y2NEQN$SHpc#N%2YPhNHVakKuH%yDL=D_Yq!Cq2?dq!A3<L(m5{IuY1k5>4| zTIuEiPaT`oXz-?gzE;A{BbMrzo?xAV>0qQu1L0^5Q04%5U<r?T;qg7OPQ>3u=Cau! z#KVRyrc~LMEG7!(WWZxC*e(zYHawF6Ep2K*@%*n|HejDo%*I73W+v9=!<gsLKd^Vc zBEVD{Bd55_1#jW+qu)#cR8dp`?7#`<VQZ!!J5VsNP(v87zCbK&1ekC3DV}RKtetBM zFv6}mrt^4~My!kV?7#!C>h|-AKkj5;35aL(CU|^HJTZ;=0uul`v0`5ld)x>z<_CoW zU>M16j-ak4;wEx+LHsUz6vcxA*d!I5gz%!&iN~nac_5~7L;&&VO>P0K2`6|eI+-EL z5$?f&9aBJzgJO)0#Yz}YOB4)*$0ttk2pl$OaJLjKRVz@i638|f>$xBqqtdGn-e@8q zEppC2!;XDhtbwsnhzSuq2+(TMOL5~HyX)U<?!LLZGlk!wAXe#sr<>IWB=A%yrVY4L zDQ*$KzNX;;;|nHx*c=pl&q%d`O#$pj)Yqc)2KQ=nu>Gz`J|0xje?>O1nG;{O0wufV ze^}!C%dUOqf{$q1p4a>a6TQUd_8+c&gS_XZuZvx_&uxnPMF-!U=zl=Umpzh|=J-WN zyOcSCezD6Jo6`12oyA;i(5u}3$DF<)%`cd^O``wN3*BWqpk(E8$$`h#ylGdiW^u>P z`wVZ!;0Y=mDj~MT^<7zYQ(FA{X@?TKSTSyF;*ojWx0>PH3d63fxO;)SbGZMH#}4pB zt9UBVbbk^z?r|e;jF*GN#<aMqdWX{2)_j8VXXs8ey&EH*z{z)Ihq#t)J)3TAnMm4@ zD%fqWKS_<J;_bxj8GYRL5zmfMmUvDgCpE&UCgOQx@zOo+PT^%b14;4DD{+e0y5=|D z@k$BqLy7(U?1AWl^GUQDa0R3rl&uS3-DFn!mS`8GErMNl8~p-UUw*I=)q>}*TZlMr zBA&_`j&{JK`grgWj~n997d%^GC~J5mkv*8=sjA&%n}-GD+X(!tV*md6kQQ+FKbXBL z<^=`LKkK(Kgx8ypzO9;$g>`|)TbR4NGLsj*r3vPKL;lKm8=nXAPWo-+_8)M@)<R;x zrdXRF0&g&Ou?O6gnb{Z*yc*_Wct#jMHZCe{md+*5E5G&rH<~TJstD9Ckm0*;_Wqmh zdF=unUiFf+h4gQLfY~+|k+OB~8}XN$TCe(M^_z^+;jLf#CUX6{BOZBHUM*6l72;J( z!?rbj=Dy%18gay5^=5o2l}5R6PBz9j|C)H}oFvSzGQTZbnK`^UKbdJD6t#!9S81r) z!^AmRlvb*@mg7Z*s^$*NIpa}8;Z3DRm8sqo8FRI2u!eqhwt8b6N?rBFxERy?4G!dZ zE6)9~8ytk2oz{v6Z^lC$PMe+crA*y%wvw`N8sf7<c=uC$+ydv};uUD)WFBw-<IPgM z=`T+CKYnq<a3fQEN=JFs9BZEHxqZP)!1WC%4ulq`%i>^$AvohBHgv^8cKTx+<%&~E zao8eGY{7Z0C?;$PBhE*`p(Z$3WjMlH%{ZFTynl!jKX6ur_<R74lNwsR_-lH+>arQ* zw3oSfRFv7f!KCP|N4mk(`~oP~n>D^M!lpwm5avH3=ep~)w-#x`d3Kjy!%U~vT)Myi zXhYR;h&Hjl+%g8v@9``dBYoMEF)`;hoI`Zkq>uJaVixNQpN@g|P4=)TBi>|zd>Ny4 z-5s+FVEso3wxOn7cygOI68@l(zI58lh573XH793N9OfpzHiEP6u<wO`r@LhMJkseU z2k}8qoEDA4J#oGlKGkD9BN%@}oTZ5~a>a?PGaUa{uuVp<0&ex@V8MBr^;aI*N34`L z$He<(WJ^@p5jE|f8a4;Tx53EqpQdLEaU?5wug!+^PpR?K;x*%3#aM~5;ZnvIP7q(5 z6t7Xv@RghFGu(z9;3j;FgIs$n!NBCzhPjw_?b*o%&~C8yxB%9T#;mu*+f>U?z3V2n zek<oxyiCuY;Zt1NAM=QWyFc_#)9Tz{lKJ`>)f;bKIiLFF<&_g^QaR2}>2G4zC3^nh z>9E>vde@(OYkW=V8b@?r^&Zl@)zd@oTAUK`X?6bzKfdbyp6;G+kN-9Ja{T%2@nnJ9 zk1yZ7-1n~k`nk7RJfywro;+>)CfoAB5wm>P179XOeO@F#R8V~J03V#gXAzR@tY_u< z)(Jj?GR0@Raqz47IG6ZxDVrV*2l$Yg__mZdLm79$vv0Dn`4P^i7hg2O=PU=}R{2$L zx7z=9yH0;k*XaAt-+wp%_2=FfsI<7h-=+8XxTlclustl+DqVc<4$5rTPl7T&!XQ5B zg42uf=|OyvMa;g{nJp$0;=_UXTpB(RGEomk$8i`q&KBR&!KugisHO4fXB>Gwz;{6L z5g2^s6`zVSe~%o;=woew)Bkb&FK&Tn&pPi9D}0yqc8f#)7l-W*QOZujP=CC+JO1=Y zkY1_$EWhmc?Q-$qX}#DzXKk(1&HVu`L9_UN;pzcu-9gLa4{<s>4hM<E=VoU(ms}j! z{3%_m51(%L%k2)bAriuJ`~3nRue$Ex%!qeCF4p@)y109HT9>^0`rSX$_4;dg_Gx)| z!Z&*H%e%Dq<;%A}ypNW{ufF=+%YAse+<r>;nXONLEExat8Q$ryV9dJe{rzdNULBsL zBmQ-}y;Hw6R?u2LntEtWEh`?HJC)tKjTL-q@Hi-u#>B^AmH*)+k}4VJVggeAYORXG zj}Ny1DTd%Q>I1yDc?aH^gUta8ZXm}>U7%HwAM49ikUKmQ*c#Ad`~5$cr~>KcpeAGX z@ov-T(!9y)yHTptwjDj*_+UMin<&lOEWer3yxsE0gc`Pc^cj<($YHkXk}sN>pF<=Q z4$^zN{HO(4`jtV$>G=egd^}Oi39KA1s21g^@pF$Vrg-Mi3Z?cGj-ZH-yNl0`V`>v+ z-(s`EBtgtx`Z6AWrpy~DDACha&SEKf1v!0YJ^SRkEkwZ{6TZ4+fS?}@#A3lpIvFnE z{W{+QB?@NW#6Eo=+Y(!ZP2S$Dp6jcKw)9G$k~ZpS+w#eC=7XZqc!A$)A*6>AKl2Ya zH_?b#pw1_bnt)k<(VjG(ojqvfXAu@m5XGm-mf0fJ@O7Sw@+zJLR{n>jq&2c|bHSRm zB33G5B)*q!jah8J)*z2#F__vMuv(G`_!tX55shy*j)%A>Q8O=Wu`5)d=Q$cyamLxB zX*vp`yew;9@3L*b{B9x1jb;S0@|7OG!(KzWOE*jY43&smg?4zQ0JoCH;`X8Vgjo&K zB2m77csxAALwrhS0gP|SCHRWl%Rg%XAFW$iAr2@bhG5!Gf-JphLZBbIvSe1p#{fj< zu61IEm}QHnzpM_QZayurtMcd6!9)%nvD_pi>0&*RH6I>ePIBu;3r;h2Y_;HlKy#~8 z`p42?0>bd<v}XuYEiibOGO!&t8hy2lc&8ceOQ|(+t17S#fO&2d2d-#(>H)N3SShHk z4!||PRj)=B_XDivT7~m0Zcy7M%`ej$bIP_!46G)jHSgn#dkeSADlANv);L7mQtTJh zW4ZkKX?<8du2b`(zI=`{JFQB^3FWo+XkNy-`Q|2lT;MiH`oqgTE0e;NI*>K8Z9l9g z5?QM25w=y@Jj@w(7i}A~JG1GOfgaj5WQ>niopddc!E{^2Lw^x7ek;kg-GX7l#Rp}p z@HYDDB5T163f<)Q`szq(c%<39W9`^zJKUSr6yrX+la@s-x*6UO9VcTNd+8Q$W0UBl z*U-4QXI_@(Zrn?ocIh*+oe$wo)!x*ila)*j`ljq)qoO+#P9hdG>Rm@P*u1J2bkZd@ z1x>cmO^1*dP86Hh;DXceors&e=~jqrX5kLXS~lNYXBz+)1KYIk6x5Q*w1e(ut2lMk z+@y=PWm_pu)wZNzNYLDD^RfwnlekqaH=PrS$-iN5(@BMLb^Z6W`G8i^Q-^hRNvkf| zP!KA&@?O$cC%+bK_PhBiU8jZv58CLbo2c0iURI!PaGNpfb@@J?X;avCRa`KUaB*5e z@LZZK=%t&^1^t8}nid)Ju5=p>HZNM{KDq?QyuI&~tA$NA=UrqW{^)^i>J)aeseQ~1 zx|!VErdzo8C68b_O{~n`baYZlZyhqT^zpd%dfZyz(B<a2WsaMhaSQU*N!sEz>!qyW zb*@xCDSsV_Pv72)H(2np4c^_tn_lu28TD<ZzuLX;w8mRGc-g0q&-hMpBmrL2n#JRR zcyUbp=Jxl+ZneMz`goP++yC+N*FWC8`>#Kyn>#$lx7~bwBc9&h?f)D0?DE@2`FlWv zev-uE{L^GS98B;*M?7F<c%x^~pG7zVbeiBeP`nQ|!Q(FUeWC=1u4>Jk-Y)uKvD&@c z;$61IdW{!|j;{&f-J<1ovtOZ)?{?|O#S(8xtv2`X_S=W_UEYQL%d1BJ`#*_SjaCnj zs5!T5@p6oK>FseLUV!Q?A07oW++V!kikG8$=|A!O;%2eNPgna}`Cr#~iz)qbCI8-U z(|v!n{nO$RFHbE$tu}bq?oW@u-Tw(M=dJ&Qsk_+YtluiQtnuh%qrbafua?<e+WN3! z_6PYI!G3(uy1PHztydp(`99(WF;zdja`x#@yT#^?f|mVdc>RUi3Oq#&%}kfuCt>E1 zh?a7NZ2IN*v3ObVE`1bE-z>8xH&5%e@dd)8c>oIU=9ajXKSUm(rev-;{SZ%)K5bTi ze@bs37t7!9rkwV-aZ7u(6-h~-IU6R&_v8NazX;QQ*rki#?zaEPUibX4+T`!=!TcXr z_fJ_H_?>t=aJ$4SjhlmLHUG)s<7$l;^h7C1VNmgsU-n<Htaxz`Ki_Ykc6j;_MMk_X jy1Co#@MF{)ym+`2jb*(?H`+wjpkLnDM9(1@ir@bSL&~nD diff --git a/tests/golden/run/all_static.json b/tests/golden/run/all_static.json deleted file mode 100644 index d4a52f1c..00000000 --- a/tests/golden/run/all_static.json +++ /dev/null @@ -1,9097 +0,0 @@ -{ - "overall_score_label": "5/10", - "sections": [ - { - "title": "CLONE DETECTION", - "score_label": "2/10", - "status": "High duplication - significant refactoring recommended", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Clone Pairs", - "value": "1154" - }, - { - "label": "Clone Ratio", - "value": "0.8" - } - ], - "distribution": [ - { - "label": "Type-1 (Exact)", - "percent": 0.33362218370883884, - "count": 385 - }, - { - "label": "Type-2 (Renamed)", - "percent": 0.2668977469670711, - "count": 308 - }, - { - "label": "Type-3 (Near-miss)", - "percent": 0.39948006932409014, - "count": 461 - } - ], - "issues": [ - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int32.go::Int32KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int.go::IntKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64KeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::NewInt64 \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e int32.go::Int32KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e int32.go::NewInt32", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int32.go::Int32.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int.go::Int.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int32.go::Int32.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int.go::Int.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32KeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32KeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::NewInt32 \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::NewInt32 \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e int32.go::Int32.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e int32.go::Int32.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int.go::Int.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int32.go::Int32.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int.go::NewInt", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int32.go::NewInt32", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e int32.go::Int32.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - } - ], - "score": 0.24822695035460995 - }, - { - "title": "COMPLEXITY", - "score_label": "8/10", - "status": "Good complexity - functions have reasonable complexity", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Avg Complexity", - "value": "2.0" - }, - { - "label": "Max Complexity", - "value": "17" - }, - { - "label": "Total Complexity", - "value": "281" - }, - { - "label": "Cognitive Total", - "value": "19" - }, - { - "label": "Decision Points", - "value": "140" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.9290780141843972, - "count": 131 - }, - { - "label": "Moderate (6-10)", - "percent": 0.06382978723404255, - "count": 9 - }, - { - "label": "Complex (11-20)", - "percent": 0.0070921985815602835, - "count": 1 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "CC=17 | Cog=52 | Nest=3", - "severity": "poor" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=4 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "CC=4 | Cog=4 | Nest=2", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.8 - }, - { - "title": "COMMENTS", - "score_label": "6/10", - "status": "Fair comment quality - consider improving placement", - "metrics": [ - { - "label": "Total Comments", - "value": "301" - }, - { - "label": "Good Comments", - "value": "250" - }, - { - "label": "Bad Comments", - "value": "51" - }, - { - "label": "Doc Coverage", - "value": "70.0%" - }, - { - "label": "Good Ratio", - "value": "58.8%" - }, - { - "label": "Total Functions", - "value": "147" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 0.7619047619047619, - "count": 112 - }, - { - "label": "Undocumented", - "percent": 0.23809523809523808, - "count": 35 - } - ], - "issues": [ - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\tsize int\n\t\tminStringLen int\n\t\tmaxStringLen int\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - } - ], - "score": 0.5881985815602837 - }, - { - "title": "HALSTEAD", - "score_label": "3/10", - "status": "Low Halstead complexity - well-structured code", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Distinct Operators (n1)", - "value": "54" - }, - { - "label": "Distinct Operands (n2)", - "value": "41" - }, - { - "label": "Total Operators (N1)", - "value": "96" - }, - { - "label": "Total Operands (N2)", - "value": "209" - }, - { - "label": "Vocabulary", - "value": "95" - }, - { - "label": "Volume", - "value": "2243.6" - }, - { - "label": "Difficulty", - "value": "132.6" - }, - { - "label": "Effort", - "value": "714652.1" - }, - { - "label": "Est. Bugs", - "value": "0.7" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 0.8156028368794326, - "count": 115 - }, - { - "label": "Medium (101-1000)", - "percent": 0.1773049645390071, - "count": 25 - }, - { - "label": "High (1001-5000)", - "percent": 0.0070921985815602835, - "count": 1 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "effort=72439.0 | vol=1530.0 | bugs=0.5", - "severity": "poor" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "effort=37694.5 | vol=689.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "effort=35426.9 | vol=622.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "effort=23594.7 | vol=512.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "effort=23264.8 | vol=541.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "effort=18401.0 | vol=737.0 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "effort=16400.9 | vol=660.6 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "effort=11571.0 | vol=555.4 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "effort=9773.4 | vol=498.3 | bugs=0.2", - "severity": "good" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "effort=9370.1 | vol=369.9 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "effort=8517.0 | vol=346.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "effort=4707.8 | vol=239.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "effort=4479.5 | vol=255.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "effort=3707.3 | vol=255.2 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "effort=3697.7 | vol=227.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "effort=2905.1 | vol=223.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "effort=1949.4 | vol=200.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "effort=1875.5 | vol=205.1 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "effort=1796.0 | vol=146.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "effort=1775.6 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "effort=1772.1 | vol=182.8 | bugs=0.1", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "effort=1135.2 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "effort=791.2 | vol=137.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "effort=491.7 | vol=124.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "effort=483.5 | vol=150.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "effort=479.2 | vol=89.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "effort=445.4 | vol=93.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "effort=395.3 | vol=109.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "effort=364.6 | vol=78.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "effort=352.7 | vol=72.3 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "effort=299.9 | vol=91.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "effort=293.6 | vol=77.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "effort=291.3 | vol=92.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "effort=269.5 | vol=58.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "effort=244.7 | vol=56.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "effort=207.2 | vol=66.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "effort=181.6 | vol=68.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "effort=168.3 | vol=64.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "effort=147.9 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "effort=115.1 | vol=51.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=109.6 | vol=46.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "effort=107.5 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "effort=92.0 | vol=50.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "effort=77.7 | vol=46.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "effort=64.8 | vol=41.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "effort=41.1 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "effort=40.7 | vol=23.3 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=40.0 | vol=30.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "effort=36.2 | vol=31.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "effort=15.5 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "effort=10.9 | vol=18.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "title": "COHESION", - "score_label": "3/10", - "status": "Good overall cohesion with room for improvement", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "LCOM Score", - "value": "0.7" - }, - { - "label": "Cohesion Score", - "value": "0.3" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.9285714285714286, - "count": 130 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.07142857142857142, - "count": 10 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Len", - "location": "int32.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "0.5", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.3321315364073984 - }, - { - "title": "IMPORTS", - "score_label": "Info", - "status": "Found 7 unique imports", - "metrics": [ - { - "label": "Unique Imports", - "value": "7" - }, - { - "label": "Total Files", - "value": "10" - } - ], - "issues": [ - { - "name": "reflect", - "location": "", - "value": "2", - "severity": "info" - }, - { - "name": "testing", - "location": "", - "value": "2", - "severity": "info" - }, - { - "name": "fmt", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "math/rand", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "cmp", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "slices", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "k8s.io/apimachinery/pkg/util/sets", - "location": "", - "value": "1", - "severity": "info" - } - ], - "score": -1 - }, - { - "title": "COMPOSITION", - "score_label": "Info", - "status": "File composition analysis completed", - "metrics": [ - { - "label": "Total Files", - "value": "10" - }, - { - "label": "Source Files", - "value": "10" - }, - { - "label": "Source %", - "value": "100.0%" - } - ], - "distribution": [ - { - "label": "source", - "percent": 1, - "count": 10 - } - ], - "issues": [], - "score": -1 - } - ], - "overall_score": 0.45371141366445844 -} diff --git a/tests/golden/run/all_static.yaml b/tests/golden/run/all_static.yaml deleted file mode 100644 index e7d918bd..00000000 --- a/tests/golden/run/all_static.yaml +++ /dev/null @@ -1,11886 +0,0 @@ -total_functions: 141 -total_clone_pairs: 1154 -clone_ratio: 0.75177304964539 -clone_type_distribution: - Type-1: 385 - Type-2: 308 - Type-3: 461 -clone_pairs: - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: int64.go::NewInt64 - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: int32.go::Int32KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: int64.go::Int64KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: int.go::IntKeySet - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Has - func_b: string.go::String.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64KeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::NewInt64 - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.PopAny - func_b: string.go::String.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.PopAny - func_b: int64.go::Int64.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.UnsortedList - func_b: string.go::String.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.UnsortedList - func_b: int64.go::Int64.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::IntKeySet - func_b: int64.go::Int64KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Clone - func_b: string.go::String.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Clone - func_b: int64.go::Int64.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::IntKeySet - func_b: int32.go::Int32KeySet - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::IntKeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::NewInt - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::NewInt - func_b: int64.go::NewInt64 - similarity: 1 - clone_type: Type-1 - - func_a: int.go::NewInt - func_b: int32.go::NewInt32 - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: int64.go::Int64.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: int32.go::Int32.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: int.go::Int.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: string.go::String.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: int32.go::Int32.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: int.go::Int.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: string.go::String.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: int64.go::Int64.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Has - func_b: string.go::String.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: byte.go::Byte.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int.go::Int.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32KeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32KeySet - func_b: int64.go::Int64KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::NewInt32 - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::NewInt32 - func_b: int64.go::NewInt64 - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.PopAny - func_b: string.go::String.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.PopAny - func_b: int64.go::Int64.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.PopAny - func_b: int32.go::Int32.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.UnsortedList - func_b: string.go::String.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.UnsortedList - func_b: int64.go::Int64.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.UnsortedList - func_b: int32.go::Int32.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: int.go::Int.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: int32.go::Int32.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: int64.go::Int64.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: string.go::String.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: byte.go::Byte.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int.go::Int.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: int.go::NewInt - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: byte.go::Byte.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: int32.go::NewInt32 - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Clone - func_b: string.go::String.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Clone - func_b: int64.go::Int64.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Clone - func_b: int32.go::Int32.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int64.go::Int64.Has - func_b: int64.go::Int64.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: byte.go::Byte.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: byte.go::Byte.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: byte.go::Byte.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: byte.go::Byte.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: string.go::String.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: string.go::String.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 -message: High duplication - significant refactoring recommended - -function_complexity: - - name: BenchmarkStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 17 - cognitive_complexity: 52 - nesting_depth: 3 - lines_of_code: 71 - complexity_density: 0.23943661971830985 - risk_level: CRITICAL - - name: TestSet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 37 - complexity_density: 0.2702702702702703 - risk_level: HIGH - - name: TestSetEquals - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 53 - complexity_density: 0.18867924528301888 - risk_level: HIGH - - name: TestStringSetEquals - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 53 - complexity_density: 0.18867924528301888 - risk_level: HIGH - - name: TestStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 37 - complexity_density: 0.2702702702702703 - risk_level: HIGH - - name: TestSetDeleteMultiples - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 22 - complexity_density: 0.2727272727272727 - risk_level: MEDIUM - - name: TestSetDifference - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 18 - complexity_density: 0.3333333333333333 - risk_level: MEDIUM - - name: TestNewEmptySet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 4 - nesting_depth: 1 - lines_of_code: 13 - complexity_density: 0.46153846153846156 - risk_level: MEDIUM - - name: TestStringSetDifference - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 18 - complexity_density: 0.3333333333333333 - risk_level: MEDIUM - - name: TestStringSetDeleteMultiples - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 22 - complexity_density: 0.2727272727272727 - risk_level: MEDIUM - - name: TestNewSet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 5 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 9 - complexity_density: 0.5555555555555556 - risk_level: MEDIUM - - name: TestNewStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 5 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 9 - complexity_density: 0.5555555555555556 - risk_level: MEDIUM - - name: TestSetClearWithSharedReference - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 16 - complexity_density: 0.25 - risk_level: LOW - - name: Intersection - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 17 - complexity_density: 0.23529411764705882 - risk_level: LOW - - name: makeString - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 4 - nesting_depth: 2 - lines_of_code: 11 - complexity_density: 0.36363636363636365 - risk_level: LOW - - name: TestStringUnion - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 39 - complexity_density: 0.10256410256410256 - risk_level: LOW - - name: TestStringIntersection - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 44 - complexity_density: 0.09090909090909091 - risk_level: LOW - - name: TestIntersection - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 44 - complexity_density: 0.09090909090909091 - risk_level: LOW - - name: TestUnion - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 39 - complexity_density: 0.10256410256410256 - risk_level: LOW - - name: TestSetClearInSeparateFunction - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 15 - complexity_density: 0.26666666666666666 - risk_level: LOW - - name: TestStringSetHasAny - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 11 - complexity_density: 0.2727272727272727 - risk_level: LOW - - name: BenchmarkListLarge - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 9 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: TestSetClear - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 12 - complexity_density: 0.25 - risk_level: LOW - - name: TestSetHasAny - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 11 - complexity_density: 0.2727272727272727 - risk_level: LOW - - name: TestSetSymmetricDifference - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 12 - complexity_density: 0.25 - risk_level: LOW - - name: Difference - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 9 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 8 - complexity_density: 0.375 - risk_level: LOW - - name: HasAny - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 8 - complexity_density: 0.375 - risk_level: LOW - - name: TestStringSetSymmetricDifference - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 12 - complexity_density: 0.25 - risk_level: LOW - - name: IsSuperset - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 8 - complexity_density: 0.375 - risk_level: LOW - - name: Union - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: UnsortedList - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: PopAny - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 8 - complexity_density: 0.25 - risk_level: LOW - - name: KeySet - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: Insert - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: TestStringSetList - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: TestSortedList - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: TestKeySet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: BenchmarkListSmall - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: New - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 5 - complexity_density: 0.2 - risk_level: LOW - - name: HasAny - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Int32KeySet - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewInt32 - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Int64KeySet - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewInt64 - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewInt - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clear - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 4 - complexity_density: 0.25 - risk_level: LOW - - name: Equal - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 4 - complexity_density: 0.25 - risk_level: LOW - - name: Insert - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 5 - complexity_density: 0.2 - risk_level: LOW - - name: HasAll - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: cast - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 1 - complexity_density: 1 - risk_level: LOW - - name: IsSuperset - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IntKeySet - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewByte - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: clearSetAndAdd - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 4 - complexity_density: 0.25 - risk_level: LOW - - name: Len - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: ByteKeySet - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewString - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: StringKeySet - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW -distribution: - complex: 1 - moderate: 9 - simple: 131 -high_risk_functions: - - name: BenchmarkStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 17 - cognitive_complexity: 52 - risk_level: CRITICAL - issues: - - High cyclomatic complexity - - High cognitive complexity - - name: TestSet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity - - name: TestSetEquals - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity - - name: TestStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity - - name: TestStringSetEquals - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity -aggregate: - total_functions: 141 - average_complexity: 1.9929078014184398 - max_complexity: 17 - total_complexity: 281 - cognitive_complexity: 0 - nesting_depth: 0 - decision_points: 140 - health_score: 90.0709219858156 - message: Good complexity - functions have reasonable complexity - -comment_quality: - - line_number: 1 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: doc.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: empty.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 17 - source_file: doc.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 18 - source_file: doc.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: empty.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: empty.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 30 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 31 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 39 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 60 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 76 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 111 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 115 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 116 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 127 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 128 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 130 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 131 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 132 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 137 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 138 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 139 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 140 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 141 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 142 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 149 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 151 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 152 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 153 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 154 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 155 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 156 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 162 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 174 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 179 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 184 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 185 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 186 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 191 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 192 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 193 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 194 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 201 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 210 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 220 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 222 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 229 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 235 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 252 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 -function_documentation: - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: empty.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented -undocumented_functions: - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: empty.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM -aggregate: - total_comments: 301 - good_comments: 250 - bad_comments: 51 - overall_score: 0.5881985815602837 - total_functions: 147 - documented_functions: 112 - good_comments_ratio: 0.5881985815602837 - documentation_coverage: 0.7 - health_score: 58.819858156028374 - message: Fair comment quality - consider improving placement - -function_halstead: - - name: BenchmarkStringSet - source_file: set_test.go - language: go - directory: . - volume: 1530.009463523262 - difficulty: 47.345454545454544 - effort: 72438.99350935589 - time_to_program: 4024.3885282975493 - delivered_bugs: 0.510003154507754 - complexity_level: High - - name: TestIntersection - source_file: set_generic_test.go - language: go - directory: . - volume: 737.0232685160352 - difficulty: 24.96666666666667 - effort: 18401.01427061701 - time_to_program: 1022.278570589834 - delivered_bugs: 0.24567442283867838 - complexity_level: Medium - - name: TestSetEquals - source_file: set_generic_test.go - language: go - directory: . - volume: 689.8333013081152 - difficulty: 54.64285714285714 - effort: 37694.46253576486 - time_to_program: 2094.1368075424925 - delivered_bugs: 0.22994443376937174 - complexity_level: Medium - - name: TestUnion - source_file: set_generic_test.go - language: go - directory: . - volume: 660.591225855113 - difficulty: 24.82758620689655 - effort: 16400.885607437285 - time_to_program: 911.1603115242937 - delivered_bugs: 0.22019707528503765 - complexity_level: Medium - - name: TestStringSetEquals - source_file: set_test.go - language: go - directory: . - volume: 622.8902627834143 - difficulty: 56.875 - effort: 35426.88369580669 - time_to_program: 1968.1602053225938 - delivered_bugs: 0.20763008759447144 - complexity_level: Medium - - name: TestStringIntersection - source_file: set_test.go - language: go - directory: . - volume: 555.4086945462124 - difficulty: 20.833333333333332 - effort: 11571.014469712756 - time_to_program: 642.8341372062642 - delivered_bugs: 0.18513623151540412 - complexity_level: Medium - - name: TestSet - source_file: set_generic_test.go - language: go - directory: . - volume: 541.7831500254108 - difficulty: 42.94117647058823 - effort: 23264.805854032344 - time_to_program: 1292.489214112908 - delivered_bugs: 0.1805943833418036 - complexity_level: Medium - - name: TestStringSet - source_file: set_test.go - language: go - directory: . - volume: 512.9283016944967 - difficulty: 46 - effort: 23594.701877946845 - time_to_program: 1310.816770997047 - delivered_bugs: 0.17097610056483223 - complexity_level: Medium - - name: TestStringUnion - source_file: set_test.go - language: go - directory: . - volume: 498.2523364294818 - difficulty: 19.615384615384617 - effort: 9773.411214578296 - time_to_program: 542.9672896987943 - delivered_bugs: 0.1660841121431606 - complexity_level: Medium - - name: TestSetDifference - source_file: set_generic_test.go - language: go - directory: . - volume: 369.87118895965546 - difficulty: 25.333333333333332 - effort: 9370.070120311271 - time_to_program: 520.5594511284039 - delivered_bugs: 0.12329039631988516 - complexity_level: Medium - - name: TestStringSetDifference - source_file: set_test.go - language: go - directory: . - volume: 346.3849750966349 - difficulty: 24.58823529411765 - effort: 8516.99527002314 - time_to_program: 473.1664038901745 - delivered_bugs: 0.11546165836554496 - complexity_level: Medium - - name: TestSetDeleteMultiples - source_file: set_generic_test.go - language: go - directory: . - volume: 255.41209043760983 - difficulty: 17.538461538461537 - effort: 4479.53512459808 - time_to_program: 248.86306247767112 - delivered_bugs: 0.08513736347920328 - complexity_level: Medium - - name: TestSetSymmetricDifference - source_file: set_generic_test.go - language: go - directory: . - volume: 255.15831097164298 - difficulty: 14.529411764705882 - effort: 3707.3001652938715 - time_to_program: 205.96112029410398 - delivered_bugs: 0.08505277032388099 - complexity_level: Medium - - name: TestStringSetDeleteMultiples - source_file: set_test.go - language: go - directory: . - volume: 239.7487836710217 - difficulty: 19.636363636363637 - effort: 4707.794297540063 - time_to_program: 261.5441276411146 - delivered_bugs: 0.0799162612236739 - complexity_level: Medium - - name: TestNewEmptySet - source_file: set_generic_test.go - language: go - directory: . - volume: 227.5489532989615 - difficulty: 16.25 - effort: 3697.670491108124 - time_to_program: 205.4261383948958 - delivered_bugs: 0.07584965109965383 - complexity_level: Medium - - name: TestStringSetSymmetricDifference - source_file: set_test.go - language: go - directory: . - volume: 223.4671257758683 - difficulty: 13 - effort: 2905.072635086288 - time_to_program: 161.39292417146044 - delivered_bugs: 0.07448904192528943 - complexity_level: Medium - - name: TestSetClearWithSharedReference - source_file: set_generic_test.go - language: go - directory: . - volume: 205.13385445731566 - difficulty: 9.142857142857142 - effort: 1875.509526466886 - time_to_program: 104.19497369260478 - delivered_bugs: 0.06837795148577189 - complexity_level: Medium - - name: Intersection - source_file: set.go - language: go - directory: . - volume: 200.67442283867837 - difficulty: 9.714285714285714 - effort: 1949.408679004304 - time_to_program: 108.30048216690578 - delivered_bugs: 0.06689147427955945 - complexity_level: Medium - - name: TestSetClearInSeparateFunction - source_file: set_generic_test.go - language: go - directory: . - volume: 182.83669636412918 - difficulty: 9.692307692307692 - effort: 1772.109518606175 - time_to_program: 98.45052881145416 - delivered_bugs: 0.06094556545470973 - complexity_level: Medium - - name: TestNewSet - source_file: set_generic_test.go - language: go - directory: . - volume: 153.73110979725664 - difficulty: 11.55 - effort: 1775.5943181583143 - time_to_program: 98.64412878657302 - delivered_bugs: 0.05124370326575221 - complexity_level: Medium - - name: makeString - source_file: set_test.go - language: go - directory: . - volume: 153.73110979725664 - difficulty: 7.384615384615385 - effort: 1135.2451185028183 - time_to_program: 63.069173250156574 - delivered_bugs: 0.05124370326575221 - complexity_level: Medium - - name: TestKeySet - source_file: set_generic_test.go - language: go - directory: . - volume: 150.41407098051494 - difficulty: 3.2142857142857144 - effort: 483.4737995802266 - time_to_program: 26.85965553223481 - delivered_bugs: 0.05013802366017164 - complexity_level: Medium - - name: TestNewStringSet - source_file: set_test.go - language: go - directory: . - volume: 146.94555522617034 - difficulty: 12.222222222222223 - effort: 1796.001230542082 - time_to_program: 99.77784614122679 - delivered_bugs: 0.048981851742056784 - complexity_level: Medium - - name: TestSetClear - source_file: set_generic_test.go - language: go - directory: . - volume: 137.6075250475963 - difficulty: 5.75 - effort: 791.2432690236787 - time_to_program: 43.95795939020437 - delivered_bugs: 0.04586917501586544 - complexity_level: Medium - - name: TestSortedList - source_file: set_generic_test.go - language: go - directory: . - volume: 124.86408532184433 - difficulty: 3.9375 - effort: 491.65233595476207 - time_to_program: 27.314018664153448 - delivered_bugs: 0.04162136177394811 - complexity_level: Medium - - name: TestStringSetList - source_file: set_test.go - language: go - directory: . - volume: 109.80793556946902 - difficulty: 3.5999999999999996 - effort: 395.30856805008847 - time_to_program: 21.961587113893803 - delivered_bugs: 0.03660264518982301 - complexity_level: Medium - - name: Difference - source_file: set.go - language: go - directory: . - volume: 93.76537429460444 - difficulty: 4.75 - effort: 445.3855278993711 - time_to_program: 24.743640438853948 - delivered_bugs: 0.03125512476486815 - complexity_level: Low - - name: KeySet - source_file: set.go - language: go - directory: . - volume: 92 - difficulty: 3.1666666666666665 - effort: 291.3333333333333 - time_to_program: 16.185185185185183 - delivered_bugs: 0.030666666666666665 - complexity_level: Low - - name: BenchmarkListLarge - source_file: set_generic_test.go - language: go - directory: . - volume: 91.73835003173087 - difficulty: 3.269230769230769 - effort: 299.9138366421971 - time_to_program: 16.661879813455396 - delivered_bugs: 0.030579450010576957 - complexity_level: Low - - name: UnsortedList - source_file: set.go - language: go - directory: . - volume: 89.85848369899593 - difficulty: 5.333333333333333 - effort: 479.24524639464494 - time_to_program: 26.62473591081361 - delivered_bugs: 0.02995282789966531 - complexity_level: Low - - name: TestSetHasAny - source_file: set_generic_test.go - language: go - directory: . - volume: 78.13781191217038 - difficulty: 4.666666666666667 - effort: 364.6431222567951 - time_to_program: 20.257951236488616 - delivered_bugs: 0.026045937304056792 - complexity_level: Low - - name: Clone - source_file: set.go - language: go - directory: . - volume: 77.70923408096293 - difficulty: 3.7777777777777777 - effort: 293.5682176391933 - time_to_program: 16.309345424399627 - delivered_bugs: 0.025903078026987644 - complexity_level: Low - - name: TestStringSetHasAny - source_file: set_test.go - language: go - directory: . - volume: 72.33974351909448 - difficulty: 4.875 - effort: 352.6562496555856 - time_to_program: 19.592013869754755 - delivered_bugs: 0.024113247839698163 - complexity_level: Low - - name: Union - source_file: set.go - language: go - directory: . - volume: 68.11428751370197 - difficulty: 2.6666666666666665 - effort: 181.63810003653856 - time_to_program: 10.091005557585476 - delivered_bugs: 0.022704762504567322 - complexity_level: Low - - name: New - source_file: set.go - language: go - directory: . - volume: 66.60791492653966 - difficulty: 3.111111111111111 - effort: 207.22462421590117 - time_to_program: 11.51247912310562 - delivered_bugs: 0.022202638308846556 - complexity_level: Low - - name: List - source_file: set.go - language: go - directory: . - volume: 64.72503367497927 - difficulty: 2.6 - effort: 168.2850875549461 - time_to_program: 9.34917153083034 - delivered_bugs: 0.02157501122499309 - complexity_level: Low - - name: Equal - source_file: set.go - language: go - directory: . - volume: 58.81033751683405 - difficulty: 4.583333333333333 - effort: 269.5473802854894 - time_to_program: 14.974854460304966 - delivered_bugs: 0.019603445838944685 - complexity_level: Low - - name: SymmetricDifference - source_file: set.go - language: go - directory: . - volume: 56.47277761308516 - difficulty: 4.333333333333333 - effort: 244.71536965670234 - time_to_program: 13.59529831426124 - delivered_bugs: 0.018824259204361718 - complexity_level: Low - - name: IsSuperset - source_file: set.go - language: go - directory: . - volume: 53.77443751081734 - difficulty: 2.75 - effort: 147.8797031547477 - time_to_program: 8.21553906415265 - delivered_bugs: 0.01792481250360578 - complexity_level: Low - - name: PopAny - source_file: set.go - language: go - directory: . - volume: 53.77443751081734 - difficulty: 2 - effort: 107.54887502163469 - time_to_program: 5.974937501201927 - delivered_bugs: 0.01792481250360578 - complexity_level: Low - - name: HasAll - source_file: set.go - language: go - directory: . - volume: 51.80615605397529 - difficulty: 2.2222222222222223 - effort: 115.12479123105621 - time_to_program: 6.395821735058679 - delivered_bugs: 0.01726871868465843 - complexity_level: Low - - name: BenchmarkListSmall - source_file: set_generic_test.go - language: go - directory: . - volume: 50.18947501009619 - difficulty: 1.8333333333333335 - effort: 92.01403751850968 - time_to_program: 5.111890973250538 - delivered_bugs: 0.016729825003365395 - complexity_level: Low - - name: HasAny - source_file: set.go - language: go - directory: . - volume: 46.60451250937503 - difficulty: 1.6666666666666667 - effort: 77.67418751562505 - time_to_program: 4.315232639756947 - delivered_bugs: 0.01553483750312501 - complexity_level: Low - - name: Insert - source_file: set.go - language: go - directory: . - volume: 46.50699332842307 - difficulty: 2.357142857142857 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.015502331109474358 - complexity_level: Low - - name: Delete - source_file: set.go - language: go - directory: . - volume: 41.20902501875006 - difficulty: 1.5714285714285714 - effort: 64.75703931517867 - time_to_program: 3.597613295287704 - delivered_bugs: 0.013736341672916687 - complexity_level: Low - - name: Union - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: clearSetAndAdd - source_file: set_generic_test.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 1.125 - effort: 41.10886017423111 - time_to_program: 2.2838255652350616 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Has - source_file: set.go - language: go - directory: . - volume: 31.699250014423125 - difficulty: 1.1428571428571428 - effort: 36.22771430219785 - time_to_program: 2.0126507945665475 - delivered_bugs: 0.010566416671474375 - complexity_level: Low - - name: Insert - source_file: set.go - language: go - directory: . - volume: 30 - difficulty: 1.3333333333333333 - effort: 40 - time_to_program: 2.2222222222222223 - delivered_bugs: 0.01 - complexity_level: Low - - name: Delete - source_file: byte.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: int64.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: string.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: int32.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: string.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Int32KeySet - source_file: int32.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: StringKeySet - source_file: string.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: ByteKeySet - source_file: byte.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: byte.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: int.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Int64KeySet - source_file: int64.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: IntKeySet - source_file: int.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: int.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: int32.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: int64.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Equal - source_file: byte.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: byte.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: string.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: int.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: int.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: int32.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: int32.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: string.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: int64.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: int64.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: UnsortedList - source_file: int.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: string.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: int64.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: int.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: string.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: int32.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: int64.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: int64.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: int.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: int32.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: int32.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: byte.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: byte.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: byte.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: string.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clear - source_file: set.go - language: go - directory: . - volume: 23.264662506490403 - difficulty: 1.75 - effort: 40.71315938635821 - time_to_program: 2.2618421881310113 - delivered_bugs: 0.007754887502163467 - complexity_level: Low - - name: HasAny - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewInt64 - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewInt - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewByte - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewInt32 - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewString - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: cast - source_file: set.go - language: go - directory: . - volume: 18.094737505048094 - difficulty: 0.6 - effort: 10.856842503028856 - time_to_program: 0.6031579168349365 - delivered_bugs: 0.006031579168349364 - complexity_level: Low - - name: PopAny - source_file: int.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: Len - source_file: set.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1 - effort: 15.509775004326936 - time_to_program: 0.861654166907052 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: int64.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: int32.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: byte.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: string.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: Len - source_file: int64.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: int32.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: int.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: byte.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: string.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low -distribution: - low: 115 - medium: 25 - high: 1 - very_high: 0 -high_effort_functions: - - name: BenchmarkStringSet - source_file: set_test.go - volume: 1530.009463523262 - effort: 72438.99350935589 - time_to_program: 4024.3885282975493 - delivered_bugs: 0.510003154507754 - risk_level: MEDIUM -aggregate: - total_functions: 141 - volume: 2243.569310989035 - difficulty: 132.5965954137723 - effort: 714652.1317873196 - time_to_program: 39702.896210406645 - delivered_bugs: 0.7478564369963447 - distinct_operators: 0 - distinct_operands: 0 - total_operators: 0 - total_operands: 0 - vocabulary: 0 - length: 0 - estimated_length: 584.0119514998204 - health_score: 100 - message: Moderate Halstead complexity - acceptable - -function_cohesion: - - name: Len - source_file: string.go - cohesion: 0.4 - quality_level: Good - - name: Len - source_file: byte.go - cohesion: 0.4 - quality_level: Good - - name: Len - source_file: int32.go - cohesion: 0.4 - quality_level: Good - - name: Len - source_file: int64.go - cohesion: 0.4 - quality_level: Good - - name: List - source_file: set.go - cohesion: 0.45454545454545453 - quality_level: Good - - name: Int64KeySet - source_file: int64.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: IntKeySet - source_file: int.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: ByteKeySet - source_file: byte.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: StringKeySet - source_file: string.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: Int32KeySet - source_file: int32.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: Clear - source_file: set.go - cohesion: 0.6 - quality_level: Excellent - - name: Len - source_file: int.go - cohesion: 0.6 - quality_level: Excellent - - name: NewByte - source_file: byte.go - cohesion: 0.6 - quality_level: Excellent - - name: NewInt - source_file: int.go - cohesion: 0.6 - quality_level: Excellent - - name: NewInt32 - source_file: int32.go - cohesion: 0.6 - quality_level: Excellent - - name: NewInt64 - source_file: int64.go - cohesion: 0.6 - quality_level: Excellent - - name: NewString - source_file: string.go - cohesion: 0.6 - quality_level: Excellent - - name: TestStringSetList - source_file: set_test.go - cohesion: 0.6666666666666666 - quality_level: Excellent - - name: TestSortedList - source_file: set_generic_test.go - cohesion: 0.6923076923076923 - quality_level: Excellent - - name: TestKeySet - source_file: set_generic_test.go - cohesion: 0.75 - quality_level: Excellent - - name: BenchmarkStringSet - source_file: set_test.go - cohesion: 0.7674418604651163 - quality_level: Excellent - - name: KeySet - source_file: set.go - cohesion: 0.7692307692307693 - quality_level: Excellent - - name: TestStringSetEquals - source_file: set_test.go - cohesion: 0.7692307692307693 - quality_level: Excellent - - name: PopAny - source_file: set.go - cohesion: 0.7777777777777778 - quality_level: Excellent - - name: BenchmarkListLarge - source_file: set_generic_test.go - cohesion: 0.7857142857142857 - quality_level: Excellent - - name: UnsortedList - source_file: set.go - cohesion: 0.8 - quality_level: Excellent - - name: TestNewStringSet - source_file: set_test.go - cohesion: 0.8 - quality_level: Excellent - - name: BenchmarkListSmall - source_file: set_generic_test.go - cohesion: 0.8 - quality_level: Excellent - - name: TestSetEquals - source_file: set_generic_test.go - cohesion: 0.8 - quality_level: Excellent - - name: TestStringUnion - source_file: set_test.go - cohesion: 0.8 - quality_level: Excellent - - name: makeString - source_file: set_test.go - cohesion: 0.8125 - quality_level: Excellent - - name: TestIntersection - source_file: set_generic_test.go - cohesion: 0.8181818181818182 - quality_level: Excellent - - name: TestNewSet - source_file: set_generic_test.go - cohesion: 0.8181818181818182 - quality_level: Excellent - - name: TestUnion - source_file: set_generic_test.go - cohesion: 0.8181818181818182 - quality_level: Excellent - - name: TestStringSetSymmetricDifference - source_file: set_test.go - cohesion: 0.8235294117647058 - quality_level: Excellent - - name: Len - source_file: set.go - cohesion: 0.8333333333333334 - quality_level: Excellent - - name: cast - source_file: set.go - cohesion: 0.8333333333333334 - quality_level: Excellent - - name: TestSetSymmetricDifference - source_file: set_generic_test.go - cohesion: 0.8333333333333334 - quality_level: Excellent - - name: TestSetDifference - source_file: set_generic_test.go - cohesion: 0.8421052631578947 - quality_level: Excellent - - name: TestNewEmptySet - source_file: set_generic_test.go - cohesion: 0.8461538461538461 - quality_level: Excellent - - name: TestStringIntersection - source_file: set_test.go - cohesion: 0.85 - quality_level: Excellent - - name: SymmetricDifference - source_file: set.go - cohesion: 0.8571428571428571 - quality_level: Excellent - - name: TestSetClearWithSharedReference - source_file: set_generic_test.go - cohesion: 0.8666666666666667 - quality_level: Excellent - - name: Intersection - source_file: set.go - cohesion: 0.8666666666666667 - quality_level: Excellent - - name: Delete - source_file: set.go - cohesion: 0.875 - quality_level: Excellent - - name: Equal - source_file: set.go - cohesion: 0.875 - quality_level: Excellent - - name: TestStringSet - source_file: set_test.go - cohesion: 0.875 - quality_level: Excellent - - name: TestSet - source_file: set_generic_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: HasAny - source_file: set.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: clearSetAndAdd - source_file: set_generic_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: HasAll - source_file: set.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: Has - source_file: set.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: TestStringSetHasAny - source_file: set_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: TestStringSetDifference - source_file: set_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: TestSetHasAny - source_file: set_generic_test.go - cohesion: 0.9 - quality_level: Excellent - - name: Union - source_file: set.go - cohesion: 0.9 - quality_level: Excellent - - name: New - source_file: set.go - cohesion: 0.9 - quality_level: Excellent - - name: Clone - source_file: set.go - cohesion: 0.9 - quality_level: Excellent - - name: TestSetClear - source_file: set_generic_test.go - cohesion: 0.9230769230769231 - quality_level: Excellent - - name: TestSetDeleteMultiples - source_file: set_generic_test.go - cohesion: 0.9285714285714286 - quality_level: Excellent - - name: TestSetClearInSeparateFunction - source_file: set_generic_test.go - cohesion: 0.9285714285714286 - quality_level: Excellent - - name: SymmetricDifference - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: set.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: set.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: set.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: TestStringSetDeleteMultiples - source_file: set_test.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: int.go - cohesion: 1 - quality_level: Excellent -distribution: - excellent: 130 - good: 10 -low_cohesion_functions: [] -aggregate: - total_functions: 141 - lcom: 0.6678684635926015 - lcom_variant: LCOM-HS (Henderson-Sellers) - cohesion_score: 0.3321315364073984 - function_cohesion: 0.913608323772477 - health_score: 33.21315364073984 - message: Good overall cohesion with room for improvement - -import_list: - - path: cmp - category: external - is_external: true - - path: k8s.io/apimachinery/pkg/util/sets - category: external - is_external: true - - path: reflect - category: external - is_external: true - - path: slices - category: external - is_external: true - - path: testing - category: external - is_external: true - - path: fmt - category: stdlib - is_external: false - - path: math/rand - category: stdlib - is_external: false -categories: - - category: external - count: 5 - - category: stdlib - count: 2 -dependencies: [] -aggregate: - total_imports: 7 - external_imports: 5 - internal_imports: 2 - unique_packages: 7 - external_ratio: 0.7142857142857143 - -breakdown: - binary: 0 - configuration: 0 - documentation: 0 - dotfile: 0 - generated: 0 - image: 0 - source: 10 - vendor: 0 -percentages: - binary: 0 - configuration: 0 - documentation: 0 - dotfile: 0 - generated: 0 - image: 0 - source: 100 - vendor: 0 -total_files: 10 diff --git a/tests/golden/run/burndown.bin b/tests/golden/run/burndown.bin deleted file mode 100644 index 34a58ab1be6da074827cfa456d89cc66a1f52078..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367 zcmZ9IK~KXl428#E@i_+y;y{l~+MhsE<ThRu&2y?GElgGaJ8ruI>e0{f_uk9#^ilbz z!(jyUK9R>lP>)bzLAGt55=pTfgfi4s_44-mZXU~y@Vei`ff6#?+@~Q7N=OT@GJt}7 z&mr56Qi#qvyesk9fb=)pptza8P8n|<FMev3u)UJAm#`@Mep4I`#8GK8W-{P8Hfp#z zL;X3zO0q#5eJ^HftzDh%<S*=;$2Lg|y7(oETAQDzvw=Cue;1X0qwMq`MZcphA&Q2S Ho_YEKuVH`Z diff --git a/tests/golden/run/burndown.compact b/tests/golden/run/burndown.compact deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/golden/run/burndown.json b/tests/golden/run/burndown.json deleted file mode 100644 index cc7f5d1e..00000000 --- a/tests/golden/run/burndown.json +++ /dev/null @@ -1 +0,0 @@ -{"aggregate":{"total_current_lines":112539,"total_peak_lines":112539,"overall_survival_rate":1,"analysis_period_days":0,"num_bands":1,"num_samples":1,"tracked_files":0,"tracked_developers":0},"global_survival":[{"sample_index":0,"total_lines":112539,"survival_rate":1,"band_breakdown":[112539]}],"file_survival":[],"developer_survival":[],"interactions":null} \ No newline at end of file diff --git a/tests/golden/run/burndown.ndjson b/tests/golden/run/burndown.ndjson deleted file mode 100644 index bef32a59..00000000 --- a/tests/golden/run/burndown.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"hash":"2c4b3a562ce34cddc3f8218a2c4d11c7310e6d56","tick":0,"author_id":0,"timestamp":"2014-06-06T16:40:48-07:00","analyzer":"burndown","data":{"GlobalDeltas":{"0":{"0":14110}},"PeopleDeltas":null,"MatrixDeltas":null,"FileDeltas":null,"FileOwnership":null,"LinesAdded":14110,"LinesRemoved":0}} -{"hash":"07d298755dbb0a42da24268f8e4d8ebad087515f","tick":1,"author_id":0,"timestamp":"2014-06-06T19:46:45-07:00","analyzer":"burndown","data":{"GlobalDeltas":{"1":{"0":-5,"1":5}},"PeopleDeltas":null,"MatrixDeltas":null,"FileDeltas":null,"FileOwnership":null,"LinesAdded":5,"LinesRemoved":5}} -{"hash":"ba593770151eb32d44e66ea5326c0164924aabf3","tick":1,"author_id":1,"timestamp":"2014-06-06T23:40:15-07:00","analyzer":"burndown","data":{"GlobalDeltas":{"0":{"0":-2547,"1":-1},"1":{"0":-25,"1":2597}},"PeopleDeltas":null,"MatrixDeltas":null,"FileDeltas":null,"FileOwnership":null,"LinesAdded":2597,"LinesRemoved":25}} -{"hash":"a70141307aabeaccbc4dc4d6f5278882705ae069","tick":1,"author_id":1,"timestamp":"2014-06-06T23:43:08-07:00","analyzer":"burndown","data":{"GlobalDeltas":{"1":{"1":6}},"PeopleDeltas":null,"MatrixDeltas":null,"FileDeltas":null,"FileOwnership":null,"LinesAdded":6,"LinesRemoved":0}} -{"hash":"9f25cd29c536262c2ff4a84416921dcaf6cef268","tick":1,"author_id":0,"timestamp":"2014-06-07T07:30:20-07:00","analyzer":"burndown","data":{"GlobalDeltas":{"1":{"0":-3,"1":5}},"PeopleDeltas":null,"MatrixDeltas":null,"FileDeltas":null,"FileOwnership":null,"LinesAdded":5,"LinesRemoved":3}} diff --git a/tests/golden/run/burndown.text b/tests/golden/run/burndown.text deleted file mode 100644 index b663ac1d..00000000 --- a/tests/golden/run/burndown.text +++ /dev/null @@ -1,18 +0,0 @@ -codefang (v2): - version: 0 - hash: <unknown> -history/burndown: -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ Burndown: project 0d ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary - ──────────────────────────────────────────────────────────────────────────── - Current Lines 112,539 - Peak Lines 112,539 - Survival Rate [████████████████████] 100.0% - - Code Age Distribution - ──────────────────────────────────────────────────────────────────────────── - < 1 month ████████████████████ 100% (112539) - diff --git a/tests/golden/run/burndown.timeseries b/tests/golden/run/burndown.timeseries deleted file mode 100644 index dc64f343..00000000 --- a/tests/golden/run/burndown.timeseries +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": "codefang.timeseries.v1", - "tick_size_hours": 24, - "analyzers": [ - "burndown" - ], - "commits": [ - { - "author": "", - "burndown": { - "lines_added": 112539, - "lines_removed": 0 - }, - "hash": "2c9cc8da1aa316c30cfba4210cfcd09aff193c81", - "tick": 0, - "timestamp": "2026-01-27T03:23:53+05:30" - } - ] -} diff --git a/tests/golden/run/burndown.timeseries.ndjson b/tests/golden/run/burndown.timeseries.ndjson deleted file mode 100644 index fa816c11..00000000 --- a/tests/golden/run/burndown.timeseries.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"author":"","burndown":{"lines_added":14110,"lines_removed":0},"hash":"2c4b3a562ce34cddc3f8218a2c4d11c7310e6d56","tick":0,"timestamp":"2014-06-06T16:40:48-07:00"} -{"author":"","burndown":{"lines_added":5,"lines_removed":5},"hash":"07d298755dbb0a42da24268f8e4d8ebad087515f","tick":1,"timestamp":"2014-06-06T19:46:45-07:00"} -{"author":"","burndown":{"lines_added":2597,"lines_removed":25},"hash":"ba593770151eb32d44e66ea5326c0164924aabf3","tick":1,"timestamp":"2014-06-06T23:40:15-07:00"} -{"author":"","burndown":{"lines_added":6,"lines_removed":0},"hash":"a70141307aabeaccbc4dc4d6f5278882705ae069","tick":1,"timestamp":"2014-06-06T23:43:08-07:00"} -{"author":"","burndown":{"lines_added":5,"lines_removed":3},"hash":"9f25cd29c536262c2ff4a84416921dcaf6cef268","tick":1,"timestamp":"2014-06-07T07:30:20-07:00"} diff --git a/tests/golden/run/burndown.yaml b/tests/golden/run/burndown.yaml deleted file mode 100644 index f39e1f72..00000000 --- a/tests/golden/run/burndown.yaml +++ /dev/null @@ -1,22 +0,0 @@ -codefang (v2): - version: 0 - hash: <unknown> -history/burndown: -aggregate: - total_current_lines: 112539 - total_peak_lines: 112539 - overall_survival_rate: 1 - analysis_period_days: 0 - num_bands: 1 - num_samples: 1 - tracked_files: 0 - tracked_developers: 0 -global_survival: - - sample_index: 0 - total_lines: 112539 - survival_rate: 1 - band_breakdown: - - 112539 -file_survival: [] -developer_survival: [] -interactions: [] diff --git a/tests/golden/run/history_anomaly.json b/tests/golden/run/history_anomaly.json deleted file mode 100644 index 38468ed9..00000000 --- a/tests/golden/run/history_anomaly.json +++ /dev/null @@ -1 +0,0 @@ -{"anomalies":null,"time_series":[{"tick":0,"start_time":"2026-01-26T21:53:53Z","end_time":"2026-01-26T21:53:53Z","metrics":{"files_changed":11,"lines_added":0,"lines_removed":0,"net_churn":0,"language_diversity":3,"author_count":1},"is_anomaly":false,"churn_z_score":0,"language_diversity":3,"author_count":1}],"aggregate":{"total_ticks":1,"total_anomalies":0,"anomaly_rate":0,"threshold":2,"window_size":20,"churn_mean":0,"churn_stddev":0,"files_mean":11,"files_stddev":0,"lang_diversity_mean":3,"lang_diversity_stddev":0,"author_count_mean":1,"author_count_stddev":0}} \ No newline at end of file diff --git a/tests/golden/run/history_couples.json b/tests/golden/run/history_couples.json deleted file mode 100644 index 6a13d7ef..00000000 --- a/tests/golden/run/history_couples.json +++ /dev/null @@ -1 +0,0 @@ -{"file_coupling":[{"file1":"api/openapi-spec/swagger.json","file2":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"staging/src/k8s.io/api/admissionregistration/v1/types.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"pkg/api/testing/validation_test.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/swagger.json","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"staging/src/k8s.io/api/admissionregistration/v1/types.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"pkg/api/testing/validation_test.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","co_changes":1,"coupling_strength":1},{"file1":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","file2":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/api/testing/validation_test.go","file2":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/apis/admissionregistration/validation/validation.go","file2":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/apis/admissionregistration/validation/validation.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/apis/admissionregistration/validation/validation.go","file2":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/apis/admissionregistration/validation/validation.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/apis/admissionregistration/validation/validation.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/apis/admissionregistration/validation/validation.go","file2":"staging/src/k8s.io/api/admissionregistration/v1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/apis/admissionregistration/validation/validation.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1/types.go","co_changes":1,"coupling_strength":1},{"file1":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/types.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/types.go","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/types.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1/types.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1},{"file1":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","file2":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","co_changes":1,"coupling_strength":1}],"developer_coupling":[],"file_ownership":[{"file":"api/openapi-spec/swagger.json","lines":95904,"contributors":1},{"file":"api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json","lines":7649,"contributors":1},{"file":"pkg/api/testing/validation_test.go","lines":105,"contributors":1},{"file":"pkg/apis/admissionregistration/validation/validation.go","lines":1537,"contributors":1},{"file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","lines":152,"contributors":1},{"file":"staging/src/k8s.io/api/admissionregistration/v1/generated.proto","lines":1115,"contributors":1},{"file":"staging/src/k8s.io/api/admissionregistration/v1/types.go","lines":1241,"contributors":1},{"file":"staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto","lines":915,"contributors":1},{"file":"staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go","lines":1016,"contributors":1},{"file":"staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto","lines":1371,"contributors":1},{"file":"staging/src/k8s.io/api/admissionregistration/v1beta1/types.go","lines":1534,"contributors":1}],"aggregate":{"total_files":11,"total_developers":1,"total_co_changes":55,"avg_coupling_strength":1,"highly_coupled_pairs":0}} \ No newline at end of file diff --git a/tests/golden/run/history_devs.bin b/tests/golden/run/history_devs.bin deleted file mode 100644 index d35623afd5cb616e68512c77c6c718e784b79063..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 839 zcmcJN$xg#C5QZ!9BFI;gDhX{txqv|82{=(zmg7uQQ^&5n6h&3vov{<tv|>3S`7-}_ zwr}S7<tcx-noJIaHI0FWIUwbMIPI9ET(_-omXvuiC{ie}l+`uVq|DA$1Fhbl={4*@ z>K&Lq1>;WafnKO!ooKOaP5Yuhy1x%+ipkCjOR<d5m_+a`mc!n5M!y4hwDOj&F<6^% z%S<(%H4wfY6EZHkJP^@Or&x<L$ktb26gaTgnDgV>=t?`15NMf@=r6Kab~i6(H`{xg za*;;=qqjKsRA~b{*-abaHolr7Q4Ab&y(HS(W0t+bH`wt&7ki;mjRVQkD9THjkhL(@ zMG%93Yz-6#O!;w@kWqpqn#k)5S1@CMa2+WTObClftz#zCh=7W$Sfp8=7K_&+FXy-T v-U1&L*74hqsykgn%#VAZ1N;-D(BtJn290mLQUB4_-#ZB#=r<DXXLbAp4%8MJ diff --git a/tests/golden/run/history_devs.json b/tests/golden/run/history_devs.json deleted file mode 100644 index f0076314..00000000 --- a/tests/golden/run/history_devs.json +++ /dev/null @@ -1 +0,0 @@ -{"aggregate":{"total_commits":1,"total_lines_added":0,"total_lines_removed":0,"total_developers":1,"active_developers":1,"estimated_total_developers":1,"estimated_active_developers":1,"analysis_period_ticks":0,"project_bus_factor":0,"total_languages":0},"developers":[{"id":0,"name":"kubernetes prow robot","email":"20407524+k8s-ci-robot@users.noreply.github.com","commits":1,"lines_added":0,"lines_removed":0,"lines_changed":0,"net_lines":0,"languages":null,"first_tick":0,"last_tick":0,"active_ticks":1}],"languages":[],"busfactor":[],"activity":[{"tick":0,"start_time":"2026-01-26T21:53:53Z","end_time":"2026-01-26T21:53:53Z","by_developer":[{"dev_id":0,"commits":1}],"total_commits":1}],"churn":[{"tick":0,"start_time":"2026-01-26T21:53:53Z","end_time":"2026-01-26T21:53:53Z","lines_added":0,"lines_removed":0,"net_change":0}]} \ No newline at end of file diff --git a/tests/golden/run/history_devs.yaml b/tests/golden/run/history_devs.yaml deleted file mode 100644 index 0e50859e..00000000 --- a/tests/golden/run/history_devs.yaml +++ /dev/null @@ -1,45 +0,0 @@ -codefang (v2): - version: 0 - hash: <unknown> -history/devs: -aggregate: - total_commits: 1 - total_lines_added: 0 - total_lines_removed: 0 - total_developers: 1 - active_developers: 1 - estimated_total_developers: 1 - estimated_active_developers: 1 - analysis_period_ticks: 0 - project_bus_factor: 0 - total_languages: 0 -developers: - - id: 0 - name: kubernetes prow robot - email: 20407524+k8s-ci-robot@users.noreply.github.com - commits: 1 - lines_added: 0 - lines_removed: 0 - lines_changed: 0 - net_lines: 0 - languages: [] - first_tick: 0 - last_tick: 0 - active_ticks: 1 -languages: [] -busfactor: [] -activity: - - tick: 0 - start_time: "2026-01-26T21:53:53Z" - end_time: "2026-01-26T21:53:53Z" - by_developer: - - dev_id: 0 - commits: 1 - total_commits: 1 -churn: - - tick: 0 - start_time: "2026-01-26T21:53:53Z" - end_time: "2026-01-26T21:53:53Z" - lines_added: 0 - lines_removed: 0 - net_change: 0 diff --git a/tests/golden/run/history_file-history.json b/tests/golden/run/history_file-history.json deleted file mode 100644 index c950bcc5..00000000 --- a/tests/golden/run/history_file-history.json +++ /dev/null @@ -1 +0,0 @@ -{"file_churn":[{"path":"api/kubernetes.html","commit_count":1,"contributor_count":1,"total_lines_added":2017,"total_lines_removed":0,"total_lines_changed":0,"churn_score":21.17},{"path":"examples/guestbook/guestbook.md","commit_count":6,"contributor_count":3,"total_lines_added":222,"total_lines_removed":0,"total_lines_changed":23,"churn_score":8.45},{"path":"pkg/registry/etcd_registry_test.go","commit_count":1,"contributor_count":1,"total_lines_added":623,"total_lines_removed":0,"total_lines_changed":0,"churn_score":7.23},{"path":"pkg/kubelet/kubelet.go","commit_count":1,"contributor_count":1,"total_lines_added":598,"total_lines_removed":0,"total_lines_changed":0,"churn_score":6.98},{"path":"pkg/kubelet/kubelet_test.go","commit_count":1,"contributor_count":1,"total_lines_added":562,"total_lines_removed":0,"total_lines_changed":0,"churn_score":6.62},{"path":"pkg/registry/etcd_registry.go","commit_count":1,"contributor_count":1,"total_lines_added":392,"total_lines_removed":0,"total_lines_changed":0,"churn_score":4.92},{"path":"pkg/client/client_test.go","commit_count":1,"contributor_count":1,"total_lines_added":391,"total_lines_removed":0,"total_lines_changed":0,"churn_score":4.91},{"path":"README.md","commit_count":3,"contributor_count":1,"total_lines_added":128,"total_lines_removed":0,"total_lines_changed":10,"churn_score":4.38},{"path":"pkg/proxy/config/config.go","commit_count":1,"contributor_count":1,"total_lines_added":320,"total_lines_removed":0,"total_lines_changed":0,"churn_score":4.2},{"path":"pkg/registry/replication_controller_test.go","commit_count":1,"contributor_count":1,"total_lines_added":311,"total_lines_removed":0,"total_lines_changed":0,"churn_score":4.109999999999999},{"path":"pkg/cloudcfg/cloudcfg_test.go","commit_count":1,"contributor_count":1,"total_lines_added":308,"total_lines_removed":0,"total_lines_changed":0,"churn_score":4.08},{"path":"pkg/apiserver/api_server_test.go","commit_count":1,"contributor_count":1,"total_lines_added":282,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.82},{"path":"hack/config-go.sh","commit_count":3,"contributor_count":2,"total_lines_added":62,"total_lines_removed":0,"total_lines_changed":1,"churn_score":3.63},{"path":"pkg/cloudcfg/cloudcfg.go","commit_count":1,"contributor_count":1,"total_lines_added":254,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.54},{"path":"pkg/client/client.go","commit_count":1,"contributor_count":1,"total_lines_added":251,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.51},{"path":"pkg/proxy/config/config_test.go","commit_count":1,"contributor_count":1,"total_lines_added":240,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.4},{"path":"pkg/proxy/config/etcd.go","commit_count":1,"contributor_count":1,"total_lines_added":227,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.27},{"path":"pkg/apiserver/api_server.go","commit_count":1,"contributor_count":1,"total_lines_added":209,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.09},{"path":"pkg/registry/task_registry_test.go","commit_count":1,"contributor_count":1,"total_lines_added":204,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.04},{"path":"LICENSE","commit_count":1,"contributor_count":1,"total_lines_added":202,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3.02},{"path":"api/kubernetes.raml","commit_count":1,"contributor_count":1,"total_lines_added":200,"total_lines_removed":0,"total_lines_changed":0,"churn_score":3},{"path":"pkg/registry/controller_registry_test.go","commit_count":1,"contributor_count":1,"total_lines_added":187,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.87},{"path":"pkg/registry/replication_controller.go","commit_count":1,"contributor_count":1,"total_lines_added":186,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.8600000000000003},{"path":"pkg/proxy/roundrobbin_test.go","commit_count":1,"contributor_count":1,"total_lines_added":178,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.7800000000000002},{"path":"cluster/saltbase/salt/_states/container_bridge.py","commit_count":1,"contributor_count":1,"total_lines_added":163,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.63},{"path":"pkg/api/types.go","commit_count":1,"contributor_count":1,"total_lines_added":149,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.49},{"path":"pkg/registry/memory_registry_test.go","commit_count":1,"contributor_count":1,"total_lines_added":146,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.46},{"path":"cluster/kube-up.sh","commit_count":1,"contributor_count":1,"total_lines_added":139,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.3899999999999997},{"path":"pkg/registry/memory_registry.go","commit_count":1,"contributor_count":1,"total_lines_added":137,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.37},{"path":"pkg/registry/manifest_factory_test.go","commit_count":1,"contributor_count":1,"total_lines_added":133,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.33},{"path":"cmd/cloudcfg/cloudcfg.go","commit_count":1,"contributor_count":1,"total_lines_added":126,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.26},{"path":"cluster/saltbase/salt/kube-proxy/initd","commit_count":1,"contributor_count":1,"total_lines_added":120,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.2},{"path":"cluster/saltbase/salt/controller-manager/initd","commit_count":1,"contributor_count":1,"total_lines_added":120,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.2},{"path":"pkg/registry/task_registry.go","commit_count":1,"contributor_count":1,"total_lines_added":119,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.19},{"path":"cluster/saltbase/salt/apiserver/initd","commit_count":1,"contributor_count":1,"total_lines_added":119,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.19},{"path":"cluster/saltbase/salt/kubelet/initd","commit_count":1,"contributor_count":1,"total_lines_added":119,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.19},{"path":"cluster/saltbase/salt/etcd/initd","commit_count":1,"contributor_count":1,"total_lines_added":118,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.1799999999999997},{"path":"pkg/proxy/proxier.go","commit_count":1,"contributor_count":1,"total_lines_added":117,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.17},{"path":"pkg/registry/scheduler.go","commit_count":1,"contributor_count":1,"total_lines_added":115,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.15},{"path":"pkg/proxy/config/file.go","commit_count":1,"contributor_count":1,"total_lines_added":111,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.1100000000000003},{"path":"pkg/registry/scheduler_test.go","commit_count":1,"contributor_count":1,"total_lines_added":110,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.1},{"path":"pkg/registry/endpoints_test.go","commit_count":1,"contributor_count":1,"total_lines_added":108,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.08},{"path":"pkg/proxy/roundrobbin.go","commit_count":1,"contributor_count":1,"total_lines_added":103,"total_lines_removed":0,"total_lines_changed":0,"churn_score":2.0300000000000002},{"path":"release/release.sh","commit_count":1,"contributor_count":1,"total_lines_added":99,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.99},{"path":"cmd/apiserver/apiserver.go","commit_count":1,"contributor_count":1,"total_lines_added":94,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.94},{"path":"cluster/util.sh","commit_count":1,"contributor_count":1,"total_lines_added":93,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.9300000000000002},{"path":"cmd/integration/integration.go","commit_count":1,"contributor_count":1,"total_lines_added":87,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.87},{"path":"api/doc/task-schema.json","commit_count":1,"contributor_count":1,"total_lines_added":87,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.87},{"path":"pkg/registry/service_registry.go","commit_count":1,"contributor_count":1,"total_lines_added":86,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.8599999999999999},{"path":"release/config.sh","commit_count":1,"contributor_count":1,"total_lines_added":83,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.83},{"path":"cluster/saltbase/salt/kubelet/init.sls","commit_count":1,"contributor_count":1,"total_lines_added":82,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.8199999999999998},{"path":"cluster/saltbase/salt/apiserver/init.sls","commit_count":1,"contributor_count":1,"total_lines_added":81,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.81},{"path":"cluster/saltbase/salt/controller-manager/init.sls","commit_count":1,"contributor_count":1,"total_lines_added":81,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.81},{"path":"pkg/kubelet/kubelet_server.go","commit_count":1,"contributor_count":1,"total_lines_added":80,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.8},{"path":"cluster/saltbase/salt/kube-proxy/init.sls","commit_count":1,"contributor_count":1,"total_lines_added":79,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.79},{"path":"cluster/saltbase/salt/nginx/kubernetes-site","commit_count":1,"contributor_count":1,"total_lines_added":73,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.73},{"path":"pkg/proxy/proxier_test.go","commit_count":1,"contributor_count":1,"total_lines_added":73,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.73},{"path":"pkg/registry/controller_registry.go","commit_count":1,"contributor_count":1,"total_lines_added":68,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.6800000000000002},{"path":"cmd/kubelet/kubelet.go","commit_count":1,"contributor_count":1,"total_lines_added":67,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.67},{"path":"pkg/registry/endpoints.go","commit_count":1,"contributor_count":1,"total_lines_added":65,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.65},{"path":"cmd/proxy/proxy.go","commit_count":1,"contributor_count":1,"total_lines_added":64,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.6400000000000001},{"path":"cluster/saltbase/salt/etcd/init.sls","commit_count":1,"contributor_count":1,"total_lines_added":63,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.63},{"path":"pkg/client/container_info.go","commit_count":1,"contributor_count":1,"total_lines_added":61,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.6099999999999999},{"path":"cmd/controller-manager/controller-manager.go","commit_count":1,"contributor_count":1,"total_lines_added":58,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.58},{"path":"cluster/kube-push.sh","commit_count":1,"contributor_count":1,"total_lines_added":57,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.5699999999999998},{"path":"pkg/proxy/config/etcd_test.go","commit_count":1,"contributor_count":1,"total_lines_added":56,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.56},{"path":"cluster/saltbase/salt/nginx/nginx.conf","commit_count":1,"contributor_count":1,"total_lines_added":56,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.56},{"path":"pkg/client/container_info_test.go","commit_count":1,"contributor_count":1,"total_lines_added":54,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.54},{"path":"cluster/saltbase/salt/docker/init.sls","commit_count":1,"contributor_count":1,"total_lines_added":53,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.53},{"path":"cluster/templates/salt-master.sh","commit_count":1,"contributor_count":1,"total_lines_added":52,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.52},{"path":"hack/e2e-test.sh","commit_count":1,"contributor_count":1,"total_lines_added":52,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.52},{"path":"cluster/saltbase/salt/nginx/init.sls","commit_count":1,"contributor_count":1,"total_lines_added":52,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.52},{"path":"api/doc/controller-schema.json","commit_count":1,"contributor_count":1,"total_lines_added":50,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.5},{"path":"release/launch-kubernetes-base.sh","commit_count":1,"contributor_count":1,"total_lines_added":48,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.48},{"path":"pkg/util/util.go","commit_count":1,"contributor_count":1,"total_lines_added":47,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.47},{"path":"release/master-release-install.sh","commit_count":1,"contributor_count":1,"total_lines_added":46,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.46},{"path":"api/examples/task-list.json","commit_count":1,"contributor_count":1,"total_lines_added":46,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.46},{"path":"pkg/registry/interfaces.go","commit_count":1,"contributor_count":1,"total_lines_added":44,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.44},{"path":"pkg/util/stringlist_test.go","commit_count":1,"contributor_count":1,"total_lines_added":41,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.41},{"path":"cluster/kube-down.sh","commit_count":1,"contributor_count":1,"total_lines_added":41,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.41},{"path":"pkg/registry/manifest_factory.go","commit_count":1,"contributor_count":1,"total_lines_added":41,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.41},{"path":"hack/test-go.sh","commit_count":1,"contributor_count":1,"total_lines_added":40,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.4},{"path":"cluster/templates/salt-minion.sh","commit_count":1,"contributor_count":1,"total_lines_added":38,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.38},{"path":"pkg/util/stringlist.go","commit_count":1,"contributor_count":1,"total_lines_added":37,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.37},{"path":"examples/guestbook/index.php","commit_count":1,"contributor_count":1,"total_lines_added":37,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.37},{"path":"hack/integration-test.sh","commit_count":1,"contributor_count":1,"total_lines_added":37,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.37},{"path":"api/doc/service-schema.json","commit_count":1,"contributor_count":1,"total_lines_added":36,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.3599999999999999},{"path":"api/examples/controller-list.json","commit_count":1,"contributor_count":1,"total_lines_added":30,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.3},{"path":"cluster/templates/download-release.sh","commit_count":1,"contributor_count":1,"total_lines_added":30,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.3},{"path":"hack/build-go.sh","commit_count":1,"contributor_count":1,"total_lines_added":30,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.3},{"path":"cluster/cloudcfg.sh","commit_count":1,"contributor_count":1,"total_lines_added":29,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.29},{"path":"pkg/proxy/loadbalancer.go","commit_count":1,"contributor_count":1,"total_lines_added":29,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.29},{"path":"cluster/config-test.sh","commit_count":1,"contributor_count":1,"total_lines_added":28,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.28},{"path":"cluster/config-default.sh","commit_count":1,"contributor_count":1,"total_lines_added":27,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.27},{"path":"release/make-public-gcs-acl.py","commit_count":1,"contributor_count":1,"total_lines_added":26,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.26},{"path":"hack/dev-build-and-up.sh","commit_count":1,"contributor_count":1,"total_lines_added":24,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.24},{"path":"cluster/saltbase/salt/golang.sls","commit_count":1,"contributor_count":1,"total_lines_added":24,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.24},{"path":"hack/dev-build-and-push.sh","commit_count":1,"contributor_count":1,"total_lines_added":23,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.23},{"path":"examples/guestbook/redis-master.json","commit_count":1,"contributor_count":1,"total_lines_added":19,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.19},{"path":"api/examples/service-list.json","commit_count":1,"contributor_count":1,"total_lines_added":19,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.19},{"path":"cluster/saltbase/salt/nginx/make-cert.sh","commit_count":1,"contributor_count":1,"total_lines_added":19,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.19},{"path":"api/examples/task.json","commit_count":1,"contributor_count":1,"total_lines_added":18,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.18},{"path":"examples/guestbook/frontend-controller.json","commit_count":1,"contributor_count":1,"total_lines_added":18,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.18},{"path":"examples/guestbook/redis-slave-controller.json","commit_count":1,"contributor_count":1,"total_lines_added":18,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.18},{"path":"cluster/saltbase/salt/top.sls","commit_count":1,"contributor_count":1,"total_lines_added":18,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.18},{"path":"api/examples/controller.json","commit_count":1,"contributor_count":1,"total_lines_added":18,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.18},{"path":"hooks/prepare-commit-msg","commit_count":1,"contributor_count":1,"total_lines_added":17,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.17},{"path":"hooks/commit-msg","commit_count":1,"contributor_count":1,"total_lines_added":10,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.1},{"path":"examples/guestbook/redis-master-service.json","commit_count":1,"contributor_count":1,"total_lines_added":7,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.07},{"path":"examples/guestbook/redis-slave-service.json","commit_count":1,"contributor_count":1,"total_lines_added":7,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.07},{"path":"api/examples/service.json","commit_count":1,"contributor_count":1,"total_lines_added":7,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.07},{"path":"cluster/saltbase/salt/base.sls","commit_count":1,"contributor_count":1,"total_lines_added":6,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.06},{"path":"cluster/saltbase/salt/apiserver/default","commit_count":1,"contributor_count":1,"total_lines_added":5,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.05},{"path":"cluster/saltbase/reactor/start.sls","commit_count":1,"contributor_count":1,"total_lines_added":5,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.05},{"path":"cluster/saltbase/salt/etcd/etcd.conf","commit_count":1,"contributor_count":1,"total_lines_added":4,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.04},{"path":"cluster/saltbase/pillar/mine.sls","commit_count":1,"contributor_count":1,"total_lines_added":4,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.04},{"path":"cluster/saltbase/pillar/top.sls","commit_count":1,"contributor_count":1,"total_lines_added":3,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.03},{"path":"cluster/saltbase/salt/kube-proxy/default","commit_count":1,"contributor_count":1,"total_lines_added":2,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.02},{"path":"cluster/saltbase/salt/kubelet/default","commit_count":1,"contributor_count":1,"total_lines_added":2,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.02},{"path":"cluster/saltbase/salt/controller-manager/default","commit_count":1,"contributor_count":1,"total_lines_added":2,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.02},{"path":"cluster/saltbase/salt/docker/docker-defaults","commit_count":1,"contributor_count":1,"total_lines_added":1,"total_lines_removed":0,"total_lines_changed":0,"churn_score":1.01}],"file_contributors":[{"path":"examples/guestbook/index.php","contributors":[{"dev_id":0,"added":37,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":37},{"path":"pkg/cloudcfg/cloudcfg_test.go","contributors":[{"dev_id":0,"added":308,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":308},{"path":"cluster/saltbase/salt/kube-proxy/default","contributors":[{"dev_id":0,"added":2,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":2},{"path":"examples/guestbook/redis-slave-controller.json","contributors":[{"dev_id":0,"added":18,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":18},{"path":"api/kubernetes.html","contributors":[{"dev_id":0,"added":2017,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":2017},{"path":"cluster/saltbase/salt/nginx/make-cert.sh","contributors":[{"dev_id":0,"added":19,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":19},{"path":"cmd/apiserver/apiserver.go","contributors":[{"dev_id":0,"added":94,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":94},{"path":"pkg/kubelet/kubelet_test.go","contributors":[{"dev_id":0,"added":562,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":562},{"path":"release/config.sh","contributors":[{"dev_id":0,"added":83,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":83},{"path":"cluster/templates/download-release.sh","contributors":[{"dev_id":0,"added":30,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":30},{"path":"pkg/proxy/config/file.go","contributors":[{"dev_id":0,"added":111,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":111},{"path":"pkg/registry/controller_registry_test.go","contributors":[{"dev_id":0,"added":187,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":187},{"path":"cluster/saltbase/salt/golang.sls","contributors":[{"dev_id":0,"added":24,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":24},{"path":"examples/guestbook/frontend-controller.json","contributors":[{"dev_id":0,"added":18,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":18},{"path":"pkg/kubelet/kubelet_server.go","contributors":[{"dev_id":0,"added":80,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":80},{"path":"pkg/registry/replication_controller.go","contributors":[{"dev_id":0,"added":186,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":186},{"path":"pkg/registry/task_registry.go","contributors":[{"dev_id":0,"added":119,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":119},{"path":"api/doc/controller-schema.json","contributors":[{"dev_id":0,"added":50,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":50},{"path":"cmd/integration/integration.go","contributors":[{"dev_id":0,"added":87,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":87},{"path":"pkg/cloudcfg/cloudcfg.go","contributors":[{"dev_id":0,"added":254,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":254},{"path":"pkg/proxy/roundrobbin_test.go","contributors":[{"dev_id":0,"added":178,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":178},{"path":"pkg/registry/etcd_registry.go","contributors":[{"dev_id":0,"added":392,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":392},{"path":"pkg/registry/scheduler.go","contributors":[{"dev_id":0,"added":115,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":115},{"path":"cluster/saltbase/reactor/start.sls","contributors":[{"dev_id":0,"added":5,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":5},{"path":"cluster/saltbase/salt/_states/container_bridge.py","contributors":[{"dev_id":0,"added":163,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":163},{"path":"cluster/saltbase/salt/docker/init.sls","contributors":[{"dev_id":0,"added":53,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":53},{"path":"cluster/templates/salt-minion.sh","contributors":[{"dev_id":0,"added":38,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":38},{"path":"cmd/controller-manager/controller-manager.go","contributors":[{"dev_id":0,"added":58,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":58},{"path":"hack/dev-build-and-push.sh","contributors":[{"dev_id":0,"added":23,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":23},{"path":"hack/integration-test.sh","contributors":[{"dev_id":0,"added":37,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":37},{"path":"cluster/config-default.sh","contributors":[{"dev_id":0,"added":27,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":27},{"path":"examples/guestbook/redis-master-service.json","contributors":[{"dev_id":0,"added":7,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":7},{"path":"hack/config-go.sh","contributors":[{"dev_id":0,"added":56,"removed":0,"changed":0},{"dev_id":2,"added":6,"removed":0,"changed":1}],"top_contributor_id":0,"top_contributor_lines":56},{"path":"pkg/proxy/config/etcd.go","contributors":[{"dev_id":0,"added":227,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":227},{"path":"pkg/registry/etcd_registry_test.go","contributors":[{"dev_id":0,"added":623,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":623},{"path":"cluster/saltbase/salt/etcd/init.sls","contributors":[{"dev_id":0,"added":63,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":63},{"path":"examples/guestbook/redis-master.json","contributors":[{"dev_id":0,"added":19,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":19},{"path":"pkg/registry/interfaces.go","contributors":[{"dev_id":0,"added":44,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":44},{"path":"api/examples/controller.json","contributors":[{"dev_id":0,"added":18,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":18},{"path":"cluster/saltbase/salt/etcd/etcd.conf","contributors":[{"dev_id":0,"added":4,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":4},{"path":"cmd/kubelet/kubelet.go","contributors":[{"dev_id":0,"added":67,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":67},{"path":"hack/build-go.sh","contributors":[{"dev_id":0,"added":30,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":30},{"path":"pkg/util/stringlist.go","contributors":[{"dev_id":0,"added":37,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":37},{"path":"release/make-public-gcs-acl.py","contributors":[{"dev_id":0,"added":26,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":26},{"path":"api/examples/service-list.json","contributors":[{"dev_id":0,"added":19,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":19},{"path":"cluster/saltbase/salt/apiserver/initd","contributors":[{"dev_id":0,"added":119,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":119},{"path":"pkg/registry/memory_registry.go","contributors":[{"dev_id":0,"added":137,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":137},{"path":"pkg/util/stringlist_test.go","contributors":[{"dev_id":0,"added":41,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":41},{"path":"LICENSE","contributors":[{"dev_id":0,"added":202,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":202},{"path":"cluster/kube-down.sh","contributors":[{"dev_id":0,"added":41,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":41},{"path":"cluster/kube-up.sh","contributors":[{"dev_id":0,"added":139,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":139},{"path":"cluster/saltbase/salt/apiserver/default","contributors":[{"dev_id":0,"added":5,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":5},{"path":"cluster/saltbase/salt/controller-manager/default","contributors":[{"dev_id":0,"added":2,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":2},{"path":"release/launch-kubernetes-base.sh","contributors":[{"dev_id":0,"added":48,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":48},{"path":"cluster/saltbase/salt/apiserver/init.sls","contributors":[{"dev_id":0,"added":81,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":81},{"path":"cluster/saltbase/salt/kube-proxy/initd","contributors":[{"dev_id":0,"added":120,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":120},{"path":"pkg/apiserver/api_server_test.go","contributors":[{"dev_id":0,"added":282,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":282},{"path":"pkg/proxy/config/etcd_test.go","contributors":[{"dev_id":0,"added":56,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":56},{"path":"pkg/registry/manifest_factory.go","contributors":[{"dev_id":0,"added":41,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":41},{"path":"api/kubernetes.raml","contributors":[{"dev_id":0,"added":200,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":200},{"path":"cluster/saltbase/salt/docker/docker-defaults","contributors":[{"dev_id":0,"added":1,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":1},{"path":"cluster/saltbase/salt/kube-proxy/init.sls","contributors":[{"dev_id":0,"added":79,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":79},{"path":"hack/e2e-test.sh","contributors":[{"dev_id":0,"added":52,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":52},{"path":"pkg/registry/endpoints_test.go","contributors":[{"dev_id":0,"added":108,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":108},{"path":"pkg/apiserver/api_server.go","contributors":[{"dev_id":0,"added":209,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":209},{"path":"pkg/client/container_info_test.go","contributors":[{"dev_id":0,"added":54,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":54},{"path":"release/master-release-install.sh","contributors":[{"dev_id":0,"added":46,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":46},{"path":"cluster/saltbase/salt/kubelet/init.sls","contributors":[{"dev_id":0,"added":82,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":82},{"path":"hooks/commit-msg","contributors":[{"dev_id":0,"added":10,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":10},{"path":"pkg/registry/manifest_factory_test.go","contributors":[{"dev_id":0,"added":133,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":133},{"path":"pkg/registry/replication_controller_test.go","contributors":[{"dev_id":0,"added":311,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":311},{"path":"api/examples/controller-list.json","contributors":[{"dev_id":0,"added":30,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":30},{"path":"pkg/client/client.go","contributors":[{"dev_id":0,"added":251,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":251},{"path":"api/doc/service-schema.json","contributors":[{"dev_id":0,"added":36,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":36},{"path":"cmd/cloudcfg/cloudcfg.go","contributors":[{"dev_id":0,"added":126,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":126},{"path":"examples/guestbook/guestbook.md","contributors":[{"dev_id":0,"added":222,"removed":0,"changed":18},{"dev_id":1,"added":0,"removed":0,"changed":4},{"dev_id":3,"added":0,"removed":0,"changed":1}],"top_contributor_id":0,"top_contributor_lines":240},{"path":"pkg/api/types.go","contributors":[{"dev_id":0,"added":149,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":149},{"path":"pkg/registry/controller_registry.go","contributors":[{"dev_id":0,"added":68,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":68},{"path":"pkg/util/util.go","contributors":[{"dev_id":0,"added":47,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":47},{"path":"api/examples/task-list.json","contributors":[{"dev_id":0,"added":46,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":46},{"path":"cluster/kube-push.sh","contributors":[{"dev_id":0,"added":57,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":57},{"path":"cluster/saltbase/pillar/top.sls","contributors":[{"dev_id":0,"added":3,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":3},{"path":"pkg/proxy/config/config.go","contributors":[{"dev_id":0,"added":320,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":320},{"path":"pkg/registry/task_registry_test.go","contributors":[{"dev_id":0,"added":204,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":204},{"path":"pkg/registry/endpoints.go","contributors":[{"dev_id":0,"added":65,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":65},{"path":"README.md","contributors":[{"dev_id":0,"added":128,"removed":0,"changed":10}],"top_contributor_id":0,"top_contributor_lines":138},{"path":"cluster/config-test.sh","contributors":[{"dev_id":0,"added":28,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":28},{"path":"pkg/kubelet/kubelet.go","contributors":[{"dev_id":0,"added":598,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":598},{"path":"pkg/proxy/config/config_test.go","contributors":[{"dev_id":0,"added":240,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":240},{"path":"cluster/saltbase/salt/etcd/initd","contributors":[{"dev_id":0,"added":118,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":118},{"path":"cluster/saltbase/salt/kubelet/initd","contributors":[{"dev_id":0,"added":119,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":119},{"path":"cluster/util.sh","contributors":[{"dev_id":0,"added":93,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":93},{"path":"examples/guestbook/redis-slave-service.json","contributors":[{"dev_id":0,"added":7,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":7},{"path":"pkg/registry/scheduler_test.go","contributors":[{"dev_id":0,"added":110,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":110},{"path":"api/examples/service.json","contributors":[{"dev_id":0,"added":7,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":7},{"path":"cluster/saltbase/salt/controller-manager/init.sls","contributors":[{"dev_id":0,"added":81,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":81},{"path":"pkg/proxy/proxier_test.go","contributors":[{"dev_id":0,"added":73,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":73},{"path":"api/examples/task.json","contributors":[{"dev_id":0,"added":18,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":18},{"path":"pkg/client/container_info.go","contributors":[{"dev_id":0,"added":61,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":61},{"path":"hack/dev-build-and-up.sh","contributors":[{"dev_id":0,"added":24,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":24},{"path":"pkg/registry/memory_registry_test.go","contributors":[{"dev_id":0,"added":146,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":146},{"path":"cluster/saltbase/salt/top.sls","contributors":[{"dev_id":0,"added":18,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":18},{"path":"cluster/templates/salt-master.sh","contributors":[{"dev_id":0,"added":52,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":52},{"path":"cmd/proxy/proxy.go","contributors":[{"dev_id":0,"added":64,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":64},{"path":"pkg/proxy/loadbalancer.go","contributors":[{"dev_id":0,"added":29,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":29},{"path":"pkg/proxy/proxier.go","contributors":[{"dev_id":0,"added":117,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":117},{"path":"pkg/registry/service_registry.go","contributors":[{"dev_id":0,"added":86,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":86},{"path":"cluster/saltbase/salt/base.sls","contributors":[{"dev_id":0,"added":6,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":6},{"path":"hack/test-go.sh","contributors":[{"dev_id":0,"added":40,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":40},{"path":"pkg/client/client_test.go","contributors":[{"dev_id":0,"added":391,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":391},{"path":"cluster/cloudcfg.sh","contributors":[{"dev_id":0,"added":29,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":29},{"path":"cluster/saltbase/salt/nginx/kubernetes-site","contributors":[{"dev_id":0,"added":73,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":73},{"path":"hooks/prepare-commit-msg","contributors":[{"dev_id":0,"added":17,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":17},{"path":"pkg/proxy/roundrobbin.go","contributors":[{"dev_id":0,"added":103,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":103},{"path":"cluster/saltbase/pillar/mine.sls","contributors":[{"dev_id":0,"added":4,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":4},{"path":"cluster/saltbase/salt/controller-manager/initd","contributors":[{"dev_id":0,"added":120,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":120},{"path":"cluster/saltbase/salt/kubelet/default","contributors":[{"dev_id":0,"added":2,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":2},{"path":"release/release.sh","contributors":[{"dev_id":0,"added":99,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":99},{"path":"api/doc/task-schema.json","contributors":[{"dev_id":0,"added":87,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":87},{"path":"cluster/saltbase/salt/nginx/init.sls","contributors":[{"dev_id":0,"added":52,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":52},{"path":"cluster/saltbase/salt/nginx/nginx.conf","contributors":[{"dev_id":0,"added":56,"removed":0,"changed":0}],"top_contributor_id":0,"top_contributor_lines":56}],"hotspots":[],"aggregate":{"total_files":121,"total_commits":130,"total_contributors":4,"avg_commits_per_file":1.0743801652892562,"avg_contributors_per_file":1.024793388429752,"high_churn_files":0},"composition":{"breakdown":{"configuration":9,"documentation":16,"source":308},"percentages":{"binary":0,"configuration":2.7027027027027026,"documentation":4.804804804804805,"dotfile":0,"generated":0,"image":0,"source":92.49249249249249,"vendor":0}},"composition_ts":[{"tick":0,"breakdown":{"configuration":9,"documentation":9,"source":103}},{"tick":1,"breakdown":{"documentation":7,"source":205}}]} \ No newline at end of file diff --git a/tests/golden/run/history_imports.json b/tests/golden/run/history_imports.json deleted file mode 100644 index 0fa7d27f..00000000 --- a/tests/golden/run/history_imports.json +++ /dev/null @@ -1 +0,0 @@ -{"import_list":[],"categories":[],"dependencies":null,"aggregate":{"total_imports":0,"external_imports":0,"internal_imports":0,"unique_packages":0,"external_ratio":0}} \ No newline at end of file diff --git a/tests/golden/run/history_quality.json b/tests/golden/run/history_quality.json deleted file mode 100644 index 19b3bb44..00000000 --- a/tests/golden/run/history_quality.json +++ /dev/null @@ -1 +0,0 @@ -{"time_series":[{"tick":0,"start_time":"2014-06-06T23:40:48Z","end_time":"2014-06-06T23:40:48Z","stats":{"complexity_mean":0,"complexity_median":0,"complexity_p95":0,"complexity_max":0,"halstead_vol_mean":0,"halstead_vol_median":0,"halstead_vol_p95":0,"halstead_vol_sum":0,"delivered_bugs_sum":0,"comment_score_mean":0,"comment_score_min":0,"doc_coverage_mean":0,"cohesion_mean":0,"cohesion_min":0,"files_analyzed":0,"total_functions":0,"max_complexity":0}},{"tick":1,"start_time":"2014-06-07T00:31:45Z","end_time":"2014-06-07T08:50:26Z","stats":{"complexity_mean":0,"complexity_median":0,"complexity_p95":0,"complexity_max":0,"halstead_vol_mean":0,"halstead_vol_median":0,"halstead_vol_p95":0,"halstead_vol_sum":0,"delivered_bugs_sum":0,"comment_score_mean":0,"comment_score_min":0,"doc_coverage_mean":0,"cohesion_mean":1,"cohesion_min":1,"files_analyzed":8,"total_functions":0,"max_complexity":0}}],"aggregate":{"total_ticks":2,"total_files_analyzed":8,"complexity_median_mean":0,"complexity_p95_mean":0,"halstead_vol_median_mean":0,"total_delivered_bugs":0,"comment_score_mean_mean":0,"min_comment_score":0,"cohesion_mean_mean":0.5,"min_cohesion":1}} \ No newline at end of file diff --git a/tests/golden/run/history_sentiment.json b/tests/golden/run/history_sentiment.json deleted file mode 100644 index 5d46d310..00000000 --- a/tests/golden/run/history_sentiment.json +++ /dev/null @@ -1 +0,0 @@ -{"time_series":[{"tick":0,"start_time":"2014-06-06T23:40:48Z","end_time":"2014-06-06T23:40:48Z","sentiment":0,"comment_count":0,"commit_count":1,"classification":"negative"},{"tick":1,"start_time":"2014-06-07T00:31:45Z","end_time":"2014-06-07T08:50:26Z","sentiment":0.57334095,"comment_count":6,"commit_count":9,"classification":"neutral"}],"trend":{"start_tick":0,"end_tick":1,"start_sentiment":0,"end_sentiment":0.57334095,"trend_direction":"improving","change_percent":0},"low_sentiment_periods":[{"tick":0,"sentiment":0,"comments":null,"risk_level":"HIGH"}],"aggregate":{"total_ticks":2,"total_comments":6,"total_commits":10,"average_sentiment":0.28667048,"positive_ticks":0,"neutral_ticks":1,"negative_ticks":1}} \ No newline at end of file diff --git a/tests/golden/run/history_shotness.json b/tests/golden/run/history_shotness.json deleted file mode 100644 index e3a4ee68..00000000 --- a/tests/golden/run/history_shotness.json +++ /dev/null @@ -1 +0,0 @@ -{"node_hotness":[{"name":"TestDeclarativeValidate","type":"Function","file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","change_count":1,"coupled_nodes":4,"hotness_score":1},{"name":"TestVersionedValidationByFuzzing","type":"Function","file":"pkg/api/testing/validation_test.go","change_count":1,"coupled_nodes":4,"hotness_score":1},{"name":"testDeclarativeValidateUpdate","type":"Function","file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","change_count":1,"coupled_nodes":4,"hotness_score":1},{"name":"validateMatchConditions","type":"Function","file":"pkg/apis/admissionregistration/validation/validation.go","change_count":1,"coupled_nodes":4,"hotness_score":1},{"name":"validateMutatingAdmissionPolicyBindingSpec","type":"Function","file":"pkg/apis/admissionregistration/validation/validation.go","change_count":1,"coupled_nodes":4,"hotness_score":1}],"node_coupling":[{"node1_name":"TestDeclarativeValidate","node1_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","node2_name":"TestVersionedValidationByFuzzing","node2_file":"pkg/api/testing/validation_test.go","co_changes":1,"coupling_strength":1},{"node1_name":"TestDeclarativeValidate","node1_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","node2_name":"testDeclarativeValidateUpdate","node2_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","co_changes":1,"coupling_strength":1},{"node1_name":"TestDeclarativeValidate","node1_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","node2_name":"validateMatchConditions","node2_file":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"node1_name":"TestDeclarativeValidate","node1_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","node2_name":"validateMutatingAdmissionPolicyBindingSpec","node2_file":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"node1_name":"TestVersionedValidationByFuzzing","node1_file":"pkg/api/testing/validation_test.go","node2_name":"testDeclarativeValidateUpdate","node2_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","co_changes":1,"coupling_strength":1},{"node1_name":"TestVersionedValidationByFuzzing","node1_file":"pkg/api/testing/validation_test.go","node2_name":"validateMatchConditions","node2_file":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"node1_name":"TestVersionedValidationByFuzzing","node1_file":"pkg/api/testing/validation_test.go","node2_name":"validateMutatingAdmissionPolicyBindingSpec","node2_file":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"node1_name":"testDeclarativeValidateUpdate","node1_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","node2_name":"validateMatchConditions","node2_file":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"node1_name":"testDeclarativeValidateUpdate","node1_file":"pkg/registry/admissionregistration/validatingadmissionpolicybinding/declarative_validation_test.go","node2_name":"validateMutatingAdmissionPolicyBindingSpec","node2_file":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1},{"node1_name":"validateMatchConditions","node1_file":"pkg/apis/admissionregistration/validation/validation.go","node2_name":"validateMutatingAdmissionPolicyBindingSpec","node2_file":"pkg/apis/admissionregistration/validation/validation.go","co_changes":1,"coupling_strength":1}],"hotspot_nodes":null,"aggregate":{"total_nodes":5,"total_changes":5,"total_couplings":10,"avg_changes_per_node":1,"avg_coupling_strength":1,"hot_nodes":0}} \ No newline at end of file diff --git a/tests/golden/run/history_typos.json b/tests/golden/run/history_typos.json deleted file mode 100644 index 6246cbef..00000000 --- a/tests/golden/run/history_typos.json +++ /dev/null @@ -1 +0,0 @@ -{"aggregate":{"total_typos":0,"unique_patterns":0,"affected_files":0,"affected_commits":0},"file_typos":[],"patterns":null,"typo_list":[]} \ No newline at end of file diff --git a/tests/golden/static/static_clones.bin b/tests/golden/static/static_clones.bin deleted file mode 100644 index 9baf095d58dd41841e51ecbc93244b927acc2507..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113900 zcmd5_+m0MZa<%_N=x2%`IlM@GvvzH4!-ipi+NXs<P*jT^fSjRchA;?0f9B8ehd8^3 zc_vOoR^=_CdY>e*N_q0!BCDG{J%9R_pPv5T|Ni4Y{Jy)pzI*ra?QdVM-rrqbU)}Cr zKYjl6(YHT+|MB|j@b>e&%bVk$KYj81(eD0_Zr-Jvcdws3e)Zz%t5+9Kp8xpr`HPDm z^X0p*pAT<;y}Z4<x%}nJaoOGL-*-Q!Km7jb?)AkFFCN|h$FuYwPk#9J9~ZmV&tE?M zBQNm{^l!iKj$3>CZuffk%h$WZ;}6%bU;lLcmp>i%wtIAc`ImJ0^6Kv8^V9YH>EH4V zx0jzTKfb%Uy!)Cs<mHb$O#jcfJ^k@$3Fco9U*8<=B+QZ2)rS+#o72DImSLcOKm5;; zl0->lP7vu*Vw0{oFMjb%EPwHA2B7SQkqL$kGdTi5>{BYf*`7_uMSqBIdL=I|o(00l z01v~Y?8tQ<|M~Fo!0EE)?5}PQHzPS40mFu-e0q#MBMZOKgcyd=oGg&OV;G8eTnEA( zz0&C&V<Cta7>X9KVF?v8)Ml9&Zs`@V=9ZPvfz|yH@bbAl9t2Rj{N{8H4b1Q)og8eB z|Ks}eKV5xY0J=wcxTjaVI+$|O59+U1x7Rm!hhP6KJ(8@mQ+LjCRLdcfnyI>P7WSY2 z`^&qJlBmYp-@f_s`EYZ4dbDvz6Y2aGn!F)*C`~L3$a0#}ur^5&ul!W=aL&!DM>|Fq zotdSb(@ZIYYTxLt7@iu^D=M^gsH$a%MYA5d;a%Mw=fvs7;o3xb^YzoGLpq1v|M~K_ z-wrp2tM|v(Gt*8sqW|^k_#!h7&+5B9;-0nGO4=zu8^&DurAib#Yvi`UwfNEc;uslz z6m2k(C;r51NwrYh>~U^JuC|=zl+tKC8<M=G-dfbGvq*TU<_)C}#NjyVg|aq_cI6v- z8MOed<uwA2au_qZvRsRLPED+;s5X)t(X_x@6W#ls@MYZ=Z7ttE8{+b+*n}0m{67S5 zBc+m>wXG9Ygh_lJ`<S#)UDL%Ky)rid%|Cyn-O`vXCj{iInt7u2vg-sks(QW>>L%?v zyejBgs8PW=PWdG)oYgS&$~)d%3gO;&R+C$cgv&O}GB0Dt)u@c8%^l5<UZ;#^*==H| zg{AGqIH?ucglPh$6LlM&1UZ@@o<cYq(aM5}`EOWz4Xi;~>&E|@mc|#^0cP5?vn1ub zLoNnAe-;Hw+YIGfdLyJGlGg#0HZ|YS3&dHi9?toOUKq{iFYj*E-$`mh$2&eA|GKLe ziW%9WCm=k%I=wGa1mR|m&XO_VPJi7yCtP+~ChkEu&%bH3S+G6AJ-wvr`jx>PqE}Lo zj<S41FQ<lt6HZvlt#rdsC7(_n$ovs2*GSZ%h5}J%WhERQe08wW9Ydp{v=B>J<~0Py zanGeeE-cd2E6563VvZ@8w=&mX^(hhXl1S>FFhDx9TtQQ+)n7D2#`{ZQoM>pca?1Wo z%Lpwph&J|lJ26UHJXbA4KcvQ;wotNaxH=~zmXYD0df|=$AWGpdP+`ie4inB;k1HTH zc4IIc0XvNXm=s?X5RTi^l7+Im;hx^SX@{_oj5hJY9lcPiE?G)3QJ|KNLF9pgAM}t? zeOv$j+R#sF)E%)}xn&Itfs-Q`2Y}8`z-4jw=~0aC=uKN*sOZwSK;K_GeNNkNc_CO@ z|BcrNy{$8ep-(Z>LR5<)6y4!81IS<*0pdk>_4?GCxzXpWCJmXE-)FAAy~7LzMFj8j z_wWNnz4|F$_&JPaEZpJ_r2;<;#F<Ek!2NrXprklI#vQ$kMu3K1ML+CV96aD7;@~rp zN+1vB++Q7<sw@~4A8h!ZiN5C7sK^l@gkbmm!7aTBNUCI*ho6mE#(B8M7wEy9{prw{ zdDW2lKoihODDsrR4ZU30VYE-;<W0nN(`4WTHQG+>7s^XEQnm32^vX8PMZHG=Mpim? zr*|MM^C50_nU=Rbhu%A`LFvMx1m4n>fQH0N3nApnZUjSvsr!1lH9}Im93k`3Bk2)V zzF~$}UPFS=V`RR;6~s1}&SF=r<RYmSibT@IAzA%KZL)*n)Xv2?90$d8e@Pe#MMjNM zlC^C^qPu#-*PtK<*YKEa;uI4;5=WW^R&hpcnsW-@uu~0jFhE0vssUN;oH&&~C}!d4 zzTQ^V18)H-sZ)UbNnROx>e%SV35i@ImY14%)wIM4Hv}n2>392V9fncy%|%=teq*u4 zGMh*rdSgu=8Y(R`(>>IJJtsIQHU#OTYa>|->}cg{9%@mCXLSoH8`MBr=-!c1WGKle z*HG5q|HjmbNWZnq>o9ouA`y8SkdUDhGliAGW_~`3E0#agEQukC`C_kIbqJy}3ZrEy zk2}eXrsH&((>v@xsc#<?8Jv8NA12V_+9H|J7Z+?56$V&*&*aG9#may}#W$zFxP4r< zwl|k4Fb}fwaSXB+dy=pO&5;9|ycmgNS@|g1;%4IL+q<8RUsl+foGf{7RvMYnAlp<^ zLXOOi_Ae64<}?y5F(Va&v$8aMdZU@PjivII<wS95TNenG(VGrg<WsR4IfC5tNzqGh zWRQgFN%7b~PuuA!dX0J#%gq6OWNoBshCxI`t&7|A0_3*vkb0j-Yp<GJ!zG**XK1@z zGH!^LHj*ANWr-Hg*YfSXnPIgk6pP4{{gW{*lUPJPgUlpJueu#n{W8^PDdt<*gI=nP zT3%E7N)vBN<X&VtptngBwBEEj=aU$GN9FDH6o8Wg_u`Vo<<R#QsbglOTWJHMYEM9( zQ}0o_y(ZJZm?YdMHDWzVwFAx`K$}iw3i71LT?Sj;TcS&Yru3Ezl?M?{Uw6h+*yYW} zPEF57cZDq}Mc7l8cOhHTH<&4jF7?{qYZ4F%mx0{Ry^AK7Ducx+TgFPeq5ODw*oNd| zT~}l^*)wc`_Zz3S&CnTQdj@@FyUqY_Wv4Rq9zvX!G9Sz+zIoDaUaW#_S(pEZUi#8Z z(h;1~9dsVhUGZ>n{nf;L1Z7wA?xmV|fiR_H2hL<pn<4^IJ}Ne0(y2NnW)mE>AY4;h z_$h{!xbWq0a#HVHk={|lTh6N#i*~L|I_j7|_6B}7ANiJEA*C644Zx&}RI_|TFG$@j z&wAC-h*S=bKPiHVj6?E?1Pt=YrY=r9;xje;WWw>O;>Cy$e>#^_xA;*^;^p&E%xnTU zS)Zl^WM&3U4~bwb63~CP<1_V8Q<n>O^bQOeL@WXrYufQ1ho4oi8s;NnJtgE^-j%P2 zI*X8b2BtnVaK{V`_KHZ9*bD_?BzZ$WSW*uGUMh?RpaNlQv`M@z?B4HP%wjUAO~a!? zCxgE~!!3Ypnvw>2w<7w4WC<_PBNZJ_u2x8OU+?@F)pZg!35OnI_O00H^-#r?S^@Ow zyrhYSt)SwbUK`he^wpusEElnsrK{8g+~LlIVB!R$mtIGhY0<D(1VD>64Aa))1V^mG zXCtBcG^&lMJqM;Hvy46_5nQxjLE*PR8wY03<FmKg$~*ZxM^?o2d5PQha+>byr6*I8 zjFDfhU36~xci|{IpkdkwH7_vx8p<X4wqgl}#y;-o^+-jK^DG6#)5(;0{cy_DFn%QG zmZMo)IeG}Cb>NX|Te?K~zrP$NCAP7#jKGbNkP<Q`q4Wrql`SKT;n{(f6@EzmAqx}V zQM=B=4tH?v&D=WB1DE|N;lnTx?5E>TH`Adf-Judi9>Z2a+EcHJOfN-Oec7{xv+;iN z_=g{#ynOcJ(eC}n>#M`tyRV-QyVtv)(?7m{w)^AJ@4J&y@6w-HxYJ7osU+px7p(f2 zVPCj(aFc<&-}f4=Kq$&0ES1UdVO7jVATON^`+mI-FJ+`BBh{{iq)DkK-El@1O6MX9 zs+bEuFrFBjf`$6Uyb_e3U$wF8EMg5Vn45W99Z!G0$I1O*kV8HS$S?|<8uExN*yK42 z6_nI&YO>q(8TtI0l{AtYmz;{F@?|FB(oS?Lrra_zDlkbqj+NO(948BP%FXz5leR+& z=pT-P{}QlpdCIlIdNnxnw*nmxH^v+VrW-xQtp-NXpJ7!O(eT<BZu1H_^SjT1JfpRu zDM*7Auf;?o#f`2)j^YZi$>>tP6*0%1E9rF#9)hwtVA_(-%M?F3qtvXVbE))RRhr8e zFMhmu)$)#xmNKsgC7X!#z=0XP@g?zyVDXvX7q4zy4_;w4Nsca)1Zs?VVcIHWk0$_O zVqOarEu5c&25jqaZcWay34Gd>l-v{y0h&t(v$|gkQPi`|rERN-d@+kW+B06wl7oi6 zKzTLj=$-miUVJg$GD|KVQ;#hdM4fh<7dm>f%cJ4!gma<D*{}pr2p6;pLmQl6NiE<O zG_?xnz?>C!H8)mPU%0#)#%WTNns}e%(H^dH(@=RYQDF24F$<P9k3!q7lZ3HQ#L6rG zOVn%}T@X1Yv?PLQq35ExZXg7p&5R~<9gD)^g4d#_Zr>(q&Q_t37~Gp9)rdw7oI_j* zip~G5m<f!wPSHc}+;h@^X}M0zN~l>mkn{%R<lysRz>_{Ney}yYQ}*nKr%fMylI!J9 zAxbb}U;KREAQ;wKM9bSzv$K^MD{0~M^}EB(?cx31<@J?)htmsF7blpaZlHHytf(?h zPH1$<*+SR?PKqii6bbT%mVUXGXDtYElAeye{xxhmGkgAo{U&AHCOI)otMS1j#-x$! zK(-e3&$f3&wjo7ya(Fn}fa$0$b*^RXcu-_d=|qypB<&3|YOtD&smFoM(cWbstfQu@ z#L+do3|$tvMEjdGgEBXymm1?Hvs$%out$(<Z25ZVB-B}Q6Bfe;4U`=v**zrX*lHWo zRYDAGObyycU`~jY*fw-q%5dKmw#GWyOoP#}Ip;XzEGDO%hI7#1wL$W4EAfz1g3N>( zgG{A%SWBYCWVQ-RgDEtzo7RJPJG>06b>9YcAMhx)7*%o+^VlJ(iiHd;VItBFm{?1q zQ5jvwIz{F1@o;xgAATcfCCE5kl25G);-y&4J?bX5O~~m!#rr(C2*bG2n5<X}`f8S2 zoi5f_>}{N9ldt~Bwb97ex2-Noaz<#??{SY;vTU8*0yQ;=MD}hFdV$Vbo}rH|7S1(C z+zuSwV4L)P@Z8}+M*Eeu1EA##3&vwh5h;?SJK{qmWz7-{(yKXW1q}w&7H^$bdT!Vn z9A(*iJ%3X&vvsIT5}WvXm&E|uIH6B20jBPOra!7|AQ%9TuhQ!tAC{#D{i4%BaC&NZ zP)wo~DM(c*tk8XsB)g>Fd$gIMYz<;+;WVn;lk4zF3zx-%yq0io?;uxgDEtDXy(1z> zWgW2z%X@YTYLki73N8e461<!E>;{-PWCUftzD2xL=E;dcm6^nz4{A`g(e`}@qT+3W zC+hIRg)~h<Ng2;Cvv_*{NG8w<we@*{LAtfzstEWfTsq;Q?qqW~!LSO_Ch-B~LoTgI zXM1OQ;liM{l2F!8vZcL2k3p~obqTMmMG$yQYig9$b%&m+6-#X)k!fNLYO6y`Ya&io z;o0;uS5-*hrrpuxnvL-&+gaQ(w!Rvdoz)6&gvmM1NgvEGtJ}uY*U?2x;_0jHQfF*e zeYq~-rX-02cJ9GYIhyk!=Ymq!;~`jg0lG<Co<|N-KcOlv`t`GQG@th_g=sMqQn(;u zwMAhk32IFj`^j$RoDGuHsOY<-NnShCw174`n?9K(MKBWrfmaetawB(x1~-f)3RECv zP(ik=)z0Z^D-km2qH(6M!g3a{zqPKm*5R$OD({0zCuEefL1!dx9FEEe-{5W9(D~}a z-QL(@%{<#NC^d7*i%FfyiCJeE4yyt0ECJS%Vq0OFqD=8nWFLw&$Sf&UUJnKuEdw5V zAsSF^Y(iUUH=t?fB8}^eh`_TIwv%xEt#p-_;WL*Ny3l`6UIr{btk_67qvlr-;HfjT z=f7GR<eFARlp3T(+8p4@!djYhZvd<k3;?46h)itg;-akOoedhPI#Nw5?aBvJ9vxC& z<k=yt^b~2`v{PX3(n?m>^ipQpfg}VYlOU-j2_XiJO#3f~aRt*y*b(F`{~+1Am*tQR z!qpk3I~Y+GX%MxOq(f*c0n#$KK9QuwyF24RnOwtq=uEt?WfA2D9%RWU!eF2@nL6Fc zn)U<kT}FLBZrO!#ojaQ}sWVTCGM8eakbrTf59yR6Poj|)n;=rD;}6ypH{wV~{<yMb z7E)QdW8(}Fwu8pB9#R6f_JOG!^Fg_L8$^C%w}ap*KWI^z)rt(W()Thi`!vp+)&m(3 z)}y?v&p138S%jxM_AcjBujaG`Y;v4*Mf2dUO~rk~uIcE_3g6|r^<Z1(6gdZIS`SM8 zz0};Q?R18>&UKx&S;Z%_27|_DOb7FFHU^=Y`Qn$k5@z~Z)zUAHdbRvJmoL_S=W-d; zX3QDZe4VQtv>RagN}4ur7F_=&lbJsUXVk>J+Tw_sR<wyBX+XgmrCEoyNj)2Ib|H?e zmSYn{dH@R(mw(-wCVD2=2z{BTjc+_=689XQTB=^I;`qjxTO5SjKw?8)a5P!#EFCfg z3Pm*9&ny~01s(DD?BwgSXes?G+|CtJ64Wa-D?FtNTPr$?SIz?}Z!JWGM^9!(#BwW; zAr^8iqRvQ4je*2f7ltfs4hD496fqk{jg+y+ViJxH#=M^xG@DZwNvjlT)WlMhdx=G| zV$R}mG~>g<coYR0Q1lN|Xf&OTfYb=c5DZEzPctYI(1vH4r%XPal0K5GzFEzyNjN`2 zH8XQU5Hregq=#u+1PswfLDPaXDT#@c_8hPQiESJQq=`pOXu)=o9ftx~NLNu9+R|v4 zSlo5MTwgyde#>Ds*nXfq7i?htX)@y93C&I7!|@7&G$}$wX02Sc3Z=9Wnx65jTd|fj zo@R+!JR|DXcrei=HOWaVn8k`Ta1}3IbU&My+365Zhf^=usAu%SNRq~fMyr65j-FI$ z!DSQ6GiFLsO`y@n(_jeUm^MTndMH|)S}1-A(TyKPP(sq4JV+uoi;zawS(e1XB&Ve7 zY*s5I%LSRGIRrYzluu%#Y?-DbB2EXHSc*^HlgA8b4{Rt!7M(UV%95ANnk|aD%^0B8 z!f79hSPmR*R<jIIh<fe>IB98?ogSr_C}8Ly6p+*qyej?->Z^<A7q4DC+P(jHeRX(y z_x1B(_j>np`p5U`BUcg)A6JuTb}x!p2NF$@A{yNRr3Vq&7px}P5#GY51$(rl_1YMg z=9-_Twt~>XMOA3FsOl`9PP|pM%yC9nPEH%g89dF=BA$g~Sod4`m7d;4yJ0c4>c^zc zxX7u&YO)zn6e)ITp~bR!0slcntrpgk{Am6;G#`4M8_nuD%Z}4h<Y%7NX40Ab>r`_F zjoG#2bG8xd{L21CGEDm+|6=)RwApm7t0}L-+wr-wo=aQ}=R|&a@`CqrR)+8@B85%m zBAQWg@)U<o7VRvpCN;6A!SvfXc~VQ}g!IHY5r=nHejDg%^E9ZXoPyXloGWK@rVE@E zBw;f|Ohh0i1t=}p)1acvu!slue6g?^xGn6(g_%Vwr)A6M6YOb_;d&d=Rhnb)b`Z_F zW9H^&Ns-Ktyk<JCb%HC%rGHZ7{_Pw~SE=LC&RKrYz$GhK4T@^bXFaTC(=vqWK@1mm zG>9hif;2!)%MOZfgy*X!Rs+K=)zsO<ZU(jFy<;_SE46)co?#B#tY~(zNzvTEeo)J_ z9&s}#5S@4q+{){1=G{8KSv4DyY1NK$j!wKyoFi{%PoVW=`cLSZcNP(|VBA4c3?9ig z&>fo~Qf}fA%ev0Y>zb2#V$UYEhUd?)w;Qu_Z5=ONrOY^>nbe!P?TT1)u#_0T*R{|~ z_MZ58w-AX3qRl9#k<)Z5vMl@TglJjnq<V9-h*nqVFvzl&RlXjPoKx~wk~7Otw+-t7 zQ~L<r2Q_U%G+7Ov0Q96dgQeLa6VTJ9G@==t8|~rth{#n~YOJpDNiAb~<_EeVKiW$q zY+PBSKY!d3V3y#1?W;InJ$w1$<+H}m5mLv?Uog71kJ~q2J|Avw4|lsqJ4L;D7d*c! zd(gtKeO5GV2P9gyYZLmc)uzx%dEy<QeCC71Y0Q;xrfg4rSwo3QkD@M^i3s{l!Y#s< zztTF4Ww~~vnXc2dGtFJ+G9@WE<vE9MlyfCk8xST1B~QxpEKlie8PQUu$d3wK2bOoZ zK(eRxK$SZ_N_ZIE(_+#Qp|Td2#qpt@`K-Xlk4tz<p${S`lk7{a>=Tqtz+uy(HA=pO z8^Tl8#K+q5+R)n;Derd_UPh-IoGd!a4m{~oQz3a0hz|a(IfvkPofS58?iwsgk1-l= zw51u3sm+~5WMA`Y%USk$V}MaBQ-p7JNfIxa&jYZw#s;7otqZ6TY8$pGQgkG{a7~M% ziL@X@)=AF$8a)-*)5ZDVPnCg<ty<n$o>JLLbL6dgU!(HMwKf!2!q(YFn_DJsi#sbe zM=B>qR)?6z!_fncFI-+tvcu>oTRxiMx~u@EndO7m8lC{?Qi90RjW8i^HholCITj2# z|30q5rHj<YEwtn$&#K9*m2;|D`eNqQ<Pe~MW|Kz)(?XH&aj`L}Ml{;Ab%AC!i@>O) z3uvt<8=~1M;KGn)&9Ovo`LYXQTZV$@wt=U+XawdukG8cIBkGik$n2Z3)vD`2kyw-q zdM;dT`z%|QH?M})NWb?!FM8#Y#?ExnHLrDg<hAcpZ6ZUfmy@LL3TRCma5jH*Hn7%v zPK!(wiQ+;xX@~=?lqo{=<=LjK3(Gcv)m<JR)Tzk}?PsOcT`2caX32G0bi(_e0qk8? zbCK_!JLXhenV3cBvNKLQU|MnyNSaHbwBKfQSchG3*=FS;=$;ikNki2N;9Z`atWB=K zXt1=y<=8^<5Y}nYa%=%xaGEPHN}t8t^P+&)BF&24hH6RA;%Xjsvjd+Cmc(XpW-0S( zOPEO*D)VNj$o(whq_r;7EA7GXksTt5%h_}4@zM#6?VT50#c-6g_PpVAc8D1?%xf)L zOy^e^Nv%TORwN)L*`+%?r${-euMvNOrCAF=_T<UY3W3uO5_=ECa@#>S;gg}W31TlP z9*dYRoEw4ZP<H{=4(7Nl(w&vK1(0Zx3!|B8(bvB_EvQR+j51B@VLa@YpNeshgP&$C z86NhMH#EwzQCIBCt0`D;MOfx5A7>)j+U%Am=ja|9Q$^%GleHqa>j98>5{DIBzR$zx z7Ll#d=aAWDCU<B}9(#d5v{{-bXBWfz%&t+^t~M_brc7@(HEpK7EG9lnUAFMsYaKYP zm%OHMyW<ZkA7XsA6V<kk=f~cO^RbDsZhzjI8*b5U%h$cznFuu2OdS-i59~D$MkQ{1 z*rYl+kYy^3wyF^~N?ChHt&|QEX|GHTG2ce<Bn(k@ADf7HwNG)-Dn!z6Y>yHV8#6n+ z0;v~!rd{7K=o(si<E4BlZ4`7rTtsLq%$E6WGv-IQVVr4~XNTDsWtUjDKx?{wS}%vo zI=m%a&Vbts$sW4CL$lFlY;ZQwDKc2A%w)S`$_%ccY$eW=8-uHR<dW!a)Uq9;CsSjv zNEqK8$f#8%cgQtSF@QE(d$;k?i|nrR`UZ5oXSG(*%D=Bx4i4H-`u;of4R9}CK7Z2u zJ#?5@_bW*t^>9`E)wx|*v~zq5BOq;z{MZ%0Nl<a;anQvKD_K^0Rt0k+)8?=mvgn8= zN19d9I~z4_e0@$w$*h<|>V+ScRZ&l?X)N0L26&V=i|3t^b>||{?wwf~jUYcUwaC3K zld0J0SYj)q+_=4v3UW$cr#n!|q$W3whqFehXyfRtBbJjoQilAvMS50ChZWL~m+~xy zg9L=cqsi9`Dm+UKLVF=%!Jw3|dsTk!ief>@k7mGhT4*(kXHA?#WJS-9=Q}ZL?D%@D zl8Kp`-d?TS+BMZXJ$5hzM){RoSu7mLN1<&xqjmhJwX~^|N)k5@tjhG#^gJ79Q%Red zX1AJ3JC<Ydq^;9PX)y{yMal5SVN6aVRhU`Vh7kYr(eBgX_V(R}!|wI&FP9(w>$_jS zeExX({@vZ>_0@OZe|LNN;p+0Y^xs!^-`yO3d-wkC`sVWLL;Cmk*PlKeu6{lIy8Gk* E0X-$<+5i9m diff --git a/tests/golden/static/static_clones.compact b/tests/golden/static/static_clones.compact deleted file mode 100644 index e2ea941d..00000000 --- a/tests/golden/static/static_clones.compact +++ /dev/null @@ -1 +0,0 @@ -CLONE DETECTION [██░░░░░░░░] 2/10 High duplication - significant refactoring recommended diff --git a/tests/golden/static/static_clones.json b/tests/golden/static/static_clones.json deleted file mode 100644 index 5533e02e..00000000 --- a/tests/golden/static/static_clones.json +++ /dev/null @@ -1,6045 +0,0 @@ -{ - "overall_score_label": "2/10", - "sections": [ - { - "title": "CLONE DETECTION", - "score_label": "2/10", - "status": "High duplication - significant refactoring recommended", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Clone Pairs", - "value": "1154" - }, - { - "label": "Clone Ratio", - "value": "0.8" - } - ], - "distribution": [ - { - "label": "Type-1 (Exact)", - "percent": 0.33362218370883884, - "count": 385 - }, - { - "label": "Type-2 (Renamed)", - "percent": 0.2668977469670711, - "count": 308 - }, - { - "label": "Type-3 (Near-miss)", - "percent": 0.39948006932409014, - "count": 461 - } - ], - "issues": [ - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int.go::NewInt", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int32.go::NewInt32", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int32.go::Int32KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int.go::IntKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64KeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::NewInt64 \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32KeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32KeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::NewInt32 \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::NewInt32 \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e int32.go::Int32.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e int32.go::Int32.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int.go::Int.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int32.go::Int32.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e int32.go::Int32.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e int32.go::Int32KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e int32.go::NewInt32", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int.go::Int.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int32.go::Int32.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int32.go::Int32.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int.go::Int.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - } - ], - "score": 0.24822695035460995 - } - ], - "overall_score": 0.24822695035460995 -} diff --git a/tests/golden/static/static_clones.perfile.json b/tests/golden/static/static_clones.perfile.json deleted file mode 100644 index 75972774..00000000 --- a/tests/golden/static/static_clones.perfile.json +++ /dev/null @@ -1,6046 +0,0 @@ -{ - "overall_score_label": "2/10", - "sections": [ - { - "title": "CLONE DETECTION", - "score_label": "2/10", - "status": "High duplication - significant refactoring recommended", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Clone Pairs", - "value": "1154" - }, - { - "label": "Clone Ratio", - "value": "0.8" - } - ], - "distribution": [ - { - "label": "Type-1 (Exact)", - "percent": 0.33362218370883884, - "count": 385 - }, - { - "label": "Type-2 (Renamed)", - "percent": 0.2668977469670711, - "count": 308 - }, - { - "label": "Type-3 (Near-miss)", - "percent": 0.39948006932409014, - "count": 461 - } - ], - "issues": [ - { - "name": "byte.go::Byte.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int32.go::Int32KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::ByteKeySet \u003c-\u003e int.go::IntKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::Int64KeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int64.go::NewInt64 \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int.go::Int.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int32.go::Int32.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int.go::Int.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int32.go::Int32.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e int32.go::NewInt32", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::NewInt \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e int32.go::Int32KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32KeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::Int32KeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::NewInt32 \u003c-\u003e string.go::NewString", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int32.go::NewInt32 \u003c-\u003e int64.go::NewInt64", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e string.go::String.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e int64.go::Int64.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.PopAny \u003c-\u003e int32.go::Int32.PopAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e string.go::String.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e int64.go::Int64.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.UnsortedList \u003c-\u003e int32.go::Int32.UnsortedList", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e int64.go::Int64KeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::IntKeySet \u003c-\u003e string.go::StringKeySet", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int.go::Int.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int.go::Int.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e int32.go::Int32.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e string.go::String.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e int64.go::Int64.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Clone \u003c-\u003e int32.go::Int32.Clone", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int32.go::NewInt32", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Union", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.HasAll", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.HasAny", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Intersection", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Difference", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Has", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::NewByte \u003c-\u003e int.go::NewInt", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Equal", - "location": "Type-1", - "value": "1.0", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.HasAll", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Has \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-2", - "value": "0.9", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int.go::Int.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Equal", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Has", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Intersection", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Insert", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Difference", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Union", - "location": "Type-2", - "value": "0.8", - "severity": "poor" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.8", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAll \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.HasAll \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.HasAny \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.HasAll", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.HasAny", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Equal \u003c-\u003e int.go::Int.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.IsSuperset \u003c-\u003e int32.go::Int32.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Intersection \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Difference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Equal \u003c-\u003e int32.go::Int32.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.SymmetricDifference \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int32.go::Int32.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Difference \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Union \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int32.go::Int32.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e byte.go::Byte.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int64.go::Int64.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.IsSuperset \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e int.go::Int.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e byte.go::Byte.Equal", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Intersection \u003c-\u003e string.go::String.IsSuperset", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e string.go::String.Intersection", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.Union", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e int64.go::Int64.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Equal \u003c-\u003e string.go::String.Difference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.IsSuperset \u003c-\u003e int.go::Int.SymmetricDifference", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e byte.go::Byte.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int.go::Int.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int.go::Int.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Delete \u003c-\u003e byte.go::Byte.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e string.go::String.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int64.go::Int64.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Insert \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int32.go::Int32.Delete", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int.go::Int.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "byte.go::Byte.Has \u003c-\u003e int64.go::Int64.Insert", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int64.go::Int64.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e int32.go::Int32.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - }, - { - "name": "int32.go::Int32.Delete \u003c-\u003e string.go::String.Has", - "location": "Type-3", - "value": "0.7", - "severity": "fair" - } - ], - "files": [], - "score": 0.24822695035460995 - } - ], - "overall_score": 0.24822695035460995 -} diff --git a/tests/golden/static/static_clones.text b/tests/golden/static/static_clones.text deleted file mode 100644 index 73dce7b0..00000000 --- a/tests/golden/static/static_clones.text +++ /dev/null @@ -1,25 +0,0 @@ - -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ CLONE DETECTION Score: 2/10 ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary: High duplication - significant refactoring recommended - - Key Metrics - ──────────────────────────────────────────────────────────────────────────── - Total Functions 141 Clone Pairs 1154 - Clone Ratio 0.8 - - Distribution - ──────────────────────────────────────────────────────────────────────────── - Type-1 (Exact) █████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 33% (385) - Type-2 (Renamed) ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 26% (308) - Type-3 (Near-miss) ███████████████░░░░░░░░░░░░░░░░░░░░░░░░░ 39% (461) - - Top Issues - ──────────────────────────────────────────────────────────────────────────── - byte.go::Byte.Differen... Type-1 1.0 - int32.go::Int32.Differ... Type-1 1.0 - byte.go::NewByte <-> i... Type-1 1.0 - byte.go::NewByte <-> s... Type-1 1.0 - byte.go::ByteKeySet <-... Type-1 1.0 diff --git a/tests/golden/static/static_clones.yaml b/tests/golden/static/static_clones.yaml deleted file mode 100644 index 0426bc31..00000000 --- a/tests/golden/static/static_clones.yaml +++ /dev/null @@ -1,4009 +0,0 @@ -total_functions: 141 -total_clone_pairs: 1154 -clone_ratio: 0.75177304964539 -clone_type_distribution: - Type-1: 385 - Type-2: 308 - Type-3: 461 -clone_pairs: - - func_a: int.go::Int.Difference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: int64.go::NewInt64 - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: int.go::IntKeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: int32.go::Int32KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: int64.go::Int64KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::ByteKeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::Int64KeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: int64.go::NewInt64 - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.PopAny - func_b: string.go::String.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.PopAny - func_b: int64.go::Int64.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.UnsortedList - func_b: string.go::String.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.UnsortedList - func_b: int64.go::Int64.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::IntKeySet - func_b: int32.go::Int32KeySet - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Clone - func_b: string.go::String.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Clone - func_b: int64.go::Int64.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::IntKeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::IntKeySet - func_b: int64.go::Int64KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::NewInt - func_b: int32.go::NewInt32 - similarity: 1 - clone_type: Type-1 - - func_a: int.go::NewInt - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: int.go::NewInt - func_b: int64.go::NewInt64 - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: string.go::String.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: int64.go::Int64.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: int32.go::Int32.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.PopAny - func_b: int.go::Int.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: string.go::String.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: int64.go::Int64.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: int32.go::Int32.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.UnsortedList - func_b: int.go::Int.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Has - func_b: string.go::String.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: byte.go::Byte.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Has - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Has - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: int.go::Int.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAll - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Delete - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Delete - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32KeySet - func_b: string.go::StringKeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32KeySet - func_b: int64.go::Int64KeySet - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::NewInt32 - func_b: string.go::NewString - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::NewInt32 - func_b: int64.go::NewInt64 - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.PopAny - func_b: string.go::String.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.PopAny - func_b: int64.go::Int64.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.PopAny - func_b: int32.go::Int32.PopAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.UnsortedList - func_b: int64.go::Int64.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: string.go::String.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.UnsortedList - func_b: int32.go::Int32.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.UnsortedList - func_b: string.go::String.UnsortedList - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: byte.go::Byte.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: int32.go::Int32.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: int64.go::Int64.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: string.go::String.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Clone - func_b: int.go::Int.Clone - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.HasAll - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.HasAny - func_b: string.go::String.HasAny - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: int.go::Int.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: int.go::NewInt - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: byte.go::Byte.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: string.go::String.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Insert - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::NewByte - func_b: int32.go::NewInt32 - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.SymmetricDifference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Difference - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Union - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Union - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Intersection - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Equal - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.IsSuperset - similarity: 1 - clone_type: Type-1 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Equal - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Clone - func_b: string.go::String.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Clone - func_b: int64.go::Int64.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int.go::Int.Clone - func_b: int32.go::Int32.Clone - similarity: 1 - clone_type: Type-1 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int64.go::Int64.Has - func_b: string.go::String.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.HasAll - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.HasAny - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Has - similarity: 0.890625 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: string.go::String.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Has - func_b: int.go::Int.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Has - func_b: int32.go::Int32.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.Has - func_b: byte.go::Byte.Equal - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int64.go::Int64.Has - func_b: int64.go::Int64.IsSuperset - similarity: 0.8359375 - clone_type: Type-2 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Has - similarity: 0.8359375 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Delete - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Union - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.828125 - clone_type: Type-2 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Intersection - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Difference - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Insert - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Delete - similarity: 0.828125 - clone_type: Type-2 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: byte.go::Byte.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: byte.go::Byte.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: byte.go::Byte.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int.go::Int.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: byte.go::Byte.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.IsSuperset - similarity: 0.7890625 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int.go::Int.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Equal - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.HasAny - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.HasAll - similarity: 0.7890625 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int.go::Int.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAny - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAny - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.HasAll - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.HasAll - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAny - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int32.go::Int32.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int.go::Int.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Insert - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.HasAll - func_b: int64.go::Int64.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: string.go::String.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int.go::Int.HasAll - func_b: int32.go::Int32.Delete - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.HasAny - similarity: 0.734375 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Difference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Intersection - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Difference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Intersection - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.SymmetricDifference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.SymmetricDifference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int.go::Int.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Union - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Union - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int.go::Int.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.IsSuperset - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Intersection - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: int32.go::Int32.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int32.go::Int32.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Equal - func_b: int32.go::Int32.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: byte.go::Byte.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.Equal - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: string.go::String.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.IsSuperset - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.IsSuperset - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Intersection - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: string.go::String.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: string.go::String.SymmetricDifference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Union - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Union - func_b: byte.go::Byte.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int32.go::Int32.Equal - func_b: int64.go::Int64.Difference - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Difference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int.go::Int.IsSuperset - similarity: 0.7265625 - clone_type: Type-3 - - func_a: int.go::Int.SymmetricDifference - func_b: int64.go::Int64.Equal - similarity: 0.7265625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: string.go::String.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: byte.go::Byte.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: string.go::String.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: byte.go::Byte.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int64.go::Int64.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Has - func_b: int32.go::Int32.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Insert - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int64.go::Int64.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: string.go::String.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int.go::Int.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int.go::Int.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int.go::Int.Insert - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: byte.go::Byte.Has - func_b: int32.go::Int32.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Insert - func_b: int32.go::Int32.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Delete - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: string.go::String.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int64.go::Int64.Delete - func_b: string.go::String.Has - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Has - func_b: int64.go::Int64.Insert - similarity: 0.6640625 - clone_type: Type-3 - - func_a: int32.go::Int32.Delete - func_b: int64.go::Int64.Has - similarity: 0.6640625 - clone_type: Type-3 -message: High duplication - significant refactoring recommended diff --git a/tests/golden/static/static_cohesion.bin b/tests/golden/static/static_cohesion.bin deleted file mode 100644 index 5e1b99e0baa49f4a14ddeea14dea622b90aadd7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13419 zcmcIrZExC05bjSw{&Xji$Zyz&Z%LY5(4}{$(&|2(PRKPIuxjj_ZPG?j{`<|EKw>;& zk9UDcC~D|4FV8cxv&;JG=F*DX4<DY5`%M~^ahA@bY$Xc##hCo_Y^32@7!%`8q{h%F zvP~X|`F)(oe~Z)7bI+F<{Ne4tnCI@$_-_*?arrb)#G^>ylfPxz!gv{Wp*i-^(7rvD z;%Hz+naAl;4K6OqM&b%lB48u8_TzM?aViP;T|CW1(*{A#$hTZOczb6Vp-nWyc!S=W z={9f)R1p?%7W<o+RRj`7D0cgs0X`g?umXHw^6qcsDiLAMla$v?{`JpDB#B5%MmaXg z<pTa8en?guUgCWAK!pr9Kq0eQo>r`3s2E@UeDz!m7i*e5aJOP^08=vNB7YG@xrK&y za!0hSc?q9FvhCGhOx;x`v%C}w(%w>}B99}=?$~xr-`l_UuuGIFwB~ucO@G9+aw*bi zwGQ*|`>ZxO18aCjeb0AYEAUL$8(EfzECy&L(@`}^dzUwvL#~OMPOjxjU-XJ=PgWnJ ze`OCB=@VuLiOa9Og1&S~E#8Isve~{E;&`{T%Jq+F`)gXXmO(Q}Zd8&j=*B7%<eJ#) zTqbU|4wD4qsiUg6#wzuRvPF08)!WNXH8p%q<tThO`${R`mCLoT4!?^nEq%Z(o7;Y5 zreJ9kc_FH?9<|buEGu~bV>8vf(;PCDc2`i%q80S9Ni%h_F!yyeds?pr=q36X-`|TI zT0h=C2+i2e$R4|v<$JDgj<^!SHf4}uq+2+(7qUY`SCc5@ZeD^KzQZ*jl`<10H+da2 zYWdK?TZd`aw#<?3Io8Mz#xA$*zZS~%`k_SkaExkkJ!|9y`}ZCdx|MEchad#Y?Ltgy z5y>9xjQT_-%4(DJcU-P!t1uUf&!Vj+JGiZnwp?zz5{pEvW~i#6YNdioLYq86A$yRm z7F!G%iGdCJQ6vJ{A>!?ejansxNJ9tBLH)(l+ih4}-~$hmoVo>WmyhIzAGp0p7Z-~K z#zWU^rDhjG!;Bp*E}1&+On(poH+8kzpzTqU+<{f5Bx*)GmKCBjH4KZgCC>H|7&8OB zO0pE4Zkp2ILXxAKIwb6Rc=F}?_}W{@^S6#FtnGVk$6_Y?)#|oge%d5u{E!H=Fgxwn z_NKj#g<cobX*v@RVIG#^=JmS*EW@YqgEL5A$BW!N<BOaQ;<T7;9_025S(U5x`cT_k zS6hHj<Zq09({luO0k?U`dK_HU<Wk*Ji!lOJhb;n9FQcu;1=<-v<|Hk5WeO0?;uMbR zP=y|DYn2p*s499CPcM%sq!!&Mq7E;FezcgGwv~_;GVQG@>wwr;zja;20M%PO;o&g@ z))7z_*{XgL-*x~u$46{_8OdFLX)o0YCouI|5do^hJSI#%C#Pv4^5&$d!8vDQK0Um; zzL@^2iwz(7AtmaN9G^9H(z2^)9{qD`nM{Rh!@&XdLdP+IFLjNz9M7Bz7dheNvNY`& zF4T3~imcNpTN11%200EPr-#6rs$^SS1yuFiw&3bymQKK_%0W4?_)))3@YJ^lT3bH? zREzxjVQ`g*_Cq}EM3~<ns2M^-5;%Dpbw*4&Cy)a;sUA4trbhe5&&_q>#u4=zD{&OB zRarQuhK5K$b-qbR_0CU8=mfdcQBmvdGYyh(b!q^1IjYW7EAq*5t6Qiomj<<3A4!~g zbrGciZmN?;y4e@<-=VR9yNh}JZ6hCmF(%K3*x!?%SdKX~mhuUfiDkYFjU@ZAe?(@! zduXPBhh+dJTrP95gfGY>Mwyji^8OQ+3oCgNWou(%o_n4jcyM#a8I5hvwBSOG{LB0? z%;OMlxx*tjSATvwyPXYwxfSUGzDLT^vzffRpBKNvDckOen|TpsIhZrg9mlrd0+Qpo zrtgdcxP!HGZtt%A*w}JBGjMFj_ifjQ>ntk~Cgo~}<2dJsFl$j1AzX)@7&l>@53)y* m!zItb&V#`j{F)Xq_&CVLeHfLQd|YNwWNR_lxlkA{FaHB-2(B{# diff --git a/tests/golden/static/static_cohesion.compact b/tests/golden/static/static_cohesion.compact deleted file mode 100644 index ba170086..00000000 --- a/tests/golden/static/static_cohesion.compact +++ /dev/null @@ -1 +0,0 @@ -COHESION [███░░░░░░░] 3/10 Fair overall cohesion - consider refactoring some functions diff --git a/tests/golden/static/static_cohesion.json b/tests/golden/static/static_cohesion.json deleted file mode 100644 index 1575d495..00000000 --- a/tests/golden/static/static_cohesion.json +++ /dev/null @@ -1,894 +0,0 @@ -{ - "overall_score_label": "3/10", - "sections": [ - { - "title": "COHESION", - "score_label": "3/10", - "status": "Fair overall cohesion - consider refactoring some functions", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "LCOM Score", - "value": "0.7" - }, - { - "label": "Cohesion Score", - "value": "0.3" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.9285714285714286, - "count": 130 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.07142857142857142, - "count": 10 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Len", - "location": "int32.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "0.5", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.3321315364073984 - } - ], - "overall_score": 0.3321315364073984 -} diff --git a/tests/golden/static/static_cohesion.perfile.json b/tests/golden/static/static_cohesion.perfile.json deleted file mode 100644 index bb1ecc87..00000000 --- a/tests/golden/static/static_cohesion.perfile.json +++ /dev/null @@ -1,2176 +0,0 @@ -{ - "overall_score_label": "3/10", - "sections": [ - { - "title": "COHESION", - "score_label": "3/10", - "status": "Good overall cohesion with room for improvement", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "LCOM Score", - "value": "0.7" - }, - { - "label": "Cohesion Score", - "value": "0.3" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.9285714285714286, - "count": 130 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.07142857142857142, - "count": 10 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Len", - "location": "string.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "0.5", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - } - ], - "files": [ - { - "file_path": "doc.go", - "score_label": "10/10", - "status": "No functions found", - "metrics": [ - { - "label": "Total Functions", - "value": "0" - }, - { - "label": "LCOM Score", - "value": "0.0" - }, - { - "label": "Cohesion Score", - "value": "1.0" - }, - { - "label": "Avg Cohesion", - "value": "1.0" - } - ], - "issues": [], - "score": 1 - }, - { - "file_path": "int.go", - "score_label": "2/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "LCOM Score", - "value": "0.8" - }, - { - "label": "Cohesion Score", - "value": "0.2" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.9444444444444444, - "count": 17 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.05555555555555555, - "count": 1 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "NewInt", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.1717171717171717 - }, - { - "file_path": "int32.go", - "score_label": "2/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "LCOM Score", - "value": "0.8" - }, - { - "label": "Cohesion Score", - "value": "0.2" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.8888888888888888, - "count": 16 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.1111111111111111, - "count": 2 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Len", - "location": "int32.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.16666666666666663 - }, - { - "file_path": "int64.go", - "score_label": "2/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "LCOM Score", - "value": "0.8" - }, - { - "label": "Cohesion Score", - "value": "0.2" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.8888888888888888, - "count": 16 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.1111111111111111, - "count": 2 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Len", - "location": "int64.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.16666666666666663 - }, - { - "file_path": "set.go", - "score_label": "1/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "21" - }, - { - "label": "LCOM Score", - "value": "0.9" - }, - { - "label": "Cohesion Score", - "value": "0.1" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.9523809523809523, - "count": 20 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.047619047619047616, - "count": 1 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "List", - "location": "set.go", - "value": "0.5", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.1384615384615384 - }, - { - "file_path": "set_test.go", - "score_label": "2/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "12" - }, - { - "label": "LCOM Score", - "value": "0.8" - }, - { - "label": "Cohesion Score", - "value": "0.2" - }, - { - "label": "Avg Cohesion", - "value": "0.8" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 1, - "count": 12 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0, - "count": 0 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.17333333333333334 - }, - { - "file_path": "byte.go", - "score_label": "2/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "LCOM Score", - "value": "0.8" - }, - { - "label": "Cohesion Score", - "value": "0.2" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.8888888888888888, - "count": 16 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.1111111111111111, - "count": 2 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Len", - "location": "byte.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.16666666666666663 - }, - { - "file_path": "empty.go", - "score_label": "10/10", - "status": "No functions found", - "metrics": [ - { - "label": "Total Functions", - "value": "0" - }, - { - "label": "LCOM Score", - "value": "0.0" - }, - { - "label": "Cohesion Score", - "value": "1.0" - }, - { - "label": "Avg Cohesion", - "value": "1.0" - } - ], - "issues": [], - "score": 1 - }, - { - "file_path": "set_generic_test.go", - "score_label": "2/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "LCOM Score", - "value": "0.8" - }, - { - "label": "Cohesion Score", - "value": "0.2" - }, - { - "label": "Avg Cohesion", - "value": "0.8" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 1, - "count": 18 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0, - "count": 0 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "0.7", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "0.8", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "0.9", - "severity": "good" - } - ], - "score": 0.17113665389527455 - }, - { - "file_path": "string.go", - "score_label": "2/10", - "status": "Poor cohesion - functions lack focus and should be refactored", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "LCOM Score", - "value": "0.8" - }, - { - "label": "Cohesion Score", - "value": "0.2" - }, - { - "label": "Avg Cohesion", - "value": "0.9" - } - ], - "distribution": [ - { - "label": "Excellent (\u003e0.6)", - "percent": 0.8888888888888888, - "count": 16 - }, - { - "label": "Good (0.4-0.6)", - "percent": 0.1111111111111111, - "count": 2 - }, - { - "label": "Fair (0.3-0.4)", - "percent": 0, - "count": 0 - }, - { - "label": "Poor (\u003c0.3)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Len", - "location": "string.go", - "value": "0.4", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "0.6", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "1.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "1.0", - "severity": "good" - } - ], - "score": 0.16666666666666663 - } - ], - "score": 0.3321315364073984 - } - ], - "overall_score": 0.3321315364073984 -} diff --git a/tests/golden/static/static_cohesion.text b/tests/golden/static/static_cohesion.text deleted file mode 100644 index a39afa4b..00000000 --- a/tests/golden/static/static_cohesion.text +++ /dev/null @@ -1,11 +0,0 @@ - -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ COHESION Score: 3/10 ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary: Good overall cohesion with room for improvement - - Key Metrics - ──────────────────────────────────────────────────────────────────────────── - Total Functions 141 LCOM Score 0.7 - Cohesion Score 0.3 Avg Cohesion 0.9 diff --git a/tests/golden/static/static_cohesion.yaml b/tests/golden/static/static_cohesion.yaml deleted file mode 100644 index 750ed405..00000000 --- a/tests/golden/static/static_cohesion.yaml +++ /dev/null @@ -1,573 +0,0 @@ -function_cohesion: - - name: Len - source_file: int64.go - cohesion: 0.4 - quality_level: Good - - name: Len - source_file: string.go - cohesion: 0.4 - quality_level: Good - - name: Len - source_file: byte.go - cohesion: 0.4 - quality_level: Good - - name: Len - source_file: int32.go - cohesion: 0.4 - quality_level: Good - - name: List - source_file: set.go - cohesion: 0.45454545454545453 - quality_level: Good - - name: Int64KeySet - source_file: int64.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: IntKeySet - source_file: int.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: ByteKeySet - source_file: byte.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: StringKeySet - source_file: string.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: Int32KeySet - source_file: int32.go - cohesion: 0.5714285714285714 - quality_level: Good - - name: Clear - source_file: set.go - cohesion: 0.6 - quality_level: Excellent - - name: Len - source_file: int.go - cohesion: 0.6 - quality_level: Excellent - - name: NewByte - source_file: byte.go - cohesion: 0.6 - quality_level: Excellent - - name: NewInt - source_file: int.go - cohesion: 0.6 - quality_level: Excellent - - name: NewInt32 - source_file: int32.go - cohesion: 0.6 - quality_level: Excellent - - name: NewInt64 - source_file: int64.go - cohesion: 0.6 - quality_level: Excellent - - name: NewString - source_file: string.go - cohesion: 0.6 - quality_level: Excellent - - name: TestStringSetList - source_file: set_test.go - cohesion: 0.6666666666666666 - quality_level: Excellent - - name: TestSortedList - source_file: set_generic_test.go - cohesion: 0.6923076923076923 - quality_level: Excellent - - name: TestKeySet - source_file: set_generic_test.go - cohesion: 0.75 - quality_level: Excellent - - name: BenchmarkStringSet - source_file: set_test.go - cohesion: 0.7674418604651163 - quality_level: Excellent - - name: KeySet - source_file: set.go - cohesion: 0.7692307692307693 - quality_level: Excellent - - name: TestStringSetEquals - source_file: set_test.go - cohesion: 0.7692307692307693 - quality_level: Excellent - - name: PopAny - source_file: set.go - cohesion: 0.7777777777777778 - quality_level: Excellent - - name: BenchmarkListLarge - source_file: set_generic_test.go - cohesion: 0.7857142857142857 - quality_level: Excellent - - name: UnsortedList - source_file: set.go - cohesion: 0.8 - quality_level: Excellent - - name: TestNewStringSet - source_file: set_test.go - cohesion: 0.8 - quality_level: Excellent - - name: BenchmarkListSmall - source_file: set_generic_test.go - cohesion: 0.8 - quality_level: Excellent - - name: TestSetEquals - source_file: set_generic_test.go - cohesion: 0.8 - quality_level: Excellent - - name: TestStringUnion - source_file: set_test.go - cohesion: 0.8 - quality_level: Excellent - - name: makeString - source_file: set_test.go - cohesion: 0.8125 - quality_level: Excellent - - name: TestIntersection - source_file: set_generic_test.go - cohesion: 0.8181818181818182 - quality_level: Excellent - - name: TestNewSet - source_file: set_generic_test.go - cohesion: 0.8181818181818182 - quality_level: Excellent - - name: TestUnion - source_file: set_generic_test.go - cohesion: 0.8181818181818182 - quality_level: Excellent - - name: TestStringSetSymmetricDifference - source_file: set_test.go - cohesion: 0.8235294117647058 - quality_level: Excellent - - name: Len - source_file: set.go - cohesion: 0.8333333333333334 - quality_level: Excellent - - name: cast - source_file: set.go - cohesion: 0.8333333333333334 - quality_level: Excellent - - name: TestSetSymmetricDifference - source_file: set_generic_test.go - cohesion: 0.8333333333333334 - quality_level: Excellent - - name: TestSetDifference - source_file: set_generic_test.go - cohesion: 0.8421052631578947 - quality_level: Excellent - - name: TestNewEmptySet - source_file: set_generic_test.go - cohesion: 0.8461538461538461 - quality_level: Excellent - - name: TestStringIntersection - source_file: set_test.go - cohesion: 0.85 - quality_level: Excellent - - name: SymmetricDifference - source_file: set.go - cohesion: 0.8571428571428571 - quality_level: Excellent - - name: TestSetClearWithSharedReference - source_file: set_generic_test.go - cohesion: 0.8666666666666667 - quality_level: Excellent - - name: Intersection - source_file: set.go - cohesion: 0.8666666666666667 - quality_level: Excellent - - name: TestStringSet - source_file: set_test.go - cohesion: 0.875 - quality_level: Excellent - - name: Equal - source_file: set.go - cohesion: 0.875 - quality_level: Excellent - - name: Delete - source_file: set.go - cohesion: 0.875 - quality_level: Excellent - - name: TestSet - source_file: set_generic_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: HasAny - source_file: set.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: Has - source_file: set.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: clearSetAndAdd - source_file: set_generic_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: HasAll - source_file: set.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: TestStringSetHasAny - source_file: set_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: TestStringSetDifference - source_file: set_test.go - cohesion: 0.8888888888888888 - quality_level: Excellent - - name: Union - source_file: set.go - cohesion: 0.9 - quality_level: Excellent - - name: TestSetHasAny - source_file: set_generic_test.go - cohesion: 0.9 - quality_level: Excellent - - name: Clone - source_file: set.go - cohesion: 0.9 - quality_level: Excellent - - name: New - source_file: set.go - cohesion: 0.9 - quality_level: Excellent - - name: TestSetClear - source_file: set_generic_test.go - cohesion: 0.9230769230769231 - quality_level: Excellent - - name: TestSetDeleteMultiples - source_file: set_generic_test.go - cohesion: 0.9285714285714286 - quality_level: Excellent - - name: TestSetClearInSeparateFunction - source_file: set_generic_test.go - cohesion: 0.9285714285714286 - quality_level: Excellent - - name: SymmetricDifference - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: set.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: List - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: HasAll - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: PopAny - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Has - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: SymmetricDifference - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: IsSuperset - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Equal - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: set.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Intersection - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: HasAny - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Difference - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: TestStringSetDeleteMultiples - source_file: set_test.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Delete - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Union - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: string.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: byte.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: int64.go - cohesion: 1 - quality_level: Excellent - - name: UnsortedList - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: int.go - cohesion: 1 - quality_level: Excellent - - name: Clone - source_file: int32.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: set.go - cohesion: 1 - quality_level: Excellent - - name: Insert - source_file: int32.go - cohesion: 1 - quality_level: Excellent -distribution: - excellent: 130 - good: 10 -low_cohesion_functions: [] -aggregate: - total_functions: 141 - lcom: 0.6678684635926015 - lcom_variant: LCOM-HS (Henderson-Sellers) - cohesion_score: 0.3321315364073984 - function_cohesion: 0.913608323772477 - health_score: 33.21315364073984 - message: Good overall cohesion with room for improvement diff --git a/tests/golden/static/static_comments.bin b/tests/golden/static/static_comments.bin deleted file mode 100644 index 00bc6065180b04c118bbe4964e145b83bf2a8a9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75710 zcmeI5%Wm6N5Qf_~F}UeQHAG!3*~?x6bdhCNK_M{mu|ia$9FhuP7=EojUyq#Far94M z%9g*ABD0HC*B@uj|IfLQ9R2?4xBTB<e*Wn?o3B@^P_?rUn_^kESJ~Oy>ugz8VODKc z??RoO<-M#~Z|ZrNU6jj^on`N?+HkyBXT5A$REtfq*nW8X^Lbf^dAqLTU&)T+f4~2m zS^S~By4?Qa_FqN42<@yYR-Zp;=Ic8CvwzcjjNQcluMJI`jNjH}wMa!jU(ZvaW8zbx z%c?yYrGkgmWqXwh9wQ$P62eoVw=Q3VD%9m1b^Bzu)PaVZCS;%PvQf(kot$6<-k=p; z@G<k0>nt``ZiG%&+ChI;@#wHC9+bSoNw=GHOm!w5@|&_z#mh1t9d)6zeiJ<DH-X2@ zbBed<l;Q`wRhFCG2ErzXT;QnLgwC2x@TlI0yP6}%UErYLh0Tt-;M0yeYPiv^=D5?6 zfwIXBjN(bVN$jZREIM{jrHKbijhvI9ZnNmribmL=(}hi1P2i~2gpST_Lp?3PPQ!<? ze&-Z7OuCX&I;eMrvx>7akIgL_JU6!tt#u?+-IYx$&I%kmCeYAXy<5R^dY5}I<hTo* zoLPq!owA$7PB`eS;w5-+UYC0>9D8Ir9TJ?$#!9Tx!78J0a?7T4)N7-JUQ;$%-j$v> z@ucKs)KSS>bg0gats0BfIi;hTQ#`0Sg_D{y>6GR~@9d}xo*Z?7lQn*9(XnN;NryCd zlJH#a#Hz7W+!h<F?;1I1HHD*PM)72QKM|+1!3&-oc!4L*Jaz{_N1dEqOgdHP<eVZ# zo{UY3-3pv5@Iq(HjNnnH3!OU~o5*^G)4Q@ky(>I%(y`^U5jpwzj^fF=Wg_>sNzDnI zEHfH9XmyDkt?y>&*yJ{`?yM)IlZuzn*>M*<YB-AyttBT(_v=b0H7B97niD)&W-T_i zusBKgO!CCqt5aUH*j#<L=-ikKoi&@_$wDLhwxn>d%t+wqv}(~gy9pf?{50WyUD=@C z6&^K*jk?m)k2+T4r?PqDN{z`*8#yOB$vf#cfwPL2y%QO`z|mS`(K*QrJ@L4MwMIk7 z?zl~p?%Yf|SLY;l((3|e^)7f&?<qG=Q-Py3)}lkl1!m-2eV52tz039q9d?UO4ZE<( zDkt#tqmD`b)Y>y+vztjz7(1vsGjzyq%0?A`D%&Tr&MBQ7bt`mg)Gan=Hleec)BOwi zV8^N8Nzn-$^_xZKWM|PS!wH*vjOx_dJ8@YTIw^O7qjI<Cl;DKT)>zp-#l%yg3p;V> zq~Im?n0L<W7M<Hc$mR{yoW)KUIjeXpcusUe2mO}!Wz&qN-r#ehx8a!T4Sd>>CrfSK zmrYc}gSi(xntP$ct`k1#Itz}K+q|z^8FKq<@LAtU_~g(FoZCj)i3cl=#Ew=R>2sNu z4%a)0o}FwgJXrB8JhYk3bE)>A<#X{ISnL!|T2ASy^Uew0h#j5cm5-L2T+YtY<FRrh zv6HeFIO=$zvy-eTJJ9sfG#?IM_~_sZoz%U+$!XStbIbbN!h@<Gr+XPM&lA20nCEhC z8bt>tUhr_a5k5P~TX<}e7dTmJ&F@Fygs1rA!E@DCgJ)f*d~l{4$UYvN;|FqYl!S}n z$+QbRY2>KeHFR_%JrF)R$D4Sr*roh(pgiTb1LgWPF`(s!X;<`$CmpYJu+l1=o!ylm zg9rVda(>sZYauNsbsmI|^br{Beb9WA^7hC`ZjX>@x5j+dhK731G9RbR{WxXrwVgjW z@MD=@NqEZhvaxkuhD!d#QXP5)la!-BNnUfNDZ`(p6#vu`KXqcLZ_0r+N8gwt;d)n) zTiu){RQo`mFy+!d$P+Fq2ZN+=bBbAnD%9nioN5Mod=mvQOK~MbtMDed5_8!}T*=?P z>|#^R+j3pa&e!wJDpYN;{V6+pd!1FqDr9F_wtF+P`_V_)-2d5do}FD3%O>=)uSm0I zzOEx?>yF6YG?DnGy8D&*f8F%_N<1f(>%_g2c|D2Mv#i6sUDsE!C>+Py>^bqAtfLN= zMYY%zi|vfP*#7J~@tn-V?4n$5B@tGa?Nzsp{Yw{=&iAhTLTC^B(CqGYJR_cw_K{tv zYs59;8rk>k#$EG0yQb>{HozLqFGWsZqjx+jo|R7r_K&YlTq~{>*UFRcGI_O)2T1Oq z|Dl7rz5oZT;`~x92YFUJD_!e~YsIzVT6xYY&K=|q`oB4-^W(8Ey@vA(**&lIjCe*K zyYJ%~agDe}_Psagu5s76Yuq*4_fnqoen_k%Pk4<6SV{X;b$r9eN3i?bP@XH#)l0l( z=elxTxvtW_UE>~f5B^vl?A~;WSg-rl*u4O@b;)z%xp~4_h3n=U{WaWE-|qO~x$)e@ zv+=z<#q86w_Qlr|d$jBJYf011!1eNtHH~Z6?1|-ialOR1BszC4bG^7;Trao11|Pq} z8`~6a9<&6U-SsSty#f!?rmza(yqV3{t8M?o`!<Dj+59tGhL2$x2kP~ofBg04^?{lZ zI?wZYmUVBV>3VU!xL&@rJaj*}ANFc#cNoER?pWiv$5%_9%-im*E)|zb$M05hskl^J zD)9~0gBhV~UnB>1I|13FQI4n7Q~IehwTtFKGk>>*+rn)j-(7GpTXgLJ;=s;6^pxIx z6N`(+MRQ-g-4<?({q7&fg|Lg}u%d}i3wQg@o5PfD#~tuY+mq?Z?D*u3OU0$)Qi+Ri zH-a0XyAhtygyvxDBxi?)UYmN(J?D>nNv!+(IZuGc>DN8x9&?YGUqJ1(<+-(`d!voR z<=UCMU&8FU-RG^S?VRWuat(Ryd;ICuAI2+tG!ZyZwRK{cAN)D9eO_YsaqRjYv1he* ztuC9;%l3T(-Ie3YDZVG=#KsN9Vo`@h(Z;Sh*IB!6i{)LTh1k7e*w1^hztj5s!DoYU zzn8r$9)2{AzqS4t>SDRPZx(Spo=)@A>3EutPx^!DaMJty*B>;8$Vc(hFOB9O_KnE% z!47#=Z@W=sXMN)@-~EkG{VL|S*RqHa^^YUqeJGag`@0JpPmjO8u2C;rg{CRCzn{G- t%KGSYqlu#rn_^kES4Y3bPpYOo5A{*Gx~$h9%W83Sxh&@4wjagK&41T@wAlav diff --git a/tests/golden/static/static_comments.compact b/tests/golden/static/static_comments.compact deleted file mode 100644 index 5bf2ad43..00000000 --- a/tests/golden/static/static_comments.compact +++ /dev/null @@ -1 +0,0 @@ -COMMENTS [█████░░░░░] 6/10 Fair comment quality - consider improving placement diff --git a/tests/golden/static/static_comments.json b/tests/golden/static/static_comments.json deleted file mode 100644 index 392b5b11..00000000 --- a/tests/golden/static/static_comments.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMMENTS", - "score_label": "6/10", - "status": "Good comment quality with room for improvement", - "metrics": [ - { - "label": "Total Comments", - "value": "301" - }, - { - "label": "Good Comments", - "value": "250" - }, - { - "label": "Bad Comments", - "value": "51" - }, - { - "label": "Doc Coverage", - "value": "70.0%" - }, - { - "label": "Good Ratio", - "value": "58.8%" - }, - { - "label": "Total Functions", - "value": "147" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 0.7619047619047619, - "count": 112 - }, - { - "label": "Undocumented", - "percent": 0.23809523809523808, - "count": 35 - } - ], - "issues": [ - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\tsize int\n\t\tminStringLen int\n\t\tmaxStringLen int\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - } - ], - "score": 0.5881985815602837 - } - ], - "overall_score": 0.5881985815602837 -} diff --git a/tests/golden/static/static_comments.perfile.json b/tests/golden/static/static_comments.perfile.json deleted file mode 100644 index 0285f2bc..00000000 --- a/tests/golden/static/static_comments.perfile.json +++ /dev/null @@ -1,914 +0,0 @@ -{ - "overall_score_label": "6/10", - "sections": [ - { - "title": "COMMENTS", - "score_label": "6/10", - "status": "Fair comment quality - consider improving placement", - "metrics": [ - { - "label": "Total Comments", - "value": "301" - }, - { - "label": "Good Comments", - "value": "250" - }, - { - "label": "Bad Comments", - "value": "51" - }, - { - "label": "Doc Coverage", - "value": "70.0%" - }, - { - "label": "Good Ratio", - "value": "58.8%" - }, - { - "label": "Total Functions", - "value": "147" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 0.7619047619047619, - "count": 112 - }, - { - "label": "Undocumented", - "percent": 0.23809523809523808, - "count": 35 - } - ], - "issues": [ - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\tsize int\n\t\tminStringLen int\n\t\tmaxStringLen int\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - } - ], - "files": [ - { - "file_path": "doc.go", - "score_label": "0/10", - "status": "Poor comment quality - significant improvement needed", - "metrics": [ - { - "label": "Total Comments", - "value": "3" - }, - { - "label": "Good Comments", - "value": "0" - }, - { - "label": "Bad Comments", - "value": "3" - }, - { - "label": "Doc Coverage", - "value": "0.0%" - }, - { - "label": "Good Ratio", - "value": "0.0%" - }, - { - "label": "Total Functions", - "value": "0" - } - ], - "issues": [], - "score": 0 - }, - { - "file_path": "empty.go", - "score_label": "7/10", - "status": "Good comment quality with room for improvement", - "metrics": [ - { - "label": "Total Comments", - "value": "3" - }, - { - "label": "Good Comments", - "value": "2" - }, - { - "label": "Bad Comments", - "value": "1" - }, - { - "label": "Doc Coverage", - "value": "100.0%" - }, - { - "label": "Good Ratio", - "value": "66.7%" - }, - { - "label": "Total Functions", - "value": "1" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 1, - "count": 1 - }, - { - "label": "Undocumented", - "percent": 0, - "count": 0 - } - ], - "issues": [], - "score": 0.6666666666666666 - }, - { - "file_path": "int32.go", - "score_label": "9/10", - "status": "Excellent comment quality and placement", - "metrics": [ - { - "label": "Total Comments", - "value": "47" - }, - { - "label": "Good Comments", - "value": "40" - }, - { - "label": "Bad Comments", - "value": "7" - }, - { - "label": "Doc Coverage", - "value": "100.0%" - }, - { - "label": "Good Ratio", - "value": "85.1%" - }, - { - "label": "Total Functions", - "value": "18" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 1, - "count": 18 - }, - { - "label": "Undocumented", - "percent": 0, - "count": 0 - } - ], - "issues": [], - "score": 0.851063829787234 - }, - { - "file_path": "int64.go", - "score_label": "9/10", - "status": "Excellent comment quality and placement", - "metrics": [ - { - "label": "Total Comments", - "value": "47" - }, - { - "label": "Good Comments", - "value": "40" - }, - { - "label": "Bad Comments", - "value": "7" - }, - { - "label": "Doc Coverage", - "value": "100.0%" - }, - { - "label": "Good Ratio", - "value": "85.1%" - }, - { - "label": "Total Functions", - "value": "18" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 1, - "count": 18 - }, - { - "label": "Undocumented", - "percent": 0, - "count": 0 - } - ], - "issues": [], - "score": 0.851063829787234 - }, - { - "file_path": "set.go", - "score_label": "10/10", - "status": "Excellent comment quality and placement", - "metrics": [ - { - "label": "Total Comments", - "value": "50" - }, - { - "label": "Good Comments", - "value": "48" - }, - { - "label": "Bad Comments", - "value": "2" - }, - { - "label": "Doc Coverage", - "value": "100.0%" - }, - { - "label": "Good Ratio", - "value": "96.0%" - }, - { - "label": "Total Functions", - "value": "21" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 1, - "count": 21 - }, - { - "label": "Undocumented", - "percent": 0, - "count": 0 - } - ], - "issues": [], - "score": 0.96 - }, - { - "file_path": "set_test.go", - "score_label": "0/10", - "status": "Poor comment quality - significant improvement needed", - "metrics": [ - { - "label": "Total Comments", - "value": "5" - }, - { - "label": "Good Comments", - "value": "0" - }, - { - "label": "Bad Comments", - "value": "5" - }, - { - "label": "Doc Coverage", - "value": "0.0%" - }, - { - "label": "Good Ratio", - "value": "0.0%" - }, - { - "label": "Total Functions", - "value": "15" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 0, - "count": 0 - }, - { - "label": "Undocumented", - "percent": 1, - "count": 15 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 String\n\t\ts2 String\n\t\texpected String\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\tsize int\n\t\tminStringLen int\n\t\tmaxStringLen int\n\t}", - "location": "set_test.go", - "value": "undocumented", - "severity": "poor" - } - ], - "score": 0 - }, - { - "file_path": "string.go", - "score_label": "9/10", - "status": "Excellent comment quality and placement", - "metrics": [ - { - "label": "Total Comments", - "value": "47" - }, - { - "label": "Good Comments", - "value": "40" - }, - { - "label": "Bad Comments", - "value": "7" - }, - { - "label": "Doc Coverage", - "value": "100.0%" - }, - { - "label": "Good Ratio", - "value": "85.1%" - }, - { - "label": "Total Functions", - "value": "18" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 1, - "count": 18 - }, - { - "label": "Undocumented", - "percent": 0, - "count": 0 - } - ], - "issues": [], - "score": 0.851063829787234 - }, - { - "file_path": "int.go", - "score_label": "9/10", - "status": "Excellent comment quality and placement", - "metrics": [ - { - "label": "Total Comments", - "value": "47" - }, - { - "label": "Good Comments", - "value": "40" - }, - { - "label": "Bad Comments", - "value": "7" - }, - { - "label": "Doc Coverage", - "value": "100.0%" - }, - { - "label": "Good Ratio", - "value": "85.1%" - }, - { - "label": "Total Functions", - "value": "18" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 1, - "count": 18 - }, - { - "label": "Undocumented", - "percent": 0, - "count": 0 - } - ], - "issues": [], - "score": 0.851063829787234 - }, - { - "file_path": "set_generic_test.go", - "score_label": "0/10", - "status": "Poor comment quality - significant improvement needed", - "metrics": [ - { - "label": "Total Comments", - "value": "5" - }, - { - "label": "Good Comments", - "value": "0" - }, - { - "label": "Bad Comments", - "value": "5" - }, - { - "label": "Doc Coverage", - "value": "0.0%" - }, - { - "label": "Good Ratio", - "value": "0.0%" - }, - { - "label": "Total Functions", - "value": "20" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 0, - "count": 0 - }, - { - "label": "Undocumented", - "percent": 1, - "count": 20 - } - ], - "issues": [ - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - }, - { - "name": "struct {\n\t\ts1 sets.Set[string]\n\t\ts2 sets.Set[string]\n\t\texpected sets.Set[string]\n\t}", - "location": "set_generic_test.go", - "value": "undocumented", - "severity": "poor" - } - ], - "score": 0 - }, - { - "file_path": "byte.go", - "score_label": "9/10", - "status": "Excellent comment quality and placement", - "metrics": [ - { - "label": "Total Comments", - "value": "47" - }, - { - "label": "Good Comments", - "value": "40" - }, - { - "label": "Bad Comments", - "value": "7" - }, - { - "label": "Doc Coverage", - "value": "100.0%" - }, - { - "label": "Good Ratio", - "value": "85.1%" - }, - { - "label": "Total Functions", - "value": "18" - } - ], - "distribution": [ - { - "label": "Documented", - "percent": 1, - "count": 18 - }, - { - "label": "Undocumented", - "percent": 0, - "count": 0 - } - ], - "issues": [], - "score": 0.851063829787234 - } - ], - "score": 0.5881985815602837 - } - ], - "overall_score": 0.5881985815602837 -} diff --git a/tests/golden/static/static_comments.text b/tests/golden/static/static_comments.text deleted file mode 100644 index 6b5e9124..00000000 --- a/tests/golden/static/static_comments.text +++ /dev/null @@ -1,17 +0,0 @@ - -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ COMMENTS Score: 6/10 ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary: Fair comment quality - consider improving placement - - Key Metrics - ──────────────────────────────────────────────────────────────────────────── - Total Comments 301 Good Comments 250 - Bad Comments 51 Doc Coverage 70.0% - Good Ratio 58.8% Total Functions 147 - - Distribution - ──────────────────────────────────────────────────────────────────────────── - Documented ██████████████████████████████░░░░░░░░░░ 76% (112) - Undocumented █████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 23% (35) diff --git a/tests/golden/static/static_comments.yaml b/tests/golden/static/static_comments.yaml deleted file mode 100644 index 1402d49d..00000000 --- a/tests/golden/static/static_comments.yaml +++ /dev/null @@ -1,4333 +0,0 @@ -comment_quality: - - line_number: 1 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: doc.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: empty.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 1 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 17 - source_file: doc.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 18 - source_file: doc.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 19 - source_file: empty.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 20 - source_file: empty.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 21 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 22 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 23 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 24 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 27 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 30 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 31 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 32 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 33 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 38 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 39 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 43 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 48 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 53 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 58 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 60 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 63 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 68 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 69 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 70 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 71 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 72 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 73 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 76 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 78 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 79 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 80 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 81 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 82 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 83 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 88 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 89 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 90 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 91 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 92 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 93 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 98 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 99 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 100 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 101 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 102 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 107 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 111 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 112 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 113 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 114 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 115 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 116 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 119 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 124 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 127 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 128 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 129 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 130 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 131 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 132 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: int64.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: byte.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: int.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: string.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 134 - source_file: int32.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 137 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 138 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 139 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 140 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 141 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 142 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 149 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 151 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 152 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 153 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 154 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 155 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 156 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 162 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 174 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 179 - source_file: set_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 184 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 185 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 186 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 191 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 192 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 193 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 194 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 201 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 210 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 220 - source_file: set.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 222 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 229 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 235 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 - - line_number: 252 - source_file: set_generic_test.go - language: go - directory: . - quality: "" - type: "" - target_name: "" - score: 0 -function_documentation: - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: byte.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: empty.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int32.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: int64.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_generic_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: set_test.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented - - name: "" - source_file: string.go - language: go - directory: . - is_documented: false - documentation_score: 0 - status: Undocumented -undocumented_functions: - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: byte.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: empty.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int32.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: int64.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_generic_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: set_test.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM - - name: "" - source_file: string.go - language: go - directory: . - needs_comment: false - risk_level: MEDIUM -aggregate: - total_comments: 301 - good_comments: 250 - bad_comments: 51 - overall_score: 0.5881985815602837 - total_functions: 147 - documented_functions: 112 - good_comments_ratio: 0.5881985815602837 - documentation_coverage: 0.7 - health_score: 58.819858156028374 - message: Fair comment quality - consider improving placement diff --git a/tests/golden/static/static_complexity.bin b/tests/golden/static/static_complexity.bin deleted file mode 100644 index 6cc8c139a293afbbfef0ccead038ad4ad41887ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33127 zcmds=-*4JT5XbwcM1JNXYz!FQlJrN=rFT_n)rYD=j@f`!V{^9YHHz|Z|M`tgAQ;aq zPQ1ouAVLZT%!iqso$u_f_v-znKKbv>n+NS~<vXz(_!B2sEIsktjqkPL_Xo|l7eX6q zm%?}E3p@Ndjzia<jYX`rv?y4Gj+orJUh<Mi#FJP=@%b!(W1j8LR`x78kp4e)L*c|h z2%oB*!*S={@q&dNyH4hedLIr2GvAHfUm|m)YqT^UJ|50KnTlmR*M=Q9jSJ6Bf;)KW z6wcMxlecdG;B|hKg0#<#&cN*SditQRTb=g6>bA7djebr%@k@B{ovuEQzKpIeZnVeN z7WxaEjv}22Uxe`G%M-UL;z14===Oj+>F<RC_uIzKAN*Ds-s{naYlQXPKP%gdP}??y zxC<n>iFM2B^#_L8HNe8~k5vSD<IWQBnoV30FlytPQB`~MTj7aVd|Y|43o)%?x9(Bx z7(&fn;L5jS1EL%5)kZ&i-*f7Z@7|8SeneXGTlelxgb=m`YH4MyxfHezZ4}4u?A;1s z|0Vu?w^+val?QDS+ALseos3{+Pw#fD=SMhACTJ7J-o&s2CNJH%N$MgVuGrMuJfuh) zua{hL+Xg4E+`qw~coqh(doY$k-h#L3ZuVBpyA#}vgQJnGv~e2*3x>V&gdKi!<N0`Q zhhq9!93_fF+<Lvi^)xV`NhtibnFx0C_qW}?YUIZvj6fKG^1zH+N=uMT`EUij?_OfP z79uE9r)vyMUGMkIe!FW~@Ln0>!u~1N4vYbo*AFCRzAHi~NxjtRW$y<J`6ZD`X*2!m z6Sk}nwTMAGWLVPMMz?3~JjxCFjC#m>YbG{R*T7u3J;>g{3uG>w5Fcb!mQa!oUBt>t znQ2#U2CQ0_e(HKg{#Y#S(2m9Xr;-pFx2pdIiliDk_ynG77`?sQVHe+~8P|4n;oqa! zrIU>`_s8m65S$NYRyrB3vl`KWs*tQfZ>xDXZWQ0xA(ZK`YMwG1mx3na_MSq7lgOCU zu>IuY4W`w>4+xJg(gllH;=IkUrz$6YGU)hzu@I1XI0w{kL*QR+UO8T=M44=!a9}Sc z<x@yS%`<UZ&%+vKeQ0!Xs(+v@(v9#GY0{Rq(cnefTg^S?tJ~Qq8n2d#C`CuOPSJd1 z8e0k`{eiJW26B|WpaIY9scU-F%IfxY)7U=B`}D7V6oj#uCTWcF(9s0IPr;JNMC-F1 z85NBKLGe#P73S-MQRIjtKN4ZXT#zivy)18D*VmiXx)h;Zd4Z2ApOg2;Opi@P$MWT= zQXx{25mQuYFECM-`ZtA_j-(A?RTNe;b&B!A#sxw{m=*c6G^cU>lGCfGVIG)-w3oEO zGkZx}Dig7C^2<6D7|>JRo1}FyEEz4rMb{AJ*(V|Lz(yUTR1Z>$Mv;|>jD`ywn1~3X zYzXOrRVIkEQ0D=7Bl2V7JjkF67$s3M*AMW(BUC#NxFYpG9;A5Pj0>Bja>fG@mA!!0 zGpk9?@^#P3QfRzzQ6XEAG+I&ONz|K|z|oVkc>t!%D6nq5s#bSI&jh{4mkb+x<>8XF zOlRPev`l9K2c0!7(|Hg|WjY5$sZ3`8NXT>sWNVq;Gnw{!v`lAUyMi*E0VyfdS+L1v zIs-%^t7b5SLTVufY?9fipc*&;rc1}Mo{s?>SwCXHMwR`lLOWvF4@JAZkHyIsG|4`^ z8nPo6jSA*rC(lVI(<hI%mO)roaPnw`c0GBnbP;{dVlbz_J~DDK?xAVZcp6zNi=v?( zEcLII>(w?Q%CNP<C^8ZjB3N)$v1X*y%)K)65RsX&Jakx*{E34*I(7w1m`b^0`sVy} zUwB%!%v!R14ji(+?pIMX9LX+N!vv5XabsXUbe$Fl!_mXsCvUX`4e#JMpygx=2XDkh z3kNcJ<@@9vlAeBd@@Qn$`{c>c!f<7c4Kl!Etre3lhO4gfeZ=}OUioH$Rz$k703~YQ zEZCs<eF-B>xg>=JoReH@bu8iwe|j;U<`|)_pl)q(DJ}l$!X#F`V_`Z;oq-3RtkA%O z5Y^%=*a$Jsh7D2UT-b8G01Gf&kF!9N2yz~L_)IhwY`7$6fp!U5Gf_!r!H3oyqVy_} z)znjtSa!hOvP!rkRvd^59~U}$g)I|8yenq{hfWEqFsK}{-4UZ$d<=(Psg#H9sQF!7 z=w!R_Fkz&Zp)zTKSgDT*8Wf?&sT=0RITJu)1c3?N+IEB(;=)*D8wwtw6d$r=Vn^wS z^GP%QXlYZJ^BlUjt8}});lpO58EpvKdI1|~C%ZmrL)g(Ha+8Ul@O(#qY-w|MHlL)E zJ~z9BN6p(CJTobEyPJ1QbE9YlTmKHfYuA8r_Pp=-+4D9#(5<WwZgw8uJ@%tfJA!90 z(@ne!A|e>6lFVPzNbRsEC@g$uNpLfDFll6kk=%TOpk!D2<3dR6QhHYvy&@EdF7|8| zidh;$9<(@!ZEvy@FLV=vjQtB@%uGynn%+=99}J8^yKlAOC6?J4zy=cw`*-#=a6$3) zC+@z}uwFK(J{67|!S0lkWdM_-ASUQ0{2Fs%d+~e{IYF2NzxH{%-yRtH!0K9h7cT0( o5K#o*SsQ8}f?&E68P5JV+wz^w0qralb`<#bttWO~raeCX56Fm)Hvj+t diff --git a/tests/golden/static/static_complexity.compact b/tests/golden/static/static_complexity.compact deleted file mode 100644 index 0b6b4bb8..00000000 --- a/tests/golden/static/static_complexity.compact +++ /dev/null @@ -1 +0,0 @@ -COMPLEXITY [████████░░] 8/10 Good complexity - functions have reasonable complexity diff --git a/tests/golden/static/static_complexity.json b/tests/golden/static/static_complexity.json deleted file mode 100644 index 247f8ffb..00000000 --- a/tests/golden/static/static_complexity.json +++ /dev/null @@ -1,908 +0,0 @@ -{ - "overall_score_label": "8/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "8/10", - "status": "Good complexity - functions have reasonable complexity", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Avg Complexity", - "value": "2.0" - }, - { - "label": "Max Complexity", - "value": "17" - }, - { - "label": "Total Complexity", - "value": "281" - }, - { - "label": "Cognitive Total", - "value": "19" - }, - { - "label": "Decision Points", - "value": "140" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.9290780141843972, - "count": 131 - }, - { - "label": "Moderate (6-10)", - "percent": 0.06382978723404255, - "count": 9 - }, - { - "label": "Complex (11-20)", - "percent": 0.0070921985815602835, - "count": 1 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "CC=17 | Cog=52 | Nest=3", - "severity": "poor" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=4 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "CC=4 | Cog=4 | Nest=2", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.8 - } - ], - "overall_score": 0.8 -} diff --git a/tests/golden/static/static_complexity.perfile.json b/tests/golden/static/static_complexity.perfile.json deleted file mode 100644 index a4fbe9fc..00000000 --- a/tests/golden/static/static_complexity.perfile.json +++ /dev/null @@ -1,2270 +0,0 @@ -{ - "overall_score_label": "8/10", - "sections": [ - { - "title": "COMPLEXITY", - "score_label": "8/10", - "status": "Good complexity - functions have reasonable complexity", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Avg Complexity", - "value": "2.0" - }, - { - "label": "Max Complexity", - "value": "17" - }, - { - "label": "Total Complexity", - "value": "281" - }, - { - "label": "Cognitive Total", - "value": "19" - }, - { - "label": "Decision Points", - "value": "140" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.9290780141843972, - "count": 131 - }, - { - "label": "Moderate (6-10)", - "percent": 0.06382978723404255, - "count": 9 - }, - { - "label": "Complex (11-20)", - "percent": 0.0070921985815602835, - "count": 1 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "CC=17 | Cog=52 | Nest=3", - "severity": "poor" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=4 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "CC=4 | Cog=4 | Nest=2", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "files": [ - { - "file_path": "byte.go", - "score_label": "10/10", - "status": "Excellent complexity - functions are simple and maintainable", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Avg Complexity", - "value": "1.0" - }, - { - "label": "Max Complexity", - "value": "1" - }, - { - "label": "Total Complexity", - "value": "18" - }, - { - "label": "Cognitive Total", - "value": "1" - }, - { - "label": "Decision Points", - "value": "0" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 1, - "count": 18 - }, - { - "label": "Moderate (6-10)", - "percent": 0, - "count": 0 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "List", - "location": "byte.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 1 - }, - { - "file_path": "doc.go", - "score_label": "10/10", - "status": "No functions found", - "metrics": [ - { - "label": "Total Functions", - "value": "0" - }, - { - "label": "Avg Complexity", - "value": "0.0" - }, - { - "label": "Max Complexity", - "value": "0" - }, - { - "label": "Total Complexity", - "value": "0" - }, - { - "label": "Cognitive Total", - "value": "0" - }, - { - "label": "Decision Points", - "value": "0" - } - ], - "issues": [], - "score": 1 - }, - { - "file_path": "int.go", - "score_label": "10/10", - "status": "Excellent complexity - functions are simple and maintainable", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Avg Complexity", - "value": "1.0" - }, - { - "label": "Max Complexity", - "value": "1" - }, - { - "label": "Total Complexity", - "value": "18" - }, - { - "label": "Cognitive Total", - "value": "1" - }, - { - "label": "Decision Points", - "value": "0" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 1, - "count": 18 - }, - { - "label": "Moderate (6-10)", - "percent": 0, - "count": 0 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "List", - "location": "int.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 1 - }, - { - "file_path": "int64.go", - "score_label": "10/10", - "status": "Excellent complexity - functions are simple and maintainable", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Avg Complexity", - "value": "1.0" - }, - { - "label": "Max Complexity", - "value": "1" - }, - { - "label": "Total Complexity", - "value": "18" - }, - { - "label": "Cognitive Total", - "value": "1" - }, - { - "label": "Decision Points", - "value": "0" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 1, - "count": 18 - }, - { - "label": "Moderate (6-10)", - "percent": 0, - "count": 0 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "List", - "location": "int64.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 1 - }, - { - "file_path": "set.go", - "score_label": "8/10", - "status": "Good complexity - functions have reasonable complexity", - "metrics": [ - { - "label": "Total Functions", - "value": "21" - }, - { - "label": "Avg Complexity", - "value": "1.9" - }, - { - "label": "Max Complexity", - "value": "4" - }, - { - "label": "Total Complexity", - "value": "39" - }, - { - "label": "Cognitive Total", - "value": "24" - }, - { - "label": "Decision Points", - "value": "18" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 1, - "count": 21 - }, - { - "label": "Moderate (6-10)", - "percent": 0, - "count": 0 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Intersection", - "location": "set.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "CC=3 | Cog=3 | Nest=2", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.8 - }, - { - "file_path": "set_generic_test.go", - "score_label": "6/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Avg Complexity", - "value": "4.3" - }, - { - "label": "Max Complexity", - "value": "10" - }, - { - "label": "Total Complexity", - "value": "78" - }, - { - "label": "Cognitive Total", - "value": "62" - }, - { - "label": "Decision Points", - "value": "60" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.7222222222222222, - "count": 13 - }, - { - "label": "Moderate (6-10)", - "percent": 0.2777777777777778, - "count": 5 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "CC=6 | Cog=4 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "CC=4 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 0.6 - }, - { - "file_path": "set_test.go", - "score_label": "4/10", - "status": "Fair complexity - some functions could be simplified", - "metrics": [ - { - "label": "Total Functions", - "value": "12" - }, - { - "label": "Avg Complexity", - "value": "6.2" - }, - { - "label": "Max Complexity", - "value": "17" - }, - { - "label": "Total Complexity", - "value": "74" - }, - { - "label": "Cognitive Total", - "value": "102" - }, - { - "label": "Decision Points", - "value": "62" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 0.5833333333333334, - "count": 7 - }, - { - "label": "Moderate (6-10)", - "percent": 0.3333333333333333, - "count": 4 - }, - { - "label": "Complex (11-20)", - "percent": 0.08333333333333333, - "count": 1 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "CC=17 | Cog=52 | Nest=3", - "severity": "poor" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "CC=10 | Cog=9 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "CC=6 | Cog=5 | Nest=1", - "severity": "fair" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "CC=5 | Cog=3 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "CC=4 | Cog=5 | Nest=2", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "CC=4 | Cog=4 | Nest=2", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "CC=3 | Cog=2 | Nest=1", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "CC=2 | Cog=1 | Nest=1", - "severity": "good" - } - ], - "score": 0.4 - }, - { - "file_path": "empty.go", - "score_label": "10/10", - "status": "No functions found", - "metrics": [ - { - "label": "Total Functions", - "value": "0" - }, - { - "label": "Avg Complexity", - "value": "0.0" - }, - { - "label": "Max Complexity", - "value": "0" - }, - { - "label": "Total Complexity", - "value": "0" - }, - { - "label": "Cognitive Total", - "value": "0" - }, - { - "label": "Decision Points", - "value": "0" - } - ], - "issues": [], - "score": 1 - }, - { - "file_path": "int32.go", - "score_label": "10/10", - "status": "Excellent complexity - functions are simple and maintainable", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Avg Complexity", - "value": "1.0" - }, - { - "label": "Max Complexity", - "value": "1" - }, - { - "label": "Total Complexity", - "value": "18" - }, - { - "label": "Cognitive Total", - "value": "1" - }, - { - "label": "Decision Points", - "value": "0" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 1, - "count": 18 - }, - { - "label": "Moderate (6-10)", - "percent": 0, - "count": 0 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "List", - "location": "int32.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 1 - }, - { - "file_path": "string.go", - "score_label": "10/10", - "status": "Excellent complexity - functions are simple and maintainable", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Avg Complexity", - "value": "1.0" - }, - { - "label": "Max Complexity", - "value": "1" - }, - { - "label": "Total Complexity", - "value": "18" - }, - { - "label": "Cognitive Total", - "value": "1" - }, - { - "label": "Decision Points", - "value": "0" - } - ], - "distribution": [ - { - "label": "Simple (1-5)", - "percent": 1, - "count": 18 - }, - { - "label": "Moderate (6-10)", - "percent": 0, - "count": 0 - }, - { - "label": "Complex (11-20)", - "percent": 0, - "count": 0 - }, - { - "label": "Very Complex (\u003e20)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "List", - "location": "string.go", - "value": "CC=1 | Cog=1 | Nest=0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "CC=1 | Cog=0 | Nest=0", - "severity": "good" - } - ], - "score": 1 - } - ], - "score": 0.8 - } - ], - "overall_score": 0.8 -} diff --git a/tests/golden/static/static_complexity.text b/tests/golden/static/static_complexity.text deleted file mode 100644 index a0035b1a..00000000 --- a/tests/golden/static/static_complexity.text +++ /dev/null @@ -1,12 +0,0 @@ - -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ COMPLEXITY Score: 8/10 ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary: Good complexity - functions have reasonable complexity - - Key Metrics - ──────────────────────────────────────────────────────────────────────────── - Total Functions 141 Avg Complexity 2.0 - Max Complexity 17 Total Complexity 281 - Cognitive Total 19 Decision Points 140 diff --git a/tests/golden/static/static_complexity.yaml b/tests/golden/static/static_complexity.yaml deleted file mode 100644 index 0c65f4f9..00000000 --- a/tests/golden/static/static_complexity.yaml +++ /dev/null @@ -1,1472 +0,0 @@ -function_complexity: - - name: BenchmarkStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 17 - cognitive_complexity: 52 - nesting_depth: 3 - lines_of_code: 71 - complexity_density: 0.23943661971830985 - risk_level: CRITICAL - - name: TestSet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 37 - complexity_density: 0.2702702702702703 - risk_level: HIGH - - name: TestSetEquals - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 53 - complexity_density: 0.18867924528301888 - risk_level: HIGH - - name: TestStringSetEquals - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 53 - complexity_density: 0.18867924528301888 - risk_level: HIGH - - name: TestStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - nesting_depth: 1 - lines_of_code: 37 - complexity_density: 0.2702702702702703 - risk_level: HIGH - - name: TestSetDeleteMultiples - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 22 - complexity_density: 0.2727272727272727 - risk_level: MEDIUM - - name: TestSetDifference - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 18 - complexity_density: 0.3333333333333333 - risk_level: MEDIUM - - name: TestNewEmptySet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 4 - nesting_depth: 1 - lines_of_code: 13 - complexity_density: 0.46153846153846156 - risk_level: MEDIUM - - name: TestStringSetDifference - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 18 - complexity_density: 0.3333333333333333 - risk_level: MEDIUM - - name: TestStringSetDeleteMultiples - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 6 - cognitive_complexity: 5 - nesting_depth: 1 - lines_of_code: 22 - complexity_density: 0.2727272727272727 - risk_level: MEDIUM - - name: TestNewSet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 5 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 9 - complexity_density: 0.5555555555555556 - risk_level: MEDIUM - - name: TestNewStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 5 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 9 - complexity_density: 0.5555555555555556 - risk_level: MEDIUM - - name: TestSetClearWithSharedReference - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 16 - complexity_density: 0.25 - risk_level: LOW - - name: Intersection - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 17 - complexity_density: 0.23529411764705882 - risk_level: LOW - - name: makeString - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 4 - nesting_depth: 2 - lines_of_code: 11 - complexity_density: 0.36363636363636365 - risk_level: LOW - - name: TestStringUnion - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 39 - complexity_density: 0.10256410256410256 - risk_level: LOW - - name: TestStringIntersection - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 44 - complexity_density: 0.09090909090909091 - risk_level: LOW - - name: TestIntersection - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 44 - complexity_density: 0.09090909090909091 - risk_level: LOW - - name: TestUnion - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 5 - nesting_depth: 2 - lines_of_code: 39 - complexity_density: 0.10256410256410256 - risk_level: LOW - - name: TestSetClearInSeparateFunction - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 4 - cognitive_complexity: 3 - nesting_depth: 1 - lines_of_code: 15 - complexity_density: 0.26666666666666666 - risk_level: LOW - - name: TestStringSetHasAny - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 11 - complexity_density: 0.2727272727272727 - risk_level: LOW - - name: BenchmarkListLarge - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 9 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: TestSetClear - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 12 - complexity_density: 0.25 - risk_level: LOW - - name: TestSetHasAny - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 11 - complexity_density: 0.2727272727272727 - risk_level: LOW - - name: TestSetSymmetricDifference - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 12 - complexity_density: 0.25 - risk_level: LOW - - name: Difference - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 9 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 8 - complexity_density: 0.375 - risk_level: LOW - - name: HasAny - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 8 - complexity_density: 0.375 - risk_level: LOW - - name: TestStringSetSymmetricDifference - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 2 - nesting_depth: 1 - lines_of_code: 12 - complexity_density: 0.25 - risk_level: LOW - - name: IsSuperset - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 3 - cognitive_complexity: 3 - nesting_depth: 2 - lines_of_code: 8 - complexity_density: 0.375 - risk_level: LOW - - name: Union - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: UnsortedList - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: PopAny - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 8 - complexity_density: 0.25 - risk_level: LOW - - name: KeySet - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: Insert - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: TestStringSetList - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: TestSortedList - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: TestKeySet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 7 - complexity_density: 0.2857142857142857 - risk_level: LOW - - name: BenchmarkListSmall - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 2 - cognitive_complexity: 1 - nesting_depth: 1 - lines_of_code: 6 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: New - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 5 - complexity_density: 0.2 - risk_level: LOW - - name: HasAny - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Int32KeySet - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewInt32 - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Int64KeySet - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewInt64 - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: int64.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewInt - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clear - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 4 - complexity_density: 0.25 - risk_level: LOW - - name: Equal - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 4 - complexity_density: 0.25 - risk_level: LOW - - name: Insert - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 5 - complexity_density: 0.2 - risk_level: LOW - - name: HasAll - source_file: int32.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: cast - source_file: set.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 1 - complexity_density: 1 - risk_level: LOW - - name: IsSuperset - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IntKeySet - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: int.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewByte - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: clearSetAndAdd - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 4 - complexity_density: 0.25 - risk_level: LOW - - name: Len - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: ByteKeySet - source_file: byte.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: List - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 1 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Clone - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Delete - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Difference - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Equal - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Has - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAll - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: HasAny - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Insert - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Intersection - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: IsSuperset - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Len - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: NewString - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: PopAny - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: StringKeySet - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: SymmetricDifference - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: Union - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW - - name: UnsortedList - source_file: string.go - language: go - directory: . - cyclomatic_complexity: 1 - cognitive_complexity: 0 - nesting_depth: 0 - lines_of_code: 3 - complexity_density: 0.3333333333333333 - risk_level: LOW -distribution: - complex: 1 - moderate: 9 - simple: 131 -high_risk_functions: - - name: BenchmarkStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 17 - cognitive_complexity: 52 - risk_level: CRITICAL - issues: - - High cyclomatic complexity - - High cognitive complexity - - name: TestSet - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity - - name: TestSetEquals - source_file: set_generic_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity - - name: TestStringSet - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity - - name: TestStringSetEquals - source_file: set_test.go - language: go - directory: . - cyclomatic_complexity: 10 - cognitive_complexity: 9 - risk_level: HIGH - issues: - - High cyclomatic complexity -aggregate: - total_functions: 141 - average_complexity: 1.9929078014184398 - max_complexity: 17 - total_complexity: 281 - cognitive_complexity: 0 - nesting_depth: 0 - decision_points: 140 - health_score: 90.0709219858156 - message: Good complexity - functions have reasonable complexity diff --git a/tests/golden/static/static_composition.bin b/tests/golden/static/static_composition.bin deleted file mode 100644 index af31175fec0ee00c5f518e2bcdba1f9d8ee58332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 269 zcmcJJu@1s83`EPok9h9TtQ|q(H;_r($fC88n@}Otf5(&sv9tN!o$h=*9m0OsG*7q~ z;Wg<Z(7RwkdqCf{5LE`w)v&O-=Ok&Y6G{0dW#ExEH<OTuDQ$`;%pX2$wdiN?hC4}8 c^T67|jU0Pfrupy7Khs4Ej_3F1J{`LC1<0pYIsgCw diff --git a/tests/golden/static/static_composition.compact b/tests/golden/static/static_composition.compact deleted file mode 100644 index c0fb0d1e..00000000 --- a/tests/golden/static/static_composition.compact +++ /dev/null @@ -1 +0,0 @@ -COMPOSITION [░░░░░░░░░░] -10/10 File composition analysis completed diff --git a/tests/golden/static/static_composition.json b/tests/golden/static/static_composition.json deleted file mode 100644 index efdf8834..00000000 --- a/tests/golden/static/static_composition.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "overall_score_label": "Info", - "sections": [ - { - "title": "COMPOSITION", - "score_label": "Info", - "status": "File composition analysis completed", - "metrics": [ - { - "label": "Total Files", - "value": "10" - }, - { - "label": "Source Files", - "value": "10" - }, - { - "label": "Source %", - "value": "100.0%" - } - ], - "distribution": [ - { - "label": "source", - "percent": 1, - "count": 10 - } - ], - "issues": [], - "score": -1 - } - ], - "overall_score": -1 -} diff --git a/tests/golden/static/static_composition.perfile.json b/tests/golden/static/static_composition.perfile.json deleted file mode 100644 index 451103da..00000000 --- a/tests/golden/static/static_composition.perfile.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "overall_score_label": "Info", - "sections": [ - { - "title": "COMPOSITION", - "score_label": "Info", - "status": "File composition analysis completed", - "metrics": [ - { - "label": "Total Files", - "value": "10" - }, - { - "label": "Source Files", - "value": "10" - }, - { - "label": "Source %", - "value": "100.0%" - } - ], - "distribution": [ - { - "label": "source", - "percent": 1, - "count": 10 - } - ], - "issues": [], - "files": [ - { - "file_path": "int32.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "int64.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "set_generic_test.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "set_test.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "string.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "byte.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "doc.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "empty.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "set.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "int.go", - "score_label": "Info", - "status": "No files analyzed", - "metrics": [ - { - "label": "Total Files", - "value": "0" - }, - { - "label": "Source Files", - "value": "0" - }, - { - "label": "Source %", - "value": "0.0%" - } - ], - "issues": [], - "score": -1 - } - ], - "score": -1 - } - ], - "overall_score": -1 -} diff --git a/tests/golden/static/static_composition.text b/tests/golden/static/static_composition.text deleted file mode 100644 index 040ac162..00000000 --- a/tests/golden/static/static_composition.text +++ /dev/null @@ -1,15 +0,0 @@ - -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ COMPOSITION Score: Info ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary: File composition analysis completed - - Key Metrics - ──────────────────────────────────────────────────────────────────────────── - Total Files 10 Source Files 10 - Source % 100.0% - - Distribution - ──────────────────────────────────────────────────────────────────────────── - source ████████████████████████████████████████ 100% (10) diff --git a/tests/golden/static/static_composition.yaml b/tests/golden/static/static_composition.yaml deleted file mode 100644 index e8563f6d..00000000 --- a/tests/golden/static/static_composition.yaml +++ /dev/null @@ -1,19 +0,0 @@ -breakdown: - binary: 0 - configuration: 0 - documentation: 0 - dotfile: 0 - generated: 0 - image: 0 - source: 10 - vendor: 0 -percentages: - binary: 0 - configuration: 0 - documentation: 0 - dotfile: 0 - generated: 0 - image: 0 - source: 100 - vendor: 0 -total_files: 10 diff --git a/tests/golden/static/static_halstead.bin b/tests/golden/static/static_halstead.bin deleted file mode 100644 index a23fa5967741325c53c2e3686910b5d511a5101d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35142 zcmd^|ZExeo5y$&c0=;&Kec~?HFAC%lIW#GXIze9qg`wCyTZk-OCHm6C;XXjWb$>&j zPNcZBM4c{Y$x3VZWSQp7&iv;;58VCc`>*w1|M!=_ydAx~pIudx`Rwv`JY81h_<D5y z>)X+6d{>UnM_-q-tJ}Nr;`fVcF`3<5l-1~Lw4C2BuFA`olc{`USyq=-xva9AIX|0@ zXE*ob8~H)~@9W8;ysGAlH@uPY^Vjp~y<A!6R%J>>=WTAR@#buFJ$d<Za&<qg_^b<= zb$R1IcQz_tzML<r(Rnb=7FmooSNuOO;@PO0+?AKr{PNXeezO?g@%zfS%oas%3KK)_ zqUHMKbn?1fl-HNP-QO%n=PJv!Qp#%njtYF}Y;-lhdo?ZpJE`7WPRrNjl$-uxa&tR+ zclJReKO^dkSye8U$c5UH`nV})<zjNRhhBmWnG%H*x%LX(wDp2i(KlXUtrlH4r8A|S z3CioBbV4m!8Ize%<Uygg!Xk24E5_wMIA=^@3m^EB)XPuh_2m9;Q!^OIcmKJ^kPfUC zU&O4i)+%jPq4lO(a+i5$5DV$+uWPl~;G@f&H@VG&cZE-=#bAe-w!TnUfpO8~NxhgD zopV;MXoC)Edr9l%m)YS>#d|C`YGd*u&$Si{Z+H-vQkakz-YAskb3|G>koHcg4CnAF zI2+JdLMKt{Oe|28?F<5^RibnZXpL(q^umP_E|gZu2Sj<qV20z0$H0rI02D0OX}6)3 z+-F5-sKT<|yj55q^Fn!_!V(>QA*wJcw<tS`LnORL1#gw&>KF-jIK(<0RIwFRhHE3w z^USFN56JNVZN%T3Hi8x4RO=UL^T9|o-Xw71d<+^(N;wWr#}%B$h?F<{E2S9?lv!`U zS&&va&4+e3lAeIGKfvZrXPAc0l`?#<WHB2mL7?cg1}Ow03xoZvG$N>sQ}H%pSYu_0 zJAp@=Xfjtsqq&NS_2oNpM{|)#5VNSRP!`SZu8;%A;anpyNUt<T;-VKfZ>ocPTg%T) zX9Sia1mbq#Qm~<|#;CLpK}C?kC1Hc;6oV)D<^VE2g^~v<pdU|%JBf=$W*{LJ`;D&9 z8y2Bs=3y&^TYYWB+!%t*WTCao156|3BOnYh8<dSc7AQ^-scR8{dGT-|3)Bp<h_d>Y z6@%E!uF3<4U0@Ns)Ve5QE-}P$V@>lgdA0@*`K|#VgbWxS=3*p>n2_QlxgmsMRfI1C zeX+oALN_>G<Y(UMNYK)Ql(TY|Eh-vrHP(3urh{B67@2i)>kY>t{MPW1V^RVqjhG`M zKwoos8ca{8P{d(Y+7qTNu?(icZzuEuoO<nZf)^`;?+)=IjHaBHRrwQqdjjV@G+{Fm zd06_WU>IJ(HQV~pA#3_nF8pEzG{n!yPk|<v$3h^q1GB~zSwXz@#)aUq&}8IM5_y7U zbwb1n4OiM7XUgj0&D~vz0bOk|p8bQ7Xi2kx8a#xRnYf8PSkZd}dLNJ0TEe|Zk`%zr zM=V~Nf@3%f;DdUUTm~nSKhQxR^rSGyAW^L-0PZ1r`ru(Z*wb(~Gd5<y6+~=SSsA?% zp9Gv)-}Qe@GZF;>xOD=kl*a<QcI0)2?*LC8H`O)1b=4DM)X*EKHg_Q)wX+rdz5L_5 zyI0klzQp+>Ovm6s5DhqKB(%utH}MCEkt}BAQUH0#Jn<M>PXZ-Gu&WDf1vaeVC4@ov zaMDyrDS#R>=g3z+IzSm882_%0wTHPF;xU$DauWK&cTk7hU8#hak`tc{1qc|jDG;}# z5{-G-2&HoxO7E3WzJ&XbErZielQ<IQCBpA)AQ;9s({jA{*QC0=xE)iS_-FZPV!1$p zwifaVSAqZfjaVMDRr35kkzT6`#+PTrxf}xQeUZ$ewIYR%#6wKYpsA2HAC?66m}^Ix z9<<bMc2d>0+-O+w$&miGN*o)g0_3!H(NF~<)+tqpiqNJ~j)aE^FuTGK!?ChHRgjl! z7pT|7ngi{qik~S!t*qw7?4o=%UW}{q`}bA60~0`9NP>qHAS@^uH_0*Z8;vFPU43n6 zDg=`O6S*!Rv|6(6j(@?tslDMe)O1Kuh1w<D3;N*l+)Fu75{Dt~1^yl$ddUVdJV=6e z3<gIMRA_KNDkrUS3s6L%##vp|v0m^Yr3o)_h)CygPnG5~LWzj`B%(Pr7Xzi<$$Rd` zzn2f-n>3H%FuP3y5R-MjM3iVmA_`S`2C*Zl$dw`6N@>H}O!=sTHMs(*a;dc?3}a7q zhzI?x`~)$Xq5`DXk@P@Y!MM0#7&aqOe2=bmnUbHc!V!sZ2JMGxP>3={(79tIQmU%+ zFeyY?Qq{CpD3Wpsyq44fR=J~*h*rd1R($SIrb3=1?wez;QkJf3&P`{bAl-Ul4=bJM z5Is!-4Z@H`s{*I!=p+KxA&9l%J;n#g!@3NP_>8ILq*8=IirPbzvI5(rLiQ(cWF{E} z*c=mg6b*%*HbErMnsv$$HBN)yQ`&@s76h!0CY;S;PArL*4@ugU)&#T&&l0}^X@-?4 zX}^@$KTekECeTNyLW<R7y9IVl8-o&iTNemso7#ZIW@J6q=8=#<g*&AV_%X>f)hh5A zyn>>o^#QY|JgKSVNbzS^NUfvr%2j$*vInv%DlRHWV~c{EEcd1=^4NNT6DxPIk#Y=5 zq>n*$vnZta*0B<!B_YhEszY%Bpb%Ym9Y)Xtwny-e6hWn#v{R&cJX0RV(jrlo6a$w< z5m4_9Fr^gP2rH0UVh$h^3Oa)bmlB;{aqt*?gF2EdcUp9^2Z=0ow}_CP6#d8fAI;uZ ze>fQ^b7#4skj+;^WY}1{`j!xxG*Dn~{lA{oruET$?IiPu-msYekK+YR-&AjSe^@}i zK!%AeTc`l7l}%Rw4x8k2?Fn@1vM7lxzHjJIrK|zfcu!_SV4&5EIi$%ap@$j*_n`TP zE)h48@PrL>_+_?y6f1`rh4enjx{-!VU+}%Hhs_!+KVBh%6w=^YhAulkM2!$1>LLP) zz}f**DGdOHNv}Yq(o7fk@_1z$5H{3MgJfC#FkXH&dvjpp2=wG^0I=zR3at=dZqN!E z!mSs}wo(cRK)2Y!7U1rZi>7p<D4)pBh_;S|w<j}^M3?ozIGxani1%(cj&G*(S<+R8 zSw*0@rLcd(CKYH(fNcc;o*S>V2KaDBlt6FTg^m(#J63^vSmrCpx!@hFxe@Q{#tHhN ziu@10DcucBY!?l*g6AXFF+6;da#|Zf`CLb`#v0*UR<Y6jHSh?GUwRv~^-`Rh3S&<3 z7%-uP2HTL#E~N*`hZZlQc7`Ifu(|D)(A&itW(m@smb@ZJwObnUZA(DL_Nx&?NJ%N6 z7WEvAl=IRmppOGKD0D<oO&C{N1vE5-jlpN}1u~RfR6xd&0)~MK0Kwo)BwQrMEP&i! zW7X-Uv|kNRphRqdM@AepmQ1H6<qG8U!UsSaD)rb)kN6bX(ItV3Yy|qYi}-!aaiIJV z8`Mq*Gf;qwwW~Jd#{kw6Ypg}4HLw=OqDY$(0!N0g$Pr-83USg?Le%211~i8HU|cF) z$=e7B^{B_kuXj%Khh_#j0nz|~;*t${rS;ZoORl8{rKp<2Dkwcj^*j|1H8WPU9LS9r zf1zStn^PD07z;r)jz7g^Xov5_nRcvv4_khQT3n!gLrY!ge;un@>S!rL(0&yPjl*(S zX6k#mEU9Hu9|Q|xKP@~!h9(G~NdO={Vw^~z5{ixO5RbP5;EUzO{VN%zOJ<Y<wIC6X z+!3RI#)Qo}gK4D;)Lm(8GoT>Te}Nui!VB;dExb@k{s^$7#m3!88^!zlL#21S)c=^j z>fu9&m3Y0*8kob}@p;^qc4%_WiB2K#f&QdrPcI`K@TtiR7Fa_3bb0{4*W|_|uV|Ve zk4d)uezTI)9NuA!Eo)(T3iqHqRNtUQ<Q!B-dTL3Uo)PIY-6deY=;`)s0ADLty0{ys zwQ;$Bu0-XLR2_Oko&p`z8>&HZZ4hX100x#A^J08Y@<e_Y&+V{5lt;l9N>!I+Aqr_{ zV9bSbfh26qB%sS(Frc6R4f6)-jKapn6>B5j;KT5o_6ze~!5~zc$SGYeY>VILzB?LV zoEQ|K0w<P7X{N_J01Of}mVx|&wb(@gFJ{Y<x#mRfJXiraRdeV(vow+yZ+Zs<u+8%G zT7Ehiq=3xm1;)2zVj8~CQ2>=#sVh?BW|83sy$T?V)Xfoa)k+h2J0N~rj}9C_0~AT+ zCQ-aVos3a6c>y%8r2rYdS(yMiVPGoSMpjwVgF(8E1|$wjj5U%8GcC~*2p=X+7=fce zL${PGZP!3gCQug`hgpI3AboT2W<v>$=zqv%r*2CQOra1$gbngXw+nqGjc=;!U(K$+ zy1q^=|3?7gHa#%i+#HZ#Diqp4r&3x3m~SdP5MLmB6bGgDjH~q&1*k_1*Ttg^?#ZmO z=48!a7fyWI8u%8r-ZQ`XYz1GQHuj|fpVcN14SJAj8_qo;ynn>Ze|uAvCoA`hM;{kE zZ9Cru+)sM((~kZOMLz9F_Nb4a^lCpUIg5U>u)YhlJy#ZUvWvdW+Mg$V!_7}v@Le4J zxdYv4Tfifk%PB8BrTJ7<ZXmc*d5=69!vqzZN=R9JvntyxO3U}_sw~N^p|Qzh{Pe9N z`&J~XveLbvJ4gSYu>&SrWhyY$pQLe5BhT}2kQAonQ`;+)hbqlmwJCGE8`JE`IzHPe zJ*Qe*DIs?es;AmMoTQpUo=&W1^lAIZ>{we$<|J0-^_*QzWZqaFx~DtE!*mHD<(b!( zrAX<z+j!@Z68cb5->(Ovd-f;UXu$Ody$5WafYkf2(qj_4$8A~cp=I<Fkoxo&n^o<Z zzy1U(P7`qrYi#Yns6K;_j2p7)3$qmoSYsryGnjH9RJ*8)hdsi7iiLkt(tl0SxJUNG z2eT(7c@MxHUh3lr;ll{&abY@%b&rjI0`Bs}jJ)qDO(Z|;l0PxIlk-3a7y3AS`x!E} zPvF>3X|2;k;CsZb&VXBknRy0Y6I_fLVct$AmAj<QkIX9{`~C057N67#3<t<R|Mt_i zf7EwA7@+@rtv}+nsQw59NSn<`xa`orQTO4dPmcO&?e7z%|9sPb^hxC5xFaLajYo?b zv_c-WOq8wLGxt3Y(a08m+l%p!in5n4d`@YSPyX#=Q=e0a^rOtjWmah!j?SLaVz3m0 z!`qWIsKH_4b4u|jQXQL*_W{+w3e4wRk3JEO$~Cqb)lmtPMyow?=qG1ZM<&CgR&`{u zB;EW7136E{ef-!B8{yWwwK90)9h=kE`+PO1J9{fN5+|qF2Y^Q(*$|BVaCrneN#=R_ zpC?Os(qDG@e>ia@(a4mebQ+JElk;rn_B{^)4>v&B5L$MZWn%`1%p79V&RANNe#2I- z>{7~Ri|p9KeyxB>CxwyyQP|Xkja6LEQ_XD6n64kP;|F_1$ngPeC*^i}@HaJHb=nfM z+sg|X744Y!9;E2lS9%X>egYy7V~s~9aB7namial#JTzZBwv|roXLtG-mfE%E)b0DZ zg{p@Tos#-=VGNnSW|piY^l6hZ;oOORh)!GR2lKY3v(_h0$B_G?9v1BgzE6UD8qs=a z#q0#6eh!PBz_b&0?o=V+nXdGyi+wdtpT01VoE2>5CZ~<C*Bx~)zHe8_I38*Jkb@lP z$!^hX?#cdM9MzNDBbeWiy)@Y)S9WBLZ2#9&CTo9+zq`FS*t1{e@XVt=#H#V+m^@$B zDv35KqN)06qBtmLgSEE*OwAUuC2Q-swq1n&u}mH<K5(74N!(cF#78^wVhM8Mq&!9$ zIpwB4hC5LK-bdd4MIKyANJ#Qzm$_f;!R+J&#NH$KI032midn}bcM8iu@1c&p*~(di zS8Dc*qqsI+>4-%4r~TR&o%bM_PhX;8@#cxkGaO!?xIp_L$7dJm-@>}&&VReFCiB_o z{OxGUX-em8(S0_$E3YT_ccXLJCE|8+bIaRjqu1r)&E><t)VqiGFCTUmxP1Bk+{@+Y z{MWam+4!y;osT~3wtZ2TJo-+6Tpus;dOp3E-;@&%IB<?*5p2EJb7Q_Wa12VpvE6L^ zDu;8)$)z27<P-;)$=On}hcaF8`b?G%KVpA+InjvYES0p%&qj;M^7qSW`MR8P?>~L_ z?TcT28ofhH<C~jBdBfqJqM~YEji*g<Iqwe5yuWb{VUQzT*u9ve2RV^N*mslJLMG(k zK#oh}h>+0S7@h4f*v^)-blCNnLoJhAKeOev;v7&8f#H-_j*3d3N6t3-<Ob~i&-TBx z!0TD(Wi^>yRhM%%`5#yF1*=r=gwcF^cKz_~gMjT%Hr`u(?DhO={M-F>ym(VzYg*23 zDy)Rda(?0D4sBhd%ZE4Fot@1=v>Z1Z*_T{4Y`!hW)9Utec{N|idMkeKu3Rq19A0%k rV$X;#ei%=eRXM)?Vx5<K@#PnPl+)>#*zEmPb<Y{S{P4OQy?gh6Tu6>A diff --git a/tests/golden/static/static_halstead.compact b/tests/golden/static/static_halstead.compact deleted file mode 100644 index d0b55746..00000000 --- a/tests/golden/static/static_halstead.compact +++ /dev/null @@ -1 +0,0 @@ -HALSTEAD [███░░░░░░░] 3/10 Moderate Halstead complexity - acceptable diff --git a/tests/golden/static/static_halstead.json b/tests/golden/static/static_halstead.json deleted file mode 100644 index ceb37430..00000000 --- a/tests/golden/static/static_halstead.json +++ /dev/null @@ -1,924 +0,0 @@ -{ - "overall_score_label": "3/10", - "sections": [ - { - "title": "HALSTEAD", - "score_label": "3/10", - "status": "Moderate Halstead complexity - acceptable", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Distinct Operators (n1)", - "value": "54" - }, - { - "label": "Distinct Operands (n2)", - "value": "41" - }, - { - "label": "Total Operators (N1)", - "value": "96" - }, - { - "label": "Total Operands (N2)", - "value": "209" - }, - { - "label": "Vocabulary", - "value": "95" - }, - { - "label": "Volume", - "value": "2243.6" - }, - { - "label": "Difficulty", - "value": "132.6" - }, - { - "label": "Effort", - "value": "714652.1" - }, - { - "label": "Est. Bugs", - "value": "0.7" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 0.8156028368794326, - "count": 115 - }, - { - "label": "Medium (101-1000)", - "percent": 0.1773049645390071, - "count": 25 - }, - { - "label": "High (1001-5000)", - "percent": 0.0070921985815602835, - "count": 1 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "effort=72439.0 | vol=1530.0 | bugs=0.5", - "severity": "poor" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "effort=37694.5 | vol=689.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "effort=35426.9 | vol=622.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "effort=23594.7 | vol=512.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "effort=23264.8 | vol=541.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "effort=18401.0 | vol=737.0 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "effort=16400.9 | vol=660.6 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "effort=11571.0 | vol=555.4 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "effort=9773.4 | vol=498.3 | bugs=0.2", - "severity": "good" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "effort=9370.1 | vol=369.9 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "effort=8517.0 | vol=346.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "effort=4707.8 | vol=239.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "effort=4479.5 | vol=255.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "effort=3707.3 | vol=255.2 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "effort=3697.7 | vol=227.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "effort=2905.1 | vol=223.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "effort=1949.4 | vol=200.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "effort=1875.5 | vol=205.1 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "effort=1796.0 | vol=146.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "effort=1775.6 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "effort=1772.1 | vol=182.8 | bugs=0.1", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "effort=1135.2 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "effort=791.2 | vol=137.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "effort=491.7 | vol=124.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "effort=483.5 | vol=150.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "effort=479.2 | vol=89.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "effort=445.4 | vol=93.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "effort=395.3 | vol=109.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "effort=364.6 | vol=78.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "effort=352.7 | vol=72.3 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "effort=299.9 | vol=91.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "effort=293.6 | vol=77.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "effort=291.3 | vol=92.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "effort=269.5 | vol=58.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "effort=244.7 | vol=56.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "effort=207.2 | vol=66.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "effort=181.6 | vol=68.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "effort=168.3 | vol=64.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "effort=147.9 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "effort=115.1 | vol=51.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=109.6 | vol=46.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "effort=107.5 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "effort=92.0 | vol=50.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "effort=77.7 | vol=46.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "effort=64.8 | vol=41.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "effort=41.1 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "effort=40.7 | vol=23.3 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=40.0 | vol=30.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "effort=36.2 | vol=31.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "effort=15.5 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "effort=10.9 | vol=18.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - } - ], - "overall_score": 0.3 -} diff --git a/tests/golden/static/static_halstead.perfile.json b/tests/golden/static/static_halstead.perfile.json deleted file mode 100644 index 1ef3a41e..00000000 --- a/tests/golden/static/static_halstead.perfile.json +++ /dev/null @@ -1,2446 +0,0 @@ -{ - "overall_score_label": "3/10", - "sections": [ - { - "title": "HALSTEAD", - "score_label": "3/10", - "status": "Moderate Halstead complexity - acceptable", - "metrics": [ - { - "label": "Total Functions", - "value": "141" - }, - { - "label": "Distinct Operators (n1)", - "value": "54" - }, - { - "label": "Distinct Operands (n2)", - "value": "41" - }, - { - "label": "Total Operators (N1)", - "value": "96" - }, - { - "label": "Total Operands (N2)", - "value": "209" - }, - { - "label": "Vocabulary", - "value": "95" - }, - { - "label": "Volume", - "value": "2243.6" - }, - { - "label": "Difficulty", - "value": "132.6" - }, - { - "label": "Effort", - "value": "714652.1" - }, - { - "label": "Est. Bugs", - "value": "0.7" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 0.8156028368794326, - "count": 115 - }, - { - "label": "Medium (101-1000)", - "percent": 0.1773049645390071, - "count": 25 - }, - { - "label": "High (1001-5000)", - "percent": 0.0070921985815602835, - "count": 1 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "effort=72439.0 | vol=1530.0 | bugs=0.5", - "severity": "poor" - }, - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "effort=37694.5 | vol=689.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "effort=35426.9 | vol=622.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "effort=23594.7 | vol=512.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "effort=23264.8 | vol=541.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "effort=18401.0 | vol=737.0 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "effort=16400.9 | vol=660.6 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "effort=11571.0 | vol=555.4 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "effort=9773.4 | vol=498.3 | bugs=0.2", - "severity": "good" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "effort=9370.1 | vol=369.9 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "effort=8517.0 | vol=346.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "effort=4707.8 | vol=239.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "effort=4479.5 | vol=255.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "effort=3707.3 | vol=255.2 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "effort=3697.7 | vol=227.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "effort=2905.1 | vol=223.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "Intersection", - "location": "set.go", - "value": "effort=1949.4 | vol=200.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "effort=1875.5 | vol=205.1 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "effort=1796.0 | vol=146.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "effort=1775.6 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "effort=1772.1 | vol=182.8 | bugs=0.1", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "effort=1135.2 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "effort=791.2 | vol=137.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "effort=491.7 | vol=124.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "effort=483.5 | vol=150.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "effort=479.2 | vol=89.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "effort=445.4 | vol=93.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "effort=395.3 | vol=109.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "effort=364.6 | vol=78.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "effort=352.7 | vol=72.3 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "effort=299.9 | vol=91.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "effort=293.6 | vol=77.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "effort=291.3 | vol=92.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "effort=269.5 | vol=58.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "effort=244.7 | vol=56.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "effort=207.2 | vol=66.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "effort=181.6 | vol=68.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "effort=168.3 | vol=64.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "effort=147.9 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "effort=115.1 | vol=51.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=109.6 | vol=46.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "effort=107.5 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "effort=92.0 | vol=50.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "effort=77.7 | vol=46.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "effort=64.8 | vol=41.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "effort=41.1 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "effort=40.7 | vol=23.3 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=40.0 | vol=30.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "effort=36.2 | vol=31.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "effort=15.5 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "effort=10.9 | vol=18.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "files": [ - { - "file_path": "int64.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Distinct Operators (n1)", - "value": "49" - }, - { - "label": "Distinct Operands (n2)", - "value": "29" - }, - { - "label": "Total Operators (N1)", - "value": "68" - }, - { - "label": "Total Operands (N2)", - "value": "83" - }, - { - "label": "Vocabulary", - "value": "78" - }, - { - "label": "Volume", - "value": "949.1" - }, - { - "label": "Difficulty", - "value": "70.1" - }, - { - "label": "Effort", - "value": "66551.2" - }, - { - "label": "Est. Bugs", - "value": "0.3" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 1, - "count": 18 - }, - { - "label": "Medium (101-1000)", - "percent": 0, - "count": 0 - }, - { - "label": "High (1001-5000)", - "percent": 0, - "count": 0 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Difference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int64.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int64.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int64.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int64.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int64KeySet", - "location": "int64.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt64", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int64.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int64.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int64.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "file_path": "set.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "21" - }, - { - "label": "Distinct Operators (n1)", - "value": "52" - }, - { - "label": "Distinct Operands (n2)", - "value": "55" - }, - { - "label": "Total Operators (N1)", - "value": "80" - }, - { - "label": "Total Operands (N2)", - "value": "270" - }, - { - "label": "Vocabulary", - "value": "107" - }, - { - "label": "Volume", - "value": "2359.5" - }, - { - "label": "Difficulty", - "value": "127.6" - }, - { - "label": "Effort", - "value": "301159.7" - }, - { - "label": "Est. Bugs", - "value": "0.8" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 0.9523809523809523, - "count": 20 - }, - { - "label": "Medium (101-1000)", - "percent": 0.047619047619047616, - "count": 1 - }, - { - "label": "High (1001-5000)", - "percent": 0, - "count": 0 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Intersection", - "location": "set.go", - "value": "effort=1949.4 | vol=200.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "set.go", - "value": "effort=479.2 | vol=89.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "Difference", - "location": "set.go", - "value": "effort=445.4 | vol=93.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "set.go", - "value": "effort=293.6 | vol=77.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "KeySet", - "location": "set.go", - "value": "effort=291.3 | vol=92.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "set.go", - "value": "effort=269.5 | vol=58.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "set.go", - "value": "effort=244.7 | vol=56.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "New", - "location": "set.go", - "value": "effort=207.2 | vol=66.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "set.go", - "value": "effort=181.6 | vol=68.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "set.go", - "value": "effort=168.3 | vol=64.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "set.go", - "value": "effort=147.9 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "set.go", - "value": "effort=115.1 | vol=51.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=109.6 | vol=46.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "set.go", - "value": "effort=107.5 | vol=53.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "set.go", - "value": "effort=77.7 | vol=46.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "set.go", - "value": "effort=64.8 | vol=41.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clear", - "location": "set.go", - "value": "effort=40.7 | vol=23.3 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "set.go", - "value": "effort=40.0 | vol=30.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "set.go", - "value": "effort=36.2 | vol=31.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "set.go", - "value": "effort=15.5 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "cast", - "location": "set.go", - "value": "effort=10.9 | vol=18.1 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "file_path": "doc.go", - "score_label": "10/10", - "status": "Excellent complexity - code is simple and maintainable", - "metrics": [ - { - "label": "Total Functions", - "value": "0" - }, - { - "label": "Distinct Operators (n1)", - "value": "0" - }, - { - "label": "Distinct Operands (n2)", - "value": "0" - }, - { - "label": "Total Operators (N1)", - "value": "0" - }, - { - "label": "Total Operands (N2)", - "value": "0" - }, - { - "label": "Vocabulary", - "value": "0" - }, - { - "label": "Volume", - "value": "0.0" - }, - { - "label": "Difficulty", - "value": "0.0" - }, - { - "label": "Effort", - "value": "0.0" - }, - { - "label": "Est. Bugs", - "value": "0.0" - } - ], - "issues": [], - "score": 1 - }, - { - "file_path": "int.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Distinct Operators (n1)", - "value": "49" - }, - { - "label": "Distinct Operands (n2)", - "value": "29" - }, - { - "label": "Total Operators (N1)", - "value": "68" - }, - { - "label": "Total Operands (N2)", - "value": "83" - }, - { - "label": "Vocabulary", - "value": "78" - }, - { - "label": "Volume", - "value": "949.1" - }, - { - "label": "Difficulty", - "value": "70.1" - }, - { - "label": "Effort", - "value": "66551.2" - }, - { - "label": "Est. Bugs", - "value": "0.3" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 1, - "count": 18 - }, - { - "label": "Medium (101-1000)", - "percent": 0, - "count": 0 - }, - { - "label": "High (1001-5000)", - "percent": 0, - "count": 0 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Difference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "IntKeySet", - "location": "int.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "file_path": "set_generic_test.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Distinct Operators (n1)", - "value": "119" - }, - { - "label": "Distinct Operands (n2)", - "value": "92" - }, - { - "label": "Total Operators (N1)", - "value": "276" - }, - { - "label": "Total Operands (N2)", - "value": "735" - }, - { - "label": "Vocabulary", - "value": "211" - }, - { - "label": "Volume", - "value": "7806.0" - }, - { - "label": "Difficulty", - "value": "475.4" - }, - { - "label": "Effort", - "value": "3710622.4" - }, - { - "label": "Est. Bugs", - "value": "2.6" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 0.2222222222222222, - "count": 4 - }, - { - "label": "Medium (101-1000)", - "percent": 0.7777777777777778, - "count": 14 - }, - { - "label": "High (1001-5000)", - "percent": 0, - "count": 0 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "TestSetEquals", - "location": "set_generic_test.go", - "value": "effort=37694.5 | vol=689.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestSet", - "location": "set_generic_test.go", - "value": "effort=23264.8 | vol=541.8 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestIntersection", - "location": "set_generic_test.go", - "value": "effort=18401.0 | vol=737.0 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestUnion", - "location": "set_generic_test.go", - "value": "effort=16400.9 | vol=660.6 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestSetDifference", - "location": "set_generic_test.go", - "value": "effort=9370.1 | vol=369.9 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetDeleteMultiples", - "location": "set_generic_test.go", - "value": "effort=4479.5 | vol=255.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetSymmetricDifference", - "location": "set_generic_test.go", - "value": "effort=3707.3 | vol=255.2 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewEmptySet", - "location": "set_generic_test.go", - "value": "effort=3697.7 | vol=227.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearWithSharedReference", - "location": "set_generic_test.go", - "value": "effort=1875.5 | vol=205.1 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewSet", - "location": "set_generic_test.go", - "value": "effort=1775.6 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClearInSeparateFunction", - "location": "set_generic_test.go", - "value": "effort=1772.1 | vol=182.8 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetClear", - "location": "set_generic_test.go", - "value": "effort=791.2 | vol=137.6 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestSortedList", - "location": "set_generic_test.go", - "value": "effort=491.7 | vol=124.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestKeySet", - "location": "set_generic_test.go", - "value": "effort=483.5 | vol=150.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestSetHasAny", - "location": "set_generic_test.go", - "value": "effort=364.6 | vol=78.1 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListLarge", - "location": "set_generic_test.go", - "value": "effort=299.9 | vol=91.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "BenchmarkListSmall", - "location": "set_generic_test.go", - "value": "effort=92.0 | vol=50.2 | bugs=0.0", - "severity": "good" - }, - { - "name": "clearSetAndAdd", - "location": "set_generic_test.go", - "value": "effort=41.1 | vol=36.5 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "file_path": "set_test.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "12" - }, - { - "label": "Distinct Operators (n1)", - "value": "130" - }, - { - "label": "Distinct Operands (n2)", - "value": "118" - }, - { - "label": "Total Operators (N1)", - "value": "270" - }, - { - "label": "Total Operands (N2)", - "value": "676" - }, - { - "label": "Vocabulary", - "value": "248" - }, - { - "label": "Volume", - "value": "7524.7" - }, - { - "label": "Difficulty", - "value": "372.4" - }, - { - "label": "Effort", - "value": "2801982.9" - }, - { - "label": "Est. Bugs", - "value": "2.5" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 0.08333333333333333, - "count": 1 - }, - { - "label": "Medium (101-1000)", - "percent": 0.8333333333333334, - "count": 10 - }, - { - "label": "High (1001-5000)", - "percent": 0.08333333333333333, - "count": 1 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "BenchmarkStringSet", - "location": "set_test.go", - "value": "effort=72439.0 | vol=1530.0 | bugs=0.5", - "severity": "poor" - }, - { - "name": "TestStringSetEquals", - "location": "set_test.go", - "value": "effort=35426.9 | vol=622.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringSet", - "location": "set_test.go", - "value": "effort=23594.7 | vol=512.9 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringIntersection", - "location": "set_test.go", - "value": "effort=11571.0 | vol=555.4 | bugs=0.2", - "severity": "fair" - }, - { - "name": "TestStringUnion", - "location": "set_test.go", - "value": "effort=9773.4 | vol=498.3 | bugs=0.2", - "severity": "good" - }, - { - "name": "TestStringSetDifference", - "location": "set_test.go", - "value": "effort=8517.0 | vol=346.4 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetDeleteMultiples", - "location": "set_test.go", - "value": "effort=4707.8 | vol=239.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetSymmetricDifference", - "location": "set_test.go", - "value": "effort=2905.1 | vol=223.5 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestNewStringSet", - "location": "set_test.go", - "value": "effort=1796.0 | vol=146.9 | bugs=0.0", - "severity": "good" - }, - { - "name": "makeString", - "location": "set_test.go", - "value": "effort=1135.2 | vol=153.7 | bugs=0.1", - "severity": "good" - }, - { - "name": "TestStringSetList", - "location": "set_test.go", - "value": "effort=395.3 | vol=109.8 | bugs=0.0", - "severity": "good" - }, - { - "name": "TestStringSetHasAny", - "location": "set_test.go", - "value": "effort=352.7 | vol=72.3 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "file_path": "string.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Distinct Operators (n1)", - "value": "49" - }, - { - "label": "Distinct Operands (n2)", - "value": "29" - }, - { - "label": "Total Operators (N1)", - "value": "68" - }, - { - "label": "Total Operands (N2)", - "value": "83" - }, - { - "label": "Vocabulary", - "value": "78" - }, - { - "label": "Volume", - "value": "949.1" - }, - { - "label": "Difficulty", - "value": "70.1" - }, - { - "label": "Effort", - "value": "66551.2" - }, - { - "label": "Est. Bugs", - "value": "0.3" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 1, - "count": 18 - }, - { - "label": "Medium (101-1000)", - "percent": 0, - "count": 0 - }, - { - "label": "High (1001-5000)", - "percent": 0, - "count": 0 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Difference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "string.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "string.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "string.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "string.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "StringKeySet", - "location": "string.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewString", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "string.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "string.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "string.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "file_path": "byte.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Distinct Operators (n1)", - "value": "49" - }, - { - "label": "Distinct Operands (n2)", - "value": "29" - }, - { - "label": "Total Operators (N1)", - "value": "68" - }, - { - "label": "Total Operands (N2)", - "value": "83" - }, - { - "label": "Vocabulary", - "value": "78" - }, - { - "label": "Volume", - "value": "949.1" - }, - { - "label": "Difficulty", - "value": "70.1" - }, - { - "label": "Effort", - "value": "66551.2" - }, - { - "label": "Est. Bugs", - "value": "0.3" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 1, - "count": 18 - }, - { - "label": "Medium (101-1000)", - "percent": 0, - "count": 0 - }, - { - "label": "High (1001-5000)", - "percent": 0, - "count": 0 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Difference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "byte.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "byte.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "byte.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "byte.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "ByteKeySet", - "location": "byte.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewByte", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "byte.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "byte.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "byte.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - }, - { - "file_path": "empty.go", - "score_label": "10/10", - "status": "Excellent complexity - code is simple and maintainable", - "metrics": [ - { - "label": "Total Functions", - "value": "0" - }, - { - "label": "Distinct Operators (n1)", - "value": "0" - }, - { - "label": "Distinct Operands (n2)", - "value": "0" - }, - { - "label": "Total Operators (N1)", - "value": "0" - }, - { - "label": "Total Operands (N2)", - "value": "0" - }, - { - "label": "Vocabulary", - "value": "0" - }, - { - "label": "Volume", - "value": "0.0" - }, - { - "label": "Difficulty", - "value": "0.0" - }, - { - "label": "Effort", - "value": "0.0" - }, - { - "label": "Est. Bugs", - "value": "0.0" - } - ], - "issues": [], - "score": 1 - }, - { - "file_path": "int32.go", - "score_label": "3/10", - "status": "High complexity - code should be refactored for better maintainability", - "metrics": [ - { - "label": "Total Functions", - "value": "18" - }, - { - "label": "Distinct Operators (n1)", - "value": "49" - }, - { - "label": "Distinct Operands (n2)", - "value": "29" - }, - { - "label": "Total Operators (N1)", - "value": "68" - }, - { - "label": "Total Operands (N2)", - "value": "83" - }, - { - "label": "Vocabulary", - "value": "78" - }, - { - "label": "Volume", - "value": "949.1" - }, - { - "label": "Difficulty", - "value": "70.1" - }, - { - "label": "Effort", - "value": "66551.2" - }, - { - "label": "Est. Bugs", - "value": "0.3" - } - ], - "distribution": [ - { - "label": "Low (\u003c=100)", - "percent": 1, - "count": 18 - }, - { - "label": "Medium (101-1000)", - "percent": 0, - "count": 0 - }, - { - "label": "High (1001-5000)", - "percent": 0, - "count": 0 - }, - { - "label": "Very High (\u003e5000)", - "percent": 0, - "count": 0 - } - ], - "issues": [ - { - "name": "Difference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Intersection", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Union", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "SymmetricDifference", - "location": "int32.go", - "value": "effort=109.6 | vol=36.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "IsSuperset", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Equal", - "location": "int32.go", - "value": "effort=67.5 | vol=27.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Delete", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Insert", - "location": "int32.go", - "value": "effort=57.1 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "UnsortedList", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Clone", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "List", - "location": "int32.go", - "value": "effort=48.0 | vol=24.0 | bugs=0.0", - "severity": "good" - }, - { - "name": "Int32KeySet", - "location": "int32.go", - "value": "effort=42.8 | vol=28.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "NewInt32", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAll", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "Has", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "HasAny", - "location": "int32.go", - "value": "effort=29.5 | vol=19.7 | bugs=0.0", - "severity": "good" - }, - { - "name": "PopAny", - "location": "int32.go", - "value": "effort=23.3 | vol=15.5 | bugs=0.0", - "severity": "good" - }, - { - "name": "Len", - "location": "int32.go", - "value": "effort=8.0 | vol=8.0 | bugs=0.0", - "severity": "good" - } - ], - "score": 0.3 - } - ], - "score": 0.3 - } - ], - "overall_score": 0.3 -} diff --git a/tests/golden/static/static_halstead.text b/tests/golden/static/static_halstead.text deleted file mode 100644 index 3274807a..00000000 --- a/tests/golden/static/static_halstead.text +++ /dev/null @@ -1,14 +0,0 @@ - -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ HALSTEAD Score: 3/10 ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary: Moderate Halstead complexity - acceptable - - Key Metrics - ──────────────────────────────────────────────────────────────────────────── - Total Functions 141 Distinct Operators (n1)54 - Distinct Operands (n2)41 Total Operators (N1)96 - Total Operands (N2) 209 Vocabulary 95 - Volume 2243.6 Difficulty 132.6 - Effort 714652.1 Est. Bugs 0.7 diff --git a/tests/golden/static/static_halstead.yaml b/tests/golden/static/static_halstead.yaml deleted file mode 100644 index 1c2f5f44..00000000 --- a/tests/golden/static/static_halstead.yaml +++ /dev/null @@ -1,1440 +0,0 @@ -function_halstead: - - name: BenchmarkStringSet - source_file: set_test.go - language: go - directory: . - volume: 1530.009463523262 - difficulty: 47.345454545454544 - effort: 72438.99350935589 - time_to_program: 4024.3885282975493 - delivered_bugs: 0.510003154507754 - complexity_level: High - - name: TestIntersection - source_file: set_generic_test.go - language: go - directory: . - volume: 737.0232685160352 - difficulty: 24.96666666666667 - effort: 18401.01427061701 - time_to_program: 1022.278570589834 - delivered_bugs: 0.24567442283867838 - complexity_level: Medium - - name: TestSetEquals - source_file: set_generic_test.go - language: go - directory: . - volume: 689.8333013081152 - difficulty: 54.64285714285714 - effort: 37694.46253576486 - time_to_program: 2094.1368075424925 - delivered_bugs: 0.22994443376937174 - complexity_level: Medium - - name: TestUnion - source_file: set_generic_test.go - language: go - directory: . - volume: 660.591225855113 - difficulty: 24.82758620689655 - effort: 16400.885607437285 - time_to_program: 911.1603115242937 - delivered_bugs: 0.22019707528503765 - complexity_level: Medium - - name: TestStringSetEquals - source_file: set_test.go - language: go - directory: . - volume: 622.8902627834143 - difficulty: 56.875 - effort: 35426.88369580669 - time_to_program: 1968.1602053225938 - delivered_bugs: 0.20763008759447144 - complexity_level: Medium - - name: TestStringIntersection - source_file: set_test.go - language: go - directory: . - volume: 555.4086945462124 - difficulty: 20.833333333333332 - effort: 11571.014469712756 - time_to_program: 642.8341372062642 - delivered_bugs: 0.18513623151540412 - complexity_level: Medium - - name: TestSet - source_file: set_generic_test.go - language: go - directory: . - volume: 541.7831500254108 - difficulty: 42.94117647058823 - effort: 23264.805854032344 - time_to_program: 1292.489214112908 - delivered_bugs: 0.1805943833418036 - complexity_level: Medium - - name: TestStringSet - source_file: set_test.go - language: go - directory: . - volume: 512.9283016944967 - difficulty: 46 - effort: 23594.701877946845 - time_to_program: 1310.816770997047 - delivered_bugs: 0.17097610056483223 - complexity_level: Medium - - name: TestStringUnion - source_file: set_test.go - language: go - directory: . - volume: 498.2523364294818 - difficulty: 19.615384615384617 - effort: 9773.411214578296 - time_to_program: 542.9672896987943 - delivered_bugs: 0.1660841121431606 - complexity_level: Medium - - name: TestSetDifference - source_file: set_generic_test.go - language: go - directory: . - volume: 369.87118895965546 - difficulty: 25.333333333333332 - effort: 9370.070120311271 - time_to_program: 520.5594511284039 - delivered_bugs: 0.12329039631988516 - complexity_level: Medium - - name: TestStringSetDifference - source_file: set_test.go - language: go - directory: . - volume: 346.3849750966349 - difficulty: 24.58823529411765 - effort: 8516.99527002314 - time_to_program: 473.1664038901745 - delivered_bugs: 0.11546165836554496 - complexity_level: Medium - - name: TestSetDeleteMultiples - source_file: set_generic_test.go - language: go - directory: . - volume: 255.41209043760983 - difficulty: 17.538461538461537 - effort: 4479.53512459808 - time_to_program: 248.86306247767112 - delivered_bugs: 0.08513736347920328 - complexity_level: Medium - - name: TestSetSymmetricDifference - source_file: set_generic_test.go - language: go - directory: . - volume: 255.15831097164298 - difficulty: 14.529411764705882 - effort: 3707.3001652938715 - time_to_program: 205.96112029410398 - delivered_bugs: 0.08505277032388099 - complexity_level: Medium - - name: TestStringSetDeleteMultiples - source_file: set_test.go - language: go - directory: . - volume: 239.7487836710217 - difficulty: 19.636363636363637 - effort: 4707.794297540063 - time_to_program: 261.5441276411146 - delivered_bugs: 0.0799162612236739 - complexity_level: Medium - - name: TestNewEmptySet - source_file: set_generic_test.go - language: go - directory: . - volume: 227.5489532989615 - difficulty: 16.25 - effort: 3697.670491108124 - time_to_program: 205.4261383948958 - delivered_bugs: 0.07584965109965383 - complexity_level: Medium - - name: TestStringSetSymmetricDifference - source_file: set_test.go - language: go - directory: . - volume: 223.4671257758683 - difficulty: 13 - effort: 2905.072635086288 - time_to_program: 161.39292417146044 - delivered_bugs: 0.07448904192528943 - complexity_level: Medium - - name: TestSetClearWithSharedReference - source_file: set_generic_test.go - language: go - directory: . - volume: 205.13385445731566 - difficulty: 9.142857142857142 - effort: 1875.509526466886 - time_to_program: 104.19497369260478 - delivered_bugs: 0.06837795148577189 - complexity_level: Medium - - name: Intersection - source_file: set.go - language: go - directory: . - volume: 200.67442283867837 - difficulty: 9.714285714285714 - effort: 1949.408679004304 - time_to_program: 108.30048216690578 - delivered_bugs: 0.06689147427955945 - complexity_level: Medium - - name: TestSetClearInSeparateFunction - source_file: set_generic_test.go - language: go - directory: . - volume: 182.83669636412918 - difficulty: 9.692307692307692 - effort: 1772.109518606175 - time_to_program: 98.45052881145416 - delivered_bugs: 0.06094556545470973 - complexity_level: Medium - - name: TestNewSet - source_file: set_generic_test.go - language: go - directory: . - volume: 153.73110979725664 - difficulty: 11.55 - effort: 1775.5943181583143 - time_to_program: 98.64412878657302 - delivered_bugs: 0.05124370326575221 - complexity_level: Medium - - name: makeString - source_file: set_test.go - language: go - directory: . - volume: 153.73110979725664 - difficulty: 7.384615384615385 - effort: 1135.2451185028183 - time_to_program: 63.069173250156574 - delivered_bugs: 0.05124370326575221 - complexity_level: Medium - - name: TestKeySet - source_file: set_generic_test.go - language: go - directory: . - volume: 150.41407098051494 - difficulty: 3.2142857142857144 - effort: 483.4737995802266 - time_to_program: 26.85965553223481 - delivered_bugs: 0.05013802366017164 - complexity_level: Medium - - name: TestNewStringSet - source_file: set_test.go - language: go - directory: . - volume: 146.94555522617034 - difficulty: 12.222222222222223 - effort: 1796.001230542082 - time_to_program: 99.77784614122679 - delivered_bugs: 0.048981851742056784 - complexity_level: Medium - - name: TestSetClear - source_file: set_generic_test.go - language: go - directory: . - volume: 137.6075250475963 - difficulty: 5.75 - effort: 791.2432690236787 - time_to_program: 43.95795939020437 - delivered_bugs: 0.04586917501586544 - complexity_level: Medium - - name: TestSortedList - source_file: set_generic_test.go - language: go - directory: . - volume: 124.86408532184433 - difficulty: 3.9375 - effort: 491.65233595476207 - time_to_program: 27.314018664153448 - delivered_bugs: 0.04162136177394811 - complexity_level: Medium - - name: TestStringSetList - source_file: set_test.go - language: go - directory: . - volume: 109.80793556946902 - difficulty: 3.5999999999999996 - effort: 395.30856805008847 - time_to_program: 21.961587113893803 - delivered_bugs: 0.03660264518982301 - complexity_level: Medium - - name: Difference - source_file: set.go - language: go - directory: . - volume: 93.76537429460444 - difficulty: 4.75 - effort: 445.3855278993711 - time_to_program: 24.743640438853948 - delivered_bugs: 0.03125512476486815 - complexity_level: Low - - name: KeySet - source_file: set.go - language: go - directory: . - volume: 92 - difficulty: 3.1666666666666665 - effort: 291.3333333333333 - time_to_program: 16.185185185185183 - delivered_bugs: 0.030666666666666665 - complexity_level: Low - - name: BenchmarkListLarge - source_file: set_generic_test.go - language: go - directory: . - volume: 91.73835003173087 - difficulty: 3.269230769230769 - effort: 299.9138366421971 - time_to_program: 16.661879813455396 - delivered_bugs: 0.030579450010576957 - complexity_level: Low - - name: UnsortedList - source_file: set.go - language: go - directory: . - volume: 89.85848369899593 - difficulty: 5.333333333333333 - effort: 479.24524639464494 - time_to_program: 26.62473591081361 - delivered_bugs: 0.02995282789966531 - complexity_level: Low - - name: TestSetHasAny - source_file: set_generic_test.go - language: go - directory: . - volume: 78.13781191217038 - difficulty: 4.666666666666667 - effort: 364.6431222567951 - time_to_program: 20.257951236488616 - delivered_bugs: 0.026045937304056792 - complexity_level: Low - - name: Clone - source_file: set.go - language: go - directory: . - volume: 77.70923408096293 - difficulty: 3.7777777777777777 - effort: 293.5682176391933 - time_to_program: 16.309345424399627 - delivered_bugs: 0.025903078026987644 - complexity_level: Low - - name: TestStringSetHasAny - source_file: set_test.go - language: go - directory: . - volume: 72.33974351909448 - difficulty: 4.875 - effort: 352.6562496555856 - time_to_program: 19.592013869754755 - delivered_bugs: 0.024113247839698163 - complexity_level: Low - - name: Union - source_file: set.go - language: go - directory: . - volume: 68.11428751370197 - difficulty: 2.6666666666666665 - effort: 181.63810003653856 - time_to_program: 10.091005557585476 - delivered_bugs: 0.022704762504567322 - complexity_level: Low - - name: New - source_file: set.go - language: go - directory: . - volume: 66.60791492653966 - difficulty: 3.111111111111111 - effort: 207.22462421590117 - time_to_program: 11.51247912310562 - delivered_bugs: 0.022202638308846556 - complexity_level: Low - - name: List - source_file: set.go - language: go - directory: . - volume: 64.72503367497927 - difficulty: 2.6 - effort: 168.2850875549461 - time_to_program: 9.34917153083034 - delivered_bugs: 0.02157501122499309 - complexity_level: Low - - name: Equal - source_file: set.go - language: go - directory: . - volume: 58.81033751683405 - difficulty: 4.583333333333333 - effort: 269.5473802854894 - time_to_program: 14.974854460304966 - delivered_bugs: 0.019603445838944685 - complexity_level: Low - - name: SymmetricDifference - source_file: set.go - language: go - directory: . - volume: 56.47277761308516 - difficulty: 4.333333333333333 - effort: 244.71536965670234 - time_to_program: 13.59529831426124 - delivered_bugs: 0.018824259204361718 - complexity_level: Low - - name: IsSuperset - source_file: set.go - language: go - directory: . - volume: 53.77443751081734 - difficulty: 2.75 - effort: 147.8797031547477 - time_to_program: 8.21553906415265 - delivered_bugs: 0.01792481250360578 - complexity_level: Low - - name: PopAny - source_file: set.go - language: go - directory: . - volume: 53.77443751081734 - difficulty: 2 - effort: 107.54887502163469 - time_to_program: 5.974937501201927 - delivered_bugs: 0.01792481250360578 - complexity_level: Low - - name: HasAll - source_file: set.go - language: go - directory: . - volume: 51.80615605397529 - difficulty: 2.2222222222222223 - effort: 115.12479123105621 - time_to_program: 6.395821735058679 - delivered_bugs: 0.01726871868465843 - complexity_level: Low - - name: BenchmarkListSmall - source_file: set_generic_test.go - language: go - directory: . - volume: 50.18947501009619 - difficulty: 1.8333333333333335 - effort: 92.01403751850968 - time_to_program: 5.111890973250538 - delivered_bugs: 0.016729825003365395 - complexity_level: Low - - name: HasAny - source_file: set.go - language: go - directory: . - volume: 46.60451250937503 - difficulty: 1.6666666666666667 - effort: 77.67418751562505 - time_to_program: 4.315232639756947 - delivered_bugs: 0.01553483750312501 - complexity_level: Low - - name: Insert - source_file: set.go - language: go - directory: . - volume: 46.50699332842307 - difficulty: 2.357142857142857 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.015502331109474358 - complexity_level: Low - - name: Delete - source_file: set.go - language: go - directory: . - volume: 41.20902501875006 - difficulty: 1.5714285714285714 - effort: 64.75703931517867 - time_to_program: 3.597613295287704 - delivered_bugs: 0.013736341672916687 - complexity_level: Low - - name: Union - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: clearSetAndAdd - source_file: set_generic_test.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 1.125 - effort: 41.10886017423111 - time_to_program: 2.2838255652350616 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: int32.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: byte.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: string.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Union - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Intersection - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: SymmetricDifference - source_file: int64.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Difference - source_file: int.go - language: go - directory: . - volume: 36.541209043760986 - difficulty: 3 - effort: 109.62362713128296 - time_to_program: 6.090201507293497 - delivered_bugs: 0.012180403014586996 - complexity_level: Low - - name: Has - source_file: set.go - language: go - directory: . - volume: 31.699250014423125 - difficulty: 1.1428571428571428 - effort: 36.22771430219785 - time_to_program: 2.0126507945665475 - delivered_bugs: 0.010566416671474375 - complexity_level: Low - - name: Insert - source_file: set.go - language: go - directory: . - volume: 30 - difficulty: 1.3333333333333333 - effort: 40 - time_to_program: 2.2222222222222223 - delivered_bugs: 0.01 - complexity_level: Low - - name: Delete - source_file: byte.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: int64.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: string.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: int32.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: string.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Int32KeySet - source_file: int32.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: StringKeySet - source_file: string.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: ByteKeySet - source_file: byte.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: byte.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: int.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Int64KeySet - source_file: int64.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: IntKeySet - source_file: int.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 1.5 - effort: 42.793987519471216 - time_to_program: 2.377443751081734 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: int.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Delete - source_file: int32.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Insert - source_file: int64.go - language: go - directory: . - volume: 28.529325012980813 - difficulty: 2 - effort: 57.058650025961626 - time_to_program: 3.1699250014423126 - delivered_bugs: 0.009509775004326938 - complexity_level: Low - - name: Equal - source_file: byte.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: byte.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: string.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: int.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: int.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: int32.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: int32.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: string.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: IsSuperset - source_file: int64.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: Equal - source_file: int64.go - language: go - directory: . - volume: 27 - difficulty: 2.5 - effort: 67.5 - time_to_program: 3.75 - delivered_bugs: 0.009 - complexity_level: Low - - name: UnsortedList - source_file: int.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: string.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: int64.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: int.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: string.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: int32.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: int64.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: int64.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: int.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: int32.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: int32.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clone - source_file: byte.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: byte.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: UnsortedList - source_file: byte.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: List - source_file: string.go - language: go - directory: . - volume: 24 - difficulty: 2 - effort: 48 - time_to_program: 2.6666666666666665 - delivered_bugs: 0.008 - complexity_level: Low - - name: Clear - source_file: set.go - language: go - directory: . - volume: 23.264662506490403 - difficulty: 1.75 - effort: 40.71315938635821 - time_to_program: 2.2618421881310113 - delivered_bugs: 0.007754887502163467 - complexity_level: Low - - name: HasAny - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewInt64 - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: int64.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewInt - source_file: int.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewByte - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: byte.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewInt32 - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: NewString - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAny - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: int32.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: Has - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: HasAll - source_file: string.go - language: go - directory: . - volume: 19.651484454403228 - difficulty: 1.5 - effort: 29.47722668160484 - time_to_program: 1.6376237045336022 - delivered_bugs: 0.00655049481813441 - complexity_level: Low - - name: cast - source_file: set.go - language: go - directory: . - volume: 18.094737505048094 - difficulty: 0.6 - effort: 10.856842503028856 - time_to_program: 0.6031579168349365 - delivered_bugs: 0.006031579168349364 - complexity_level: Low - - name: PopAny - source_file: int.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: Len - source_file: set.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1 - effort: 15.509775004326936 - time_to_program: 0.861654166907052 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: int64.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: int32.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: byte.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: PopAny - source_file: string.go - language: go - directory: . - volume: 15.509775004326936 - difficulty: 1.5 - effort: 23.264662506490403 - time_to_program: 1.292481250360578 - delivered_bugs: 0.005169925001442312 - complexity_level: Low - - name: Len - source_file: int64.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: int32.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: int.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: byte.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low - - name: Len - source_file: string.go - language: go - directory: . - volume: 8 - difficulty: 1 - effort: 8 - time_to_program: 0.4444444444444444 - delivered_bugs: 0.0026666666666666666 - complexity_level: Low -distribution: - low: 115 - medium: 25 - high: 1 - very_high: 0 -high_effort_functions: - - name: BenchmarkStringSet - source_file: set_test.go - volume: 1530.009463523262 - effort: 72438.99350935589 - time_to_program: 4024.3885282975493 - delivered_bugs: 0.510003154507754 - risk_level: MEDIUM -aggregate: - total_functions: 141 - volume: 2243.569310989035 - difficulty: 132.5965954137723 - effort: 714652.1317873196 - time_to_program: 39702.896210406645 - delivered_bugs: 0.7478564369963447 - distinct_operators: 0 - distinct_operands: 0 - total_operators: 0 - total_operands: 0 - vocabulary: 0 - length: 0 - estimated_length: 584.0119514998204 - health_score: 100 - message: Low Halstead complexity - well-structured code diff --git a/tests/golden/static/static_imports.bin b/tests/golden/static/static_imports.bin deleted file mode 100644 index 2162676d93469680445d0b1e7e62f3c03eb79e7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693 zcmbVKL2AP=5Db0Fo=x1iH3>eZ&`*?NRC%o|mL*kMLxb^?`p3#mQQS~^@I|1JW=7hX zr|0AD^KP>lfqG{>*O~%D^)iAJe1ZyO?;ryy7?t(s2;c{!H$q30LY;OM`vIpc@w!9U zlFfyqUdR&}^yl1lDj%419+1N-RgbMk$*Z_Qlf-qljDg9hRkm$^^W4CVrnlSiwL*vG z{Q5Zl+zZqEHvNzMnsTD}&pE@tW0Q6;G2?PhYjGn<|C|?76l~C;Y0$`TNi%4jfl$h$ zij9D31hy=6eJM{g-DjyFSFns#F_+d{!6Jjf(EEV36S5O(e)5v-1(Sv9e!Jg27Kbve H57YDo-sJIw diff --git a/tests/golden/static/static_imports.compact b/tests/golden/static/static_imports.compact deleted file mode 100644 index cfffce0c..00000000 --- a/tests/golden/static/static_imports.compact +++ /dev/null @@ -1 +0,0 @@ -IMPORTS [░░░░░░░░░░] -10/10 Found 7 unique imports diff --git a/tests/golden/static/static_imports.json b/tests/golden/static/static_imports.json deleted file mode 100644 index 13572e4c..00000000 --- a/tests/golden/static/static_imports.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "overall_score_label": "Info", - "sections": [ - { - "title": "IMPORTS", - "score_label": "Info", - "status": "Found 7 unique imports", - "metrics": [ - { - "label": "Unique Imports", - "value": "7" - }, - { - "label": "Total Files", - "value": "10" - } - ], - "issues": [ - { - "name": "reflect", - "location": "", - "value": "2", - "severity": "info" - }, - { - "name": "testing", - "location": "", - "value": "2", - "severity": "info" - }, - { - "name": "cmp", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "slices", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "k8s.io/apimachinery/pkg/util/sets", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "fmt", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "math/rand", - "location": "", - "value": "1", - "severity": "info" - } - ], - "score": -1 - } - ], - "overall_score": -1 -} diff --git a/tests/golden/static/static_imports.perfile.json b/tests/golden/static/static_imports.perfile.json deleted file mode 100644 index 690ad5b8..00000000 --- a/tests/golden/static/static_imports.perfile.json +++ /dev/null @@ -1,295 +0,0 @@ -{ - "overall_score_label": "Info", - "sections": [ - { - "title": "IMPORTS", - "score_label": "Info", - "status": "Found 7 unique imports", - "metrics": [ - { - "label": "Unique Imports", - "value": "7" - }, - { - "label": "Total Files", - "value": "10" - } - ], - "issues": [ - { - "name": "reflect", - "location": "", - "value": "2", - "severity": "info" - }, - { - "name": "testing", - "location": "", - "value": "2", - "severity": "info" - }, - { - "name": "slices", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "k8s.io/apimachinery/pkg/util/sets", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "fmt", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "math/rand", - "location": "", - "value": "1", - "severity": "info" - }, - { - "name": "cmp", - "location": "", - "value": "1", - "severity": "info" - } - ], - "files": [ - { - "file_path": "int64.go", - "score_label": "Info", - "status": "No import data available", - "metrics": [ - { - "label": "Unique Imports", - "value": "0" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "set_test.go", - "score_label": "Info", - "status": "Found 4 unique imports", - "metrics": [ - { - "label": "Unique Imports", - "value": "4" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [ - { - "name": "fmt", - "location": "set_test.go", - "value": "1", - "severity": "info" - }, - { - "name": "math/rand", - "location": "set_test.go", - "value": "1", - "severity": "info" - }, - { - "name": "reflect", - "location": "set_test.go", - "value": "1", - "severity": "info" - }, - { - "name": "testing", - "location": "set_test.go", - "value": "1", - "severity": "info" - } - ], - "score": -1 - }, - { - "file_path": "string.go", - "score_label": "Info", - "status": "No import data available", - "metrics": [ - { - "label": "Unique Imports", - "value": "0" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "byte.go", - "score_label": "Info", - "status": "No import data available", - "metrics": [ - { - "label": "Unique Imports", - "value": "0" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "doc.go", - "score_label": "Info", - "status": "No import data available", - "metrics": [ - { - "label": "Unique Imports", - "value": "0" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "empty.go", - "score_label": "Info", - "status": "No import data available", - "metrics": [ - { - "label": "Unique Imports", - "value": "0" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "set.go", - "score_label": "Info", - "status": "Found 2 unique imports", - "metrics": [ - { - "label": "Unique Imports", - "value": "2" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [ - { - "name": "cmp", - "location": "set.go", - "value": "1", - "severity": "info" - }, - { - "name": "slices", - "location": "set.go", - "value": "1", - "severity": "info" - } - ], - "score": -1 - }, - { - "file_path": "set_generic_test.go", - "score_label": "Info", - "status": "Found 3 unique imports", - "metrics": [ - { - "label": "Unique Imports", - "value": "3" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [ - { - "name": "k8s.io/apimachinery/pkg/util/sets", - "location": "set_generic_test.go", - "value": "1", - "severity": "info" - }, - { - "name": "reflect", - "location": "set_generic_test.go", - "value": "1", - "severity": "info" - }, - { - "name": "testing", - "location": "set_generic_test.go", - "value": "1", - "severity": "info" - } - ], - "score": -1 - }, - { - "file_path": "int.go", - "score_label": "Info", - "status": "No import data available", - "metrics": [ - { - "label": "Unique Imports", - "value": "0" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [], - "score": -1 - }, - { - "file_path": "int32.go", - "score_label": "Info", - "status": "No import data available", - "metrics": [ - { - "label": "Unique Imports", - "value": "0" - }, - { - "label": "Total Files", - "value": "0" - } - ], - "issues": [], - "score": -1 - } - ], - "score": -1 - } - ], - "overall_score": -1 -} diff --git a/tests/golden/static/static_imports.text b/tests/golden/static/static_imports.text deleted file mode 100644 index b55dc713..00000000 --- a/tests/golden/static/static_imports.text +++ /dev/null @@ -1,18 +0,0 @@ - -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ IMPORTS Score: Info ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Summary: Found 7 unique imports - - Key Metrics - ──────────────────────────────────────────────────────────────────────────── - Unique Imports 7 Total Files 10 - - Top Issues - ──────────────────────────────────────────────────────────────────────────── - reflect 2 - testing 2 - fmt 1 - math/rand 1 - cmp 1 diff --git a/tests/golden/static/static_imports.yaml b/tests/golden/static/static_imports.yaml deleted file mode 100644 index 4031495f..00000000 --- a/tests/golden/static/static_imports.yaml +++ /dev/null @@ -1,34 +0,0 @@ -import_list: - - path: cmp - category: external - is_external: true - - path: k8s.io/apimachinery/pkg/util/sets - category: external - is_external: true - - path: reflect - category: external - is_external: true - - path: slices - category: external - is_external: true - - path: testing - category: external - is_external: true - - path: fmt - category: stdlib - is_external: false - - path: math/rand - category: stdlib - is_external: false -categories: - - category: external - count: 5 - - category: stdlib - count: 2 -dependencies: [] -aggregate: - total_imports: 7 - external_imports: 5 - internal_imports: 2 - unique_packages: 7 - external_ratio: 0.7142857142857143 diff --git a/tests/golden/uast/analyze.json b/tests/golden/uast/analyze.json deleted file mode 100644 index c2664fb6..00000000 --- a/tests/golden/uast/analyze.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "file": "/home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go", - "total_nodes": 493, - "leaf_nodes": 235, - "leaf_ratio": 0.4766734279918864, - "max_depth": 11, - "avg_depth": 4.616632860040568, - "max_children": 67, - "avg_branching": 1.9069767441860466, - "type_diversity": 13, - "types": { - "Block": 18, - "Call": 48, - "Comment": 47, - "File": 1, - "Function": 2, - "Identifier": 180, - "List": 72, - "Method": 16, - "Package": 1, - "Parameter": 68, - "Return": 18, - "Slice": 2, - "Synthetic": 20 - }, - "roles": { - "Body": 18, - "Call": 48, - "Comment": 47, - "Declaration": 18, - "Function": 18, - "Member": 16, - "Module": 1, - "Name": 180, - "Parameter": 68, - "Return": 18, - "Type": 54 - }, - "role_coverage": 0.8073022312373225, - "pos_coverage": 1, - "synthetic_nodes": 20 - } -] diff --git a/tests/golden/uast/analyze.text b/tests/golden/uast/analyze.text deleted file mode 100644 index b105a15e..00000000 --- a/tests/golden/uast/analyze.text +++ /dev/null @@ -1,40 +0,0 @@ -File: /home/dmitriy/sources/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/sets/byte.go - Tree shape: - Total nodes: 493 - Leaf nodes: 235 (48%) - Max depth: 11 - Avg depth: 4.6 - Max children: 67 - Avg branching: 1.9 - Coverage: - Role coverage: 81% - Pos coverage: 100% - Synthetic: 20 - Type diversity: 13 - Node types: - Identifier 180 - List 72 - Parameter 68 - Call 48 - Comment 47 - Synthetic 20 - Block 18 - Return 18 - Method 16 - Slice 2 - Function 2 - Package 1 - File 1 - Roles: - Name 180 - Parameter 68 - Type 54 - Call 48 - Comment 47 - Body 18 - Return 18 - Function 18 - Declaration 18 - Member 16 - Module 1 - diff --git a/tests/golden/uast/parse.compact b/tests/golden/uast/parse.compact deleted file mode 100644 index 07a07f43..00000000 --- a/tests/golden/uast/parse.compact +++ /dev/null @@ -1 +0,0 @@ -{"children":[{"id":"26d00635698d2406","pos":{"end_col":3,"end_line":15,"end_offset":569,"start_col":1,"start_line":1,"start_offset":0},"roles":["Comment"],"token":"/*\nCopyright 2022 The Kubernetes Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/","type":"Comment"},{"children":[{"id":"01cf11a965f2f6b4","pos":{"end_col":13,"end_line":17,"end_offset":583,"start_col":9,"start_line":17,"start_offset":579},"roles":["Name"],"token":"sets","type":"Identifier"}],"id":"203ff992bf5623f3","pos":{"end_col":13,"end_line":17,"end_offset":583,"start_col":1,"start_line":17,"start_offset":571},"roles":["Module"],"token":"package sets","type":"Package"},{"id":"a788f213c71243b6","pos":{"end_col":93,"end_line":19,"end_offset":677,"start_col":1,"start_line":19,"start_offset":585},"roles":["Comment"],"token":"// Byte is a set of bytes, implemented via map[byte]struct{} for minimal memory consumption.","type":"Comment"},{"id":"51fb18e1a6b2b91a","pos":{"end_col":3,"end_line":20,"end_offset":680,"start_col":1,"start_line":20,"start_offset":678},"roles":["Comment"],"token":"//","type":"Comment"},{"id":"cabf4b659236fe0f","pos":{"end_col":40,"end_line":21,"end_offset":720,"start_col":1,"start_line":21,"start_offset":681},"roles":["Comment"],"token":"// Deprecated: use generic Set instead.","type":"Comment"},{"id":"83431be209f35b4f","pos":{"end_col":13,"end_line":22,"end_offset":733,"start_col":1,"start_line":22,"start_offset":721},"roles":["Comment"],"token":"// new ways:","type":"Comment"},{"id":"6e82d6df0eb47b51","pos":{"end_col":21,"end_line":23,"end_offset":754,"start_col":1,"start_line":23,"start_offset":734},"roles":["Comment"],"token":"// s1 := Set[byte]{}","type":"Comment"},{"id":"61b58a8eb956a313","pos":{"end_col":21,"end_line":24,"end_offset":775,"start_col":1,"start_line":24,"start_offset":755},"roles":["Comment"],"token":"// s2 := New[byte]()","type":"Comment"},{"children":[{"children":[{"id":"e788897c64e80036","pos":{"end_col":10,"end_line":25,"end_offset":785,"start_col":6,"start_line":25,"start_offset":781},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"id":"c58582cd366cf8bf","pos":{"end_col":19,"end_line":25,"end_offset":794,"start_col":15,"start_line":25,"start_offset":790},"roles":["Name","Type"],"token":"byte","type":"Identifier"},{"id":"399ab824047d9da2","pos":{"end_col":25,"end_line":25,"end_offset":800,"start_col":20,"start_line":25,"start_offset":795},"roles":["Name","Type"],"token":"Empty","type":"Identifier"}],"id":"99f41cb2f4681ba2","pos":{"end_col":25,"end_line":25,"end_offset":800,"start_col":11,"start_line":25,"start_offset":786},"roles":[],"token":"map[byte]Empty","type":"Synthetic"}],"id":"8d861f7f28d627b4","pos":{"end_col":25,"end_line":25,"end_offset":800,"start_col":6,"start_line":25,"start_offset":781},"props":{"name":"Byte"},"roles":[],"token":"Byte map[byte]Empty","type":"List"}],"id":"0fec74df99ebb291","pos":{"end_col":25,"end_line":25,"end_offset":800,"start_col":1,"start_line":25,"start_offset":776},"roles":[],"type":"Synthetic"},{"id":"1e1d6b2d65e5b5c2","pos":{"end_col":49,"end_line":27,"end_offset":850,"start_col":1,"start_line":27,"start_offset":802},"roles":["Comment"],"token":"// NewByte creates a Byte from a list of values.","type":"Comment"},{"children":[{"id":"88c908f1c1b7df09","pos":{"end_col":13,"end_line":28,"end_offset":863,"start_col":6,"start_line":28,"start_offset":856},"roles":["Name"],"token":"NewByte","type":"Identifier"},{"children":[{"children":[{"id":"b829185c699fcb4a","pos":{"end_col":19,"end_line":28,"end_offset":869,"start_col":14,"start_line":28,"start_offset":864},"roles":["Name"],"token":"items","type":"Identifier"},{"id":"3c6ee13301476e8c","pos":{"end_col":27,"end_line":28,"end_offset":877,"start_col":23,"start_line":28,"start_offset":873},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"1aa1514a4f3f52d9","pos":{"end_col":27,"end_line":28,"end_offset":877,"start_col":14,"start_line":28,"start_offset":864},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"id":"118505cc769c642f","pos":{"end_col":28,"end_line":28,"end_offset":878,"start_col":13,"start_line":28,"start_offset":863},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"id":"7c4f5cd7f9eea166","pos":{"end_col":33,"end_line":28,"end_offset":883,"start_col":29,"start_line":28,"start_offset":879},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"89cf2ab2e31c2bf3","pos":{"end_col":13,"end_line":29,"end_offset":898,"start_col":9,"start_line":29,"start_offset":894},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"id":"3e1985081cb134a1","pos":{"end_col":17,"end_line":29,"end_offset":902,"start_col":14,"start_line":29,"start_offset":899},"roles":["Name"],"token":"New","type":"Identifier"},{"children":[{"children":[{"id":"75fb1a65485ff02b","pos":{"end_col":22,"end_line":29,"end_offset":907,"start_col":18,"start_line":29,"start_offset":903},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"875399e017bb3297","pos":{"end_col":22,"end_line":29,"end_offset":907,"start_col":18,"start_line":29,"start_offset":903},"roles":[],"type":"Synthetic"}],"id":"75686a6454eb690f","pos":{"end_col":23,"end_line":29,"end_offset":908,"start_col":17,"start_line":29,"start_offset":902},"roles":[],"token":"[byte]","type":"Synthetic"},{"children":[{"children":[{"id":"c48fb9ee1ed15e38","pos":{"end_col":29,"end_line":29,"end_offset":914,"start_col":24,"start_line":29,"start_offset":909},"roles":["Name"],"token":"items","type":"Identifier"}],"id":"22be5659a9eef6d7","pos":{"end_col":32,"end_line":29,"end_offset":917,"start_col":24,"start_line":29,"start_offset":909},"roles":[],"token":"items...","type":"List"}],"id":"f6bd57260ccd8dd3","pos":{"end_col":33,"end_line":29,"end_offset":918,"start_col":23,"start_line":29,"start_offset":908},"roles":[],"token":"(items...)","type":"List"}],"id":"81979a9df2071661","pos":{"end_col":33,"end_line":29,"end_offset":918,"start_col":14,"start_line":29,"start_offset":899},"roles":["Call"],"token":"New[byte](items...)","type":"Call"}],"id":"d541313083135cca","pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":13,"start_line":29,"start_offset":898},"roles":[],"token":"(New[byte](items...))","type":"List"}],"id":"517f67cb07cf529b","pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":9,"start_line":29,"start_offset":894},"roles":["Call"],"token":"Byte(New[byte](items...))","type":"Call"}],"id":"5fe8108c3d05e985","pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":9,"start_line":29,"start_offset":894},"roles":[],"type":"List"}],"id":"df51022abbfa4878","pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":2,"start_line":29,"start_offset":887},"roles":["Return"],"token":"return Byte(New[byte](items...))","type":"Return"}],"id":"37a7053322456887","pos":{"end_col":2,"end_line":30,"end_offset":921,"start_col":34,"start_line":28,"start_offset":884},"roles":["Body"],"token":"{\n\treturn Byte(New[byte](items...))\n}","type":"Block"}],"id":"c3d1d1e75c441a4f","pos":{"end_col":2,"end_line":30,"end_offset":921,"start_col":1,"start_line":28,"start_offset":851},"props":{"name":"NewByte"},"roles":["Function","Declaration"],"token":"func NewByte(items ...byte) Byte {\n\treturn Byte(New[byte](items...))\n}","type":"Function"},{"id":"8904ae60de71d712","pos":{"end_col":80,"end_line":32,"end_offset":1002,"start_col":1,"start_line":32,"start_offset":923},"roles":["Comment"],"token":"// ByteKeySet creates a Byte from a keys of a map[byte](? extends interface{}).","type":"Comment"},{"id":"578ee02118c79115","pos":{"end_col":66,"end_line":33,"end_offset":1068,"start_col":1,"start_line":33,"start_offset":1003},"roles":["Comment"],"token":"// If the value passed in is not actually a map, this will panic.","type":"Comment"},{"children":[{"id":"d14ee5da81922a98","pos":{"end_col":16,"end_line":34,"end_offset":1084,"start_col":6,"start_line":34,"start_offset":1074},"roles":["Name"],"token":"ByteKeySet","type":"Identifier"},{"children":[{"children":[{"id":"12254760f0e3569f","pos":{"end_col":18,"end_line":34,"end_offset":1086,"start_col":17,"start_line":34,"start_offset":1085},"roles":["Name"],"token":"T","type":"Identifier"},{"children":[{"id":"1aac4a26dd04cec6","pos":{"end_col":22,"end_line":34,"end_offset":1090,"start_col":19,"start_line":34,"start_offset":1087},"roles":["Name","Type"],"token":"any","type":"Identifier"}],"id":"7e05695e16898f6f","pos":{"end_col":22,"end_line":34,"end_offset":1090,"start_col":19,"start_line":34,"start_offset":1087},"roles":[],"type":"Synthetic"}],"id":"45bb697b105c2e64","pos":{"end_col":22,"end_line":34,"end_offset":1090,"start_col":17,"start_line":34,"start_offset":1085},"props":{"name":"T"},"roles":["Parameter"],"token":"T any","type":"Parameter"}],"id":"fc0206b36a781033","pos":{"end_col":23,"end_line":34,"end_offset":1091,"start_col":16,"start_line":34,"start_offset":1084},"roles":["Parameter"],"type":"Parameter"},{"children":[{"children":[{"id":"6c18e67997b06032","pos":{"end_col":30,"end_line":34,"end_offset":1098,"start_col":24,"start_line":34,"start_offset":1092},"roles":["Name"],"token":"theMap","type":"Identifier"},{"children":[{"id":"3ce834b5ba0214ee","pos":{"end_col":39,"end_line":34,"end_offset":1107,"start_col":35,"start_line":34,"start_offset":1103},"roles":["Name","Type"],"token":"byte","type":"Identifier"},{"id":"3b364c872f4402cb","pos":{"end_col":41,"end_line":34,"end_offset":1109,"start_col":40,"start_line":34,"start_offset":1108},"roles":["Name","Type"],"token":"T","type":"Identifier"}],"id":"0125614ff37dfd5b","pos":{"end_col":41,"end_line":34,"end_offset":1109,"start_col":31,"start_line":34,"start_offset":1099},"roles":[],"token":"map[byte]T","type":"Synthetic"}],"id":"fd5dfe662283e6ee","pos":{"end_col":41,"end_line":34,"end_offset":1109,"start_col":24,"start_line":34,"start_offset":1092},"props":{"name":"theMap"},"roles":["Parameter"],"type":"Parameter"}],"id":"88b9855b5a112824","pos":{"end_col":42,"end_line":34,"end_offset":1110,"start_col":23,"start_line":34,"start_offset":1091},"roles":["Parameter"],"token":"(theMap map[byte]T)","type":"Parameter"},{"id":"2ce3e3effa3a4a99","pos":{"end_col":47,"end_line":34,"end_offset":1115,"start_col":43,"start_line":34,"start_offset":1111},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"8cf5ef284e76788a","pos":{"end_col":13,"end_line":35,"end_offset":1130,"start_col":9,"start_line":35,"start_offset":1126},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"id":"039386cf7f2b21c9","pos":{"end_col":20,"end_line":35,"end_offset":1137,"start_col":14,"start_line":35,"start_offset":1131},"roles":["Name"],"token":"KeySet","type":"Identifier"},{"children":[{"id":"dfd07c2610bb0996","pos":{"end_col":27,"end_line":35,"end_offset":1144,"start_col":21,"start_line":35,"start_offset":1138},"roles":["Name"],"token":"theMap","type":"Identifier"}],"id":"4973aed79c303ca0","pos":{"end_col":28,"end_line":35,"end_offset":1145,"start_col":20,"start_line":35,"start_offset":1137},"roles":[],"token":"(theMap)","type":"List"}],"id":"58a0b74f11fcaf14","pos":{"end_col":28,"end_line":35,"end_offset":1145,"start_col":14,"start_line":35,"start_offset":1131},"roles":["Call"],"token":"KeySet(theMap)","type":"Call"}],"id":"0e4300e4ca3095cb","pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":13,"start_line":35,"start_offset":1130},"roles":[],"token":"(KeySet(theMap))","type":"List"}],"id":"b28bdf0ed64e8820","pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":9,"start_line":35,"start_offset":1126},"roles":["Call"],"token":"Byte(KeySet(theMap))","type":"Call"}],"id":"5d56f010d469b7cd","pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":9,"start_line":35,"start_offset":1126},"roles":[],"type":"List"}],"id":"e2c0173b16926fd1","pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":2,"start_line":35,"start_offset":1119},"roles":["Return"],"token":"return Byte(KeySet(theMap))","type":"Return"}],"id":"edf5a856714ca9f3","pos":{"end_col":2,"end_line":36,"end_offset":1148,"start_col":48,"start_line":34,"start_offset":1116},"roles":["Body"],"token":"{\n\treturn Byte(KeySet(theMap))\n}","type":"Block"}],"id":"ef8658e6bf69517b","pos":{"end_col":2,"end_line":36,"end_offset":1148,"start_col":1,"start_line":34,"start_offset":1069},"props":{"name":"ByteKeySet"},"roles":["Function","Declaration"],"token":"func ByteKeySet[T any](theMap map[byte]T) Byte {\n\treturn Byte(KeySet(theMap))\n}","type":"Function"},{"id":"95c8a54149f0e77e","pos":{"end_col":33,"end_line":38,"end_offset":1182,"start_col":1,"start_line":38,"start_offset":1150},"roles":["Comment"],"token":"// Insert adds items to the set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"41a5c517f54d994c","pos":{"end_col":8,"end_line":39,"end_offset":1190,"start_col":7,"start_line":39,"start_offset":1189},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"3302a6e0296277be","pos":{"end_col":13,"end_line":39,"end_offset":1195,"start_col":9,"start_line":39,"start_offset":1191},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"0c499813e6c60194","pos":{"end_col":13,"end_line":39,"end_offset":1195,"start_col":7,"start_line":39,"start_offset":1189},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"c6b0fb9105a228db","pos":{"end_col":14,"end_line":39,"end_offset":1196,"start_col":6,"start_line":39,"start_offset":1188},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"ed4ecc19669d8e01","pos":{"end_col":21,"end_line":39,"end_offset":1203,"start_col":15,"start_line":39,"start_offset":1197},"roles":["Name"],"token":"Insert","type":"Identifier"},{"children":[{"children":[{"id":"29da4d9d36d6d5a5","pos":{"end_col":27,"end_line":39,"end_offset":1209,"start_col":22,"start_line":39,"start_offset":1204},"roles":["Name"],"token":"items","type":"Identifier"},{"id":"acb3019f9947c447","pos":{"end_col":35,"end_line":39,"end_offset":1217,"start_col":31,"start_line":39,"start_offset":1213},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"92c07aa45b56c9e8","pos":{"end_col":35,"end_line":39,"end_offset":1217,"start_col":22,"start_line":39,"start_offset":1204},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"id":"f81897a52a2124a3","pos":{"end_col":36,"end_line":39,"end_offset":1218,"start_col":21,"start_line":39,"start_offset":1203},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"id":"cde55ea652429623","pos":{"end_col":41,"end_line":39,"end_offset":1223,"start_col":37,"start_line":39,"start_offset":1219},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"07173253244b6fd2","pos":{"end_col":13,"end_line":40,"end_offset":1238,"start_col":9,"start_line":40,"start_offset":1234},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"6db7bc690078db43","pos":{"end_col":18,"end_line":40,"end_offset":1243,"start_col":14,"start_line":40,"start_offset":1239},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"7e5190b2603057a0","pos":{"end_col":20,"end_line":40,"end_offset":1245,"start_col":19,"start_line":40,"start_offset":1244},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"bf9330f0b5f78b75","pos":{"end_col":21,"end_line":40,"end_offset":1246,"start_col":18,"start_line":40,"start_offset":1243},"roles":[],"token":"(s)","type":"List"}],"id":"86127d9330b665b0","pos":{"end_col":21,"end_line":40,"end_offset":1246,"start_col":14,"start_line":40,"start_offset":1239},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"e13e2da30e16dbc3","pos":{"end_col":28,"end_line":40,"end_offset":1253,"start_col":22,"start_line":40,"start_offset":1247},"roles":["Name"],"token":"Insert","type":"Identifier"}],"id":"12c058e2c5355ba9","pos":{"end_col":28,"end_line":40,"end_offset":1253,"start_col":14,"start_line":40,"start_offset":1239},"roles":[],"token":"cast(s).Insert","type":"Synthetic"},{"children":[{"children":[{"id":"c615c9219da806dc","pos":{"end_col":34,"end_line":40,"end_offset":1259,"start_col":29,"start_line":40,"start_offset":1254},"roles":["Name"],"token":"items","type":"Identifier"}],"id":"16b21056f61bbf00","pos":{"end_col":37,"end_line":40,"end_offset":1262,"start_col":29,"start_line":40,"start_offset":1254},"roles":[],"token":"items...","type":"List"}],"id":"b673d965bc6d79f0","pos":{"end_col":38,"end_line":40,"end_offset":1263,"start_col":28,"start_line":40,"start_offset":1253},"roles":[],"token":"(items...)","type":"List"}],"id":"a9a1fde785d2f135","pos":{"end_col":38,"end_line":40,"end_offset":1263,"start_col":14,"start_line":40,"start_offset":1239},"roles":["Call"],"token":"cast(s).Insert(items...)","type":"Call"}],"id":"046bdf78df9da65e","pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":13,"start_line":40,"start_offset":1238},"roles":[],"token":"(cast(s).Insert(items...))","type":"List"}],"id":"8409b714dd834aab","pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":9,"start_line":40,"start_offset":1234},"roles":["Call"],"token":"Byte(cast(s).Insert(items...))","type":"Call"}],"id":"e2b9bc3070a0c053","pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":9,"start_line":40,"start_offset":1234},"roles":[],"type":"List"}],"id":"9b2ffa3e759bd7f9","pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":2,"start_line":40,"start_offset":1227},"roles":["Return"],"token":"return Byte(cast(s).Insert(items...))","type":"Return"}],"id":"61ca07e573bc1fde","pos":{"end_col":2,"end_line":41,"end_offset":1266,"start_col":42,"start_line":39,"start_offset":1224},"roles":["Body"],"token":"{\n\treturn Byte(cast(s).Insert(items...))\n}","type":"Block"}],"id":"66ac27b3199e740a","pos":{"end_col":2,"end_line":41,"end_offset":1266,"start_col":1,"start_line":39,"start_offset":1183},"props":{"name":"Insert"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"3ef2082447e9c906","pos":{"end_col":42,"end_line":43,"end_offset":1309,"start_col":1,"start_line":43,"start_offset":1268},"roles":["Comment"],"token":"// Delete removes all items from the set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"859ef36c74fa6145","pos":{"end_col":8,"end_line":44,"end_offset":1317,"start_col":7,"start_line":44,"start_offset":1316},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"53e20497bd784b74","pos":{"end_col":13,"end_line":44,"end_offset":1322,"start_col":9,"start_line":44,"start_offset":1318},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"5f5f043b37405427","pos":{"end_col":13,"end_line":44,"end_offset":1322,"start_col":7,"start_line":44,"start_offset":1316},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"82c8360b1264f82f","pos":{"end_col":14,"end_line":44,"end_offset":1323,"start_col":6,"start_line":44,"start_offset":1315},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"26c38e14c8241d67","pos":{"end_col":21,"end_line":44,"end_offset":1330,"start_col":15,"start_line":44,"start_offset":1324},"roles":["Name"],"token":"Delete","type":"Identifier"},{"children":[{"children":[{"id":"d0d2993877119284","pos":{"end_col":27,"end_line":44,"end_offset":1336,"start_col":22,"start_line":44,"start_offset":1331},"roles":["Name"],"token":"items","type":"Identifier"},{"id":"1bd790b9805a6c68","pos":{"end_col":35,"end_line":44,"end_offset":1344,"start_col":31,"start_line":44,"start_offset":1340},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"468fb6809ea7cb08","pos":{"end_col":35,"end_line":44,"end_offset":1344,"start_col":22,"start_line":44,"start_offset":1331},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"id":"72b9179af501940b","pos":{"end_col":36,"end_line":44,"end_offset":1345,"start_col":21,"start_line":44,"start_offset":1330},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"id":"617037ef807431ef","pos":{"end_col":41,"end_line":44,"end_offset":1350,"start_col":37,"start_line":44,"start_offset":1346},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"89cfe7b492f44a44","pos":{"end_col":13,"end_line":45,"end_offset":1365,"start_col":9,"start_line":45,"start_offset":1361},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"84cf4781948f9833","pos":{"end_col":18,"end_line":45,"end_offset":1370,"start_col":14,"start_line":45,"start_offset":1366},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"ee7d6857c0e7b1b9","pos":{"end_col":20,"end_line":45,"end_offset":1372,"start_col":19,"start_line":45,"start_offset":1371},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"1c1a41584e1f7275","pos":{"end_col":21,"end_line":45,"end_offset":1373,"start_col":18,"start_line":45,"start_offset":1370},"roles":[],"token":"(s)","type":"List"}],"id":"e805da7b0b93e96c","pos":{"end_col":21,"end_line":45,"end_offset":1373,"start_col":14,"start_line":45,"start_offset":1366},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"aa22bd19ea04a7aa","pos":{"end_col":28,"end_line":45,"end_offset":1380,"start_col":22,"start_line":45,"start_offset":1374},"roles":["Name"],"token":"Delete","type":"Identifier"}],"id":"c475c9f273faa3fb","pos":{"end_col":28,"end_line":45,"end_offset":1380,"start_col":14,"start_line":45,"start_offset":1366},"roles":[],"token":"cast(s).Delete","type":"Synthetic"},{"children":[{"children":[{"id":"662cd65df36043b3","pos":{"end_col":34,"end_line":45,"end_offset":1386,"start_col":29,"start_line":45,"start_offset":1381},"roles":["Name"],"token":"items","type":"Identifier"}],"id":"d445fde0c013033c","pos":{"end_col":37,"end_line":45,"end_offset":1389,"start_col":29,"start_line":45,"start_offset":1381},"roles":[],"token":"items...","type":"List"}],"id":"b4e6ae9b52b020ce","pos":{"end_col":38,"end_line":45,"end_offset":1390,"start_col":28,"start_line":45,"start_offset":1380},"roles":[],"token":"(items...)","type":"List"}],"id":"2ea04b61588bebf0","pos":{"end_col":38,"end_line":45,"end_offset":1390,"start_col":14,"start_line":45,"start_offset":1366},"roles":["Call"],"token":"cast(s).Delete(items...)","type":"Call"}],"id":"e588d45162ddf317","pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":13,"start_line":45,"start_offset":1365},"roles":[],"token":"(cast(s).Delete(items...))","type":"List"}],"id":"82419aa786f9d334","pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":9,"start_line":45,"start_offset":1361},"roles":["Call"],"token":"Byte(cast(s).Delete(items...))","type":"Call"}],"id":"200ec9e57de9704c","pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":9,"start_line":45,"start_offset":1361},"roles":[],"type":"List"}],"id":"4f07a37ac2580a6a","pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":2,"start_line":45,"start_offset":1354},"roles":["Return"],"token":"return Byte(cast(s).Delete(items...))","type":"Return"}],"id":"6f5ee0ae0c95c4bf","pos":{"end_col":2,"end_line":46,"end_offset":1393,"start_col":42,"start_line":44,"start_offset":1351},"roles":["Body"],"token":"{\n\treturn Byte(cast(s).Delete(items...))\n}","type":"Block"}],"id":"61ae4c7f7e8f9b2d","pos":{"end_col":2,"end_line":46,"end_offset":1393,"start_col":1,"start_line":44,"start_offset":1310},"props":{"name":"Delete"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"d0cc3aed2e617eab","pos":{"end_col":65,"end_line":48,"end_offset":1459,"start_col":1,"start_line":48,"start_offset":1395},"roles":["Comment"],"token":"// Has returns true if and only if item is contained in the set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"6fc3eba1a948deff","pos":{"end_col":8,"end_line":49,"end_offset":1467,"start_col":7,"start_line":49,"start_offset":1466},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"6baee743a66fd9da","pos":{"end_col":13,"end_line":49,"end_offset":1472,"start_col":9,"start_line":49,"start_offset":1468},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"f8edc98e2e9beed6","pos":{"end_col":13,"end_line":49,"end_offset":1472,"start_col":7,"start_line":49,"start_offset":1466},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"4fc86927cd06540f","pos":{"end_col":14,"end_line":49,"end_offset":1473,"start_col":6,"start_line":49,"start_offset":1465},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"c78da716c035560a","pos":{"end_col":18,"end_line":49,"end_offset":1477,"start_col":15,"start_line":49,"start_offset":1474},"roles":["Name"],"token":"Has","type":"Identifier"},{"children":[{"children":[{"id":"8ec1aeb28d4a2847","pos":{"end_col":23,"end_line":49,"end_offset":1482,"start_col":19,"start_line":49,"start_offset":1478},"roles":["Name"],"token":"item","type":"Identifier"},{"id":"829c8e03838f3af0","pos":{"end_col":28,"end_line":49,"end_offset":1487,"start_col":24,"start_line":49,"start_offset":1483},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"8ef62c773b42b60a","pos":{"end_col":28,"end_line":49,"end_offset":1487,"start_col":19,"start_line":49,"start_offset":1478},"props":{"name":"item"},"roles":["Parameter"],"type":"Parameter"}],"id":"3d13c4edf7ea1554","pos":{"end_col":29,"end_line":49,"end_offset":1488,"start_col":18,"start_line":49,"start_offset":1477},"roles":["Parameter"],"token":"(item byte)","type":"Parameter"},{"id":"1350b70ac95e1008","pos":{"end_col":34,"end_line":49,"end_offset":1493,"start_col":30,"start_line":49,"start_offset":1489},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"id":"2bce9e8811df42da","pos":{"end_col":13,"end_line":50,"end_offset":1508,"start_col":9,"start_line":50,"start_offset":1504},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"3a85b31b4af83699","pos":{"end_col":15,"end_line":50,"end_offset":1510,"start_col":14,"start_line":50,"start_offset":1509},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"bea412476c53a14e","pos":{"end_col":16,"end_line":50,"end_offset":1511,"start_col":13,"start_line":50,"start_offset":1508},"roles":[],"token":"(s)","type":"List"}],"id":"9e9cc11c9630c9cf","pos":{"end_col":16,"end_line":50,"end_offset":1511,"start_col":9,"start_line":50,"start_offset":1504},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"bcee419bc39c8fb6","pos":{"end_col":20,"end_line":50,"end_offset":1515,"start_col":17,"start_line":50,"start_offset":1512},"roles":["Name"],"token":"Has","type":"Identifier"}],"id":"2ba2e8b5784cc6a5","pos":{"end_col":20,"end_line":50,"end_offset":1515,"start_col":9,"start_line":50,"start_offset":1504},"roles":[],"token":"cast(s).Has","type":"Synthetic"},{"children":[{"id":"2b844abdcc7c4fbb","pos":{"end_col":25,"end_line":50,"end_offset":1520,"start_col":21,"start_line":50,"start_offset":1516},"roles":["Name"],"token":"item","type":"Identifier"}],"id":"3d7272506fdf3e4b","pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":20,"start_line":50,"start_offset":1515},"roles":[],"token":"(item)","type":"List"}],"id":"d8abc1642454e811","pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":9,"start_line":50,"start_offset":1504},"roles":["Call"],"token":"cast(s).Has(item)","type":"Call"}],"id":"ac4b4cab64708ae0","pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":9,"start_line":50,"start_offset":1504},"roles":[],"type":"List"}],"id":"dd1e1bf13175432f","pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":2,"start_line":50,"start_offset":1497},"roles":["Return"],"token":"return cast(s).Has(item)","type":"Return"}],"id":"feca1c645384aacc","pos":{"end_col":2,"end_line":51,"end_offset":1523,"start_col":35,"start_line":49,"start_offset":1494},"roles":["Body"],"token":"{\n\treturn cast(s).Has(item)\n}","type":"Block"}],"id":"ed228010867d6b9b","pos":{"end_col":2,"end_line":51,"end_offset":1523,"start_col":1,"start_line":49,"start_offset":1460},"props":{"name":"Has"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"d39ad3002d2bbc73","pos":{"end_col":74,"end_line":53,"end_offset":1598,"start_col":1,"start_line":53,"start_offset":1525},"roles":["Comment"],"token":"// HasAll returns true if and only if all items are contained in the set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"6957e0d21821aaf5","pos":{"end_col":8,"end_line":54,"end_offset":1606,"start_col":7,"start_line":54,"start_offset":1605},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"306b1654783b1a5d","pos":{"end_col":13,"end_line":54,"end_offset":1611,"start_col":9,"start_line":54,"start_offset":1607},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"db5af44f28ae42e0","pos":{"end_col":13,"end_line":54,"end_offset":1611,"start_col":7,"start_line":54,"start_offset":1605},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"5df0113dbb7692e5","pos":{"end_col":14,"end_line":54,"end_offset":1612,"start_col":6,"start_line":54,"start_offset":1604},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"42699c1ed45ddd67","pos":{"end_col":21,"end_line":54,"end_offset":1619,"start_col":15,"start_line":54,"start_offset":1613},"roles":["Name"],"token":"HasAll","type":"Identifier"},{"children":[{"children":[{"id":"ae7cf20c65f9e4c2","pos":{"end_col":27,"end_line":54,"end_offset":1625,"start_col":22,"start_line":54,"start_offset":1620},"roles":["Name"],"token":"items","type":"Identifier"},{"id":"48ee9dab38eed422","pos":{"end_col":35,"end_line":54,"end_offset":1633,"start_col":31,"start_line":54,"start_offset":1629},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"8fe5189d249990f8","pos":{"end_col":35,"end_line":54,"end_offset":1633,"start_col":22,"start_line":54,"start_offset":1620},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"id":"31cc4bb86822c908","pos":{"end_col":36,"end_line":54,"end_offset":1634,"start_col":21,"start_line":54,"start_offset":1619},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"id":"c924e5c01c5755c6","pos":{"end_col":41,"end_line":54,"end_offset":1639,"start_col":37,"start_line":54,"start_offset":1635},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"id":"169a20df9fb54d8b","pos":{"end_col":13,"end_line":55,"end_offset":1654,"start_col":9,"start_line":55,"start_offset":1650},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"e04e6d970ecb039f","pos":{"end_col":15,"end_line":55,"end_offset":1656,"start_col":14,"start_line":55,"start_offset":1655},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"0d377c2c9116faed","pos":{"end_col":16,"end_line":55,"end_offset":1657,"start_col":13,"start_line":55,"start_offset":1654},"roles":[],"token":"(s)","type":"List"}],"id":"9a7368c7e0f0e392","pos":{"end_col":16,"end_line":55,"end_offset":1657,"start_col":9,"start_line":55,"start_offset":1650},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"e2837f1a90a3fcd7","pos":{"end_col":23,"end_line":55,"end_offset":1664,"start_col":17,"start_line":55,"start_offset":1658},"roles":["Name"],"token":"HasAll","type":"Identifier"}],"id":"a4d3661d5140f636","pos":{"end_col":23,"end_line":55,"end_offset":1664,"start_col":9,"start_line":55,"start_offset":1650},"roles":[],"token":"cast(s).HasAll","type":"Synthetic"},{"children":[{"children":[{"id":"0304c5b1cd55458b","pos":{"end_col":29,"end_line":55,"end_offset":1670,"start_col":24,"start_line":55,"start_offset":1665},"roles":["Name"],"token":"items","type":"Identifier"}],"id":"4778ec428e64baf8","pos":{"end_col":32,"end_line":55,"end_offset":1673,"start_col":24,"start_line":55,"start_offset":1665},"roles":[],"token":"items...","type":"List"}],"id":"b8e57e1974bd5f4a","pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":23,"start_line":55,"start_offset":1664},"roles":[],"token":"(items...)","type":"List"}],"id":"e09361660e338546","pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":9,"start_line":55,"start_offset":1650},"roles":["Call"],"token":"cast(s).HasAll(items...)","type":"Call"}],"id":"6e5d342aaa141ce4","pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":9,"start_line":55,"start_offset":1650},"roles":[],"type":"List"}],"id":"d074a372f54f9646","pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":2,"start_line":55,"start_offset":1643},"roles":["Return"],"token":"return cast(s).HasAll(items...)","type":"Return"}],"id":"8e27302c3f7aa38c","pos":{"end_col":2,"end_line":56,"end_offset":1676,"start_col":42,"start_line":54,"start_offset":1640},"roles":["Body"],"token":"{\n\treturn cast(s).HasAll(items...)\n}","type":"Block"}],"id":"d2d30d64cc2e28f0","pos":{"end_col":2,"end_line":56,"end_offset":1676,"start_col":1,"start_line":54,"start_offset":1599},"props":{"name":"HasAll"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"ee847ebe5a758cc4","pos":{"end_col":62,"end_line":58,"end_offset":1739,"start_col":1,"start_line":58,"start_offset":1678},"roles":["Comment"],"token":"// HasAny returns true if any items are contained in the set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"e043b7601712f60f","pos":{"end_col":8,"end_line":59,"end_offset":1747,"start_col":7,"start_line":59,"start_offset":1746},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"c82743e929848a37","pos":{"end_col":13,"end_line":59,"end_offset":1752,"start_col":9,"start_line":59,"start_offset":1748},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"9b57e75e87420108","pos":{"end_col":13,"end_line":59,"end_offset":1752,"start_col":7,"start_line":59,"start_offset":1746},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"e65b55a8bcf0d027","pos":{"end_col":14,"end_line":59,"end_offset":1753,"start_col":6,"start_line":59,"start_offset":1745},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"12de634220ef262b","pos":{"end_col":21,"end_line":59,"end_offset":1760,"start_col":15,"start_line":59,"start_offset":1754},"roles":["Name"],"token":"HasAny","type":"Identifier"},{"children":[{"children":[{"id":"58892b6cbee71205","pos":{"end_col":27,"end_line":59,"end_offset":1766,"start_col":22,"start_line":59,"start_offset":1761},"roles":["Name"],"token":"items","type":"Identifier"},{"id":"2060d5c409be14e5","pos":{"end_col":35,"end_line":59,"end_offset":1774,"start_col":31,"start_line":59,"start_offset":1770},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"8c2444d697a2b9dd","pos":{"end_col":35,"end_line":59,"end_offset":1774,"start_col":22,"start_line":59,"start_offset":1761},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"id":"fc1e27c19d09d21d","pos":{"end_col":36,"end_line":59,"end_offset":1775,"start_col":21,"start_line":59,"start_offset":1760},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"id":"3ad2f01b0f2072f0","pos":{"end_col":41,"end_line":59,"end_offset":1780,"start_col":37,"start_line":59,"start_offset":1776},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"id":"2623593fbc2ba49c","pos":{"end_col":13,"end_line":60,"end_offset":1795,"start_col":9,"start_line":60,"start_offset":1791},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"888c08fe61389a20","pos":{"end_col":15,"end_line":60,"end_offset":1797,"start_col":14,"start_line":60,"start_offset":1796},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"fa266d38fe501d6f","pos":{"end_col":16,"end_line":60,"end_offset":1798,"start_col":13,"start_line":60,"start_offset":1795},"roles":[],"token":"(s)","type":"List"}],"id":"4209131baa68f3f5","pos":{"end_col":16,"end_line":60,"end_offset":1798,"start_col":9,"start_line":60,"start_offset":1791},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"5da7abc801ae0be3","pos":{"end_col":23,"end_line":60,"end_offset":1805,"start_col":17,"start_line":60,"start_offset":1799},"roles":["Name"],"token":"HasAny","type":"Identifier"}],"id":"4438b9e014a93046","pos":{"end_col":23,"end_line":60,"end_offset":1805,"start_col":9,"start_line":60,"start_offset":1791},"roles":[],"token":"cast(s).HasAny","type":"Synthetic"},{"children":[{"children":[{"id":"a137f1162c204ae9","pos":{"end_col":29,"end_line":60,"end_offset":1811,"start_col":24,"start_line":60,"start_offset":1806},"roles":["Name"],"token":"items","type":"Identifier"}],"id":"4f46c471860ba4f4","pos":{"end_col":32,"end_line":60,"end_offset":1814,"start_col":24,"start_line":60,"start_offset":1806},"roles":[],"token":"items...","type":"List"}],"id":"8e511a60c4092ff4","pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":23,"start_line":60,"start_offset":1805},"roles":[],"token":"(items...)","type":"List"}],"id":"28df55d1324f96ad","pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":9,"start_line":60,"start_offset":1791},"roles":["Call"],"token":"cast(s).HasAny(items...)","type":"Call"}],"id":"67f0019a3035b720","pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":9,"start_line":60,"start_offset":1791},"roles":[],"type":"List"}],"id":"8b2524a813cfdc57","pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":2,"start_line":60,"start_offset":1784},"roles":["Return"],"token":"return cast(s).HasAny(items...)","type":"Return"}],"id":"c019c9b1eec89420","pos":{"end_col":2,"end_line":61,"end_offset":1817,"start_col":42,"start_line":59,"start_offset":1781},"roles":["Body"],"token":"{\n\treturn cast(s).HasAny(items...)\n}","type":"Block"}],"id":"810e26e7f446cc59","pos":{"end_col":2,"end_line":61,"end_offset":1817,"start_col":1,"start_line":59,"start_offset":1740},"props":{"name":"HasAny"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"35717aca77fc92d8","pos":{"end_col":63,"end_line":63,"end_offset":1881,"start_col":1,"start_line":63,"start_offset":1819},"roles":["Comment"],"token":"// Clone returns a new set which is a copy of the current set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"08ee52f7b1f98330","pos":{"end_col":8,"end_line":64,"end_offset":1889,"start_col":7,"start_line":64,"start_offset":1888},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"aceaa8ad0fc277b8","pos":{"end_col":13,"end_line":64,"end_offset":1894,"start_col":9,"start_line":64,"start_offset":1890},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"edc3c8b76bea6028","pos":{"end_col":13,"end_line":64,"end_offset":1894,"start_col":7,"start_line":64,"start_offset":1888},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"f776df5a8fd71f6b","pos":{"end_col":14,"end_line":64,"end_offset":1895,"start_col":6,"start_line":64,"start_offset":1887},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"35cf99f488cefd27","pos":{"end_col":20,"end_line":64,"end_offset":1901,"start_col":15,"start_line":64,"start_offset":1896},"roles":["Name"],"token":"Clone","type":"Identifier"},{"id":"68746c06d95ea8bc","pos":{"end_col":22,"end_line":64,"end_offset":1903,"start_col":20,"start_line":64,"start_offset":1901},"roles":["Parameter"],"token":"()","type":"Parameter"},{"id":"2fd449d355d70848","pos":{"end_col":27,"end_line":64,"end_offset":1908,"start_col":23,"start_line":64,"start_offset":1904},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"ec08fdad2131373f","pos":{"end_col":13,"end_line":65,"end_offset":1923,"start_col":9,"start_line":65,"start_offset":1919},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"cc1bc2364f813aa9","pos":{"end_col":18,"end_line":65,"end_offset":1928,"start_col":14,"start_line":65,"start_offset":1924},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"9e874a0fbf43d4ad","pos":{"end_col":20,"end_line":65,"end_offset":1930,"start_col":19,"start_line":65,"start_offset":1929},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"55064ba66b649821","pos":{"end_col":21,"end_line":65,"end_offset":1931,"start_col":18,"start_line":65,"start_offset":1928},"roles":[],"token":"(s)","type":"List"}],"id":"aa5cb72ea40070e1","pos":{"end_col":21,"end_line":65,"end_offset":1931,"start_col":14,"start_line":65,"start_offset":1924},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"c1ba8748825f9b35","pos":{"end_col":27,"end_line":65,"end_offset":1937,"start_col":22,"start_line":65,"start_offset":1932},"roles":["Name"],"token":"Clone","type":"Identifier"}],"id":"6a34170eb7e307f5","pos":{"end_col":27,"end_line":65,"end_offset":1937,"start_col":14,"start_line":65,"start_offset":1924},"roles":[],"token":"cast(s).Clone","type":"Synthetic"},{"id":"6290cfa667135ddd","pos":{"end_col":29,"end_line":65,"end_offset":1939,"start_col":27,"start_line":65,"start_offset":1937},"roles":[],"token":"()","type":"List"}],"id":"975367db8159fb08","pos":{"end_col":29,"end_line":65,"end_offset":1939,"start_col":14,"start_line":65,"start_offset":1924},"roles":["Call"],"token":"cast(s).Clone()","type":"Call"}],"id":"e8b4c72e3fec059c","pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":13,"start_line":65,"start_offset":1923},"roles":[],"token":"(cast(s).Clone())","type":"List"}],"id":"1d7fe158ae70d3eb","pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":9,"start_line":65,"start_offset":1919},"roles":["Call"],"token":"Byte(cast(s).Clone())","type":"Call"}],"id":"c2e38a1c0b401884","pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":9,"start_line":65,"start_offset":1919},"roles":[],"type":"List"}],"id":"9990ca66f2a7d99f","pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":2,"start_line":65,"start_offset":1912},"roles":["Return"],"token":"return Byte(cast(s).Clone())","type":"Return"}],"id":"e73623b9b17bd0f9","pos":{"end_col":2,"end_line":66,"end_offset":1942,"start_col":28,"start_line":64,"start_offset":1909},"roles":["Body"],"token":"{\n\treturn Byte(cast(s).Clone())\n}","type":"Block"}],"id":"9013b21631f2b51c","pos":{"end_col":2,"end_line":66,"end_offset":1942,"start_col":1,"start_line":64,"start_offset":1882},"props":{"name":"Clone"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"17faf28a5d8425ca","pos":{"end_col":59,"end_line":68,"end_offset":2002,"start_col":1,"start_line":68,"start_offset":1944},"roles":["Comment"],"token":"// Difference returns a set of objects that are not in s2.","type":"Comment"},{"id":"f597aa1945c98e42","pos":{"end_col":16,"end_line":69,"end_offset":2018,"start_col":1,"start_line":69,"start_offset":2003},"roles":["Comment"],"token":"// For example:","type":"Comment"},{"id":"a895914c08358c29","pos":{"end_col":21,"end_line":70,"end_offset":2039,"start_col":1,"start_line":70,"start_offset":2019},"roles":["Comment"],"token":"// s1 = {a1, a2, a3}","type":"Comment"},{"id":"bce95b1083323a25","pos":{"end_col":25,"end_line":71,"end_offset":2064,"start_col":1,"start_line":71,"start_offset":2040},"roles":["Comment"],"token":"// s2 = {a1, a2, a4, a5}","type":"Comment"},{"id":"10993063a80d0e2b","pos":{"end_col":28,"end_line":72,"end_offset":2092,"start_col":1,"start_line":72,"start_offset":2065},"roles":["Comment"],"token":"// s1.Difference(s2) = {a3}","type":"Comment"},{"id":"08fb6deb2e5214ef","pos":{"end_col":32,"end_line":73,"end_offset":2124,"start_col":1,"start_line":73,"start_offset":2093},"roles":["Comment"],"token":"// s2.Difference(s1) = {a4, a5}","type":"Comment"},{"children":[{"children":[{"children":[{"id":"db2daee4ef965eb7","pos":{"end_col":9,"end_line":74,"end_offset":2133,"start_col":7,"start_line":74,"start_offset":2131},"roles":["Name"],"token":"s1","type":"Identifier"},{"id":"078e41b46a176a65","pos":{"end_col":14,"end_line":74,"end_offset":2138,"start_col":10,"start_line":74,"start_offset":2134},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"186cdcacb2a8af0c","pos":{"end_col":14,"end_line":74,"end_offset":2138,"start_col":7,"start_line":74,"start_offset":2131},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"id":"6949bffe93381706","pos":{"end_col":15,"end_line":74,"end_offset":2139,"start_col":6,"start_line":74,"start_offset":2130},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"id":"892235c9a0763e02","pos":{"end_col":26,"end_line":74,"end_offset":2150,"start_col":16,"start_line":74,"start_offset":2140},"roles":["Name"],"token":"Difference","type":"Identifier"},{"children":[{"children":[{"id":"b4a40b3b73c313c4","pos":{"end_col":29,"end_line":74,"end_offset":2153,"start_col":27,"start_line":74,"start_offset":2151},"roles":["Name"],"token":"s2","type":"Identifier"},{"id":"3f7e5b0e3163ed8b","pos":{"end_col":34,"end_line":74,"end_offset":2158,"start_col":30,"start_line":74,"start_offset":2154},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"09949b5d037ee4c5","pos":{"end_col":34,"end_line":74,"end_offset":2158,"start_col":27,"start_line":74,"start_offset":2151},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"id":"83ffb280302b88d4","pos":{"end_col":35,"end_line":74,"end_offset":2159,"start_col":26,"start_line":74,"start_offset":2150},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"id":"b404beffe4dafc9c","pos":{"end_col":40,"end_line":74,"end_offset":2164,"start_col":36,"start_line":74,"start_offset":2160},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"85a37fb4426b202f","pos":{"end_col":13,"end_line":75,"end_offset":2179,"start_col":9,"start_line":75,"start_offset":2175},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"a34c0aad25bf57f2","pos":{"end_col":18,"end_line":75,"end_offset":2184,"start_col":14,"start_line":75,"start_offset":2180},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"d5c3f448eee69112","pos":{"end_col":21,"end_line":75,"end_offset":2187,"start_col":19,"start_line":75,"start_offset":2185},"roles":["Name"],"token":"s1","type":"Identifier"}],"id":"21f3f71a72fb511d","pos":{"end_col":22,"end_line":75,"end_offset":2188,"start_col":18,"start_line":75,"start_offset":2184},"roles":[],"token":"(s1)","type":"List"}],"id":"efe473ded2be592f","pos":{"end_col":22,"end_line":75,"end_offset":2188,"start_col":14,"start_line":75,"start_offset":2180},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"id":"f33713a54db19d87","pos":{"end_col":33,"end_line":75,"end_offset":2199,"start_col":23,"start_line":75,"start_offset":2189},"roles":["Name"],"token":"Difference","type":"Identifier"}],"id":"70cf5817d5d32d79","pos":{"end_col":33,"end_line":75,"end_offset":2199,"start_col":14,"start_line":75,"start_offset":2180},"roles":[],"token":"cast(s1).Difference","type":"Synthetic"},{"children":[{"children":[{"id":"126f289947fee01b","pos":{"end_col":38,"end_line":75,"end_offset":2204,"start_col":34,"start_line":75,"start_offset":2200},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"d75d335eef4db7f0","pos":{"end_col":41,"end_line":75,"end_offset":2207,"start_col":39,"start_line":75,"start_offset":2205},"roles":["Name"],"token":"s2","type":"Identifier"}],"id":"5d1b1c9d9da114b7","pos":{"end_col":42,"end_line":75,"end_offset":2208,"start_col":38,"start_line":75,"start_offset":2204},"roles":[],"token":"(s2)","type":"List"}],"id":"42bbf23cb82ac47d","pos":{"end_col":42,"end_line":75,"end_offset":2208,"start_col":34,"start_line":75,"start_offset":2200},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"id":"75689f0f74698949","pos":{"end_col":43,"end_line":75,"end_offset":2209,"start_col":33,"start_line":75,"start_offset":2199},"roles":[],"token":"(cast(s2))","type":"List"}],"id":"6fb46f98c2027e62","pos":{"end_col":43,"end_line":75,"end_offset":2209,"start_col":14,"start_line":75,"start_offset":2180},"roles":["Call"],"token":"cast(s1).Difference(cast(s2))","type":"Call"}],"id":"7b936ea83998f17c","pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":13,"start_line":75,"start_offset":2179},"roles":[],"token":"(cast(s1).Difference(cast(s2)))","type":"List"}],"id":"1999c4f189e8ea5e","pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":9,"start_line":75,"start_offset":2175},"roles":["Call"],"token":"Byte(cast(s1).Difference(cast(s2)))","type":"Call"}],"id":"158fdf432af3f5f5","pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":9,"start_line":75,"start_offset":2175},"roles":[],"type":"List"}],"id":"3233e0d3984b80ed","pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":2,"start_line":75,"start_offset":2168},"roles":["Return"],"token":"return Byte(cast(s1).Difference(cast(s2)))","type":"Return"}],"id":"609026e6b4f55390","pos":{"end_col":2,"end_line":76,"end_offset":2212,"start_col":41,"start_line":74,"start_offset":2165},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).Difference(cast(s2)))\n}","type":"Block"}],"id":"1411290aca96d400","pos":{"end_col":2,"end_line":76,"end_offset":2212,"start_col":1,"start_line":74,"start_offset":2125},"props":{"name":"Difference"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"717592ff479823de","pos":{"end_col":113,"end_line":78,"end_offset":2326,"start_col":1,"start_line":78,"start_offset":2214},"roles":["Comment"],"token":"// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.","type":"Comment"},{"id":"0a1069a8624bc8a8","pos":{"end_col":16,"end_line":79,"end_offset":2342,"start_col":1,"start_line":79,"start_offset":2327},"roles":["Comment"],"token":"// For example:","type":"Comment"},{"id":"b37da3baf120ec1e","pos":{"end_col":21,"end_line":80,"end_offset":2363,"start_col":1,"start_line":80,"start_offset":2343},"roles":["Comment"],"token":"// s1 = {a1, a2, a3}","type":"Comment"},{"id":"80deb7c64733baed","pos":{"end_col":25,"end_line":81,"end_offset":2388,"start_col":1,"start_line":81,"start_offset":2364},"roles":["Comment"],"token":"// s2 = {a1, a2, a4, a5}","type":"Comment"},{"id":"6c500ba94cd45636","pos":{"end_col":45,"end_line":82,"end_offset":2433,"start_col":1,"start_line":82,"start_offset":2389},"roles":["Comment"],"token":"// s1.SymmetricDifference(s2) = {a3, a4, a5}","type":"Comment"},{"id":"4aa95ef08fd6e00c","pos":{"end_col":45,"end_line":83,"end_offset":2478,"start_col":1,"start_line":83,"start_offset":2434},"roles":["Comment"],"token":"// s2.SymmetricDifference(s1) = {a3, a4, a5}","type":"Comment"},{"children":[{"children":[{"children":[{"id":"82fe6f2ffd1abd4a","pos":{"end_col":9,"end_line":84,"end_offset":2487,"start_col":7,"start_line":84,"start_offset":2485},"roles":["Name"],"token":"s1","type":"Identifier"},{"id":"6dcf01b2dc1df915","pos":{"end_col":14,"end_line":84,"end_offset":2492,"start_col":10,"start_line":84,"start_offset":2488},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"c1c4cb1f0c59138f","pos":{"end_col":14,"end_line":84,"end_offset":2492,"start_col":7,"start_line":84,"start_offset":2485},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"id":"e88d0d20e5818ca5","pos":{"end_col":15,"end_line":84,"end_offset":2493,"start_col":6,"start_line":84,"start_offset":2484},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"id":"b9ae1600e7fe8e0d","pos":{"end_col":35,"end_line":84,"end_offset":2513,"start_col":16,"start_line":84,"start_offset":2494},"roles":["Name"],"token":"SymmetricDifference","type":"Identifier"},{"children":[{"children":[{"id":"6d1914600c408244","pos":{"end_col":38,"end_line":84,"end_offset":2516,"start_col":36,"start_line":84,"start_offset":2514},"roles":["Name"],"token":"s2","type":"Identifier"},{"id":"7befa79a7b3b8a19","pos":{"end_col":43,"end_line":84,"end_offset":2521,"start_col":39,"start_line":84,"start_offset":2517},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"4fa74cefbb3e5f0b","pos":{"end_col":43,"end_line":84,"end_offset":2521,"start_col":36,"start_line":84,"start_offset":2514},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"id":"26df223a25743642","pos":{"end_col":44,"end_line":84,"end_offset":2522,"start_col":35,"start_line":84,"start_offset":2513},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"id":"1bdb68f273aa42ec","pos":{"end_col":49,"end_line":84,"end_offset":2527,"start_col":45,"start_line":84,"start_offset":2523},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"0f2fb17e3a79f585","pos":{"end_col":13,"end_line":85,"end_offset":2542,"start_col":9,"start_line":85,"start_offset":2538},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"f836738db28aea1d","pos":{"end_col":18,"end_line":85,"end_offset":2547,"start_col":14,"start_line":85,"start_offset":2543},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"26012d26ccc02eab","pos":{"end_col":21,"end_line":85,"end_offset":2550,"start_col":19,"start_line":85,"start_offset":2548},"roles":["Name"],"token":"s1","type":"Identifier"}],"id":"1e5621bcfd7417ea","pos":{"end_col":22,"end_line":85,"end_offset":2551,"start_col":18,"start_line":85,"start_offset":2547},"roles":[],"token":"(s1)","type":"List"}],"id":"c757f1114a551350","pos":{"end_col":22,"end_line":85,"end_offset":2551,"start_col":14,"start_line":85,"start_offset":2543},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"id":"091350e82578aeee","pos":{"end_col":42,"end_line":85,"end_offset":2571,"start_col":23,"start_line":85,"start_offset":2552},"roles":["Name"],"token":"SymmetricDifference","type":"Identifier"}],"id":"5bead0814f902111","pos":{"end_col":42,"end_line":85,"end_offset":2571,"start_col":14,"start_line":85,"start_offset":2543},"roles":[],"token":"cast(s1).SymmetricDifference","type":"Synthetic"},{"children":[{"children":[{"id":"35aee9f276ee738f","pos":{"end_col":47,"end_line":85,"end_offset":2576,"start_col":43,"start_line":85,"start_offset":2572},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"fe2e9a04080595f5","pos":{"end_col":50,"end_line":85,"end_offset":2579,"start_col":48,"start_line":85,"start_offset":2577},"roles":["Name"],"token":"s2","type":"Identifier"}],"id":"ec21cf29d0e7a39a","pos":{"end_col":51,"end_line":85,"end_offset":2580,"start_col":47,"start_line":85,"start_offset":2576},"roles":[],"token":"(s2)","type":"List"}],"id":"d606bda043c80fb8","pos":{"end_col":51,"end_line":85,"end_offset":2580,"start_col":43,"start_line":85,"start_offset":2572},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"id":"c7d7af6dfb16a2e9","pos":{"end_col":52,"end_line":85,"end_offset":2581,"start_col":42,"start_line":85,"start_offset":2571},"roles":[],"token":"(cast(s2))","type":"List"}],"id":"d90a2fd58eb3975f","pos":{"end_col":52,"end_line":85,"end_offset":2581,"start_col":14,"start_line":85,"start_offset":2543},"roles":["Call"],"token":"cast(s1).SymmetricDifference(cast(s2))","type":"Call"}],"id":"d46050e6ebbf409e","pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":13,"start_line":85,"start_offset":2542},"roles":[],"token":"(cast(s1).SymmetricDifference(cast(s2)))","type":"List"}],"id":"39391acd0d2fbca3","pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":9,"start_line":85,"start_offset":2538},"roles":["Call"],"token":"Byte(cast(s1).SymmetricDifference(cast(s2)))","type":"Call"}],"id":"bb1a4b3175f4064e","pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":9,"start_line":85,"start_offset":2538},"roles":[],"type":"List"}],"id":"c231d6e0f500620b","pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":2,"start_line":85,"start_offset":2531},"roles":["Return"],"token":"return Byte(cast(s1).SymmetricDifference(cast(s2)))","type":"Return"}],"id":"ed1b9bf74b8b8c0f","pos":{"end_col":2,"end_line":86,"end_offset":2584,"start_col":50,"start_line":84,"start_offset":2528},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).SymmetricDifference(cast(s2)))\n}","type":"Block"}],"id":"cd8d1263cca8f985","pos":{"end_col":2,"end_line":86,"end_offset":2584,"start_col":1,"start_line":84,"start_offset":2479},"props":{"name":"SymmetricDifference"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"cd430df4baae034c","pos":{"end_col":68,"end_line":88,"end_offset":2653,"start_col":1,"start_line":88,"start_offset":2586},"roles":["Comment"],"token":"// Union returns a new set which includes items in either s1 or s2.","type":"Comment"},{"id":"20683ad6aaa8cd09","pos":{"end_col":16,"end_line":89,"end_offset":2669,"start_col":1,"start_line":89,"start_offset":2654},"roles":["Comment"],"token":"// For example:","type":"Comment"},{"id":"d7b13d53ff881233","pos":{"end_col":17,"end_line":90,"end_offset":2686,"start_col":1,"start_line":90,"start_offset":2670},"roles":["Comment"],"token":"// s1 = {a1, a2}","type":"Comment"},{"id":"c3e62dff84ca334d","pos":{"end_col":17,"end_line":91,"end_offset":2703,"start_col":1,"start_line":91,"start_offset":2687},"roles":["Comment"],"token":"// s2 = {a3, a4}","type":"Comment"},{"id":"ee719c20c16b6c75","pos":{"end_col":35,"end_line":92,"end_offset":2738,"start_col":1,"start_line":92,"start_offset":2704},"roles":["Comment"],"token":"// s1.Union(s2) = {a1, a2, a3, a4}","type":"Comment"},{"id":"d6335603ab9066db","pos":{"end_col":35,"end_line":93,"end_offset":2773,"start_col":1,"start_line":93,"start_offset":2739},"roles":["Comment"],"token":"// s2.Union(s1) = {a1, a2, a3, a4}","type":"Comment"},{"children":[{"children":[{"children":[{"id":"8162520fca0c0dca","pos":{"end_col":9,"end_line":94,"end_offset":2782,"start_col":7,"start_line":94,"start_offset":2780},"roles":["Name"],"token":"s1","type":"Identifier"},{"id":"85a3355b54935527","pos":{"end_col":14,"end_line":94,"end_offset":2787,"start_col":10,"start_line":94,"start_offset":2783},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"f8649cfe5d0c2697","pos":{"end_col":14,"end_line":94,"end_offset":2787,"start_col":7,"start_line":94,"start_offset":2780},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"id":"e5ebd60b2ede8a0a","pos":{"end_col":15,"end_line":94,"end_offset":2788,"start_col":6,"start_line":94,"start_offset":2779},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"id":"9500e0a9239d6345","pos":{"end_col":21,"end_line":94,"end_offset":2794,"start_col":16,"start_line":94,"start_offset":2789},"roles":["Name"],"token":"Union","type":"Identifier"},{"children":[{"children":[{"id":"cb362fa4411e7503","pos":{"end_col":24,"end_line":94,"end_offset":2797,"start_col":22,"start_line":94,"start_offset":2795},"roles":["Name"],"token":"s2","type":"Identifier"},{"id":"431fa00f9177c144","pos":{"end_col":29,"end_line":94,"end_offset":2802,"start_col":25,"start_line":94,"start_offset":2798},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"56205af69417f7dc","pos":{"end_col":29,"end_line":94,"end_offset":2802,"start_col":22,"start_line":94,"start_offset":2795},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"id":"41b4fa372325c12b","pos":{"end_col":30,"end_line":94,"end_offset":2803,"start_col":21,"start_line":94,"start_offset":2794},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"id":"6e699caaf150d3df","pos":{"end_col":35,"end_line":94,"end_offset":2808,"start_col":31,"start_line":94,"start_offset":2804},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"109cfbe7afbeb035","pos":{"end_col":13,"end_line":95,"end_offset":2823,"start_col":9,"start_line":95,"start_offset":2819},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"79f1159934239336","pos":{"end_col":18,"end_line":95,"end_offset":2828,"start_col":14,"start_line":95,"start_offset":2824},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"a80cb25b4e21bf60","pos":{"end_col":21,"end_line":95,"end_offset":2831,"start_col":19,"start_line":95,"start_offset":2829},"roles":["Name"],"token":"s1","type":"Identifier"}],"id":"908f9317eb118fed","pos":{"end_col":22,"end_line":95,"end_offset":2832,"start_col":18,"start_line":95,"start_offset":2828},"roles":[],"token":"(s1)","type":"List"}],"id":"7bdd6e80a604e8b7","pos":{"end_col":22,"end_line":95,"end_offset":2832,"start_col":14,"start_line":95,"start_offset":2824},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"id":"3691af365f91b76c","pos":{"end_col":28,"end_line":95,"end_offset":2838,"start_col":23,"start_line":95,"start_offset":2833},"roles":["Name"],"token":"Union","type":"Identifier"}],"id":"d365d684e990acf4","pos":{"end_col":28,"end_line":95,"end_offset":2838,"start_col":14,"start_line":95,"start_offset":2824},"roles":[],"token":"cast(s1).Union","type":"Synthetic"},{"children":[{"children":[{"id":"4f28cda9a70d4b24","pos":{"end_col":33,"end_line":95,"end_offset":2843,"start_col":29,"start_line":95,"start_offset":2839},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"bc1608c83a4a644d","pos":{"end_col":36,"end_line":95,"end_offset":2846,"start_col":34,"start_line":95,"start_offset":2844},"roles":["Name"],"token":"s2","type":"Identifier"}],"id":"52d8168a3ccb799d","pos":{"end_col":37,"end_line":95,"end_offset":2847,"start_col":33,"start_line":95,"start_offset":2843},"roles":[],"token":"(s2)","type":"List"}],"id":"5d91c7f93c5cf0e1","pos":{"end_col":37,"end_line":95,"end_offset":2847,"start_col":29,"start_line":95,"start_offset":2839},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"id":"cfa50a5f03648f29","pos":{"end_col":38,"end_line":95,"end_offset":2848,"start_col":28,"start_line":95,"start_offset":2838},"roles":[],"token":"(cast(s2))","type":"List"}],"id":"fd7b74d00b18fc80","pos":{"end_col":38,"end_line":95,"end_offset":2848,"start_col":14,"start_line":95,"start_offset":2824},"roles":["Call"],"token":"cast(s1).Union(cast(s2))","type":"Call"}],"id":"e51493a7483387a6","pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":13,"start_line":95,"start_offset":2823},"roles":[],"token":"(cast(s1).Union(cast(s2)))","type":"List"}],"id":"6a52a3121aa062a5","pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":9,"start_line":95,"start_offset":2819},"roles":["Call"],"token":"Byte(cast(s1).Union(cast(s2)))","type":"Call"}],"id":"0b4d062e348aac36","pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":9,"start_line":95,"start_offset":2819},"roles":[],"type":"List"}],"id":"0fdc64587b752430","pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":2,"start_line":95,"start_offset":2812},"roles":["Return"],"token":"return Byte(cast(s1).Union(cast(s2)))","type":"Return"}],"id":"172abb07ef3383f3","pos":{"end_col":2,"end_line":96,"end_offset":2851,"start_col":36,"start_line":94,"start_offset":2809},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).Union(cast(s2)))\n}","type":"Block"}],"id":"08582b77bb3e80dc","pos":{"end_col":2,"end_line":96,"end_offset":2851,"start_col":1,"start_line":94,"start_offset":2774},"props":{"name":"Union"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"2fc9a7b264f5907f","pos":{"end_col":76,"end_line":98,"end_offset":2928,"start_col":1,"start_line":98,"start_offset":2853},"roles":["Comment"],"token":"// Intersection returns a new set which includes the item in BOTH s1 and s2","type":"Comment"},{"id":"8648621ebaece137","pos":{"end_col":16,"end_line":99,"end_offset":2944,"start_col":1,"start_line":99,"start_offset":2929},"roles":["Comment"],"token":"// For example:","type":"Comment"},{"id":"68f6fd7b86b09709","pos":{"end_col":17,"end_line":100,"end_offset":2961,"start_col":1,"start_line":100,"start_offset":2945},"roles":["Comment"],"token":"// s1 = {a1, a2}","type":"Comment"},{"id":"471b4c7572699c4b","pos":{"end_col":17,"end_line":101,"end_offset":2978,"start_col":1,"start_line":101,"start_offset":2962},"roles":["Comment"],"token":"// s2 = {a2, a3}","type":"Comment"},{"id":"8766e2930169eb4e","pos":{"end_col":30,"end_line":102,"end_offset":3008,"start_col":1,"start_line":102,"start_offset":2979},"roles":["Comment"],"token":"// s1.Intersection(s2) = {a2}","type":"Comment"},{"children":[{"children":[{"children":[{"id":"e869691a564b0d62","pos":{"end_col":9,"end_line":103,"end_offset":3017,"start_col":7,"start_line":103,"start_offset":3015},"roles":["Name"],"token":"s1","type":"Identifier"},{"id":"4a6c9a3b8651ef00","pos":{"end_col":14,"end_line":103,"end_offset":3022,"start_col":10,"start_line":103,"start_offset":3018},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"e9bf16012fc30786","pos":{"end_col":14,"end_line":103,"end_offset":3022,"start_col":7,"start_line":103,"start_offset":3015},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"id":"67bb533e5a599dcd","pos":{"end_col":15,"end_line":103,"end_offset":3023,"start_col":6,"start_line":103,"start_offset":3014},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"id":"d05ecc0249f604b5","pos":{"end_col":28,"end_line":103,"end_offset":3036,"start_col":16,"start_line":103,"start_offset":3024},"roles":["Name"],"token":"Intersection","type":"Identifier"},{"children":[{"children":[{"id":"fefe48cd027458f8","pos":{"end_col":31,"end_line":103,"end_offset":3039,"start_col":29,"start_line":103,"start_offset":3037},"roles":["Name"],"token":"s2","type":"Identifier"},{"id":"1f8c94a27652e3dc","pos":{"end_col":36,"end_line":103,"end_offset":3044,"start_col":32,"start_line":103,"start_offset":3040},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"81fc6388bbc7ed1d","pos":{"end_col":36,"end_line":103,"end_offset":3044,"start_col":29,"start_line":103,"start_offset":3037},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"id":"55df18ec49b783dd","pos":{"end_col":37,"end_line":103,"end_offset":3045,"start_col":28,"start_line":103,"start_offset":3036},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"id":"507da24ef1ef1c7f","pos":{"end_col":42,"end_line":103,"end_offset":3050,"start_col":38,"start_line":103,"start_offset":3046},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"ca3ac82e8043e740","pos":{"end_col":13,"end_line":104,"end_offset":3065,"start_col":9,"start_line":104,"start_offset":3061},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"da69af10f1b57125","pos":{"end_col":18,"end_line":104,"end_offset":3070,"start_col":14,"start_line":104,"start_offset":3066},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"7055a6a1a4ab0e8e","pos":{"end_col":21,"end_line":104,"end_offset":3073,"start_col":19,"start_line":104,"start_offset":3071},"roles":["Name"],"token":"s1","type":"Identifier"}],"id":"a48f099523f11e66","pos":{"end_col":22,"end_line":104,"end_offset":3074,"start_col":18,"start_line":104,"start_offset":3070},"roles":[],"token":"(s1)","type":"List"}],"id":"eeda8f32f566437e","pos":{"end_col":22,"end_line":104,"end_offset":3074,"start_col":14,"start_line":104,"start_offset":3066},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"id":"3246abb86f8268d0","pos":{"end_col":35,"end_line":104,"end_offset":3087,"start_col":23,"start_line":104,"start_offset":3075},"roles":["Name"],"token":"Intersection","type":"Identifier"}],"id":"529515e017dd06ef","pos":{"end_col":35,"end_line":104,"end_offset":3087,"start_col":14,"start_line":104,"start_offset":3066},"roles":[],"token":"cast(s1).Intersection","type":"Synthetic"},{"children":[{"children":[{"id":"01e3a11aad60126a","pos":{"end_col":40,"end_line":104,"end_offset":3092,"start_col":36,"start_line":104,"start_offset":3088},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"6d840d8d13e4a72c","pos":{"end_col":43,"end_line":104,"end_offset":3095,"start_col":41,"start_line":104,"start_offset":3093},"roles":["Name"],"token":"s2","type":"Identifier"}],"id":"029dfe1fae36ff23","pos":{"end_col":44,"end_line":104,"end_offset":3096,"start_col":40,"start_line":104,"start_offset":3092},"roles":[],"token":"(s2)","type":"List"}],"id":"ea6b3979d1b03add","pos":{"end_col":44,"end_line":104,"end_offset":3096,"start_col":36,"start_line":104,"start_offset":3088},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"id":"d41311f966b8b907","pos":{"end_col":45,"end_line":104,"end_offset":3097,"start_col":35,"start_line":104,"start_offset":3087},"roles":[],"token":"(cast(s2))","type":"List"}],"id":"23feae1ec96563ec","pos":{"end_col":45,"end_line":104,"end_offset":3097,"start_col":14,"start_line":104,"start_offset":3066},"roles":["Call"],"token":"cast(s1).Intersection(cast(s2))","type":"Call"}],"id":"a3541706f314ac0e","pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":13,"start_line":104,"start_offset":3065},"roles":[],"token":"(cast(s1).Intersection(cast(s2)))","type":"List"}],"id":"94075c3950e21c9d","pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":9,"start_line":104,"start_offset":3061},"roles":["Call"],"token":"Byte(cast(s1).Intersection(cast(s2)))","type":"Call"}],"id":"419559198f78b134","pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":9,"start_line":104,"start_offset":3061},"roles":[],"type":"List"}],"id":"1cab7726425a4008","pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":2,"start_line":104,"start_offset":3054},"roles":["Return"],"token":"return Byte(cast(s1).Intersection(cast(s2)))","type":"Return"}],"id":"cec30481348e9759","pos":{"end_col":2,"end_line":105,"end_offset":3100,"start_col":43,"start_line":103,"start_offset":3051},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).Intersection(cast(s2)))\n}","type":"Block"}],"id":"ac16cea2adc0ef50","pos":{"end_col":2,"end_line":105,"end_offset":3100,"start_col":1,"start_line":103,"start_offset":3009},"props":{"name":"Intersection"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"01f42bff46ce75be","pos":{"end_col":66,"end_line":107,"end_offset":3167,"start_col":1,"start_line":107,"start_offset":3102},"roles":["Comment"],"token":"// IsSuperset returns true if and only if s1 is a superset of s2.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"51f3598e76a09ab7","pos":{"end_col":9,"end_line":108,"end_offset":3176,"start_col":7,"start_line":108,"start_offset":3174},"roles":["Name"],"token":"s1","type":"Identifier"},{"id":"b76c6cac67354cb9","pos":{"end_col":14,"end_line":108,"end_offset":3181,"start_col":10,"start_line":108,"start_offset":3177},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"246c449d8cffffc9","pos":{"end_col":14,"end_line":108,"end_offset":3181,"start_col":7,"start_line":108,"start_offset":3174},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"id":"24dfd227022df574","pos":{"end_col":15,"end_line":108,"end_offset":3182,"start_col":6,"start_line":108,"start_offset":3173},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"id":"9fc7cb33fd89fce8","pos":{"end_col":26,"end_line":108,"end_offset":3193,"start_col":16,"start_line":108,"start_offset":3183},"roles":["Name"],"token":"IsSuperset","type":"Identifier"},{"children":[{"children":[{"id":"426e8c77be76f91e","pos":{"end_col":29,"end_line":108,"end_offset":3196,"start_col":27,"start_line":108,"start_offset":3194},"roles":["Name"],"token":"s2","type":"Identifier"},{"id":"0b900c940493db28","pos":{"end_col":34,"end_line":108,"end_offset":3201,"start_col":30,"start_line":108,"start_offset":3197},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"444421a8f73489cb","pos":{"end_col":34,"end_line":108,"end_offset":3201,"start_col":27,"start_line":108,"start_offset":3194},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"id":"e0215127efdf6547","pos":{"end_col":35,"end_line":108,"end_offset":3202,"start_col":26,"start_line":108,"start_offset":3193},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"id":"3730ee822c09c69a","pos":{"end_col":40,"end_line":108,"end_offset":3207,"start_col":36,"start_line":108,"start_offset":3203},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"id":"1c522b9b8df06d14","pos":{"end_col":13,"end_line":109,"end_offset":3222,"start_col":9,"start_line":109,"start_offset":3218},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"0218678dcbf7fed4","pos":{"end_col":16,"end_line":109,"end_offset":3225,"start_col":14,"start_line":109,"start_offset":3223},"roles":["Name"],"token":"s1","type":"Identifier"}],"id":"c2d87a7fd1ee6a49","pos":{"end_col":17,"end_line":109,"end_offset":3226,"start_col":13,"start_line":109,"start_offset":3222},"roles":[],"token":"(s1)","type":"List"}],"id":"b58c81247fc4a1b6","pos":{"end_col":17,"end_line":109,"end_offset":3226,"start_col":9,"start_line":109,"start_offset":3218},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"id":"3445a3d8d5ba91b5","pos":{"end_col":28,"end_line":109,"end_offset":3237,"start_col":18,"start_line":109,"start_offset":3227},"roles":["Name"],"token":"IsSuperset","type":"Identifier"}],"id":"07e8d054202317e8","pos":{"end_col":28,"end_line":109,"end_offset":3237,"start_col":9,"start_line":109,"start_offset":3218},"roles":[],"token":"cast(s1).IsSuperset","type":"Synthetic"},{"children":[{"children":[{"id":"b637847418b1f4dc","pos":{"end_col":33,"end_line":109,"end_offset":3242,"start_col":29,"start_line":109,"start_offset":3238},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"4e284346e75041bc","pos":{"end_col":36,"end_line":109,"end_offset":3245,"start_col":34,"start_line":109,"start_offset":3243},"roles":["Name"],"token":"s2","type":"Identifier"}],"id":"33a794dd17a95c66","pos":{"end_col":37,"end_line":109,"end_offset":3246,"start_col":33,"start_line":109,"start_offset":3242},"roles":[],"token":"(s2)","type":"List"}],"id":"b4bcf04400b40377","pos":{"end_col":37,"end_line":109,"end_offset":3246,"start_col":29,"start_line":109,"start_offset":3238},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"id":"1ff66ff2ffc4c1ec","pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":28,"start_line":109,"start_offset":3237},"roles":[],"token":"(cast(s2))","type":"List"}],"id":"622d55448d602f10","pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":9,"start_line":109,"start_offset":3218},"roles":["Call"],"token":"cast(s1).IsSuperset(cast(s2))","type":"Call"}],"id":"ac29904a9121d1b2","pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":9,"start_line":109,"start_offset":3218},"roles":[],"type":"List"}],"id":"1a433a7cb794c911","pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":2,"start_line":109,"start_offset":3211},"roles":["Return"],"token":"return cast(s1).IsSuperset(cast(s2))","type":"Return"}],"id":"237999a6972d3819","pos":{"end_col":2,"end_line":110,"end_offset":3249,"start_col":41,"start_line":108,"start_offset":3208},"roles":["Body"],"token":"{\n\treturn cast(s1).IsSuperset(cast(s2))\n}","type":"Block"}],"id":"63e19288bbeb6fce","pos":{"end_col":2,"end_line":110,"end_offset":3249,"start_col":1,"start_line":108,"start_offset":3168},"props":{"name":"IsSuperset"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"2fef2bf43c58066c","pos":{"end_col":67,"end_line":112,"end_offset":3317,"start_col":1,"start_line":112,"start_offset":3251},"roles":["Comment"],"token":"// Equal returns true if and only if s1 is equal (as a set) to s2.","type":"Comment"},{"id":"1aadc8669f4523ee","pos":{"end_col":56,"end_line":113,"end_offset":3373,"start_col":1,"start_line":113,"start_offset":3318},"roles":["Comment"],"token":"// Two sets are equal if their membership is identical.","type":"Comment"},{"id":"86feb4e9f82cfbc5","pos":{"end_col":65,"end_line":114,"end_offset":3438,"start_col":1,"start_line":114,"start_offset":3374},"roles":["Comment"],"token":"// (In practice, this means same elements, order doesn't matter)","type":"Comment"},{"children":[{"children":[{"children":[{"id":"aa3128debf2d1211","pos":{"end_col":9,"end_line":115,"end_offset":3447,"start_col":7,"start_line":115,"start_offset":3445},"roles":["Name"],"token":"s1","type":"Identifier"},{"id":"38ff634c244f5e78","pos":{"end_col":14,"end_line":115,"end_offset":3452,"start_col":10,"start_line":115,"start_offset":3448},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"2b23a5c3aab484d9","pos":{"end_col":14,"end_line":115,"end_offset":3452,"start_col":7,"start_line":115,"start_offset":3445},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"id":"cc5cfe60a37135c3","pos":{"end_col":15,"end_line":115,"end_offset":3453,"start_col":6,"start_line":115,"start_offset":3444},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"id":"72d6c15592081cf5","pos":{"end_col":21,"end_line":115,"end_offset":3459,"start_col":16,"start_line":115,"start_offset":3454},"roles":["Name"],"token":"Equal","type":"Identifier"},{"children":[{"children":[{"id":"1268954ad285ae4c","pos":{"end_col":24,"end_line":115,"end_offset":3462,"start_col":22,"start_line":115,"start_offset":3460},"roles":["Name"],"token":"s2","type":"Identifier"},{"id":"2196cd305f673c16","pos":{"end_col":29,"end_line":115,"end_offset":3467,"start_col":25,"start_line":115,"start_offset":3463},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"0ac87949c6816d87","pos":{"end_col":29,"end_line":115,"end_offset":3467,"start_col":22,"start_line":115,"start_offset":3460},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"id":"c9521e63833dafd6","pos":{"end_col":30,"end_line":115,"end_offset":3468,"start_col":21,"start_line":115,"start_offset":3459},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"id":"5044cc58cea277ca","pos":{"end_col":35,"end_line":115,"end_offset":3473,"start_col":31,"start_line":115,"start_offset":3469},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"id":"7e72372a0141c11e","pos":{"end_col":13,"end_line":116,"end_offset":3488,"start_col":9,"start_line":116,"start_offset":3484},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"41f401c20ead97d0","pos":{"end_col":16,"end_line":116,"end_offset":3491,"start_col":14,"start_line":116,"start_offset":3489},"roles":["Name"],"token":"s1","type":"Identifier"}],"id":"ce56c8ded11cc42c","pos":{"end_col":17,"end_line":116,"end_offset":3492,"start_col":13,"start_line":116,"start_offset":3488},"roles":[],"token":"(s1)","type":"List"}],"id":"91657b28bb880295","pos":{"end_col":17,"end_line":116,"end_offset":3492,"start_col":9,"start_line":116,"start_offset":3484},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"id":"1a3e061a27a62105","pos":{"end_col":23,"end_line":116,"end_offset":3498,"start_col":18,"start_line":116,"start_offset":3493},"roles":["Name"],"token":"Equal","type":"Identifier"}],"id":"a066c979665b14ef","pos":{"end_col":23,"end_line":116,"end_offset":3498,"start_col":9,"start_line":116,"start_offset":3484},"roles":[],"token":"cast(s1).Equal","type":"Synthetic"},{"children":[{"children":[{"id":"986da3f6f273dd00","pos":{"end_col":28,"end_line":116,"end_offset":3503,"start_col":24,"start_line":116,"start_offset":3499},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"eeef72d7a783bb4e","pos":{"end_col":31,"end_line":116,"end_offset":3506,"start_col":29,"start_line":116,"start_offset":3504},"roles":["Name"],"token":"s2","type":"Identifier"}],"id":"ce11c3f334075045","pos":{"end_col":32,"end_line":116,"end_offset":3507,"start_col":28,"start_line":116,"start_offset":3503},"roles":[],"token":"(s2)","type":"List"}],"id":"e731c10a8bf731a1","pos":{"end_col":32,"end_line":116,"end_offset":3507,"start_col":24,"start_line":116,"start_offset":3499},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"id":"a218bd2c54388a08","pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":23,"start_line":116,"start_offset":3498},"roles":[],"token":"(cast(s2))","type":"List"}],"id":"5f510021f6c51db6","pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":9,"start_line":116,"start_offset":3484},"roles":["Call"],"token":"cast(s1).Equal(cast(s2))","type":"Call"}],"id":"43a88b5286062e66","pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":9,"start_line":116,"start_offset":3484},"roles":[],"type":"List"}],"id":"0e29378b3ae2b294","pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":2,"start_line":116,"start_offset":3477},"roles":["Return"],"token":"return cast(s1).Equal(cast(s2))","type":"Return"}],"id":"b1cedec1f15afd79","pos":{"end_col":2,"end_line":117,"end_offset":3510,"start_col":36,"start_line":115,"start_offset":3474},"roles":["Body"],"token":"{\n\treturn cast(s1).Equal(cast(s2))\n}","type":"Block"}],"id":"992e9e69d6f21dcd","pos":{"end_col":2,"end_line":117,"end_offset":3510,"start_col":1,"start_line":115,"start_offset":3439},"props":{"name":"Equal"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"7ded7634a7b2e275","pos":{"end_col":53,"end_line":119,"end_offset":3564,"start_col":1,"start_line":119,"start_offset":3512},"roles":["Comment"],"token":"// List returns the contents as a sorted byte slice.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"894e4f18c3f18102","pos":{"end_col":8,"end_line":120,"end_offset":3572,"start_col":7,"start_line":120,"start_offset":3571},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"aed7d01f6a4b977a","pos":{"end_col":13,"end_line":120,"end_offset":3577,"start_col":9,"start_line":120,"start_offset":3573},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"e01387b47eb46c02","pos":{"end_col":13,"end_line":120,"end_offset":3577,"start_col":7,"start_line":120,"start_offset":3571},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"7d8d020c4a42e563","pos":{"end_col":14,"end_line":120,"end_offset":3578,"start_col":6,"start_line":120,"start_offset":3570},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"99acd54ecd76a7ec","pos":{"end_col":19,"end_line":120,"end_offset":3583,"start_col":15,"start_line":120,"start_offset":3579},"roles":["Name"],"token":"List","type":"Identifier"},{"id":"0ce7d43d402541c7","pos":{"end_col":21,"end_line":120,"end_offset":3585,"start_col":19,"start_line":120,"start_offset":3583},"roles":["Parameter"],"token":"()","type":"Parameter"},{"children":[{"id":"c00024bb7cb89722","pos":{"end_col":28,"end_line":120,"end_offset":3592,"start_col":24,"start_line":120,"start_offset":3588},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"ebd30c720bc39eec","pos":{"end_col":28,"end_line":120,"end_offset":3592,"start_col":22,"start_line":120,"start_offset":3586},"roles":[],"token":"[]byte","type":"Slice"},{"children":[{"children":[{"children":[{"children":[{"id":"f37fff26a93e86c0","pos":{"end_col":13,"end_line":121,"end_offset":3607,"start_col":9,"start_line":121,"start_offset":3603},"roles":["Name"],"token":"List","type":"Identifier"},{"children":[{"children":[{"id":"dbed9be6d9d2e9fe","pos":{"end_col":18,"end_line":121,"end_offset":3612,"start_col":14,"start_line":121,"start_offset":3608},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"06ae1f74c6458015","pos":{"end_col":20,"end_line":121,"end_offset":3614,"start_col":19,"start_line":121,"start_offset":3613},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"99bb96d4e0d1411c","pos":{"end_col":21,"end_line":121,"end_offset":3615,"start_col":18,"start_line":121,"start_offset":3612},"roles":[],"token":"(s)","type":"List"}],"id":"0be8b57c481cb411","pos":{"end_col":21,"end_line":121,"end_offset":3615,"start_col":14,"start_line":121,"start_offset":3608},"roles":["Call"],"token":"cast(s)","type":"Call"}],"id":"d75c8c781ebf5fbb","pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":13,"start_line":121,"start_offset":3607},"roles":[],"token":"(cast(s))","type":"List"}],"id":"51d400699c4f58f1","pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":9,"start_line":121,"start_offset":3603},"roles":["Call"],"token":"List(cast(s))","type":"Call"}],"id":"c2142b57bb2869f5","pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":9,"start_line":121,"start_offset":3603},"roles":[],"type":"List"}],"id":"da2644d3c5f20949","pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":2,"start_line":121,"start_offset":3596},"roles":["Return"],"token":"return List(cast(s))","type":"Return"}],"id":"94e6789c4c03ff51","pos":{"end_col":2,"end_line":122,"end_offset":3618,"start_col":29,"start_line":120,"start_offset":3593},"roles":["Body"],"token":"{\n\treturn List(cast(s))\n}","type":"Block"}],"id":"5a2be427cf75eb9b","pos":{"end_col":2,"end_line":122,"end_offset":3618,"start_col":1,"start_line":120,"start_offset":3565},"props":{"name":"List"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"0a671eb87ba8e40e","pos":{"end_col":65,"end_line":124,"end_offset":3684,"start_col":1,"start_line":124,"start_offset":3620},"roles":["Comment"],"token":"// UnsortedList returns the slice with contents in random order.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"ccee2cb8724a13e3","pos":{"end_col":8,"end_line":125,"end_offset":3692,"start_col":7,"start_line":125,"start_offset":3691},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"35ff17536c048640","pos":{"end_col":13,"end_line":125,"end_offset":3697,"start_col":9,"start_line":125,"start_offset":3693},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"14e76f3f3ff3d025","pos":{"end_col":13,"end_line":125,"end_offset":3697,"start_col":7,"start_line":125,"start_offset":3691},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"4474f731ddf59be1","pos":{"end_col":14,"end_line":125,"end_offset":3698,"start_col":6,"start_line":125,"start_offset":3690},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"bee1a5632529d1b2","pos":{"end_col":27,"end_line":125,"end_offset":3711,"start_col":15,"start_line":125,"start_offset":3699},"roles":["Name"],"token":"UnsortedList","type":"Identifier"},{"id":"db96a420de8f6ce1","pos":{"end_col":29,"end_line":125,"end_offset":3713,"start_col":27,"start_line":125,"start_offset":3711},"roles":["Parameter"],"token":"()","type":"Parameter"},{"children":[{"id":"9ed1fce4eac27e05","pos":{"end_col":36,"end_line":125,"end_offset":3720,"start_col":32,"start_line":125,"start_offset":3716},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"b18d06f5551d0d97","pos":{"end_col":36,"end_line":125,"end_offset":3720,"start_col":30,"start_line":125,"start_offset":3714},"roles":[],"token":"[]byte","type":"Slice"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"id":"3f48373502efafdb","pos":{"end_col":13,"end_line":126,"end_offset":3735,"start_col":9,"start_line":126,"start_offset":3731},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"ee007c1065ddc68c","pos":{"end_col":15,"end_line":126,"end_offset":3737,"start_col":14,"start_line":126,"start_offset":3736},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"05c28f0dda63014d","pos":{"end_col":16,"end_line":126,"end_offset":3738,"start_col":13,"start_line":126,"start_offset":3735},"roles":[],"token":"(s)","type":"List"}],"id":"330a3a632559bf74","pos":{"end_col":16,"end_line":126,"end_offset":3738,"start_col":9,"start_line":126,"start_offset":3731},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"7f3f6eccee38f155","pos":{"end_col":29,"end_line":126,"end_offset":3751,"start_col":17,"start_line":126,"start_offset":3739},"roles":["Name"],"token":"UnsortedList","type":"Identifier"}],"id":"b84c66d8a9b6b116","pos":{"end_col":29,"end_line":126,"end_offset":3751,"start_col":9,"start_line":126,"start_offset":3731},"roles":[],"token":"cast(s).UnsortedList","type":"Synthetic"},{"id":"8df8e9ef25198dde","pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":29,"start_line":126,"start_offset":3751},"roles":[],"token":"()","type":"List"}],"id":"b3a87e0a0b9a2400","pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":9,"start_line":126,"start_offset":3731},"roles":["Call"],"token":"cast(s).UnsortedList()","type":"Call"}],"id":"14edcfa644a49fbf","pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":9,"start_line":126,"start_offset":3731},"roles":[],"type":"List"}],"id":"0ee884f5b956127e","pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":2,"start_line":126,"start_offset":3724},"roles":["Return"],"token":"return cast(s).UnsortedList()","type":"Return"}],"id":"8536265c2bb50d43","pos":{"end_col":2,"end_line":127,"end_offset":3755,"start_col":37,"start_line":125,"start_offset":3721},"roles":["Body"],"token":"{\n\treturn cast(s).UnsortedList()\n}","type":"Block"}],"id":"e6010ee8086356ff","pos":{"end_col":2,"end_line":127,"end_offset":3755,"start_col":1,"start_line":125,"start_offset":3685},"props":{"name":"UnsortedList"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"7a23caec3772ecbf","pos":{"end_col":49,"end_line":129,"end_offset":3805,"start_col":1,"start_line":129,"start_offset":3757},"roles":["Comment"],"token":"// PopAny returns a single element from the set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"5d3700e36997539a","pos":{"end_col":8,"end_line":130,"end_offset":3813,"start_col":7,"start_line":130,"start_offset":3812},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"8e75e614fef1bbb5","pos":{"end_col":13,"end_line":130,"end_offset":3818,"start_col":9,"start_line":130,"start_offset":3814},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"cbce14d753037c6c","pos":{"end_col":13,"end_line":130,"end_offset":3818,"start_col":7,"start_line":130,"start_offset":3812},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"707ba553cbb7f16a","pos":{"end_col":14,"end_line":130,"end_offset":3819,"start_col":6,"start_line":130,"start_offset":3811},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"cbe4b4deac9d1004","pos":{"end_col":21,"end_line":130,"end_offset":3826,"start_col":15,"start_line":130,"start_offset":3820},"roles":["Name"],"token":"PopAny","type":"Identifier"},{"id":"3564d9e9f316bcba","pos":{"end_col":23,"end_line":130,"end_offset":3828,"start_col":21,"start_line":130,"start_offset":3826},"roles":["Parameter"],"token":"()","type":"Parameter"},{"children":[{"children":[{"id":"08debe93d6019e39","pos":{"end_col":29,"end_line":130,"end_offset":3834,"start_col":25,"start_line":130,"start_offset":3830},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"id":"97014d488ff6ff38","pos":{"end_col":29,"end_line":130,"end_offset":3834,"start_col":25,"start_line":130,"start_offset":3830},"roles":["Parameter"],"type":"Parameter"},{"children":[{"id":"ba19ddb9be2461a6","pos":{"end_col":35,"end_line":130,"end_offset":3840,"start_col":31,"start_line":130,"start_offset":3836},"roles":["Name","Type"],"token":"bool","type":"Identifier"}],"id":"a6c2ef527591a024","pos":{"end_col":35,"end_line":130,"end_offset":3840,"start_col":31,"start_line":130,"start_offset":3836},"roles":["Parameter"],"type":"Parameter"}],"id":"96cacc4e22263828","pos":{"end_col":36,"end_line":130,"end_offset":3841,"start_col":24,"start_line":130,"start_offset":3829},"roles":["Parameter"],"token":"(byte, bool)","type":"Parameter"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"id":"9807791644d97e90","pos":{"end_col":13,"end_line":131,"end_offset":3856,"start_col":9,"start_line":131,"start_offset":3852},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"id":"e2ed7112e85951c9","pos":{"end_col":15,"end_line":131,"end_offset":3858,"start_col":14,"start_line":131,"start_offset":3857},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"4bcdb720c420ef81","pos":{"end_col":16,"end_line":131,"end_offset":3859,"start_col":13,"start_line":131,"start_offset":3856},"roles":[],"token":"(s)","type":"List"}],"id":"2a0eb2ae5c290ddb","pos":{"end_col":16,"end_line":131,"end_offset":3859,"start_col":9,"start_line":131,"start_offset":3852},"roles":["Call"],"token":"cast(s)","type":"Call"},{"id":"6da0109ac89b8d84","pos":{"end_col":23,"end_line":131,"end_offset":3866,"start_col":17,"start_line":131,"start_offset":3860},"roles":["Name"],"token":"PopAny","type":"Identifier"}],"id":"df588d361191b078","pos":{"end_col":23,"end_line":131,"end_offset":3866,"start_col":9,"start_line":131,"start_offset":3852},"roles":[],"token":"cast(s).PopAny","type":"Synthetic"},{"id":"d064c3186b8b6233","pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":23,"start_line":131,"start_offset":3866},"roles":[],"token":"()","type":"List"}],"id":"3b2b252c3946b7f3","pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":9,"start_line":131,"start_offset":3852},"roles":["Call"],"token":"cast(s).PopAny()","type":"Call"}],"id":"dfc75d29926e9abc","pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":9,"start_line":131,"start_offset":3852},"roles":[],"type":"List"}],"id":"35c2922612c28afa","pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":2,"start_line":131,"start_offset":3845},"roles":["Return"],"token":"return cast(s).PopAny()","type":"Return"}],"id":"a5bcf55af784cdc8","pos":{"end_col":2,"end_line":132,"end_offset":3870,"start_col":37,"start_line":130,"start_offset":3842},"roles":["Body"],"token":"{\n\treturn cast(s).PopAny()\n}","type":"Block"}],"id":"a4d065a66503662b","pos":{"end_col":2,"end_line":132,"end_offset":3870,"start_col":1,"start_line":130,"start_offset":3806},"props":{"name":"PopAny"},"roles":["Function","Declaration","Member"],"type":"Method"},{"id":"b4a3acc0e6615643","pos":{"end_col":36,"end_line":134,"end_offset":3907,"start_col":1,"start_line":134,"start_offset":3872},"roles":["Comment"],"token":"// Len returns the size of the set.","type":"Comment"},{"children":[{"children":[{"children":[{"id":"c2c861b727d038e0","pos":{"end_col":8,"end_line":135,"end_offset":3915,"start_col":7,"start_line":135,"start_offset":3914},"roles":["Name"],"token":"s","type":"Identifier"},{"id":"c06f2d499d4f5867","pos":{"end_col":13,"end_line":135,"end_offset":3920,"start_col":9,"start_line":135,"start_offset":3916},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"id":"e385e1f188739ee2","pos":{"end_col":13,"end_line":135,"end_offset":3920,"start_col":7,"start_line":135,"start_offset":3914},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"id":"1decc57dbeb6f820","pos":{"end_col":14,"end_line":135,"end_offset":3921,"start_col":6,"start_line":135,"start_offset":3913},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"id":"d573070f425061d7","pos":{"end_col":18,"end_line":135,"end_offset":3925,"start_col":15,"start_line":135,"start_offset":3922},"roles":["Name"],"token":"Len","type":"Identifier"},{"id":"5c94a9afe2e823bd","pos":{"end_col":20,"end_line":135,"end_offset":3927,"start_col":18,"start_line":135,"start_offset":3925},"roles":["Parameter"],"token":"()","type":"Parameter"},{"id":"3ad6c7bea8e50732","pos":{"end_col":24,"end_line":135,"end_offset":3931,"start_col":21,"start_line":135,"start_offset":3928},"roles":["Name","Type"],"token":"int","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"id":"1c042e022768b030","pos":{"end_col":12,"end_line":136,"end_offset":3945,"start_col":9,"start_line":136,"start_offset":3942},"roles":["Name"],"token":"len","type":"Identifier"},{"children":[{"id":"9b2ccb6fc11a947b","pos":{"end_col":14,"end_line":136,"end_offset":3947,"start_col":13,"start_line":136,"start_offset":3946},"roles":["Name"],"token":"s","type":"Identifier"}],"id":"815cc0e15f13935e","pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":12,"start_line":136,"start_offset":3945},"roles":[],"token":"(s)","type":"List"}],"id":"b48e7a29a53afb6a","pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":9,"start_line":136,"start_offset":3942},"roles":["Call"],"token":"len(s)","type":"Call"}],"id":"1391676fec268947","pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":9,"start_line":136,"start_offset":3942},"roles":[],"type":"List"}],"id":"ae6cbfc7c969b6a6","pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":2,"start_line":136,"start_offset":3935},"roles":["Return"],"token":"return len(s)","type":"Return"}],"id":"c1b23ed5b83bb5c8","pos":{"end_col":2,"end_line":137,"end_offset":3950,"start_col":25,"start_line":135,"start_offset":3932},"roles":["Body"],"token":"{\n\treturn len(s)\n}","type":"Block"}],"id":"624a22df526907e7","pos":{"end_col":2,"end_line":137,"end_offset":3950,"start_col":1,"start_line":135,"start_offset":3908},"props":{"name":"Len"},"roles":["Function","Declaration","Member"],"type":"Method"}],"id":"c2b43435dadfc320","pos":{"end_col":1,"end_line":138,"end_offset":3951,"start_col":1,"start_line":1,"start_offset":0},"roles":[],"type":"File"} diff --git a/tests/golden/uast/parse.json b/tests/golden/uast/parse.json deleted file mode 100644 index 2fc49935..00000000 --- a/tests/golden/uast/parse.json +++ /dev/null @@ -1,8379 +0,0 @@ -{ - "children": [ - { - "id": "26d00635698d2406", - "pos": { - "end_col": 3, - "end_line": 15, - "end_offset": 569, - "start_col": 1, - "start_line": 1, - "start_offset": 0 - }, - "roles": [ - "Comment" - ], - "token": "/*\nCopyright 2022 The Kubernetes Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/", - "type": "Comment" - }, - { - "children": [ - { - "id": "01cf11a965f2f6b4", - "pos": { - "end_col": 13, - "end_line": 17, - "end_offset": 583, - "start_col": 9, - "start_line": 17, - "start_offset": 579 - }, - "roles": [ - "Name" - ], - "token": "sets", - "type": "Identifier" - } - ], - "id": "203ff992bf5623f3", - "pos": { - "end_col": 13, - "end_line": 17, - "end_offset": 583, - "start_col": 1, - "start_line": 17, - "start_offset": 571 - }, - "roles": [ - "Module" - ], - "token": "package sets", - "type": "Package" - }, - { - "id": "a788f213c71243b6", - "pos": { - "end_col": 93, - "end_line": 19, - "end_offset": 677, - "start_col": 1, - "start_line": 19, - "start_offset": 585 - }, - "roles": [ - "Comment" - ], - "token": "// Byte is a set of bytes, implemented via map[byte]struct{} for minimal memory consumption.", - "type": "Comment" - }, - { - "id": "51fb18e1a6b2b91a", - "pos": { - "end_col": 3, - "end_line": 20, - "end_offset": 680, - "start_col": 1, - "start_line": 20, - "start_offset": 678 - }, - "roles": [ - "Comment" - ], - "token": "//", - "type": "Comment" - }, - { - "id": "cabf4b659236fe0f", - "pos": { - "end_col": 40, - "end_line": 21, - "end_offset": 720, - "start_col": 1, - "start_line": 21, - "start_offset": 681 - }, - "roles": [ - "Comment" - ], - "token": "// Deprecated: use generic Set instead.", - "type": "Comment" - }, - { - "id": "83431be209f35b4f", - "pos": { - "end_col": 13, - "end_line": 22, - "end_offset": 733, - "start_col": 1, - "start_line": 22, - "start_offset": 721 - }, - "roles": [ - "Comment" - ], - "token": "// new ways:", - "type": "Comment" - }, - { - "id": "6e82d6df0eb47b51", - "pos": { - "end_col": 21, - "end_line": 23, - "end_offset": 754, - "start_col": 1, - "start_line": 23, - "start_offset": 734 - }, - "roles": [ - "Comment" - ], - "token": "// s1 := Set[byte]{}", - "type": "Comment" - }, - { - "id": "61b58a8eb956a313", - "pos": { - "end_col": 21, - "end_line": 24, - "end_offset": 775, - "start_col": 1, - "start_line": 24, - "start_offset": 755 - }, - "roles": [ - "Comment" - ], - "token": "// s2 := New[byte]()", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "id": "e788897c64e80036", - "pos": { - "end_col": 10, - "end_line": 25, - "end_offset": 785, - "start_col": 6, - "start_line": 25, - "start_offset": 781 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "id": "c58582cd366cf8bf", - "pos": { - "end_col": 19, - "end_line": 25, - "end_offset": 794, - "start_col": 15, - "start_line": 25, - "start_offset": 790 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - }, - { - "id": "399ab824047d9da2", - "pos": { - "end_col": 25, - "end_line": 25, - "end_offset": 800, - "start_col": 20, - "start_line": 25, - "start_offset": 795 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Empty", - "type": "Identifier" - } - ], - "id": "99f41cb2f4681ba2", - "pos": { - "end_col": 25, - "end_line": 25, - "end_offset": 800, - "start_col": 11, - "start_line": 25, - "start_offset": 786 - }, - "roles": [], - "token": "map[byte]Empty", - "type": "Synthetic" - } - ], - "id": "8d861f7f28d627b4", - "pos": { - "end_col": 25, - "end_line": 25, - "end_offset": 800, - "start_col": 6, - "start_line": 25, - "start_offset": 781 - }, - "props": { - "name": "Byte" - }, - "roles": [], - "token": "Byte map[byte]Empty", - "type": "List" - } - ], - "id": "0fec74df99ebb291", - "pos": { - "end_col": 25, - "end_line": 25, - "end_offset": 800, - "start_col": 1, - "start_line": 25, - "start_offset": 776 - }, - "roles": [], - "type": "Synthetic" - }, - { - "id": "1e1d6b2d65e5b5c2", - "pos": { - "end_col": 49, - "end_line": 27, - "end_offset": 850, - "start_col": 1, - "start_line": 27, - "start_offset": 802 - }, - "roles": [ - "Comment" - ], - "token": "// NewByte creates a Byte from a list of values.", - "type": "Comment" - }, - { - "children": [ - { - "id": "88c908f1c1b7df09", - "pos": { - "end_col": 13, - "end_line": 28, - "end_offset": 863, - "start_col": 6, - "start_line": 28, - "start_offset": 856 - }, - "roles": [ - "Name" - ], - "token": "NewByte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "b829185c699fcb4a", - "pos": { - "end_col": 19, - "end_line": 28, - "end_offset": 869, - "start_col": 14, - "start_line": 28, - "start_offset": 864 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "id": "3c6ee13301476e8c", - "pos": { - "end_col": 27, - "end_line": 28, - "end_offset": 877, - "start_col": 23, - "start_line": 28, - "start_offset": 873 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "1aa1514a4f3f52d9", - "pos": { - "end_col": 27, - "end_line": 28, - "end_offset": 877, - "start_col": 14, - "start_line": 28, - "start_offset": 864 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "118505cc769c642f", - "pos": { - "end_col": 28, - "end_line": 28, - "end_offset": 878, - "start_col": 13, - "start_line": 28, - "start_offset": 863 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "id": "7c4f5cd7f9eea166", - "pos": { - "end_col": 33, - "end_line": 28, - "end_offset": 883, - "start_col": 29, - "start_line": 28, - "start_offset": 879 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "89cf2ab2e31c2bf3", - "pos": { - "end_col": 13, - "end_line": 29, - "end_offset": 898, - "start_col": 9, - "start_line": 29, - "start_offset": 894 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "3e1985081cb134a1", - "pos": { - "end_col": 17, - "end_line": 29, - "end_offset": 902, - "start_col": 14, - "start_line": 29, - "start_offset": 899 - }, - "roles": [ - "Name" - ], - "token": "New", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "75fb1a65485ff02b", - "pos": { - "end_col": 22, - "end_line": 29, - "end_offset": 907, - "start_col": 18, - "start_line": 29, - "start_offset": 903 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "875399e017bb3297", - "pos": { - "end_col": 22, - "end_line": 29, - "end_offset": 907, - "start_col": 18, - "start_line": 29, - "start_offset": 903 - }, - "roles": [], - "type": "Synthetic" - } - ], - "id": "75686a6454eb690f", - "pos": { - "end_col": 23, - "end_line": 29, - "end_offset": 908, - "start_col": 17, - "start_line": 29, - "start_offset": 902 - }, - "roles": [], - "token": "[byte]", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "c48fb9ee1ed15e38", - "pos": { - "end_col": 29, - "end_line": 29, - "end_offset": 914, - "start_col": 24, - "start_line": 29, - "start_offset": 909 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "id": "22be5659a9eef6d7", - "pos": { - "end_col": 32, - "end_line": 29, - "end_offset": 917, - "start_col": 24, - "start_line": 29, - "start_offset": 909 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "id": "f6bd57260ccd8dd3", - "pos": { - "end_col": 33, - "end_line": 29, - "end_offset": 918, - "start_col": 23, - "start_line": 29, - "start_offset": 908 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "id": "81979a9df2071661", - "pos": { - "end_col": 33, - "end_line": 29, - "end_offset": 918, - "start_col": 14, - "start_line": 29, - "start_offset": 899 - }, - "roles": [ - "Call" - ], - "token": "New[byte](items...)", - "type": "Call" - } - ], - "id": "d541313083135cca", - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 13, - "start_line": 29, - "start_offset": 898 - }, - "roles": [], - "token": "(New[byte](items...))", - "type": "List" - } - ], - "id": "517f67cb07cf529b", - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 9, - "start_line": 29, - "start_offset": 894 - }, - "roles": [ - "Call" - ], - "token": "Byte(New[byte](items...))", - "type": "Call" - } - ], - "id": "5fe8108c3d05e985", - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 9, - "start_line": 29, - "start_offset": 894 - }, - "roles": [], - "type": "List" - } - ], - "id": "df51022abbfa4878", - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 2, - "start_line": 29, - "start_offset": 887 - }, - "roles": [ - "Return" - ], - "token": "return Byte(New[byte](items...))", - "type": "Return" - } - ], - "id": "37a7053322456887", - "pos": { - "end_col": 2, - "end_line": 30, - "end_offset": 921, - "start_col": 34, - "start_line": 28, - "start_offset": 884 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(New[byte](items...))\n}", - "type": "Block" - } - ], - "id": "c3d1d1e75c441a4f", - "pos": { - "end_col": 2, - "end_line": 30, - "end_offset": 921, - "start_col": 1, - "start_line": 28, - "start_offset": 851 - }, - "props": { - "name": "NewByte" - }, - "roles": [ - "Function", - "Declaration" - ], - "token": "func NewByte(items ...byte) Byte {\n\treturn Byte(New[byte](items...))\n}", - "type": "Function" - }, - { - "id": "8904ae60de71d712", - "pos": { - "end_col": 80, - "end_line": 32, - "end_offset": 1002, - "start_col": 1, - "start_line": 32, - "start_offset": 923 - }, - "roles": [ - "Comment" - ], - "token": "// ByteKeySet creates a Byte from a keys of a map[byte](? extends interface{}).", - "type": "Comment" - }, - { - "id": "578ee02118c79115", - "pos": { - "end_col": 66, - "end_line": 33, - "end_offset": 1068, - "start_col": 1, - "start_line": 33, - "start_offset": 1003 - }, - "roles": [ - "Comment" - ], - "token": "// If the value passed in is not actually a map, this will panic.", - "type": "Comment" - }, - { - "children": [ - { - "id": "d14ee5da81922a98", - "pos": { - "end_col": 16, - "end_line": 34, - "end_offset": 1084, - "start_col": 6, - "start_line": 34, - "start_offset": 1074 - }, - "roles": [ - "Name" - ], - "token": "ByteKeySet", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "12254760f0e3569f", - "pos": { - "end_col": 18, - "end_line": 34, - "end_offset": 1086, - "start_col": 17, - "start_line": 34, - "start_offset": 1085 - }, - "roles": [ - "Name" - ], - "token": "T", - "type": "Identifier" - }, - { - "children": [ - { - "id": "1aac4a26dd04cec6", - "pos": { - "end_col": 22, - "end_line": 34, - "end_offset": 1090, - "start_col": 19, - "start_line": 34, - "start_offset": 1087 - }, - "roles": [ - "Name", - "Type" - ], - "token": "any", - "type": "Identifier" - } - ], - "id": "7e05695e16898f6f", - "pos": { - "end_col": 22, - "end_line": 34, - "end_offset": 1090, - "start_col": 19, - "start_line": 34, - "start_offset": 1087 - }, - "roles": [], - "type": "Synthetic" - } - ], - "id": "45bb697b105c2e64", - "pos": { - "end_col": 22, - "end_line": 34, - "end_offset": 1090, - "start_col": 17, - "start_line": 34, - "start_offset": 1085 - }, - "props": { - "name": "T" - }, - "roles": [ - "Parameter" - ], - "token": "T any", - "type": "Parameter" - } - ], - "id": "fc0206b36a781033", - "pos": { - "end_col": 23, - "end_line": 34, - "end_offset": 1091, - "start_col": 16, - "start_line": 34, - "start_offset": 1084 - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - }, - { - "children": [ - { - "children": [ - { - "id": "6c18e67997b06032", - "pos": { - "end_col": 30, - "end_line": 34, - "end_offset": 1098, - "start_col": 24, - "start_line": 34, - "start_offset": 1092 - }, - "roles": [ - "Name" - ], - "token": "theMap", - "type": "Identifier" - }, - { - "children": [ - { - "id": "3ce834b5ba0214ee", - "pos": { - "end_col": 39, - "end_line": 34, - "end_offset": 1107, - "start_col": 35, - "start_line": 34, - "start_offset": 1103 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - }, - { - "id": "3b364c872f4402cb", - "pos": { - "end_col": 41, - "end_line": 34, - "end_offset": 1109, - "start_col": 40, - "start_line": 34, - "start_offset": 1108 - }, - "roles": [ - "Name", - "Type" - ], - "token": "T", - "type": "Identifier" - } - ], - "id": "0125614ff37dfd5b", - "pos": { - "end_col": 41, - "end_line": 34, - "end_offset": 1109, - "start_col": 31, - "start_line": 34, - "start_offset": 1099 - }, - "roles": [], - "token": "map[byte]T", - "type": "Synthetic" - } - ], - "id": "fd5dfe662283e6ee", - "pos": { - "end_col": 41, - "end_line": 34, - "end_offset": 1109, - "start_col": 24, - "start_line": 34, - "start_offset": 1092 - }, - "props": { - "name": "theMap" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "88b9855b5a112824", - "pos": { - "end_col": 42, - "end_line": 34, - "end_offset": 1110, - "start_col": 23, - "start_line": 34, - "start_offset": 1091 - }, - "roles": [ - "Parameter" - ], - "token": "(theMap map[byte]T)", - "type": "Parameter" - }, - { - "id": "2ce3e3effa3a4a99", - "pos": { - "end_col": 47, - "end_line": 34, - "end_offset": 1115, - "start_col": 43, - "start_line": 34, - "start_offset": 1111 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "8cf5ef284e76788a", - "pos": { - "end_col": 13, - "end_line": 35, - "end_offset": 1130, - "start_col": 9, - "start_line": 35, - "start_offset": 1126 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "039386cf7f2b21c9", - "pos": { - "end_col": 20, - "end_line": 35, - "end_offset": 1137, - "start_col": 14, - "start_line": 35, - "start_offset": 1131 - }, - "roles": [ - "Name" - ], - "token": "KeySet", - "type": "Identifier" - }, - { - "children": [ - { - "id": "dfd07c2610bb0996", - "pos": { - "end_col": 27, - "end_line": 35, - "end_offset": 1144, - "start_col": 21, - "start_line": 35, - "start_offset": 1138 - }, - "roles": [ - "Name" - ], - "token": "theMap", - "type": "Identifier" - } - ], - "id": "4973aed79c303ca0", - "pos": { - "end_col": 28, - "end_line": 35, - "end_offset": 1145, - "start_col": 20, - "start_line": 35, - "start_offset": 1137 - }, - "roles": [], - "token": "(theMap)", - "type": "List" - } - ], - "id": "58a0b74f11fcaf14", - "pos": { - "end_col": 28, - "end_line": 35, - "end_offset": 1145, - "start_col": 14, - "start_line": 35, - "start_offset": 1131 - }, - "roles": [ - "Call" - ], - "token": "KeySet(theMap)", - "type": "Call" - } - ], - "id": "0e4300e4ca3095cb", - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 13, - "start_line": 35, - "start_offset": 1130 - }, - "roles": [], - "token": "(KeySet(theMap))", - "type": "List" - } - ], - "id": "b28bdf0ed64e8820", - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 9, - "start_line": 35, - "start_offset": 1126 - }, - "roles": [ - "Call" - ], - "token": "Byte(KeySet(theMap))", - "type": "Call" - } - ], - "id": "5d56f010d469b7cd", - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 9, - "start_line": 35, - "start_offset": 1126 - }, - "roles": [], - "type": "List" - } - ], - "id": "e2c0173b16926fd1", - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 2, - "start_line": 35, - "start_offset": 1119 - }, - "roles": [ - "Return" - ], - "token": "return Byte(KeySet(theMap))", - "type": "Return" - } - ], - "id": "edf5a856714ca9f3", - "pos": { - "end_col": 2, - "end_line": 36, - "end_offset": 1148, - "start_col": 48, - "start_line": 34, - "start_offset": 1116 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(KeySet(theMap))\n}", - "type": "Block" - } - ], - "id": "ef8658e6bf69517b", - "pos": { - "end_col": 2, - "end_line": 36, - "end_offset": 1148, - "start_col": 1, - "start_line": 34, - "start_offset": 1069 - }, - "props": { - "name": "ByteKeySet" - }, - "roles": [ - "Function", - "Declaration" - ], - "token": "func ByteKeySet[T any](theMap map[byte]T) Byte {\n\treturn Byte(KeySet(theMap))\n}", - "type": "Function" - }, - { - "id": "95c8a54149f0e77e", - "pos": { - "end_col": 33, - "end_line": 38, - "end_offset": 1182, - "start_col": 1, - "start_line": 38, - "start_offset": 1150 - }, - "roles": [ - "Comment" - ], - "token": "// Insert adds items to the set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "41a5c517f54d994c", - "pos": { - "end_col": 8, - "end_line": 39, - "end_offset": 1190, - "start_col": 7, - "start_line": 39, - "start_offset": 1189 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "3302a6e0296277be", - "pos": { - "end_col": 13, - "end_line": 39, - "end_offset": 1195, - "start_col": 9, - "start_line": 39, - "start_offset": 1191 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "0c499813e6c60194", - "pos": { - "end_col": 13, - "end_line": 39, - "end_offset": 1195, - "start_col": 7, - "start_line": 39, - "start_offset": 1189 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "c6b0fb9105a228db", - "pos": { - "end_col": 14, - "end_line": 39, - "end_offset": 1196, - "start_col": 6, - "start_line": 39, - "start_offset": 1188 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "ed4ecc19669d8e01", - "pos": { - "end_col": 21, - "end_line": 39, - "end_offset": 1203, - "start_col": 15, - "start_line": 39, - "start_offset": 1197 - }, - "roles": [ - "Name" - ], - "token": "Insert", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "29da4d9d36d6d5a5", - "pos": { - "end_col": 27, - "end_line": 39, - "end_offset": 1209, - "start_col": 22, - "start_line": 39, - "start_offset": 1204 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "id": "acb3019f9947c447", - "pos": { - "end_col": 35, - "end_line": 39, - "end_offset": 1217, - "start_col": 31, - "start_line": 39, - "start_offset": 1213 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "92c07aa45b56c9e8", - "pos": { - "end_col": 35, - "end_line": 39, - "end_offset": 1217, - "start_col": 22, - "start_line": 39, - "start_offset": 1204 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "f81897a52a2124a3", - "pos": { - "end_col": 36, - "end_line": 39, - "end_offset": 1218, - "start_col": 21, - "start_line": 39, - "start_offset": 1203 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "id": "cde55ea652429623", - "pos": { - "end_col": 41, - "end_line": 39, - "end_offset": 1223, - "start_col": 37, - "start_line": 39, - "start_offset": 1219 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "07173253244b6fd2", - "pos": { - "end_col": 13, - "end_line": 40, - "end_offset": 1238, - "start_col": 9, - "start_line": 40, - "start_offset": 1234 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "6db7bc690078db43", - "pos": { - "end_col": 18, - "end_line": 40, - "end_offset": 1243, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "7e5190b2603057a0", - "pos": { - "end_col": 20, - "end_line": 40, - "end_offset": 1245, - "start_col": 19, - "start_line": 40, - "start_offset": 1244 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "bf9330f0b5f78b75", - "pos": { - "end_col": 21, - "end_line": 40, - "end_offset": 1246, - "start_col": 18, - "start_line": 40, - "start_offset": 1243 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "86127d9330b665b0", - "pos": { - "end_col": 21, - "end_line": 40, - "end_offset": 1246, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "e13e2da30e16dbc3", - "pos": { - "end_col": 28, - "end_line": 40, - "end_offset": 1253, - "start_col": 22, - "start_line": 40, - "start_offset": 1247 - }, - "roles": [ - "Name" - ], - "token": "Insert", - "type": "Identifier" - } - ], - "id": "12c058e2c5355ba9", - "pos": { - "end_col": 28, - "end_line": 40, - "end_offset": 1253, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [], - "token": "cast(s).Insert", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "c615c9219da806dc", - "pos": { - "end_col": 34, - "end_line": 40, - "end_offset": 1259, - "start_col": 29, - "start_line": 40, - "start_offset": 1254 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "id": "16b21056f61bbf00", - "pos": { - "end_col": 37, - "end_line": 40, - "end_offset": 1262, - "start_col": 29, - "start_line": 40, - "start_offset": 1254 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "id": "b673d965bc6d79f0", - "pos": { - "end_col": 38, - "end_line": 40, - "end_offset": 1263, - "start_col": 28, - "start_line": 40, - "start_offset": 1253 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "id": "a9a1fde785d2f135", - "pos": { - "end_col": 38, - "end_line": 40, - "end_offset": 1263, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Insert(items...)", - "type": "Call" - } - ], - "id": "046bdf78df9da65e", - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 13, - "start_line": 40, - "start_offset": 1238 - }, - "roles": [], - "token": "(cast(s).Insert(items...))", - "type": "List" - } - ], - "id": "8409b714dd834aab", - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 9, - "start_line": 40, - "start_offset": 1234 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s).Insert(items...))", - "type": "Call" - } - ], - "id": "e2b9bc3070a0c053", - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 9, - "start_line": 40, - "start_offset": 1234 - }, - "roles": [], - "type": "List" - } - ], - "id": "9b2ffa3e759bd7f9", - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 2, - "start_line": 40, - "start_offset": 1227 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s).Insert(items...))", - "type": "Return" - } - ], - "id": "61ca07e573bc1fde", - "pos": { - "end_col": 2, - "end_line": 41, - "end_offset": 1266, - "start_col": 42, - "start_line": 39, - "start_offset": 1224 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s).Insert(items...))\n}", - "type": "Block" - } - ], - "id": "66ac27b3199e740a", - "pos": { - "end_col": 2, - "end_line": 41, - "end_offset": 1266, - "start_col": 1, - "start_line": 39, - "start_offset": 1183 - }, - "props": { - "name": "Insert" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "3ef2082447e9c906", - "pos": { - "end_col": 42, - "end_line": 43, - "end_offset": 1309, - "start_col": 1, - "start_line": 43, - "start_offset": 1268 - }, - "roles": [ - "Comment" - ], - "token": "// Delete removes all items from the set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "859ef36c74fa6145", - "pos": { - "end_col": 8, - "end_line": 44, - "end_offset": 1317, - "start_col": 7, - "start_line": 44, - "start_offset": 1316 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "53e20497bd784b74", - "pos": { - "end_col": 13, - "end_line": 44, - "end_offset": 1322, - "start_col": 9, - "start_line": 44, - "start_offset": 1318 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "5f5f043b37405427", - "pos": { - "end_col": 13, - "end_line": 44, - "end_offset": 1322, - "start_col": 7, - "start_line": 44, - "start_offset": 1316 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "82c8360b1264f82f", - "pos": { - "end_col": 14, - "end_line": 44, - "end_offset": 1323, - "start_col": 6, - "start_line": 44, - "start_offset": 1315 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "26c38e14c8241d67", - "pos": { - "end_col": 21, - "end_line": 44, - "end_offset": 1330, - "start_col": 15, - "start_line": 44, - "start_offset": 1324 - }, - "roles": [ - "Name" - ], - "token": "Delete", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "d0d2993877119284", - "pos": { - "end_col": 27, - "end_line": 44, - "end_offset": 1336, - "start_col": 22, - "start_line": 44, - "start_offset": 1331 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "id": "1bd790b9805a6c68", - "pos": { - "end_col": 35, - "end_line": 44, - "end_offset": 1344, - "start_col": 31, - "start_line": 44, - "start_offset": 1340 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "468fb6809ea7cb08", - "pos": { - "end_col": 35, - "end_line": 44, - "end_offset": 1344, - "start_col": 22, - "start_line": 44, - "start_offset": 1331 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "72b9179af501940b", - "pos": { - "end_col": 36, - "end_line": 44, - "end_offset": 1345, - "start_col": 21, - "start_line": 44, - "start_offset": 1330 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "id": "617037ef807431ef", - "pos": { - "end_col": 41, - "end_line": 44, - "end_offset": 1350, - "start_col": 37, - "start_line": 44, - "start_offset": 1346 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "89cfe7b492f44a44", - "pos": { - "end_col": 13, - "end_line": 45, - "end_offset": 1365, - "start_col": 9, - "start_line": 45, - "start_offset": 1361 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "84cf4781948f9833", - "pos": { - "end_col": 18, - "end_line": 45, - "end_offset": 1370, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "ee7d6857c0e7b1b9", - "pos": { - "end_col": 20, - "end_line": 45, - "end_offset": 1372, - "start_col": 19, - "start_line": 45, - "start_offset": 1371 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "1c1a41584e1f7275", - "pos": { - "end_col": 21, - "end_line": 45, - "end_offset": 1373, - "start_col": 18, - "start_line": 45, - "start_offset": 1370 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "e805da7b0b93e96c", - "pos": { - "end_col": 21, - "end_line": 45, - "end_offset": 1373, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "aa22bd19ea04a7aa", - "pos": { - "end_col": 28, - "end_line": 45, - "end_offset": 1380, - "start_col": 22, - "start_line": 45, - "start_offset": 1374 - }, - "roles": [ - "Name" - ], - "token": "Delete", - "type": "Identifier" - } - ], - "id": "c475c9f273faa3fb", - "pos": { - "end_col": 28, - "end_line": 45, - "end_offset": 1380, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [], - "token": "cast(s).Delete", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "662cd65df36043b3", - "pos": { - "end_col": 34, - "end_line": 45, - "end_offset": 1386, - "start_col": 29, - "start_line": 45, - "start_offset": 1381 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "id": "d445fde0c013033c", - "pos": { - "end_col": 37, - "end_line": 45, - "end_offset": 1389, - "start_col": 29, - "start_line": 45, - "start_offset": 1381 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "id": "b4e6ae9b52b020ce", - "pos": { - "end_col": 38, - "end_line": 45, - "end_offset": 1390, - "start_col": 28, - "start_line": 45, - "start_offset": 1380 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "id": "2ea04b61588bebf0", - "pos": { - "end_col": 38, - "end_line": 45, - "end_offset": 1390, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Delete(items...)", - "type": "Call" - } - ], - "id": "e588d45162ddf317", - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 13, - "start_line": 45, - "start_offset": 1365 - }, - "roles": [], - "token": "(cast(s).Delete(items...))", - "type": "List" - } - ], - "id": "82419aa786f9d334", - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 9, - "start_line": 45, - "start_offset": 1361 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s).Delete(items...))", - "type": "Call" - } - ], - "id": "200ec9e57de9704c", - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 9, - "start_line": 45, - "start_offset": 1361 - }, - "roles": [], - "type": "List" - } - ], - "id": "4f07a37ac2580a6a", - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 2, - "start_line": 45, - "start_offset": 1354 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s).Delete(items...))", - "type": "Return" - } - ], - "id": "6f5ee0ae0c95c4bf", - "pos": { - "end_col": 2, - "end_line": 46, - "end_offset": 1393, - "start_col": 42, - "start_line": 44, - "start_offset": 1351 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s).Delete(items...))\n}", - "type": "Block" - } - ], - "id": "61ae4c7f7e8f9b2d", - "pos": { - "end_col": 2, - "end_line": 46, - "end_offset": 1393, - "start_col": 1, - "start_line": 44, - "start_offset": 1310 - }, - "props": { - "name": "Delete" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "d0cc3aed2e617eab", - "pos": { - "end_col": 65, - "end_line": 48, - "end_offset": 1459, - "start_col": 1, - "start_line": 48, - "start_offset": 1395 - }, - "roles": [ - "Comment" - ], - "token": "// Has returns true if and only if item is contained in the set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "6fc3eba1a948deff", - "pos": { - "end_col": 8, - "end_line": 49, - "end_offset": 1467, - "start_col": 7, - "start_line": 49, - "start_offset": 1466 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "6baee743a66fd9da", - "pos": { - "end_col": 13, - "end_line": 49, - "end_offset": 1472, - "start_col": 9, - "start_line": 49, - "start_offset": 1468 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "f8edc98e2e9beed6", - "pos": { - "end_col": 13, - "end_line": 49, - "end_offset": 1472, - "start_col": 7, - "start_line": 49, - "start_offset": 1466 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "4fc86927cd06540f", - "pos": { - "end_col": 14, - "end_line": 49, - "end_offset": 1473, - "start_col": 6, - "start_line": 49, - "start_offset": 1465 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "c78da716c035560a", - "pos": { - "end_col": 18, - "end_line": 49, - "end_offset": 1477, - "start_col": 15, - "start_line": 49, - "start_offset": 1474 - }, - "roles": [ - "Name" - ], - "token": "Has", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "8ec1aeb28d4a2847", - "pos": { - "end_col": 23, - "end_line": 49, - "end_offset": 1482, - "start_col": 19, - "start_line": 49, - "start_offset": 1478 - }, - "roles": [ - "Name" - ], - "token": "item", - "type": "Identifier" - }, - { - "id": "829c8e03838f3af0", - "pos": { - "end_col": 28, - "end_line": 49, - "end_offset": 1487, - "start_col": 24, - "start_line": 49, - "start_offset": 1483 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "8ef62c773b42b60a", - "pos": { - "end_col": 28, - "end_line": 49, - "end_offset": 1487, - "start_col": 19, - "start_line": 49, - "start_offset": 1478 - }, - "props": { - "name": "item" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "3d13c4edf7ea1554", - "pos": { - "end_col": 29, - "end_line": 49, - "end_offset": 1488, - "start_col": 18, - "start_line": 49, - "start_offset": 1477 - }, - "roles": [ - "Parameter" - ], - "token": "(item byte)", - "type": "Parameter" - }, - { - "id": "1350b70ac95e1008", - "pos": { - "end_col": 34, - "end_line": 49, - "end_offset": 1493, - "start_col": 30, - "start_line": 49, - "start_offset": 1489 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "2bce9e8811df42da", - "pos": { - "end_col": 13, - "end_line": 50, - "end_offset": 1508, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "3a85b31b4af83699", - "pos": { - "end_col": 15, - "end_line": 50, - "end_offset": 1510, - "start_col": 14, - "start_line": 50, - "start_offset": 1509 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "bea412476c53a14e", - "pos": { - "end_col": 16, - "end_line": 50, - "end_offset": 1511, - "start_col": 13, - "start_line": 50, - "start_offset": 1508 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "9e9cc11c9630c9cf", - "pos": { - "end_col": 16, - "end_line": 50, - "end_offset": 1511, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "bcee419bc39c8fb6", - "pos": { - "end_col": 20, - "end_line": 50, - "end_offset": 1515, - "start_col": 17, - "start_line": 50, - "start_offset": 1512 - }, - "roles": [ - "Name" - ], - "token": "Has", - "type": "Identifier" - } - ], - "id": "2ba2e8b5784cc6a5", - "pos": { - "end_col": 20, - "end_line": 50, - "end_offset": 1515, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [], - "token": "cast(s).Has", - "type": "Synthetic" - }, - { - "children": [ - { - "id": "2b844abdcc7c4fbb", - "pos": { - "end_col": 25, - "end_line": 50, - "end_offset": 1520, - "start_col": 21, - "start_line": 50, - "start_offset": 1516 - }, - "roles": [ - "Name" - ], - "token": "item", - "type": "Identifier" - } - ], - "id": "3d7272506fdf3e4b", - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 20, - "start_line": 50, - "start_offset": 1515 - }, - "roles": [], - "token": "(item)", - "type": "List" - } - ], - "id": "d8abc1642454e811", - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Has(item)", - "type": "Call" - } - ], - "id": "ac4b4cab64708ae0", - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [], - "type": "List" - } - ], - "id": "dd1e1bf13175432f", - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 2, - "start_line": 50, - "start_offset": 1497 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).Has(item)", - "type": "Return" - } - ], - "id": "feca1c645384aacc", - "pos": { - "end_col": 2, - "end_line": 51, - "end_offset": 1523, - "start_col": 35, - "start_line": 49, - "start_offset": 1494 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).Has(item)\n}", - "type": "Block" - } - ], - "id": "ed228010867d6b9b", - "pos": { - "end_col": 2, - "end_line": 51, - "end_offset": 1523, - "start_col": 1, - "start_line": 49, - "start_offset": 1460 - }, - "props": { - "name": "Has" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "d39ad3002d2bbc73", - "pos": { - "end_col": 74, - "end_line": 53, - "end_offset": 1598, - "start_col": 1, - "start_line": 53, - "start_offset": 1525 - }, - "roles": [ - "Comment" - ], - "token": "// HasAll returns true if and only if all items are contained in the set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "6957e0d21821aaf5", - "pos": { - "end_col": 8, - "end_line": 54, - "end_offset": 1606, - "start_col": 7, - "start_line": 54, - "start_offset": 1605 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "306b1654783b1a5d", - "pos": { - "end_col": 13, - "end_line": 54, - "end_offset": 1611, - "start_col": 9, - "start_line": 54, - "start_offset": 1607 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "db5af44f28ae42e0", - "pos": { - "end_col": 13, - "end_line": 54, - "end_offset": 1611, - "start_col": 7, - "start_line": 54, - "start_offset": 1605 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "5df0113dbb7692e5", - "pos": { - "end_col": 14, - "end_line": 54, - "end_offset": 1612, - "start_col": 6, - "start_line": 54, - "start_offset": 1604 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "42699c1ed45ddd67", - "pos": { - "end_col": 21, - "end_line": 54, - "end_offset": 1619, - "start_col": 15, - "start_line": 54, - "start_offset": 1613 - }, - "roles": [ - "Name" - ], - "token": "HasAll", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "ae7cf20c65f9e4c2", - "pos": { - "end_col": 27, - "end_line": 54, - "end_offset": 1625, - "start_col": 22, - "start_line": 54, - "start_offset": 1620 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "id": "48ee9dab38eed422", - "pos": { - "end_col": 35, - "end_line": 54, - "end_offset": 1633, - "start_col": 31, - "start_line": 54, - "start_offset": 1629 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "8fe5189d249990f8", - "pos": { - "end_col": 35, - "end_line": 54, - "end_offset": 1633, - "start_col": 22, - "start_line": 54, - "start_offset": 1620 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "31cc4bb86822c908", - "pos": { - "end_col": 36, - "end_line": 54, - "end_offset": 1634, - "start_col": 21, - "start_line": 54, - "start_offset": 1619 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "id": "c924e5c01c5755c6", - "pos": { - "end_col": 41, - "end_line": 54, - "end_offset": 1639, - "start_col": 37, - "start_line": 54, - "start_offset": 1635 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "169a20df9fb54d8b", - "pos": { - "end_col": 13, - "end_line": 55, - "end_offset": 1654, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "e04e6d970ecb039f", - "pos": { - "end_col": 15, - "end_line": 55, - "end_offset": 1656, - "start_col": 14, - "start_line": 55, - "start_offset": 1655 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "0d377c2c9116faed", - "pos": { - "end_col": 16, - "end_line": 55, - "end_offset": 1657, - "start_col": 13, - "start_line": 55, - "start_offset": 1654 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "9a7368c7e0f0e392", - "pos": { - "end_col": 16, - "end_line": 55, - "end_offset": 1657, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "e2837f1a90a3fcd7", - "pos": { - "end_col": 23, - "end_line": 55, - "end_offset": 1664, - "start_col": 17, - "start_line": 55, - "start_offset": 1658 - }, - "roles": [ - "Name" - ], - "token": "HasAll", - "type": "Identifier" - } - ], - "id": "a4d3661d5140f636", - "pos": { - "end_col": 23, - "end_line": 55, - "end_offset": 1664, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [], - "token": "cast(s).HasAll", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "0304c5b1cd55458b", - "pos": { - "end_col": 29, - "end_line": 55, - "end_offset": 1670, - "start_col": 24, - "start_line": 55, - "start_offset": 1665 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "id": "4778ec428e64baf8", - "pos": { - "end_col": 32, - "end_line": 55, - "end_offset": 1673, - "start_col": 24, - "start_line": 55, - "start_offset": 1665 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "id": "b8e57e1974bd5f4a", - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 23, - "start_line": 55, - "start_offset": 1664 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "id": "e09361660e338546", - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [ - "Call" - ], - "token": "cast(s).HasAll(items...)", - "type": "Call" - } - ], - "id": "6e5d342aaa141ce4", - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [], - "type": "List" - } - ], - "id": "d074a372f54f9646", - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 2, - "start_line": 55, - "start_offset": 1643 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).HasAll(items...)", - "type": "Return" - } - ], - "id": "8e27302c3f7aa38c", - "pos": { - "end_col": 2, - "end_line": 56, - "end_offset": 1676, - "start_col": 42, - "start_line": 54, - "start_offset": 1640 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).HasAll(items...)\n}", - "type": "Block" - } - ], - "id": "d2d30d64cc2e28f0", - "pos": { - "end_col": 2, - "end_line": 56, - "end_offset": 1676, - "start_col": 1, - "start_line": 54, - "start_offset": 1599 - }, - "props": { - "name": "HasAll" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "ee847ebe5a758cc4", - "pos": { - "end_col": 62, - "end_line": 58, - "end_offset": 1739, - "start_col": 1, - "start_line": 58, - "start_offset": 1678 - }, - "roles": [ - "Comment" - ], - "token": "// HasAny returns true if any items are contained in the set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "e043b7601712f60f", - "pos": { - "end_col": 8, - "end_line": 59, - "end_offset": 1747, - "start_col": 7, - "start_line": 59, - "start_offset": 1746 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "c82743e929848a37", - "pos": { - "end_col": 13, - "end_line": 59, - "end_offset": 1752, - "start_col": 9, - "start_line": 59, - "start_offset": 1748 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "9b57e75e87420108", - "pos": { - "end_col": 13, - "end_line": 59, - "end_offset": 1752, - "start_col": 7, - "start_line": 59, - "start_offset": 1746 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "e65b55a8bcf0d027", - "pos": { - "end_col": 14, - "end_line": 59, - "end_offset": 1753, - "start_col": 6, - "start_line": 59, - "start_offset": 1745 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "12de634220ef262b", - "pos": { - "end_col": 21, - "end_line": 59, - "end_offset": 1760, - "start_col": 15, - "start_line": 59, - "start_offset": 1754 - }, - "roles": [ - "Name" - ], - "token": "HasAny", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "58892b6cbee71205", - "pos": { - "end_col": 27, - "end_line": 59, - "end_offset": 1766, - "start_col": 22, - "start_line": 59, - "start_offset": 1761 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "id": "2060d5c409be14e5", - "pos": { - "end_col": 35, - "end_line": 59, - "end_offset": 1774, - "start_col": 31, - "start_line": 59, - "start_offset": 1770 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "8c2444d697a2b9dd", - "pos": { - "end_col": 35, - "end_line": 59, - "end_offset": 1774, - "start_col": 22, - "start_line": 59, - "start_offset": 1761 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "fc1e27c19d09d21d", - "pos": { - "end_col": 36, - "end_line": 59, - "end_offset": 1775, - "start_col": 21, - "start_line": 59, - "start_offset": 1760 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "id": "3ad2f01b0f2072f0", - "pos": { - "end_col": 41, - "end_line": 59, - "end_offset": 1780, - "start_col": 37, - "start_line": 59, - "start_offset": 1776 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "2623593fbc2ba49c", - "pos": { - "end_col": 13, - "end_line": 60, - "end_offset": 1795, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "888c08fe61389a20", - "pos": { - "end_col": 15, - "end_line": 60, - "end_offset": 1797, - "start_col": 14, - "start_line": 60, - "start_offset": 1796 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "fa266d38fe501d6f", - "pos": { - "end_col": 16, - "end_line": 60, - "end_offset": 1798, - "start_col": 13, - "start_line": 60, - "start_offset": 1795 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "4209131baa68f3f5", - "pos": { - "end_col": 16, - "end_line": 60, - "end_offset": 1798, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "5da7abc801ae0be3", - "pos": { - "end_col": 23, - "end_line": 60, - "end_offset": 1805, - "start_col": 17, - "start_line": 60, - "start_offset": 1799 - }, - "roles": [ - "Name" - ], - "token": "HasAny", - "type": "Identifier" - } - ], - "id": "4438b9e014a93046", - "pos": { - "end_col": 23, - "end_line": 60, - "end_offset": 1805, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [], - "token": "cast(s).HasAny", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "a137f1162c204ae9", - "pos": { - "end_col": 29, - "end_line": 60, - "end_offset": 1811, - "start_col": 24, - "start_line": 60, - "start_offset": 1806 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "id": "4f46c471860ba4f4", - "pos": { - "end_col": 32, - "end_line": 60, - "end_offset": 1814, - "start_col": 24, - "start_line": 60, - "start_offset": 1806 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "id": "8e511a60c4092ff4", - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 23, - "start_line": 60, - "start_offset": 1805 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "id": "28df55d1324f96ad", - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [ - "Call" - ], - "token": "cast(s).HasAny(items...)", - "type": "Call" - } - ], - "id": "67f0019a3035b720", - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [], - "type": "List" - } - ], - "id": "8b2524a813cfdc57", - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 2, - "start_line": 60, - "start_offset": 1784 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).HasAny(items...)", - "type": "Return" - } - ], - "id": "c019c9b1eec89420", - "pos": { - "end_col": 2, - "end_line": 61, - "end_offset": 1817, - "start_col": 42, - "start_line": 59, - "start_offset": 1781 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).HasAny(items...)\n}", - "type": "Block" - } - ], - "id": "810e26e7f446cc59", - "pos": { - "end_col": 2, - "end_line": 61, - "end_offset": 1817, - "start_col": 1, - "start_line": 59, - "start_offset": 1740 - }, - "props": { - "name": "HasAny" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "35717aca77fc92d8", - "pos": { - "end_col": 63, - "end_line": 63, - "end_offset": 1881, - "start_col": 1, - "start_line": 63, - "start_offset": 1819 - }, - "roles": [ - "Comment" - ], - "token": "// Clone returns a new set which is a copy of the current set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "08ee52f7b1f98330", - "pos": { - "end_col": 8, - "end_line": 64, - "end_offset": 1889, - "start_col": 7, - "start_line": 64, - "start_offset": 1888 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "aceaa8ad0fc277b8", - "pos": { - "end_col": 13, - "end_line": 64, - "end_offset": 1894, - "start_col": 9, - "start_line": 64, - "start_offset": 1890 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "edc3c8b76bea6028", - "pos": { - "end_col": 13, - "end_line": 64, - "end_offset": 1894, - "start_col": 7, - "start_line": 64, - "start_offset": 1888 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "f776df5a8fd71f6b", - "pos": { - "end_col": 14, - "end_line": 64, - "end_offset": 1895, - "start_col": 6, - "start_line": 64, - "start_offset": 1887 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "35cf99f488cefd27", - "pos": { - "end_col": 20, - "end_line": 64, - "end_offset": 1901, - "start_col": 15, - "start_line": 64, - "start_offset": 1896 - }, - "roles": [ - "Name" - ], - "token": "Clone", - "type": "Identifier" - }, - { - "id": "68746c06d95ea8bc", - "pos": { - "end_col": 22, - "end_line": 64, - "end_offset": 1903, - "start_col": 20, - "start_line": 64, - "start_offset": 1901 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "id": "2fd449d355d70848", - "pos": { - "end_col": 27, - "end_line": 64, - "end_offset": 1908, - "start_col": 23, - "start_line": 64, - "start_offset": 1904 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "ec08fdad2131373f", - "pos": { - "end_col": 13, - "end_line": 65, - "end_offset": 1923, - "start_col": 9, - "start_line": 65, - "start_offset": 1919 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "cc1bc2364f813aa9", - "pos": { - "end_col": 18, - "end_line": 65, - "end_offset": 1928, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "9e874a0fbf43d4ad", - "pos": { - "end_col": 20, - "end_line": 65, - "end_offset": 1930, - "start_col": 19, - "start_line": 65, - "start_offset": 1929 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "55064ba66b649821", - "pos": { - "end_col": 21, - "end_line": 65, - "end_offset": 1931, - "start_col": 18, - "start_line": 65, - "start_offset": 1928 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "aa5cb72ea40070e1", - "pos": { - "end_col": 21, - "end_line": 65, - "end_offset": 1931, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "c1ba8748825f9b35", - "pos": { - "end_col": 27, - "end_line": 65, - "end_offset": 1937, - "start_col": 22, - "start_line": 65, - "start_offset": 1932 - }, - "roles": [ - "Name" - ], - "token": "Clone", - "type": "Identifier" - } - ], - "id": "6a34170eb7e307f5", - "pos": { - "end_col": 27, - "end_line": 65, - "end_offset": 1937, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [], - "token": "cast(s).Clone", - "type": "Synthetic" - }, - { - "id": "6290cfa667135ddd", - "pos": { - "end_col": 29, - "end_line": 65, - "end_offset": 1939, - "start_col": 27, - "start_line": 65, - "start_offset": 1937 - }, - "roles": [], - "token": "()", - "type": "List" - } - ], - "id": "975367db8159fb08", - "pos": { - "end_col": 29, - "end_line": 65, - "end_offset": 1939, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Clone()", - "type": "Call" - } - ], - "id": "e8b4c72e3fec059c", - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 13, - "start_line": 65, - "start_offset": 1923 - }, - "roles": [], - "token": "(cast(s).Clone())", - "type": "List" - } - ], - "id": "1d7fe158ae70d3eb", - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 9, - "start_line": 65, - "start_offset": 1919 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s).Clone())", - "type": "Call" - } - ], - "id": "c2e38a1c0b401884", - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 9, - "start_line": 65, - "start_offset": 1919 - }, - "roles": [], - "type": "List" - } - ], - "id": "9990ca66f2a7d99f", - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 2, - "start_line": 65, - "start_offset": 1912 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s).Clone())", - "type": "Return" - } - ], - "id": "e73623b9b17bd0f9", - "pos": { - "end_col": 2, - "end_line": 66, - "end_offset": 1942, - "start_col": 28, - "start_line": 64, - "start_offset": 1909 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s).Clone())\n}", - "type": "Block" - } - ], - "id": "9013b21631f2b51c", - "pos": { - "end_col": 2, - "end_line": 66, - "end_offset": 1942, - "start_col": 1, - "start_line": 64, - "start_offset": 1882 - }, - "props": { - "name": "Clone" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "17faf28a5d8425ca", - "pos": { - "end_col": 59, - "end_line": 68, - "end_offset": 2002, - "start_col": 1, - "start_line": 68, - "start_offset": 1944 - }, - "roles": [ - "Comment" - ], - "token": "// Difference returns a set of objects that are not in s2.", - "type": "Comment" - }, - { - "id": "f597aa1945c98e42", - "pos": { - "end_col": 16, - "end_line": 69, - "end_offset": 2018, - "start_col": 1, - "start_line": 69, - "start_offset": 2003 - }, - "roles": [ - "Comment" - ], - "token": "// For example:", - "type": "Comment" - }, - { - "id": "a895914c08358c29", - "pos": { - "end_col": 21, - "end_line": 70, - "end_offset": 2039, - "start_col": 1, - "start_line": 70, - "start_offset": 2019 - }, - "roles": [ - "Comment" - ], - "token": "// s1 = {a1, a2, a3}", - "type": "Comment" - }, - { - "id": "bce95b1083323a25", - "pos": { - "end_col": 25, - "end_line": 71, - "end_offset": 2064, - "start_col": 1, - "start_line": 71, - "start_offset": 2040 - }, - "roles": [ - "Comment" - ], - "token": "// s2 = {a1, a2, a4, a5}", - "type": "Comment" - }, - { - "id": "10993063a80d0e2b", - "pos": { - "end_col": 28, - "end_line": 72, - "end_offset": 2092, - "start_col": 1, - "start_line": 72, - "start_offset": 2065 - }, - "roles": [ - "Comment" - ], - "token": "// s1.Difference(s2) = {a3}", - "type": "Comment" - }, - { - "id": "08fb6deb2e5214ef", - "pos": { - "end_col": 32, - "end_line": 73, - "end_offset": 2124, - "start_col": 1, - "start_line": 73, - "start_offset": 2093 - }, - "roles": [ - "Comment" - ], - "token": "// s2.Difference(s1) = {a4, a5}", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "db2daee4ef965eb7", - "pos": { - "end_col": 9, - "end_line": 74, - "end_offset": 2133, - "start_col": 7, - "start_line": 74, - "start_offset": 2131 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "id": "078e41b46a176a65", - "pos": { - "end_col": 14, - "end_line": 74, - "end_offset": 2138, - "start_col": 10, - "start_line": 74, - "start_offset": 2134 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "186cdcacb2a8af0c", - "pos": { - "end_col": 14, - "end_line": 74, - "end_offset": 2138, - "start_col": 7, - "start_line": 74, - "start_offset": 2131 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "6949bffe93381706", - "pos": { - "end_col": 15, - "end_line": 74, - "end_offset": 2139, - "start_col": 6, - "start_line": 74, - "start_offset": 2130 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "id": "892235c9a0763e02", - "pos": { - "end_col": 26, - "end_line": 74, - "end_offset": 2150, - "start_col": 16, - "start_line": 74, - "start_offset": 2140 - }, - "roles": [ - "Name" - ], - "token": "Difference", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "b4a40b3b73c313c4", - "pos": { - "end_col": 29, - "end_line": 74, - "end_offset": 2153, - "start_col": 27, - "start_line": 74, - "start_offset": 2151 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "id": "3f7e5b0e3163ed8b", - "pos": { - "end_col": 34, - "end_line": 74, - "end_offset": 2158, - "start_col": 30, - "start_line": 74, - "start_offset": 2154 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "09949b5d037ee4c5", - "pos": { - "end_col": 34, - "end_line": 74, - "end_offset": 2158, - "start_col": 27, - "start_line": 74, - "start_offset": 2151 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "83ffb280302b88d4", - "pos": { - "end_col": 35, - "end_line": 74, - "end_offset": 2159, - "start_col": 26, - "start_line": 74, - "start_offset": 2150 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "id": "b404beffe4dafc9c", - "pos": { - "end_col": 40, - "end_line": 74, - "end_offset": 2164, - "start_col": 36, - "start_line": 74, - "start_offset": 2160 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "85a37fb4426b202f", - "pos": { - "end_col": 13, - "end_line": 75, - "end_offset": 2179, - "start_col": 9, - "start_line": 75, - "start_offset": 2175 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "a34c0aad25bf57f2", - "pos": { - "end_col": 18, - "end_line": 75, - "end_offset": 2184, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "d5c3f448eee69112", - "pos": { - "end_col": 21, - "end_line": 75, - "end_offset": 2187, - "start_col": 19, - "start_line": 75, - "start_offset": 2185 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "id": "21f3f71a72fb511d", - "pos": { - "end_col": 22, - "end_line": 75, - "end_offset": 2188, - "start_col": 18, - "start_line": 75, - "start_offset": 2184 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "id": "efe473ded2be592f", - "pos": { - "end_col": 22, - "end_line": 75, - "end_offset": 2188, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "id": "f33713a54db19d87", - "pos": { - "end_col": 33, - "end_line": 75, - "end_offset": 2199, - "start_col": 23, - "start_line": 75, - "start_offset": 2189 - }, - "roles": [ - "Name" - ], - "token": "Difference", - "type": "Identifier" - } - ], - "id": "70cf5817d5d32d79", - "pos": { - "end_col": 33, - "end_line": 75, - "end_offset": 2199, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [], - "token": "cast(s1).Difference", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "126f289947fee01b", - "pos": { - "end_col": 38, - "end_line": 75, - "end_offset": 2204, - "start_col": 34, - "start_line": 75, - "start_offset": 2200 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "d75d335eef4db7f0", - "pos": { - "end_col": 41, - "end_line": 75, - "end_offset": 2207, - "start_col": 39, - "start_line": 75, - "start_offset": 2205 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "id": "5d1b1c9d9da114b7", - "pos": { - "end_col": 42, - "end_line": 75, - "end_offset": 2208, - "start_col": 38, - "start_line": 75, - "start_offset": 2204 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "id": "42bbf23cb82ac47d", - "pos": { - "end_col": 42, - "end_line": 75, - "end_offset": 2208, - "start_col": 34, - "start_line": 75, - "start_offset": 2200 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "id": "75689f0f74698949", - "pos": { - "end_col": 43, - "end_line": 75, - "end_offset": 2209, - "start_col": 33, - "start_line": 75, - "start_offset": 2199 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "id": "6fb46f98c2027e62", - "pos": { - "end_col": 43, - "end_line": 75, - "end_offset": 2209, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Difference(cast(s2))", - "type": "Call" - } - ], - "id": "7b936ea83998f17c", - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 13, - "start_line": 75, - "start_offset": 2179 - }, - "roles": [], - "token": "(cast(s1).Difference(cast(s2)))", - "type": "List" - } - ], - "id": "1999c4f189e8ea5e", - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 9, - "start_line": 75, - "start_offset": 2175 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).Difference(cast(s2)))", - "type": "Call" - } - ], - "id": "158fdf432af3f5f5", - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 9, - "start_line": 75, - "start_offset": 2175 - }, - "roles": [], - "type": "List" - } - ], - "id": "3233e0d3984b80ed", - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 2, - "start_line": 75, - "start_offset": 2168 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).Difference(cast(s2)))", - "type": "Return" - } - ], - "id": "609026e6b4f55390", - "pos": { - "end_col": 2, - "end_line": 76, - "end_offset": 2212, - "start_col": 41, - "start_line": 74, - "start_offset": 2165 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).Difference(cast(s2)))\n}", - "type": "Block" - } - ], - "id": "1411290aca96d400", - "pos": { - "end_col": 2, - "end_line": 76, - "end_offset": 2212, - "start_col": 1, - "start_line": 74, - "start_offset": 2125 - }, - "props": { - "name": "Difference" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "717592ff479823de", - "pos": { - "end_col": 113, - "end_line": 78, - "end_offset": 2326, - "start_col": 1, - "start_line": 78, - "start_offset": 2214 - }, - "roles": [ - "Comment" - ], - "token": "// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.", - "type": "Comment" - }, - { - "id": "0a1069a8624bc8a8", - "pos": { - "end_col": 16, - "end_line": 79, - "end_offset": 2342, - "start_col": 1, - "start_line": 79, - "start_offset": 2327 - }, - "roles": [ - "Comment" - ], - "token": "// For example:", - "type": "Comment" - }, - { - "id": "b37da3baf120ec1e", - "pos": { - "end_col": 21, - "end_line": 80, - "end_offset": 2363, - "start_col": 1, - "start_line": 80, - "start_offset": 2343 - }, - "roles": [ - "Comment" - ], - "token": "// s1 = {a1, a2, a3}", - "type": "Comment" - }, - { - "id": "80deb7c64733baed", - "pos": { - "end_col": 25, - "end_line": 81, - "end_offset": 2388, - "start_col": 1, - "start_line": 81, - "start_offset": 2364 - }, - "roles": [ - "Comment" - ], - "token": "// s2 = {a1, a2, a4, a5}", - "type": "Comment" - }, - { - "id": "6c500ba94cd45636", - "pos": { - "end_col": 45, - "end_line": 82, - "end_offset": 2433, - "start_col": 1, - "start_line": 82, - "start_offset": 2389 - }, - "roles": [ - "Comment" - ], - "token": "// s1.SymmetricDifference(s2) = {a3, a4, a5}", - "type": "Comment" - }, - { - "id": "4aa95ef08fd6e00c", - "pos": { - "end_col": 45, - "end_line": 83, - "end_offset": 2478, - "start_col": 1, - "start_line": 83, - "start_offset": 2434 - }, - "roles": [ - "Comment" - ], - "token": "// s2.SymmetricDifference(s1) = {a3, a4, a5}", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "82fe6f2ffd1abd4a", - "pos": { - "end_col": 9, - "end_line": 84, - "end_offset": 2487, - "start_col": 7, - "start_line": 84, - "start_offset": 2485 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "id": "6dcf01b2dc1df915", - "pos": { - "end_col": 14, - "end_line": 84, - "end_offset": 2492, - "start_col": 10, - "start_line": 84, - "start_offset": 2488 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "c1c4cb1f0c59138f", - "pos": { - "end_col": 14, - "end_line": 84, - "end_offset": 2492, - "start_col": 7, - "start_line": 84, - "start_offset": 2485 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "e88d0d20e5818ca5", - "pos": { - "end_col": 15, - "end_line": 84, - "end_offset": 2493, - "start_col": 6, - "start_line": 84, - "start_offset": 2484 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "id": "b9ae1600e7fe8e0d", - "pos": { - "end_col": 35, - "end_line": 84, - "end_offset": 2513, - "start_col": 16, - "start_line": 84, - "start_offset": 2494 - }, - "roles": [ - "Name" - ], - "token": "SymmetricDifference", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "6d1914600c408244", - "pos": { - "end_col": 38, - "end_line": 84, - "end_offset": 2516, - "start_col": 36, - "start_line": 84, - "start_offset": 2514 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "id": "7befa79a7b3b8a19", - "pos": { - "end_col": 43, - "end_line": 84, - "end_offset": 2521, - "start_col": 39, - "start_line": 84, - "start_offset": 2517 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "4fa74cefbb3e5f0b", - "pos": { - "end_col": 43, - "end_line": 84, - "end_offset": 2521, - "start_col": 36, - "start_line": 84, - "start_offset": 2514 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "26df223a25743642", - "pos": { - "end_col": 44, - "end_line": 84, - "end_offset": 2522, - "start_col": 35, - "start_line": 84, - "start_offset": 2513 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "id": "1bdb68f273aa42ec", - "pos": { - "end_col": 49, - "end_line": 84, - "end_offset": 2527, - "start_col": 45, - "start_line": 84, - "start_offset": 2523 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "0f2fb17e3a79f585", - "pos": { - "end_col": 13, - "end_line": 85, - "end_offset": 2542, - "start_col": 9, - "start_line": 85, - "start_offset": 2538 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "f836738db28aea1d", - "pos": { - "end_col": 18, - "end_line": 85, - "end_offset": 2547, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "26012d26ccc02eab", - "pos": { - "end_col": 21, - "end_line": 85, - "end_offset": 2550, - "start_col": 19, - "start_line": 85, - "start_offset": 2548 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "id": "1e5621bcfd7417ea", - "pos": { - "end_col": 22, - "end_line": 85, - "end_offset": 2551, - "start_col": 18, - "start_line": 85, - "start_offset": 2547 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "id": "c757f1114a551350", - "pos": { - "end_col": 22, - "end_line": 85, - "end_offset": 2551, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "id": "091350e82578aeee", - "pos": { - "end_col": 42, - "end_line": 85, - "end_offset": 2571, - "start_col": 23, - "start_line": 85, - "start_offset": 2552 - }, - "roles": [ - "Name" - ], - "token": "SymmetricDifference", - "type": "Identifier" - } - ], - "id": "5bead0814f902111", - "pos": { - "end_col": 42, - "end_line": 85, - "end_offset": 2571, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [], - "token": "cast(s1).SymmetricDifference", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "35aee9f276ee738f", - "pos": { - "end_col": 47, - "end_line": 85, - "end_offset": 2576, - "start_col": 43, - "start_line": 85, - "start_offset": 2572 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "fe2e9a04080595f5", - "pos": { - "end_col": 50, - "end_line": 85, - "end_offset": 2579, - "start_col": 48, - "start_line": 85, - "start_offset": 2577 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "id": "ec21cf29d0e7a39a", - "pos": { - "end_col": 51, - "end_line": 85, - "end_offset": 2580, - "start_col": 47, - "start_line": 85, - "start_offset": 2576 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "id": "d606bda043c80fb8", - "pos": { - "end_col": 51, - "end_line": 85, - "end_offset": 2580, - "start_col": 43, - "start_line": 85, - "start_offset": 2572 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "id": "c7d7af6dfb16a2e9", - "pos": { - "end_col": 52, - "end_line": 85, - "end_offset": 2581, - "start_col": 42, - "start_line": 85, - "start_offset": 2571 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "id": "d90a2fd58eb3975f", - "pos": { - "end_col": 52, - "end_line": 85, - "end_offset": 2581, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).SymmetricDifference(cast(s2))", - "type": "Call" - } - ], - "id": "d46050e6ebbf409e", - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 13, - "start_line": 85, - "start_offset": 2542 - }, - "roles": [], - "token": "(cast(s1).SymmetricDifference(cast(s2)))", - "type": "List" - } - ], - "id": "39391acd0d2fbca3", - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 9, - "start_line": 85, - "start_offset": 2538 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).SymmetricDifference(cast(s2)))", - "type": "Call" - } - ], - "id": "bb1a4b3175f4064e", - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 9, - "start_line": 85, - "start_offset": 2538 - }, - "roles": [], - "type": "List" - } - ], - "id": "c231d6e0f500620b", - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 2, - "start_line": 85, - "start_offset": 2531 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).SymmetricDifference(cast(s2)))", - "type": "Return" - } - ], - "id": "ed1b9bf74b8b8c0f", - "pos": { - "end_col": 2, - "end_line": 86, - "end_offset": 2584, - "start_col": 50, - "start_line": 84, - "start_offset": 2528 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).SymmetricDifference(cast(s2)))\n}", - "type": "Block" - } - ], - "id": "cd8d1263cca8f985", - "pos": { - "end_col": 2, - "end_line": 86, - "end_offset": 2584, - "start_col": 1, - "start_line": 84, - "start_offset": 2479 - }, - "props": { - "name": "SymmetricDifference" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "cd430df4baae034c", - "pos": { - "end_col": 68, - "end_line": 88, - "end_offset": 2653, - "start_col": 1, - "start_line": 88, - "start_offset": 2586 - }, - "roles": [ - "Comment" - ], - "token": "// Union returns a new set which includes items in either s1 or s2.", - "type": "Comment" - }, - { - "id": "20683ad6aaa8cd09", - "pos": { - "end_col": 16, - "end_line": 89, - "end_offset": 2669, - "start_col": 1, - "start_line": 89, - "start_offset": 2654 - }, - "roles": [ - "Comment" - ], - "token": "// For example:", - "type": "Comment" - }, - { - "id": "d7b13d53ff881233", - "pos": { - "end_col": 17, - "end_line": 90, - "end_offset": 2686, - "start_col": 1, - "start_line": 90, - "start_offset": 2670 - }, - "roles": [ - "Comment" - ], - "token": "// s1 = {a1, a2}", - "type": "Comment" - }, - { - "id": "c3e62dff84ca334d", - "pos": { - "end_col": 17, - "end_line": 91, - "end_offset": 2703, - "start_col": 1, - "start_line": 91, - "start_offset": 2687 - }, - "roles": [ - "Comment" - ], - "token": "// s2 = {a3, a4}", - "type": "Comment" - }, - { - "id": "ee719c20c16b6c75", - "pos": { - "end_col": 35, - "end_line": 92, - "end_offset": 2738, - "start_col": 1, - "start_line": 92, - "start_offset": 2704 - }, - "roles": [ - "Comment" - ], - "token": "// s1.Union(s2) = {a1, a2, a3, a4}", - "type": "Comment" - }, - { - "id": "d6335603ab9066db", - "pos": { - "end_col": 35, - "end_line": 93, - "end_offset": 2773, - "start_col": 1, - "start_line": 93, - "start_offset": 2739 - }, - "roles": [ - "Comment" - ], - "token": "// s2.Union(s1) = {a1, a2, a3, a4}", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "8162520fca0c0dca", - "pos": { - "end_col": 9, - "end_line": 94, - "end_offset": 2782, - "start_col": 7, - "start_line": 94, - "start_offset": 2780 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "id": "85a3355b54935527", - "pos": { - "end_col": 14, - "end_line": 94, - "end_offset": 2787, - "start_col": 10, - "start_line": 94, - "start_offset": 2783 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "f8649cfe5d0c2697", - "pos": { - "end_col": 14, - "end_line": 94, - "end_offset": 2787, - "start_col": 7, - "start_line": 94, - "start_offset": 2780 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "e5ebd60b2ede8a0a", - "pos": { - "end_col": 15, - "end_line": 94, - "end_offset": 2788, - "start_col": 6, - "start_line": 94, - "start_offset": 2779 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "id": "9500e0a9239d6345", - "pos": { - "end_col": 21, - "end_line": 94, - "end_offset": 2794, - "start_col": 16, - "start_line": 94, - "start_offset": 2789 - }, - "roles": [ - "Name" - ], - "token": "Union", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "cb362fa4411e7503", - "pos": { - "end_col": 24, - "end_line": 94, - "end_offset": 2797, - "start_col": 22, - "start_line": 94, - "start_offset": 2795 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "id": "431fa00f9177c144", - "pos": { - "end_col": 29, - "end_line": 94, - "end_offset": 2802, - "start_col": 25, - "start_line": 94, - "start_offset": 2798 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "56205af69417f7dc", - "pos": { - "end_col": 29, - "end_line": 94, - "end_offset": 2802, - "start_col": 22, - "start_line": 94, - "start_offset": 2795 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "41b4fa372325c12b", - "pos": { - "end_col": 30, - "end_line": 94, - "end_offset": 2803, - "start_col": 21, - "start_line": 94, - "start_offset": 2794 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "id": "6e699caaf150d3df", - "pos": { - "end_col": 35, - "end_line": 94, - "end_offset": 2808, - "start_col": 31, - "start_line": 94, - "start_offset": 2804 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "109cfbe7afbeb035", - "pos": { - "end_col": 13, - "end_line": 95, - "end_offset": 2823, - "start_col": 9, - "start_line": 95, - "start_offset": 2819 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "79f1159934239336", - "pos": { - "end_col": 18, - "end_line": 95, - "end_offset": 2828, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "a80cb25b4e21bf60", - "pos": { - "end_col": 21, - "end_line": 95, - "end_offset": 2831, - "start_col": 19, - "start_line": 95, - "start_offset": 2829 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "id": "908f9317eb118fed", - "pos": { - "end_col": 22, - "end_line": 95, - "end_offset": 2832, - "start_col": 18, - "start_line": 95, - "start_offset": 2828 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "id": "7bdd6e80a604e8b7", - "pos": { - "end_col": 22, - "end_line": 95, - "end_offset": 2832, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "id": "3691af365f91b76c", - "pos": { - "end_col": 28, - "end_line": 95, - "end_offset": 2838, - "start_col": 23, - "start_line": 95, - "start_offset": 2833 - }, - "roles": [ - "Name" - ], - "token": "Union", - "type": "Identifier" - } - ], - "id": "d365d684e990acf4", - "pos": { - "end_col": 28, - "end_line": 95, - "end_offset": 2838, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [], - "token": "cast(s1).Union", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "4f28cda9a70d4b24", - "pos": { - "end_col": 33, - "end_line": 95, - "end_offset": 2843, - "start_col": 29, - "start_line": 95, - "start_offset": 2839 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "bc1608c83a4a644d", - "pos": { - "end_col": 36, - "end_line": 95, - "end_offset": 2846, - "start_col": 34, - "start_line": 95, - "start_offset": 2844 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "id": "52d8168a3ccb799d", - "pos": { - "end_col": 37, - "end_line": 95, - "end_offset": 2847, - "start_col": 33, - "start_line": 95, - "start_offset": 2843 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "id": "5d91c7f93c5cf0e1", - "pos": { - "end_col": 37, - "end_line": 95, - "end_offset": 2847, - "start_col": 29, - "start_line": 95, - "start_offset": 2839 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "id": "cfa50a5f03648f29", - "pos": { - "end_col": 38, - "end_line": 95, - "end_offset": 2848, - "start_col": 28, - "start_line": 95, - "start_offset": 2838 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "id": "fd7b74d00b18fc80", - "pos": { - "end_col": 38, - "end_line": 95, - "end_offset": 2848, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Union(cast(s2))", - "type": "Call" - } - ], - "id": "e51493a7483387a6", - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 13, - "start_line": 95, - "start_offset": 2823 - }, - "roles": [], - "token": "(cast(s1).Union(cast(s2)))", - "type": "List" - } - ], - "id": "6a52a3121aa062a5", - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 9, - "start_line": 95, - "start_offset": 2819 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).Union(cast(s2)))", - "type": "Call" - } - ], - "id": "0b4d062e348aac36", - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 9, - "start_line": 95, - "start_offset": 2819 - }, - "roles": [], - "type": "List" - } - ], - "id": "0fdc64587b752430", - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 2, - "start_line": 95, - "start_offset": 2812 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).Union(cast(s2)))", - "type": "Return" - } - ], - "id": "172abb07ef3383f3", - "pos": { - "end_col": 2, - "end_line": 96, - "end_offset": 2851, - "start_col": 36, - "start_line": 94, - "start_offset": 2809 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).Union(cast(s2)))\n}", - "type": "Block" - } - ], - "id": "08582b77bb3e80dc", - "pos": { - "end_col": 2, - "end_line": 96, - "end_offset": 2851, - "start_col": 1, - "start_line": 94, - "start_offset": 2774 - }, - "props": { - "name": "Union" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "2fc9a7b264f5907f", - "pos": { - "end_col": 76, - "end_line": 98, - "end_offset": 2928, - "start_col": 1, - "start_line": 98, - "start_offset": 2853 - }, - "roles": [ - "Comment" - ], - "token": "// Intersection returns a new set which includes the item in BOTH s1 and s2", - "type": "Comment" - }, - { - "id": "8648621ebaece137", - "pos": { - "end_col": 16, - "end_line": 99, - "end_offset": 2944, - "start_col": 1, - "start_line": 99, - "start_offset": 2929 - }, - "roles": [ - "Comment" - ], - "token": "// For example:", - "type": "Comment" - }, - { - "id": "68f6fd7b86b09709", - "pos": { - "end_col": 17, - "end_line": 100, - "end_offset": 2961, - "start_col": 1, - "start_line": 100, - "start_offset": 2945 - }, - "roles": [ - "Comment" - ], - "token": "// s1 = {a1, a2}", - "type": "Comment" - }, - { - "id": "471b4c7572699c4b", - "pos": { - "end_col": 17, - "end_line": 101, - "end_offset": 2978, - "start_col": 1, - "start_line": 101, - "start_offset": 2962 - }, - "roles": [ - "Comment" - ], - "token": "// s2 = {a2, a3}", - "type": "Comment" - }, - { - "id": "8766e2930169eb4e", - "pos": { - "end_col": 30, - "end_line": 102, - "end_offset": 3008, - "start_col": 1, - "start_line": 102, - "start_offset": 2979 - }, - "roles": [ - "Comment" - ], - "token": "// s1.Intersection(s2) = {a2}", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "e869691a564b0d62", - "pos": { - "end_col": 9, - "end_line": 103, - "end_offset": 3017, - "start_col": 7, - "start_line": 103, - "start_offset": 3015 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "id": "4a6c9a3b8651ef00", - "pos": { - "end_col": 14, - "end_line": 103, - "end_offset": 3022, - "start_col": 10, - "start_line": 103, - "start_offset": 3018 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "e9bf16012fc30786", - "pos": { - "end_col": 14, - "end_line": 103, - "end_offset": 3022, - "start_col": 7, - "start_line": 103, - "start_offset": 3015 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "67bb533e5a599dcd", - "pos": { - "end_col": 15, - "end_line": 103, - "end_offset": 3023, - "start_col": 6, - "start_line": 103, - "start_offset": 3014 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "id": "d05ecc0249f604b5", - "pos": { - "end_col": 28, - "end_line": 103, - "end_offset": 3036, - "start_col": 16, - "start_line": 103, - "start_offset": 3024 - }, - "roles": [ - "Name" - ], - "token": "Intersection", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "fefe48cd027458f8", - "pos": { - "end_col": 31, - "end_line": 103, - "end_offset": 3039, - "start_col": 29, - "start_line": 103, - "start_offset": 3037 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "id": "1f8c94a27652e3dc", - "pos": { - "end_col": 36, - "end_line": 103, - "end_offset": 3044, - "start_col": 32, - "start_line": 103, - "start_offset": 3040 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "81fc6388bbc7ed1d", - "pos": { - "end_col": 36, - "end_line": 103, - "end_offset": 3044, - "start_col": 29, - "start_line": 103, - "start_offset": 3037 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "55df18ec49b783dd", - "pos": { - "end_col": 37, - "end_line": 103, - "end_offset": 3045, - "start_col": 28, - "start_line": 103, - "start_offset": 3036 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "id": "507da24ef1ef1c7f", - "pos": { - "end_col": 42, - "end_line": 103, - "end_offset": 3050, - "start_col": 38, - "start_line": 103, - "start_offset": 3046 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "ca3ac82e8043e740", - "pos": { - "end_col": 13, - "end_line": 104, - "end_offset": 3065, - "start_col": 9, - "start_line": 104, - "start_offset": 3061 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "da69af10f1b57125", - "pos": { - "end_col": 18, - "end_line": 104, - "end_offset": 3070, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "7055a6a1a4ab0e8e", - "pos": { - "end_col": 21, - "end_line": 104, - "end_offset": 3073, - "start_col": 19, - "start_line": 104, - "start_offset": 3071 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "id": "a48f099523f11e66", - "pos": { - "end_col": 22, - "end_line": 104, - "end_offset": 3074, - "start_col": 18, - "start_line": 104, - "start_offset": 3070 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "id": "eeda8f32f566437e", - "pos": { - "end_col": 22, - "end_line": 104, - "end_offset": 3074, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "id": "3246abb86f8268d0", - "pos": { - "end_col": 35, - "end_line": 104, - "end_offset": 3087, - "start_col": 23, - "start_line": 104, - "start_offset": 3075 - }, - "roles": [ - "Name" - ], - "token": "Intersection", - "type": "Identifier" - } - ], - "id": "529515e017dd06ef", - "pos": { - "end_col": 35, - "end_line": 104, - "end_offset": 3087, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [], - "token": "cast(s1).Intersection", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "01e3a11aad60126a", - "pos": { - "end_col": 40, - "end_line": 104, - "end_offset": 3092, - "start_col": 36, - "start_line": 104, - "start_offset": 3088 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "6d840d8d13e4a72c", - "pos": { - "end_col": 43, - "end_line": 104, - "end_offset": 3095, - "start_col": 41, - "start_line": 104, - "start_offset": 3093 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "id": "029dfe1fae36ff23", - "pos": { - "end_col": 44, - "end_line": 104, - "end_offset": 3096, - "start_col": 40, - "start_line": 104, - "start_offset": 3092 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "id": "ea6b3979d1b03add", - "pos": { - "end_col": 44, - "end_line": 104, - "end_offset": 3096, - "start_col": 36, - "start_line": 104, - "start_offset": 3088 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "id": "d41311f966b8b907", - "pos": { - "end_col": 45, - "end_line": 104, - "end_offset": 3097, - "start_col": 35, - "start_line": 104, - "start_offset": 3087 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "id": "23feae1ec96563ec", - "pos": { - "end_col": 45, - "end_line": 104, - "end_offset": 3097, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Intersection(cast(s2))", - "type": "Call" - } - ], - "id": "a3541706f314ac0e", - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 13, - "start_line": 104, - "start_offset": 3065 - }, - "roles": [], - "token": "(cast(s1).Intersection(cast(s2)))", - "type": "List" - } - ], - "id": "94075c3950e21c9d", - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 9, - "start_line": 104, - "start_offset": 3061 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).Intersection(cast(s2)))", - "type": "Call" - } - ], - "id": "419559198f78b134", - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 9, - "start_line": 104, - "start_offset": 3061 - }, - "roles": [], - "type": "List" - } - ], - "id": "1cab7726425a4008", - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 2, - "start_line": 104, - "start_offset": 3054 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).Intersection(cast(s2)))", - "type": "Return" - } - ], - "id": "cec30481348e9759", - "pos": { - "end_col": 2, - "end_line": 105, - "end_offset": 3100, - "start_col": 43, - "start_line": 103, - "start_offset": 3051 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).Intersection(cast(s2)))\n}", - "type": "Block" - } - ], - "id": "ac16cea2adc0ef50", - "pos": { - "end_col": 2, - "end_line": 105, - "end_offset": 3100, - "start_col": 1, - "start_line": 103, - "start_offset": 3009 - }, - "props": { - "name": "Intersection" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "01f42bff46ce75be", - "pos": { - "end_col": 66, - "end_line": 107, - "end_offset": 3167, - "start_col": 1, - "start_line": 107, - "start_offset": 3102 - }, - "roles": [ - "Comment" - ], - "token": "// IsSuperset returns true if and only if s1 is a superset of s2.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "51f3598e76a09ab7", - "pos": { - "end_col": 9, - "end_line": 108, - "end_offset": 3176, - "start_col": 7, - "start_line": 108, - "start_offset": 3174 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "id": "b76c6cac67354cb9", - "pos": { - "end_col": 14, - "end_line": 108, - "end_offset": 3181, - "start_col": 10, - "start_line": 108, - "start_offset": 3177 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "246c449d8cffffc9", - "pos": { - "end_col": 14, - "end_line": 108, - "end_offset": 3181, - "start_col": 7, - "start_line": 108, - "start_offset": 3174 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "24dfd227022df574", - "pos": { - "end_col": 15, - "end_line": 108, - "end_offset": 3182, - "start_col": 6, - "start_line": 108, - "start_offset": 3173 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "id": "9fc7cb33fd89fce8", - "pos": { - "end_col": 26, - "end_line": 108, - "end_offset": 3193, - "start_col": 16, - "start_line": 108, - "start_offset": 3183 - }, - "roles": [ - "Name" - ], - "token": "IsSuperset", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "426e8c77be76f91e", - "pos": { - "end_col": 29, - "end_line": 108, - "end_offset": 3196, - "start_col": 27, - "start_line": 108, - "start_offset": 3194 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "id": "0b900c940493db28", - "pos": { - "end_col": 34, - "end_line": 108, - "end_offset": 3201, - "start_col": 30, - "start_line": 108, - "start_offset": 3197 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "444421a8f73489cb", - "pos": { - "end_col": 34, - "end_line": 108, - "end_offset": 3201, - "start_col": 27, - "start_line": 108, - "start_offset": 3194 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "e0215127efdf6547", - "pos": { - "end_col": 35, - "end_line": 108, - "end_offset": 3202, - "start_col": 26, - "start_line": 108, - "start_offset": 3193 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "id": "3730ee822c09c69a", - "pos": { - "end_col": 40, - "end_line": 108, - "end_offset": 3207, - "start_col": 36, - "start_line": 108, - "start_offset": 3203 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "1c522b9b8df06d14", - "pos": { - "end_col": 13, - "end_line": 109, - "end_offset": 3222, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "0218678dcbf7fed4", - "pos": { - "end_col": 16, - "end_line": 109, - "end_offset": 3225, - "start_col": 14, - "start_line": 109, - "start_offset": 3223 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "id": "c2d87a7fd1ee6a49", - "pos": { - "end_col": 17, - "end_line": 109, - "end_offset": 3226, - "start_col": 13, - "start_line": 109, - "start_offset": 3222 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "id": "b58c81247fc4a1b6", - "pos": { - "end_col": 17, - "end_line": 109, - "end_offset": 3226, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "id": "3445a3d8d5ba91b5", - "pos": { - "end_col": 28, - "end_line": 109, - "end_offset": 3237, - "start_col": 18, - "start_line": 109, - "start_offset": 3227 - }, - "roles": [ - "Name" - ], - "token": "IsSuperset", - "type": "Identifier" - } - ], - "id": "07e8d054202317e8", - "pos": { - "end_col": 28, - "end_line": 109, - "end_offset": 3237, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [], - "token": "cast(s1).IsSuperset", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "b637847418b1f4dc", - "pos": { - "end_col": 33, - "end_line": 109, - "end_offset": 3242, - "start_col": 29, - "start_line": 109, - "start_offset": 3238 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "4e284346e75041bc", - "pos": { - "end_col": 36, - "end_line": 109, - "end_offset": 3245, - "start_col": 34, - "start_line": 109, - "start_offset": 3243 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "id": "33a794dd17a95c66", - "pos": { - "end_col": 37, - "end_line": 109, - "end_offset": 3246, - "start_col": 33, - "start_line": 109, - "start_offset": 3242 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "id": "b4bcf04400b40377", - "pos": { - "end_col": 37, - "end_line": 109, - "end_offset": 3246, - "start_col": 29, - "start_line": 109, - "start_offset": 3238 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "id": "1ff66ff2ffc4c1ec", - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 28, - "start_line": 109, - "start_offset": 3237 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "id": "622d55448d602f10", - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).IsSuperset(cast(s2))", - "type": "Call" - } - ], - "id": "ac29904a9121d1b2", - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [], - "type": "List" - } - ], - "id": "1a433a7cb794c911", - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 2, - "start_line": 109, - "start_offset": 3211 - }, - "roles": [ - "Return" - ], - "token": "return cast(s1).IsSuperset(cast(s2))", - "type": "Return" - } - ], - "id": "237999a6972d3819", - "pos": { - "end_col": 2, - "end_line": 110, - "end_offset": 3249, - "start_col": 41, - "start_line": 108, - "start_offset": 3208 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s1).IsSuperset(cast(s2))\n}", - "type": "Block" - } - ], - "id": "63e19288bbeb6fce", - "pos": { - "end_col": 2, - "end_line": 110, - "end_offset": 3249, - "start_col": 1, - "start_line": 108, - "start_offset": 3168 - }, - "props": { - "name": "IsSuperset" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "2fef2bf43c58066c", - "pos": { - "end_col": 67, - "end_line": 112, - "end_offset": 3317, - "start_col": 1, - "start_line": 112, - "start_offset": 3251 - }, - "roles": [ - "Comment" - ], - "token": "// Equal returns true if and only if s1 is equal (as a set) to s2.", - "type": "Comment" - }, - { - "id": "1aadc8669f4523ee", - "pos": { - "end_col": 56, - "end_line": 113, - "end_offset": 3373, - "start_col": 1, - "start_line": 113, - "start_offset": 3318 - }, - "roles": [ - "Comment" - ], - "token": "// Two sets are equal if their membership is identical.", - "type": "Comment" - }, - { - "id": "86feb4e9f82cfbc5", - "pos": { - "end_col": 65, - "end_line": 114, - "end_offset": 3438, - "start_col": 1, - "start_line": 114, - "start_offset": 3374 - }, - "roles": [ - "Comment" - ], - "token": "// (In practice, this means same elements, order doesn't matter)", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "aa3128debf2d1211", - "pos": { - "end_col": 9, - "end_line": 115, - "end_offset": 3447, - "start_col": 7, - "start_line": 115, - "start_offset": 3445 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "id": "38ff634c244f5e78", - "pos": { - "end_col": 14, - "end_line": 115, - "end_offset": 3452, - "start_col": 10, - "start_line": 115, - "start_offset": 3448 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "2b23a5c3aab484d9", - "pos": { - "end_col": 14, - "end_line": 115, - "end_offset": 3452, - "start_col": 7, - "start_line": 115, - "start_offset": 3445 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "cc5cfe60a37135c3", - "pos": { - "end_col": 15, - "end_line": 115, - "end_offset": 3453, - "start_col": 6, - "start_line": 115, - "start_offset": 3444 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "id": "72d6c15592081cf5", - "pos": { - "end_col": 21, - "end_line": 115, - "end_offset": 3459, - "start_col": 16, - "start_line": 115, - "start_offset": 3454 - }, - "roles": [ - "Name" - ], - "token": "Equal", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "1268954ad285ae4c", - "pos": { - "end_col": 24, - "end_line": 115, - "end_offset": 3462, - "start_col": 22, - "start_line": 115, - "start_offset": 3460 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "id": "2196cd305f673c16", - "pos": { - "end_col": 29, - "end_line": 115, - "end_offset": 3467, - "start_col": 25, - "start_line": 115, - "start_offset": 3463 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "0ac87949c6816d87", - "pos": { - "end_col": 29, - "end_line": 115, - "end_offset": 3467, - "start_col": 22, - "start_line": 115, - "start_offset": 3460 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "c9521e63833dafd6", - "pos": { - "end_col": 30, - "end_line": 115, - "end_offset": 3468, - "start_col": 21, - "start_line": 115, - "start_offset": 3459 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "id": "5044cc58cea277ca", - "pos": { - "end_col": 35, - "end_line": 115, - "end_offset": 3473, - "start_col": 31, - "start_line": 115, - "start_offset": 3469 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "7e72372a0141c11e", - "pos": { - "end_col": 13, - "end_line": 116, - "end_offset": 3488, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "41f401c20ead97d0", - "pos": { - "end_col": 16, - "end_line": 116, - "end_offset": 3491, - "start_col": 14, - "start_line": 116, - "start_offset": 3489 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "id": "ce56c8ded11cc42c", - "pos": { - "end_col": 17, - "end_line": 116, - "end_offset": 3492, - "start_col": 13, - "start_line": 116, - "start_offset": 3488 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "id": "91657b28bb880295", - "pos": { - "end_col": 17, - "end_line": 116, - "end_offset": 3492, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "id": "1a3e061a27a62105", - "pos": { - "end_col": 23, - "end_line": 116, - "end_offset": 3498, - "start_col": 18, - "start_line": 116, - "start_offset": 3493 - }, - "roles": [ - "Name" - ], - "token": "Equal", - "type": "Identifier" - } - ], - "id": "a066c979665b14ef", - "pos": { - "end_col": 23, - "end_line": 116, - "end_offset": 3498, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [], - "token": "cast(s1).Equal", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "id": "986da3f6f273dd00", - "pos": { - "end_col": 28, - "end_line": 116, - "end_offset": 3503, - "start_col": 24, - "start_line": 116, - "start_offset": 3499 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "eeef72d7a783bb4e", - "pos": { - "end_col": 31, - "end_line": 116, - "end_offset": 3506, - "start_col": 29, - "start_line": 116, - "start_offset": 3504 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "id": "ce11c3f334075045", - "pos": { - "end_col": 32, - "end_line": 116, - "end_offset": 3507, - "start_col": 28, - "start_line": 116, - "start_offset": 3503 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "id": "e731c10a8bf731a1", - "pos": { - "end_col": 32, - "end_line": 116, - "end_offset": 3507, - "start_col": 24, - "start_line": 116, - "start_offset": 3499 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "id": "a218bd2c54388a08", - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 23, - "start_line": 116, - "start_offset": 3498 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "id": "5f510021f6c51db6", - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Equal(cast(s2))", - "type": "Call" - } - ], - "id": "43a88b5286062e66", - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [], - "type": "List" - } - ], - "id": "0e29378b3ae2b294", - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 2, - "start_line": 116, - "start_offset": 3477 - }, - "roles": [ - "Return" - ], - "token": "return cast(s1).Equal(cast(s2))", - "type": "Return" - } - ], - "id": "b1cedec1f15afd79", - "pos": { - "end_col": 2, - "end_line": 117, - "end_offset": 3510, - "start_col": 36, - "start_line": 115, - "start_offset": 3474 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s1).Equal(cast(s2))\n}", - "type": "Block" - } - ], - "id": "992e9e69d6f21dcd", - "pos": { - "end_col": 2, - "end_line": 117, - "end_offset": 3510, - "start_col": 1, - "start_line": 115, - "start_offset": 3439 - }, - "props": { - "name": "Equal" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "7ded7634a7b2e275", - "pos": { - "end_col": 53, - "end_line": 119, - "end_offset": 3564, - "start_col": 1, - "start_line": 119, - "start_offset": 3512 - }, - "roles": [ - "Comment" - ], - "token": "// List returns the contents as a sorted byte slice.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "894e4f18c3f18102", - "pos": { - "end_col": 8, - "end_line": 120, - "end_offset": 3572, - "start_col": 7, - "start_line": 120, - "start_offset": 3571 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "aed7d01f6a4b977a", - "pos": { - "end_col": 13, - "end_line": 120, - "end_offset": 3577, - "start_col": 9, - "start_line": 120, - "start_offset": 3573 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "e01387b47eb46c02", - "pos": { - "end_col": 13, - "end_line": 120, - "end_offset": 3577, - "start_col": 7, - "start_line": 120, - "start_offset": 3571 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "7d8d020c4a42e563", - "pos": { - "end_col": 14, - "end_line": 120, - "end_offset": 3578, - "start_col": 6, - "start_line": 120, - "start_offset": 3570 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "99acd54ecd76a7ec", - "pos": { - "end_col": 19, - "end_line": 120, - "end_offset": 3583, - "start_col": 15, - "start_line": 120, - "start_offset": 3579 - }, - "roles": [ - "Name" - ], - "token": "List", - "type": "Identifier" - }, - { - "id": "0ce7d43d402541c7", - "pos": { - "end_col": 21, - "end_line": 120, - "end_offset": 3585, - "start_col": 19, - "start_line": 120, - "start_offset": 3583 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "children": [ - { - "id": "c00024bb7cb89722", - "pos": { - "end_col": 28, - "end_line": 120, - "end_offset": 3592, - "start_col": 24, - "start_line": 120, - "start_offset": 3588 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "ebd30c720bc39eec", - "pos": { - "end_col": 28, - "end_line": 120, - "end_offset": 3592, - "start_col": 22, - "start_line": 120, - "start_offset": 3586 - }, - "roles": [], - "token": "[]byte", - "type": "Slice" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "f37fff26a93e86c0", - "pos": { - "end_col": 13, - "end_line": 121, - "end_offset": 3607, - "start_col": 9, - "start_line": 121, - "start_offset": 3603 - }, - "roles": [ - "Name" - ], - "token": "List", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "id": "dbed9be6d9d2e9fe", - "pos": { - "end_col": 18, - "end_line": 121, - "end_offset": 3612, - "start_col": 14, - "start_line": 121, - "start_offset": 3608 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "06ae1f74c6458015", - "pos": { - "end_col": 20, - "end_line": 121, - "end_offset": 3614, - "start_col": 19, - "start_line": 121, - "start_offset": 3613 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "99bb96d4e0d1411c", - "pos": { - "end_col": 21, - "end_line": 121, - "end_offset": 3615, - "start_col": 18, - "start_line": 121, - "start_offset": 3612 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "0be8b57c481cb411", - "pos": { - "end_col": 21, - "end_line": 121, - "end_offset": 3615, - "start_col": 14, - "start_line": 121, - "start_offset": 3608 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - } - ], - "id": "d75c8c781ebf5fbb", - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 13, - "start_line": 121, - "start_offset": 3607 - }, - "roles": [], - "token": "(cast(s))", - "type": "List" - } - ], - "id": "51d400699c4f58f1", - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 9, - "start_line": 121, - "start_offset": 3603 - }, - "roles": [ - "Call" - ], - "token": "List(cast(s))", - "type": "Call" - } - ], - "id": "c2142b57bb2869f5", - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 9, - "start_line": 121, - "start_offset": 3603 - }, - "roles": [], - "type": "List" - } - ], - "id": "da2644d3c5f20949", - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 2, - "start_line": 121, - "start_offset": 3596 - }, - "roles": [ - "Return" - ], - "token": "return List(cast(s))", - "type": "Return" - } - ], - "id": "94e6789c4c03ff51", - "pos": { - "end_col": 2, - "end_line": 122, - "end_offset": 3618, - "start_col": 29, - "start_line": 120, - "start_offset": 3593 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn List(cast(s))\n}", - "type": "Block" - } - ], - "id": "5a2be427cf75eb9b", - "pos": { - "end_col": 2, - "end_line": 122, - "end_offset": 3618, - "start_col": 1, - "start_line": 120, - "start_offset": 3565 - }, - "props": { - "name": "List" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "0a671eb87ba8e40e", - "pos": { - "end_col": 65, - "end_line": 124, - "end_offset": 3684, - "start_col": 1, - "start_line": 124, - "start_offset": 3620 - }, - "roles": [ - "Comment" - ], - "token": "// UnsortedList returns the slice with contents in random order.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "ccee2cb8724a13e3", - "pos": { - "end_col": 8, - "end_line": 125, - "end_offset": 3692, - "start_col": 7, - "start_line": 125, - "start_offset": 3691 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "35ff17536c048640", - "pos": { - "end_col": 13, - "end_line": 125, - "end_offset": 3697, - "start_col": 9, - "start_line": 125, - "start_offset": 3693 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "14e76f3f3ff3d025", - "pos": { - "end_col": 13, - "end_line": 125, - "end_offset": 3697, - "start_col": 7, - "start_line": 125, - "start_offset": 3691 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "4474f731ddf59be1", - "pos": { - "end_col": 14, - "end_line": 125, - "end_offset": 3698, - "start_col": 6, - "start_line": 125, - "start_offset": 3690 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "bee1a5632529d1b2", - "pos": { - "end_col": 27, - "end_line": 125, - "end_offset": 3711, - "start_col": 15, - "start_line": 125, - "start_offset": 3699 - }, - "roles": [ - "Name" - ], - "token": "UnsortedList", - "type": "Identifier" - }, - { - "id": "db96a420de8f6ce1", - "pos": { - "end_col": 29, - "end_line": 125, - "end_offset": 3713, - "start_col": 27, - "start_line": 125, - "start_offset": 3711 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "children": [ - { - "id": "9ed1fce4eac27e05", - "pos": { - "end_col": 36, - "end_line": 125, - "end_offset": 3720, - "start_col": 32, - "start_line": 125, - "start_offset": 3716 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "b18d06f5551d0d97", - "pos": { - "end_col": 36, - "end_line": 125, - "end_offset": 3720, - "start_col": 30, - "start_line": 125, - "start_offset": 3714 - }, - "roles": [], - "token": "[]byte", - "type": "Slice" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "3f48373502efafdb", - "pos": { - "end_col": 13, - "end_line": 126, - "end_offset": 3735, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "ee007c1065ddc68c", - "pos": { - "end_col": 15, - "end_line": 126, - "end_offset": 3737, - "start_col": 14, - "start_line": 126, - "start_offset": 3736 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "05c28f0dda63014d", - "pos": { - "end_col": 16, - "end_line": 126, - "end_offset": 3738, - "start_col": 13, - "start_line": 126, - "start_offset": 3735 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "330a3a632559bf74", - "pos": { - "end_col": 16, - "end_line": 126, - "end_offset": 3738, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "7f3f6eccee38f155", - "pos": { - "end_col": 29, - "end_line": 126, - "end_offset": 3751, - "start_col": 17, - "start_line": 126, - "start_offset": 3739 - }, - "roles": [ - "Name" - ], - "token": "UnsortedList", - "type": "Identifier" - } - ], - "id": "b84c66d8a9b6b116", - "pos": { - "end_col": 29, - "end_line": 126, - "end_offset": 3751, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [], - "token": "cast(s).UnsortedList", - "type": "Synthetic" - }, - { - "id": "8df8e9ef25198dde", - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 29, - "start_line": 126, - "start_offset": 3751 - }, - "roles": [], - "token": "()", - "type": "List" - } - ], - "id": "b3a87e0a0b9a2400", - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [ - "Call" - ], - "token": "cast(s).UnsortedList()", - "type": "Call" - } - ], - "id": "14edcfa644a49fbf", - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [], - "type": "List" - } - ], - "id": "0ee884f5b956127e", - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 2, - "start_line": 126, - "start_offset": 3724 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).UnsortedList()", - "type": "Return" - } - ], - "id": "8536265c2bb50d43", - "pos": { - "end_col": 2, - "end_line": 127, - "end_offset": 3755, - "start_col": 37, - "start_line": 125, - "start_offset": 3721 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).UnsortedList()\n}", - "type": "Block" - } - ], - "id": "e6010ee8086356ff", - "pos": { - "end_col": 2, - "end_line": 127, - "end_offset": 3755, - "start_col": 1, - "start_line": 125, - "start_offset": 3685 - }, - "props": { - "name": "UnsortedList" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "7a23caec3772ecbf", - "pos": { - "end_col": 49, - "end_line": 129, - "end_offset": 3805, - "start_col": 1, - "start_line": 129, - "start_offset": 3757 - }, - "roles": [ - "Comment" - ], - "token": "// PopAny returns a single element from the set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "5d3700e36997539a", - "pos": { - "end_col": 8, - "end_line": 130, - "end_offset": 3813, - "start_col": 7, - "start_line": 130, - "start_offset": 3812 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "8e75e614fef1bbb5", - "pos": { - "end_col": 13, - "end_line": 130, - "end_offset": 3818, - "start_col": 9, - "start_line": 130, - "start_offset": 3814 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "cbce14d753037c6c", - "pos": { - "end_col": 13, - "end_line": 130, - "end_offset": 3818, - "start_col": 7, - "start_line": 130, - "start_offset": 3812 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "707ba553cbb7f16a", - "pos": { - "end_col": 14, - "end_line": 130, - "end_offset": 3819, - "start_col": 6, - "start_line": 130, - "start_offset": 3811 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "cbe4b4deac9d1004", - "pos": { - "end_col": 21, - "end_line": 130, - "end_offset": 3826, - "start_col": 15, - "start_line": 130, - "start_offset": 3820 - }, - "roles": [ - "Name" - ], - "token": "PopAny", - "type": "Identifier" - }, - { - "id": "3564d9e9f316bcba", - "pos": { - "end_col": 23, - "end_line": 130, - "end_offset": 3828, - "start_col": 21, - "start_line": 130, - "start_offset": 3826 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "children": [ - { - "children": [ - { - "id": "08debe93d6019e39", - "pos": { - "end_col": 29, - "end_line": 130, - "end_offset": 3834, - "start_col": 25, - "start_line": 130, - "start_offset": 3830 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "id": "97014d488ff6ff38", - "pos": { - "end_col": 29, - "end_line": 130, - "end_offset": 3834, - "start_col": 25, - "start_line": 130, - "start_offset": 3830 - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - }, - { - "children": [ - { - "id": "ba19ddb9be2461a6", - "pos": { - "end_col": 35, - "end_line": 130, - "end_offset": 3840, - "start_col": 31, - "start_line": 130, - "start_offset": 3836 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - } - ], - "id": "a6c2ef527591a024", - "pos": { - "end_col": 35, - "end_line": 130, - "end_offset": 3840, - "start_col": 31, - "start_line": 130, - "start_offset": 3836 - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "96cacc4e22263828", - "pos": { - "end_col": 36, - "end_line": 130, - "end_offset": 3841, - "start_col": 24, - "start_line": 130, - "start_offset": 3829 - }, - "roles": [ - "Parameter" - ], - "token": "(byte, bool)", - "type": "Parameter" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "9807791644d97e90", - "pos": { - "end_col": 13, - "end_line": 131, - "end_offset": 3856, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "id": "e2ed7112e85951c9", - "pos": { - "end_col": 15, - "end_line": 131, - "end_offset": 3858, - "start_col": 14, - "start_line": 131, - "start_offset": 3857 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "4bcdb720c420ef81", - "pos": { - "end_col": 16, - "end_line": 131, - "end_offset": 3859, - "start_col": 13, - "start_line": 131, - "start_offset": 3856 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "2a0eb2ae5c290ddb", - "pos": { - "end_col": 16, - "end_line": 131, - "end_offset": 3859, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "id": "6da0109ac89b8d84", - "pos": { - "end_col": 23, - "end_line": 131, - "end_offset": 3866, - "start_col": 17, - "start_line": 131, - "start_offset": 3860 - }, - "roles": [ - "Name" - ], - "token": "PopAny", - "type": "Identifier" - } - ], - "id": "df588d361191b078", - "pos": { - "end_col": 23, - "end_line": 131, - "end_offset": 3866, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [], - "token": "cast(s).PopAny", - "type": "Synthetic" - }, - { - "id": "d064c3186b8b6233", - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 23, - "start_line": 131, - "start_offset": 3866 - }, - "roles": [], - "token": "()", - "type": "List" - } - ], - "id": "3b2b252c3946b7f3", - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [ - "Call" - ], - "token": "cast(s).PopAny()", - "type": "Call" - } - ], - "id": "dfc75d29926e9abc", - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [], - "type": "List" - } - ], - "id": "35c2922612c28afa", - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 2, - "start_line": 131, - "start_offset": 3845 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).PopAny()", - "type": "Return" - } - ], - "id": "a5bcf55af784cdc8", - "pos": { - "end_col": 2, - "end_line": 132, - "end_offset": 3870, - "start_col": 37, - "start_line": 130, - "start_offset": 3842 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).PopAny()\n}", - "type": "Block" - } - ], - "id": "a4d065a66503662b", - "pos": { - "end_col": 2, - "end_line": 132, - "end_offset": 3870, - "start_col": 1, - "start_line": 130, - "start_offset": 3806 - }, - "props": { - "name": "PopAny" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "id": "b4a3acc0e6615643", - "pos": { - "end_col": 36, - "end_line": 134, - "end_offset": 3907, - "start_col": 1, - "start_line": 134, - "start_offset": 3872 - }, - "roles": [ - "Comment" - ], - "token": "// Len returns the size of the set.", - "type": "Comment" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "c2c861b727d038e0", - "pos": { - "end_col": 8, - "end_line": 135, - "end_offset": 3915, - "start_col": 7, - "start_line": 135, - "start_offset": 3914 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "id": "c06f2d499d4f5867", - "pos": { - "end_col": 13, - "end_line": 135, - "end_offset": 3920, - "start_col": 9, - "start_line": 135, - "start_offset": 3916 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "id": "e385e1f188739ee2", - "pos": { - "end_col": 13, - "end_line": 135, - "end_offset": 3920, - "start_col": 7, - "start_line": 135, - "start_offset": 3914 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "id": "1decc57dbeb6f820", - "pos": { - "end_col": 14, - "end_line": 135, - "end_offset": 3921, - "start_col": 6, - "start_line": 135, - "start_offset": 3913 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "id": "d573070f425061d7", - "pos": { - "end_col": 18, - "end_line": 135, - "end_offset": 3925, - "start_col": 15, - "start_line": 135, - "start_offset": 3922 - }, - "roles": [ - "Name" - ], - "token": "Len", - "type": "Identifier" - }, - { - "id": "5c94a9afe2e823bd", - "pos": { - "end_col": 20, - "end_line": 135, - "end_offset": 3927, - "start_col": 18, - "start_line": 135, - "start_offset": 3925 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "id": "3ad6c7bea8e50732", - "pos": { - "end_col": 24, - "end_line": 135, - "end_offset": 3931, - "start_col": 21, - "start_line": 135, - "start_offset": 3928 - }, - "roles": [ - "Name", - "Type" - ], - "token": "int", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "id": "1c042e022768b030", - "pos": { - "end_col": 12, - "end_line": 136, - "end_offset": 3945, - "start_col": 9, - "start_line": 136, - "start_offset": 3942 - }, - "roles": [ - "Name" - ], - "token": "len", - "type": "Identifier" - }, - { - "children": [ - { - "id": "9b2ccb6fc11a947b", - "pos": { - "end_col": 14, - "end_line": 136, - "end_offset": 3947, - "start_col": 13, - "start_line": 136, - "start_offset": 3946 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "id": "815cc0e15f13935e", - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 12, - "start_line": 136, - "start_offset": 3945 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "id": "b48e7a29a53afb6a", - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 9, - "start_line": 136, - "start_offset": 3942 - }, - "roles": [ - "Call" - ], - "token": "len(s)", - "type": "Call" - } - ], - "id": "1391676fec268947", - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 9, - "start_line": 136, - "start_offset": 3942 - }, - "roles": [], - "type": "List" - } - ], - "id": "ae6cbfc7c969b6a6", - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 2, - "start_line": 136, - "start_offset": 3935 - }, - "roles": [ - "Return" - ], - "token": "return len(s)", - "type": "Return" - } - ], - "id": "c1b23ed5b83bb5c8", - "pos": { - "end_col": 2, - "end_line": 137, - "end_offset": 3950, - "start_col": 25, - "start_line": 135, - "start_offset": 3932 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn len(s)\n}", - "type": "Block" - } - ], - "id": "624a22df526907e7", - "pos": { - "end_col": 2, - "end_line": 137, - "end_offset": 3950, - "start_col": 1, - "start_line": 135, - "start_offset": 3908 - }, - "props": { - "name": "Len" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - } - ], - "id": "c2b43435dadfc320", - "pos": { - "end_col": 1, - "end_line": 138, - "end_offset": 3951, - "start_col": 1, - "start_line": 1, - "start_offset": 0 - }, - "roles": [], - "type": "File" -} diff --git a/tests/golden/uast/parse.tree b/tests/golden/uast/parse.tree deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/golden/uast/query.compact b/tests/golden/uast/query.compact deleted file mode 100644 index 0edf7aff..00000000 --- a/tests/golden/uast/query.compact +++ /dev/null @@ -1 +0,0 @@ -{"results":[{"children":[{"pos":{"end_col":13,"end_line":28,"end_offset":863,"start_col":6,"start_line":28,"start_offset":856},"roles":["Name"],"token":"NewByte","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":19,"end_line":28,"end_offset":869,"start_col":14,"start_line":28,"start_offset":864},"roles":["Name"],"token":"items","type":"Identifier"},{"pos":{"end_col":27,"end_line":28,"end_offset":877,"start_col":23,"start_line":28,"start_offset":873},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":27,"end_line":28,"end_offset":877,"start_col":14,"start_line":28,"start_offset":864},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":28,"end_line":28,"end_offset":878,"start_col":13,"start_line":28,"start_offset":863},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"pos":{"end_col":33,"end_line":28,"end_offset":883,"start_col":29,"start_line":28,"start_offset":879},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":29,"end_offset":898,"start_col":9,"start_line":29,"start_offset":894},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":17,"end_line":29,"end_offset":902,"start_col":14,"start_line":29,"start_offset":899},"roles":["Name"],"token":"New","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":22,"end_line":29,"end_offset":907,"start_col":18,"start_line":29,"start_offset":903},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":22,"end_line":29,"end_offset":907,"start_col":18,"start_line":29,"start_offset":903},"roles":[],"type":"Synthetic"}],"pos":{"end_col":23,"end_line":29,"end_offset":908,"start_col":17,"start_line":29,"start_offset":902},"roles":[],"token":"[byte]","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":29,"end_line":29,"end_offset":914,"start_col":24,"start_line":29,"start_offset":909},"roles":["Name"],"token":"items","type":"Identifier"}],"pos":{"end_col":32,"end_line":29,"end_offset":917,"start_col":24,"start_line":29,"start_offset":909},"roles":[],"token":"items...","type":"List"}],"pos":{"end_col":33,"end_line":29,"end_offset":918,"start_col":23,"start_line":29,"start_offset":908},"roles":[],"token":"(items...)","type":"List"}],"pos":{"end_col":33,"end_line":29,"end_offset":918,"start_col":14,"start_line":29,"start_offset":899},"roles":["Call"],"token":"New[byte](items...)","type":"Call"}],"pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":13,"start_line":29,"start_offset":898},"roles":[],"token":"(New[byte](items...))","type":"List"}],"pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":9,"start_line":29,"start_offset":894},"roles":["Call"],"token":"Byte(New[byte](items...))","type":"Call"}],"pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":9,"start_line":29,"start_offset":894},"roles":[],"type":"List"}],"pos":{"end_col":34,"end_line":29,"end_offset":919,"start_col":2,"start_line":29,"start_offset":887},"roles":["Return"],"token":"return Byte(New[byte](items...))","type":"Return"}],"pos":{"end_col":2,"end_line":30,"end_offset":921,"start_col":34,"start_line":28,"start_offset":884},"roles":["Body"],"token":"{\n\treturn Byte(New[byte](items...))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":30,"end_offset":921,"start_col":1,"start_line":28,"start_offset":851},"props":{"name":"NewByte"},"roles":["Function","Declaration"],"token":"func NewByte(items ...byte) Byte {\n\treturn Byte(New[byte](items...))\n}","type":"Function"},{"children":[{"pos":{"end_col":16,"end_line":34,"end_offset":1084,"start_col":6,"start_line":34,"start_offset":1074},"roles":["Name"],"token":"ByteKeySet","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":18,"end_line":34,"end_offset":1086,"start_col":17,"start_line":34,"start_offset":1085},"roles":["Name"],"token":"T","type":"Identifier"},{"children":[{"pos":{"end_col":22,"end_line":34,"end_offset":1090,"start_col":19,"start_line":34,"start_offset":1087},"roles":["Name","Type"],"token":"any","type":"Identifier"}],"pos":{"end_col":22,"end_line":34,"end_offset":1090,"start_col":19,"start_line":34,"start_offset":1087},"roles":[],"type":"Synthetic"}],"pos":{"end_col":22,"end_line":34,"end_offset":1090,"start_col":17,"start_line":34,"start_offset":1085},"props":{"name":"T"},"roles":["Parameter"],"token":"T any","type":"Parameter"}],"pos":{"end_col":23,"end_line":34,"end_offset":1091,"start_col":16,"start_line":34,"start_offset":1084},"roles":["Parameter"],"type":"Parameter"},{"children":[{"children":[{"pos":{"end_col":30,"end_line":34,"end_offset":1098,"start_col":24,"start_line":34,"start_offset":1092},"roles":["Name"],"token":"theMap","type":"Identifier"},{"children":[{"pos":{"end_col":39,"end_line":34,"end_offset":1107,"start_col":35,"start_line":34,"start_offset":1103},"roles":["Name","Type"],"token":"byte","type":"Identifier"},{"pos":{"end_col":41,"end_line":34,"end_offset":1109,"start_col":40,"start_line":34,"start_offset":1108},"roles":["Name","Type"],"token":"T","type":"Identifier"}],"pos":{"end_col":41,"end_line":34,"end_offset":1109,"start_col":31,"start_line":34,"start_offset":1099},"roles":[],"token":"map[byte]T","type":"Synthetic"}],"pos":{"end_col":41,"end_line":34,"end_offset":1109,"start_col":24,"start_line":34,"start_offset":1092},"props":{"name":"theMap"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":42,"end_line":34,"end_offset":1110,"start_col":23,"start_line":34,"start_offset":1091},"roles":["Parameter"],"token":"(theMap map[byte]T)","type":"Parameter"},{"pos":{"end_col":47,"end_line":34,"end_offset":1115,"start_col":43,"start_line":34,"start_offset":1111},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":35,"end_offset":1130,"start_col":9,"start_line":35,"start_offset":1126},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":20,"end_line":35,"end_offset":1137,"start_col":14,"start_line":35,"start_offset":1131},"roles":["Name"],"token":"KeySet","type":"Identifier"},{"children":[{"pos":{"end_col":27,"end_line":35,"end_offset":1144,"start_col":21,"start_line":35,"start_offset":1138},"roles":["Name"],"token":"theMap","type":"Identifier"}],"pos":{"end_col":28,"end_line":35,"end_offset":1145,"start_col":20,"start_line":35,"start_offset":1137},"roles":[],"token":"(theMap)","type":"List"}],"pos":{"end_col":28,"end_line":35,"end_offset":1145,"start_col":14,"start_line":35,"start_offset":1131},"roles":["Call"],"token":"KeySet(theMap)","type":"Call"}],"pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":13,"start_line":35,"start_offset":1130},"roles":[],"token":"(KeySet(theMap))","type":"List"}],"pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":9,"start_line":35,"start_offset":1126},"roles":["Call"],"token":"Byte(KeySet(theMap))","type":"Call"}],"pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":9,"start_line":35,"start_offset":1126},"roles":[],"type":"List"}],"pos":{"end_col":29,"end_line":35,"end_offset":1146,"start_col":2,"start_line":35,"start_offset":1119},"roles":["Return"],"token":"return Byte(KeySet(theMap))","type":"Return"}],"pos":{"end_col":2,"end_line":36,"end_offset":1148,"start_col":48,"start_line":34,"start_offset":1116},"roles":["Body"],"token":"{\n\treturn Byte(KeySet(theMap))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":36,"end_offset":1148,"start_col":1,"start_line":34,"start_offset":1069},"props":{"name":"ByteKeySet"},"roles":["Function","Declaration"],"token":"func ByteKeySet[T any](theMap map[byte]T) Byte {\n\treturn Byte(KeySet(theMap))\n}","type":"Function"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":39,"end_offset":1190,"start_col":7,"start_line":39,"start_offset":1189},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":39,"end_offset":1195,"start_col":9,"start_line":39,"start_offset":1191},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":39,"end_offset":1195,"start_col":7,"start_line":39,"start_offset":1189},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":39,"end_offset":1196,"start_col":6,"start_line":39,"start_offset":1188},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":21,"end_line":39,"end_offset":1203,"start_col":15,"start_line":39,"start_offset":1197},"roles":["Name"],"token":"Insert","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":27,"end_line":39,"end_offset":1209,"start_col":22,"start_line":39,"start_offset":1204},"roles":["Name"],"token":"items","type":"Identifier"},{"pos":{"end_col":35,"end_line":39,"end_offset":1217,"start_col":31,"start_line":39,"start_offset":1213},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":35,"end_line":39,"end_offset":1217,"start_col":22,"start_line":39,"start_offset":1204},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":36,"end_line":39,"end_offset":1218,"start_col":21,"start_line":39,"start_offset":1203},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"pos":{"end_col":41,"end_line":39,"end_offset":1223,"start_col":37,"start_line":39,"start_offset":1219},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":40,"end_offset":1238,"start_col":9,"start_line":40,"start_offset":1234},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":18,"end_line":40,"end_offset":1243,"start_col":14,"start_line":40,"start_offset":1239},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":20,"end_line":40,"end_offset":1245,"start_col":19,"start_line":40,"start_offset":1244},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":21,"end_line":40,"end_offset":1246,"start_col":18,"start_line":40,"start_offset":1243},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":21,"end_line":40,"end_offset":1246,"start_col":14,"start_line":40,"start_offset":1239},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":28,"end_line":40,"end_offset":1253,"start_col":22,"start_line":40,"start_offset":1247},"roles":["Name"],"token":"Insert","type":"Identifier"}],"pos":{"end_col":28,"end_line":40,"end_offset":1253,"start_col":14,"start_line":40,"start_offset":1239},"roles":[],"token":"cast(s).Insert","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":34,"end_line":40,"end_offset":1259,"start_col":29,"start_line":40,"start_offset":1254},"roles":["Name"],"token":"items","type":"Identifier"}],"pos":{"end_col":37,"end_line":40,"end_offset":1262,"start_col":29,"start_line":40,"start_offset":1254},"roles":[],"token":"items...","type":"List"}],"pos":{"end_col":38,"end_line":40,"end_offset":1263,"start_col":28,"start_line":40,"start_offset":1253},"roles":[],"token":"(items...)","type":"List"}],"pos":{"end_col":38,"end_line":40,"end_offset":1263,"start_col":14,"start_line":40,"start_offset":1239},"roles":["Call"],"token":"cast(s).Insert(items...)","type":"Call"}],"pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":13,"start_line":40,"start_offset":1238},"roles":[],"token":"(cast(s).Insert(items...))","type":"List"}],"pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":9,"start_line":40,"start_offset":1234},"roles":["Call"],"token":"Byte(cast(s).Insert(items...))","type":"Call"}],"pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":9,"start_line":40,"start_offset":1234},"roles":[],"type":"List"}],"pos":{"end_col":39,"end_line":40,"end_offset":1264,"start_col":2,"start_line":40,"start_offset":1227},"roles":["Return"],"token":"return Byte(cast(s).Insert(items...))","type":"Return"}],"pos":{"end_col":2,"end_line":41,"end_offset":1266,"start_col":42,"start_line":39,"start_offset":1224},"roles":["Body"],"token":"{\n\treturn Byte(cast(s).Insert(items...))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":41,"end_offset":1266,"start_col":1,"start_line":39,"start_offset":1183},"props":{"name":"Insert"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":44,"end_offset":1317,"start_col":7,"start_line":44,"start_offset":1316},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":44,"end_offset":1322,"start_col":9,"start_line":44,"start_offset":1318},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":44,"end_offset":1322,"start_col":7,"start_line":44,"start_offset":1316},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":44,"end_offset":1323,"start_col":6,"start_line":44,"start_offset":1315},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":21,"end_line":44,"end_offset":1330,"start_col":15,"start_line":44,"start_offset":1324},"roles":["Name"],"token":"Delete","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":27,"end_line":44,"end_offset":1336,"start_col":22,"start_line":44,"start_offset":1331},"roles":["Name"],"token":"items","type":"Identifier"},{"pos":{"end_col":35,"end_line":44,"end_offset":1344,"start_col":31,"start_line":44,"start_offset":1340},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":35,"end_line":44,"end_offset":1344,"start_col":22,"start_line":44,"start_offset":1331},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":36,"end_line":44,"end_offset":1345,"start_col":21,"start_line":44,"start_offset":1330},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"pos":{"end_col":41,"end_line":44,"end_offset":1350,"start_col":37,"start_line":44,"start_offset":1346},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":45,"end_offset":1365,"start_col":9,"start_line":45,"start_offset":1361},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":18,"end_line":45,"end_offset":1370,"start_col":14,"start_line":45,"start_offset":1366},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":20,"end_line":45,"end_offset":1372,"start_col":19,"start_line":45,"start_offset":1371},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":21,"end_line":45,"end_offset":1373,"start_col":18,"start_line":45,"start_offset":1370},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":21,"end_line":45,"end_offset":1373,"start_col":14,"start_line":45,"start_offset":1366},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":28,"end_line":45,"end_offset":1380,"start_col":22,"start_line":45,"start_offset":1374},"roles":["Name"],"token":"Delete","type":"Identifier"}],"pos":{"end_col":28,"end_line":45,"end_offset":1380,"start_col":14,"start_line":45,"start_offset":1366},"roles":[],"token":"cast(s).Delete","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":34,"end_line":45,"end_offset":1386,"start_col":29,"start_line":45,"start_offset":1381},"roles":["Name"],"token":"items","type":"Identifier"}],"pos":{"end_col":37,"end_line":45,"end_offset":1389,"start_col":29,"start_line":45,"start_offset":1381},"roles":[],"token":"items...","type":"List"}],"pos":{"end_col":38,"end_line":45,"end_offset":1390,"start_col":28,"start_line":45,"start_offset":1380},"roles":[],"token":"(items...)","type":"List"}],"pos":{"end_col":38,"end_line":45,"end_offset":1390,"start_col":14,"start_line":45,"start_offset":1366},"roles":["Call"],"token":"cast(s).Delete(items...)","type":"Call"}],"pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":13,"start_line":45,"start_offset":1365},"roles":[],"token":"(cast(s).Delete(items...))","type":"List"}],"pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":9,"start_line":45,"start_offset":1361},"roles":["Call"],"token":"Byte(cast(s).Delete(items...))","type":"Call"}],"pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":9,"start_line":45,"start_offset":1361},"roles":[],"type":"List"}],"pos":{"end_col":39,"end_line":45,"end_offset":1391,"start_col":2,"start_line":45,"start_offset":1354},"roles":["Return"],"token":"return Byte(cast(s).Delete(items...))","type":"Return"}],"pos":{"end_col":2,"end_line":46,"end_offset":1393,"start_col":42,"start_line":44,"start_offset":1351},"roles":["Body"],"token":"{\n\treturn Byte(cast(s).Delete(items...))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":46,"end_offset":1393,"start_col":1,"start_line":44,"start_offset":1310},"props":{"name":"Delete"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":49,"end_offset":1467,"start_col":7,"start_line":49,"start_offset":1466},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":49,"end_offset":1472,"start_col":9,"start_line":49,"start_offset":1468},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":49,"end_offset":1472,"start_col":7,"start_line":49,"start_offset":1466},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":49,"end_offset":1473,"start_col":6,"start_line":49,"start_offset":1465},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":18,"end_line":49,"end_offset":1477,"start_col":15,"start_line":49,"start_offset":1474},"roles":["Name"],"token":"Has","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":23,"end_line":49,"end_offset":1482,"start_col":19,"start_line":49,"start_offset":1478},"roles":["Name"],"token":"item","type":"Identifier"},{"pos":{"end_col":28,"end_line":49,"end_offset":1487,"start_col":24,"start_line":49,"start_offset":1483},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":28,"end_line":49,"end_offset":1487,"start_col":19,"start_line":49,"start_offset":1478},"props":{"name":"item"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":29,"end_line":49,"end_offset":1488,"start_col":18,"start_line":49,"start_offset":1477},"roles":["Parameter"],"token":"(item byte)","type":"Parameter"},{"pos":{"end_col":34,"end_line":49,"end_offset":1493,"start_col":30,"start_line":49,"start_offset":1489},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":50,"end_offset":1508,"start_col":9,"start_line":50,"start_offset":1504},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":15,"end_line":50,"end_offset":1510,"start_col":14,"start_line":50,"start_offset":1509},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":16,"end_line":50,"end_offset":1511,"start_col":13,"start_line":50,"start_offset":1508},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":16,"end_line":50,"end_offset":1511,"start_col":9,"start_line":50,"start_offset":1504},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":20,"end_line":50,"end_offset":1515,"start_col":17,"start_line":50,"start_offset":1512},"roles":["Name"],"token":"Has","type":"Identifier"}],"pos":{"end_col":20,"end_line":50,"end_offset":1515,"start_col":9,"start_line":50,"start_offset":1504},"roles":[],"token":"cast(s).Has","type":"Synthetic"},{"children":[{"pos":{"end_col":25,"end_line":50,"end_offset":1520,"start_col":21,"start_line":50,"start_offset":1516},"roles":["Name"],"token":"item","type":"Identifier"}],"pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":20,"start_line":50,"start_offset":1515},"roles":[],"token":"(item)","type":"List"}],"pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":9,"start_line":50,"start_offset":1504},"roles":["Call"],"token":"cast(s).Has(item)","type":"Call"}],"pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":9,"start_line":50,"start_offset":1504},"roles":[],"type":"List"}],"pos":{"end_col":26,"end_line":50,"end_offset":1521,"start_col":2,"start_line":50,"start_offset":1497},"roles":["Return"],"token":"return cast(s).Has(item)","type":"Return"}],"pos":{"end_col":2,"end_line":51,"end_offset":1523,"start_col":35,"start_line":49,"start_offset":1494},"roles":["Body"],"token":"{\n\treturn cast(s).Has(item)\n}","type":"Block"}],"pos":{"end_col":2,"end_line":51,"end_offset":1523,"start_col":1,"start_line":49,"start_offset":1460},"props":{"name":"Has"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":54,"end_offset":1606,"start_col":7,"start_line":54,"start_offset":1605},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":54,"end_offset":1611,"start_col":9,"start_line":54,"start_offset":1607},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":54,"end_offset":1611,"start_col":7,"start_line":54,"start_offset":1605},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":54,"end_offset":1612,"start_col":6,"start_line":54,"start_offset":1604},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":21,"end_line":54,"end_offset":1619,"start_col":15,"start_line":54,"start_offset":1613},"roles":["Name"],"token":"HasAll","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":27,"end_line":54,"end_offset":1625,"start_col":22,"start_line":54,"start_offset":1620},"roles":["Name"],"token":"items","type":"Identifier"},{"pos":{"end_col":35,"end_line":54,"end_offset":1633,"start_col":31,"start_line":54,"start_offset":1629},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":35,"end_line":54,"end_offset":1633,"start_col":22,"start_line":54,"start_offset":1620},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":36,"end_line":54,"end_offset":1634,"start_col":21,"start_line":54,"start_offset":1619},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"pos":{"end_col":41,"end_line":54,"end_offset":1639,"start_col":37,"start_line":54,"start_offset":1635},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":55,"end_offset":1654,"start_col":9,"start_line":55,"start_offset":1650},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":15,"end_line":55,"end_offset":1656,"start_col":14,"start_line":55,"start_offset":1655},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":16,"end_line":55,"end_offset":1657,"start_col":13,"start_line":55,"start_offset":1654},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":16,"end_line":55,"end_offset":1657,"start_col":9,"start_line":55,"start_offset":1650},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":23,"end_line":55,"end_offset":1664,"start_col":17,"start_line":55,"start_offset":1658},"roles":["Name"],"token":"HasAll","type":"Identifier"}],"pos":{"end_col":23,"end_line":55,"end_offset":1664,"start_col":9,"start_line":55,"start_offset":1650},"roles":[],"token":"cast(s).HasAll","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":29,"end_line":55,"end_offset":1670,"start_col":24,"start_line":55,"start_offset":1665},"roles":["Name"],"token":"items","type":"Identifier"}],"pos":{"end_col":32,"end_line":55,"end_offset":1673,"start_col":24,"start_line":55,"start_offset":1665},"roles":[],"token":"items...","type":"List"}],"pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":23,"start_line":55,"start_offset":1664},"roles":[],"token":"(items...)","type":"List"}],"pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":9,"start_line":55,"start_offset":1650},"roles":["Call"],"token":"cast(s).HasAll(items...)","type":"Call"}],"pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":9,"start_line":55,"start_offset":1650},"roles":[],"type":"List"}],"pos":{"end_col":33,"end_line":55,"end_offset":1674,"start_col":2,"start_line":55,"start_offset":1643},"roles":["Return"],"token":"return cast(s).HasAll(items...)","type":"Return"}],"pos":{"end_col":2,"end_line":56,"end_offset":1676,"start_col":42,"start_line":54,"start_offset":1640},"roles":["Body"],"token":"{\n\treturn cast(s).HasAll(items...)\n}","type":"Block"}],"pos":{"end_col":2,"end_line":56,"end_offset":1676,"start_col":1,"start_line":54,"start_offset":1599},"props":{"name":"HasAll"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":59,"end_offset":1747,"start_col":7,"start_line":59,"start_offset":1746},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":59,"end_offset":1752,"start_col":9,"start_line":59,"start_offset":1748},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":59,"end_offset":1752,"start_col":7,"start_line":59,"start_offset":1746},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":59,"end_offset":1753,"start_col":6,"start_line":59,"start_offset":1745},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":21,"end_line":59,"end_offset":1760,"start_col":15,"start_line":59,"start_offset":1754},"roles":["Name"],"token":"HasAny","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":27,"end_line":59,"end_offset":1766,"start_col":22,"start_line":59,"start_offset":1761},"roles":["Name"],"token":"items","type":"Identifier"},{"pos":{"end_col":35,"end_line":59,"end_offset":1774,"start_col":31,"start_line":59,"start_offset":1770},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":35,"end_line":59,"end_offset":1774,"start_col":22,"start_line":59,"start_offset":1761},"props":{"name":"items"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":36,"end_line":59,"end_offset":1775,"start_col":21,"start_line":59,"start_offset":1760},"roles":["Parameter"],"token":"(items ...byte)","type":"Parameter"},{"pos":{"end_col":41,"end_line":59,"end_offset":1780,"start_col":37,"start_line":59,"start_offset":1776},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":60,"end_offset":1795,"start_col":9,"start_line":60,"start_offset":1791},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":15,"end_line":60,"end_offset":1797,"start_col":14,"start_line":60,"start_offset":1796},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":16,"end_line":60,"end_offset":1798,"start_col":13,"start_line":60,"start_offset":1795},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":16,"end_line":60,"end_offset":1798,"start_col":9,"start_line":60,"start_offset":1791},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":23,"end_line":60,"end_offset":1805,"start_col":17,"start_line":60,"start_offset":1799},"roles":["Name"],"token":"HasAny","type":"Identifier"}],"pos":{"end_col":23,"end_line":60,"end_offset":1805,"start_col":9,"start_line":60,"start_offset":1791},"roles":[],"token":"cast(s).HasAny","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":29,"end_line":60,"end_offset":1811,"start_col":24,"start_line":60,"start_offset":1806},"roles":["Name"],"token":"items","type":"Identifier"}],"pos":{"end_col":32,"end_line":60,"end_offset":1814,"start_col":24,"start_line":60,"start_offset":1806},"roles":[],"token":"items...","type":"List"}],"pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":23,"start_line":60,"start_offset":1805},"roles":[],"token":"(items...)","type":"List"}],"pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":9,"start_line":60,"start_offset":1791},"roles":["Call"],"token":"cast(s).HasAny(items...)","type":"Call"}],"pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":9,"start_line":60,"start_offset":1791},"roles":[],"type":"List"}],"pos":{"end_col":33,"end_line":60,"end_offset":1815,"start_col":2,"start_line":60,"start_offset":1784},"roles":["Return"],"token":"return cast(s).HasAny(items...)","type":"Return"}],"pos":{"end_col":2,"end_line":61,"end_offset":1817,"start_col":42,"start_line":59,"start_offset":1781},"roles":["Body"],"token":"{\n\treturn cast(s).HasAny(items...)\n}","type":"Block"}],"pos":{"end_col":2,"end_line":61,"end_offset":1817,"start_col":1,"start_line":59,"start_offset":1740},"props":{"name":"HasAny"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":64,"end_offset":1889,"start_col":7,"start_line":64,"start_offset":1888},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":64,"end_offset":1894,"start_col":9,"start_line":64,"start_offset":1890},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":64,"end_offset":1894,"start_col":7,"start_line":64,"start_offset":1888},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":64,"end_offset":1895,"start_col":6,"start_line":64,"start_offset":1887},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":20,"end_line":64,"end_offset":1901,"start_col":15,"start_line":64,"start_offset":1896},"roles":["Name"],"token":"Clone","type":"Identifier"},{"pos":{"end_col":22,"end_line":64,"end_offset":1903,"start_col":20,"start_line":64,"start_offset":1901},"roles":["Parameter"],"token":"()","type":"Parameter"},{"pos":{"end_col":27,"end_line":64,"end_offset":1908,"start_col":23,"start_line":64,"start_offset":1904},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":65,"end_offset":1923,"start_col":9,"start_line":65,"start_offset":1919},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":18,"end_line":65,"end_offset":1928,"start_col":14,"start_line":65,"start_offset":1924},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":20,"end_line":65,"end_offset":1930,"start_col":19,"start_line":65,"start_offset":1929},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":21,"end_line":65,"end_offset":1931,"start_col":18,"start_line":65,"start_offset":1928},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":21,"end_line":65,"end_offset":1931,"start_col":14,"start_line":65,"start_offset":1924},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":27,"end_line":65,"end_offset":1937,"start_col":22,"start_line":65,"start_offset":1932},"roles":["Name"],"token":"Clone","type":"Identifier"}],"pos":{"end_col":27,"end_line":65,"end_offset":1937,"start_col":14,"start_line":65,"start_offset":1924},"roles":[],"token":"cast(s).Clone","type":"Synthetic"},{"pos":{"end_col":29,"end_line":65,"end_offset":1939,"start_col":27,"start_line":65,"start_offset":1937},"roles":[],"token":"()","type":"List"}],"pos":{"end_col":29,"end_line":65,"end_offset":1939,"start_col":14,"start_line":65,"start_offset":1924},"roles":["Call"],"token":"cast(s).Clone()","type":"Call"}],"pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":13,"start_line":65,"start_offset":1923},"roles":[],"token":"(cast(s).Clone())","type":"List"}],"pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":9,"start_line":65,"start_offset":1919},"roles":["Call"],"token":"Byte(cast(s).Clone())","type":"Call"}],"pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":9,"start_line":65,"start_offset":1919},"roles":[],"type":"List"}],"pos":{"end_col":30,"end_line":65,"end_offset":1940,"start_col":2,"start_line":65,"start_offset":1912},"roles":["Return"],"token":"return Byte(cast(s).Clone())","type":"Return"}],"pos":{"end_col":2,"end_line":66,"end_offset":1942,"start_col":28,"start_line":64,"start_offset":1909},"roles":["Body"],"token":"{\n\treturn Byte(cast(s).Clone())\n}","type":"Block"}],"pos":{"end_col":2,"end_line":66,"end_offset":1942,"start_col":1,"start_line":64,"start_offset":1882},"props":{"name":"Clone"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":9,"end_line":74,"end_offset":2133,"start_col":7,"start_line":74,"start_offset":2131},"roles":["Name"],"token":"s1","type":"Identifier"},{"pos":{"end_col":14,"end_line":74,"end_offset":2138,"start_col":10,"start_line":74,"start_offset":2134},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":14,"end_line":74,"end_offset":2138,"start_col":7,"start_line":74,"start_offset":2131},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":15,"end_line":74,"end_offset":2139,"start_col":6,"start_line":74,"start_offset":2130},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"pos":{"end_col":26,"end_line":74,"end_offset":2150,"start_col":16,"start_line":74,"start_offset":2140},"roles":["Name"],"token":"Difference","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":29,"end_line":74,"end_offset":2153,"start_col":27,"start_line":74,"start_offset":2151},"roles":["Name"],"token":"s2","type":"Identifier"},{"pos":{"end_col":34,"end_line":74,"end_offset":2158,"start_col":30,"start_line":74,"start_offset":2154},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":34,"end_line":74,"end_offset":2158,"start_col":27,"start_line":74,"start_offset":2151},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":35,"end_line":74,"end_offset":2159,"start_col":26,"start_line":74,"start_offset":2150},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"pos":{"end_col":40,"end_line":74,"end_offset":2164,"start_col":36,"start_line":74,"start_offset":2160},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":75,"end_offset":2179,"start_col":9,"start_line":75,"start_offset":2175},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":18,"end_line":75,"end_offset":2184,"start_col":14,"start_line":75,"start_offset":2180},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":21,"end_line":75,"end_offset":2187,"start_col":19,"start_line":75,"start_offset":2185},"roles":["Name"],"token":"s1","type":"Identifier"}],"pos":{"end_col":22,"end_line":75,"end_offset":2188,"start_col":18,"start_line":75,"start_offset":2184},"roles":[],"token":"(s1)","type":"List"}],"pos":{"end_col":22,"end_line":75,"end_offset":2188,"start_col":14,"start_line":75,"start_offset":2180},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"pos":{"end_col":33,"end_line":75,"end_offset":2199,"start_col":23,"start_line":75,"start_offset":2189},"roles":["Name"],"token":"Difference","type":"Identifier"}],"pos":{"end_col":33,"end_line":75,"end_offset":2199,"start_col":14,"start_line":75,"start_offset":2180},"roles":[],"token":"cast(s1).Difference","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":38,"end_line":75,"end_offset":2204,"start_col":34,"start_line":75,"start_offset":2200},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":41,"end_line":75,"end_offset":2207,"start_col":39,"start_line":75,"start_offset":2205},"roles":["Name"],"token":"s2","type":"Identifier"}],"pos":{"end_col":42,"end_line":75,"end_offset":2208,"start_col":38,"start_line":75,"start_offset":2204},"roles":[],"token":"(s2)","type":"List"}],"pos":{"end_col":42,"end_line":75,"end_offset":2208,"start_col":34,"start_line":75,"start_offset":2200},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"pos":{"end_col":43,"end_line":75,"end_offset":2209,"start_col":33,"start_line":75,"start_offset":2199},"roles":[],"token":"(cast(s2))","type":"List"}],"pos":{"end_col":43,"end_line":75,"end_offset":2209,"start_col":14,"start_line":75,"start_offset":2180},"roles":["Call"],"token":"cast(s1).Difference(cast(s2))","type":"Call"}],"pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":13,"start_line":75,"start_offset":2179},"roles":[],"token":"(cast(s1).Difference(cast(s2)))","type":"List"}],"pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":9,"start_line":75,"start_offset":2175},"roles":["Call"],"token":"Byte(cast(s1).Difference(cast(s2)))","type":"Call"}],"pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":9,"start_line":75,"start_offset":2175},"roles":[],"type":"List"}],"pos":{"end_col":44,"end_line":75,"end_offset":2210,"start_col":2,"start_line":75,"start_offset":2168},"roles":["Return"],"token":"return Byte(cast(s1).Difference(cast(s2)))","type":"Return"}],"pos":{"end_col":2,"end_line":76,"end_offset":2212,"start_col":41,"start_line":74,"start_offset":2165},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).Difference(cast(s2)))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":76,"end_offset":2212,"start_col":1,"start_line":74,"start_offset":2125},"props":{"name":"Difference"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":9,"end_line":84,"end_offset":2487,"start_col":7,"start_line":84,"start_offset":2485},"roles":["Name"],"token":"s1","type":"Identifier"},{"pos":{"end_col":14,"end_line":84,"end_offset":2492,"start_col":10,"start_line":84,"start_offset":2488},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":14,"end_line":84,"end_offset":2492,"start_col":7,"start_line":84,"start_offset":2485},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":15,"end_line":84,"end_offset":2493,"start_col":6,"start_line":84,"start_offset":2484},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"pos":{"end_col":35,"end_line":84,"end_offset":2513,"start_col":16,"start_line":84,"start_offset":2494},"roles":["Name"],"token":"SymmetricDifference","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":38,"end_line":84,"end_offset":2516,"start_col":36,"start_line":84,"start_offset":2514},"roles":["Name"],"token":"s2","type":"Identifier"},{"pos":{"end_col":43,"end_line":84,"end_offset":2521,"start_col":39,"start_line":84,"start_offset":2517},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":43,"end_line":84,"end_offset":2521,"start_col":36,"start_line":84,"start_offset":2514},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":44,"end_line":84,"end_offset":2522,"start_col":35,"start_line":84,"start_offset":2513},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"pos":{"end_col":49,"end_line":84,"end_offset":2527,"start_col":45,"start_line":84,"start_offset":2523},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":85,"end_offset":2542,"start_col":9,"start_line":85,"start_offset":2538},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":18,"end_line":85,"end_offset":2547,"start_col":14,"start_line":85,"start_offset":2543},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":21,"end_line":85,"end_offset":2550,"start_col":19,"start_line":85,"start_offset":2548},"roles":["Name"],"token":"s1","type":"Identifier"}],"pos":{"end_col":22,"end_line":85,"end_offset":2551,"start_col":18,"start_line":85,"start_offset":2547},"roles":[],"token":"(s1)","type":"List"}],"pos":{"end_col":22,"end_line":85,"end_offset":2551,"start_col":14,"start_line":85,"start_offset":2543},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"pos":{"end_col":42,"end_line":85,"end_offset":2571,"start_col":23,"start_line":85,"start_offset":2552},"roles":["Name"],"token":"SymmetricDifference","type":"Identifier"}],"pos":{"end_col":42,"end_line":85,"end_offset":2571,"start_col":14,"start_line":85,"start_offset":2543},"roles":[],"token":"cast(s1).SymmetricDifference","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":47,"end_line":85,"end_offset":2576,"start_col":43,"start_line":85,"start_offset":2572},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":50,"end_line":85,"end_offset":2579,"start_col":48,"start_line":85,"start_offset":2577},"roles":["Name"],"token":"s2","type":"Identifier"}],"pos":{"end_col":51,"end_line":85,"end_offset":2580,"start_col":47,"start_line":85,"start_offset":2576},"roles":[],"token":"(s2)","type":"List"}],"pos":{"end_col":51,"end_line":85,"end_offset":2580,"start_col":43,"start_line":85,"start_offset":2572},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"pos":{"end_col":52,"end_line":85,"end_offset":2581,"start_col":42,"start_line":85,"start_offset":2571},"roles":[],"token":"(cast(s2))","type":"List"}],"pos":{"end_col":52,"end_line":85,"end_offset":2581,"start_col":14,"start_line":85,"start_offset":2543},"roles":["Call"],"token":"cast(s1).SymmetricDifference(cast(s2))","type":"Call"}],"pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":13,"start_line":85,"start_offset":2542},"roles":[],"token":"(cast(s1).SymmetricDifference(cast(s2)))","type":"List"}],"pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":9,"start_line":85,"start_offset":2538},"roles":["Call"],"token":"Byte(cast(s1).SymmetricDifference(cast(s2)))","type":"Call"}],"pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":9,"start_line":85,"start_offset":2538},"roles":[],"type":"List"}],"pos":{"end_col":53,"end_line":85,"end_offset":2582,"start_col":2,"start_line":85,"start_offset":2531},"roles":["Return"],"token":"return Byte(cast(s1).SymmetricDifference(cast(s2)))","type":"Return"}],"pos":{"end_col":2,"end_line":86,"end_offset":2584,"start_col":50,"start_line":84,"start_offset":2528},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).SymmetricDifference(cast(s2)))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":86,"end_offset":2584,"start_col":1,"start_line":84,"start_offset":2479},"props":{"name":"SymmetricDifference"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":9,"end_line":94,"end_offset":2782,"start_col":7,"start_line":94,"start_offset":2780},"roles":["Name"],"token":"s1","type":"Identifier"},{"pos":{"end_col":14,"end_line":94,"end_offset":2787,"start_col":10,"start_line":94,"start_offset":2783},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":14,"end_line":94,"end_offset":2787,"start_col":7,"start_line":94,"start_offset":2780},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":15,"end_line":94,"end_offset":2788,"start_col":6,"start_line":94,"start_offset":2779},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"pos":{"end_col":21,"end_line":94,"end_offset":2794,"start_col":16,"start_line":94,"start_offset":2789},"roles":["Name"],"token":"Union","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":24,"end_line":94,"end_offset":2797,"start_col":22,"start_line":94,"start_offset":2795},"roles":["Name"],"token":"s2","type":"Identifier"},{"pos":{"end_col":29,"end_line":94,"end_offset":2802,"start_col":25,"start_line":94,"start_offset":2798},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":29,"end_line":94,"end_offset":2802,"start_col":22,"start_line":94,"start_offset":2795},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":30,"end_line":94,"end_offset":2803,"start_col":21,"start_line":94,"start_offset":2794},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"pos":{"end_col":35,"end_line":94,"end_offset":2808,"start_col":31,"start_line":94,"start_offset":2804},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":95,"end_offset":2823,"start_col":9,"start_line":95,"start_offset":2819},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":18,"end_line":95,"end_offset":2828,"start_col":14,"start_line":95,"start_offset":2824},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":21,"end_line":95,"end_offset":2831,"start_col":19,"start_line":95,"start_offset":2829},"roles":["Name"],"token":"s1","type":"Identifier"}],"pos":{"end_col":22,"end_line":95,"end_offset":2832,"start_col":18,"start_line":95,"start_offset":2828},"roles":[],"token":"(s1)","type":"List"}],"pos":{"end_col":22,"end_line":95,"end_offset":2832,"start_col":14,"start_line":95,"start_offset":2824},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"pos":{"end_col":28,"end_line":95,"end_offset":2838,"start_col":23,"start_line":95,"start_offset":2833},"roles":["Name"],"token":"Union","type":"Identifier"}],"pos":{"end_col":28,"end_line":95,"end_offset":2838,"start_col":14,"start_line":95,"start_offset":2824},"roles":[],"token":"cast(s1).Union","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":33,"end_line":95,"end_offset":2843,"start_col":29,"start_line":95,"start_offset":2839},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":36,"end_line":95,"end_offset":2846,"start_col":34,"start_line":95,"start_offset":2844},"roles":["Name"],"token":"s2","type":"Identifier"}],"pos":{"end_col":37,"end_line":95,"end_offset":2847,"start_col":33,"start_line":95,"start_offset":2843},"roles":[],"token":"(s2)","type":"List"}],"pos":{"end_col":37,"end_line":95,"end_offset":2847,"start_col":29,"start_line":95,"start_offset":2839},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"pos":{"end_col":38,"end_line":95,"end_offset":2848,"start_col":28,"start_line":95,"start_offset":2838},"roles":[],"token":"(cast(s2))","type":"List"}],"pos":{"end_col":38,"end_line":95,"end_offset":2848,"start_col":14,"start_line":95,"start_offset":2824},"roles":["Call"],"token":"cast(s1).Union(cast(s2))","type":"Call"}],"pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":13,"start_line":95,"start_offset":2823},"roles":[],"token":"(cast(s1).Union(cast(s2)))","type":"List"}],"pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":9,"start_line":95,"start_offset":2819},"roles":["Call"],"token":"Byte(cast(s1).Union(cast(s2)))","type":"Call"}],"pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":9,"start_line":95,"start_offset":2819},"roles":[],"type":"List"}],"pos":{"end_col":39,"end_line":95,"end_offset":2849,"start_col":2,"start_line":95,"start_offset":2812},"roles":["Return"],"token":"return Byte(cast(s1).Union(cast(s2)))","type":"Return"}],"pos":{"end_col":2,"end_line":96,"end_offset":2851,"start_col":36,"start_line":94,"start_offset":2809},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).Union(cast(s2)))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":96,"end_offset":2851,"start_col":1,"start_line":94,"start_offset":2774},"props":{"name":"Union"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":9,"end_line":103,"end_offset":3017,"start_col":7,"start_line":103,"start_offset":3015},"roles":["Name"],"token":"s1","type":"Identifier"},{"pos":{"end_col":14,"end_line":103,"end_offset":3022,"start_col":10,"start_line":103,"start_offset":3018},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":14,"end_line":103,"end_offset":3022,"start_col":7,"start_line":103,"start_offset":3015},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":15,"end_line":103,"end_offset":3023,"start_col":6,"start_line":103,"start_offset":3014},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"pos":{"end_col":28,"end_line":103,"end_offset":3036,"start_col":16,"start_line":103,"start_offset":3024},"roles":["Name"],"token":"Intersection","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":31,"end_line":103,"end_offset":3039,"start_col":29,"start_line":103,"start_offset":3037},"roles":["Name"],"token":"s2","type":"Identifier"},{"pos":{"end_col":36,"end_line":103,"end_offset":3044,"start_col":32,"start_line":103,"start_offset":3040},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":36,"end_line":103,"end_offset":3044,"start_col":29,"start_line":103,"start_offset":3037},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":37,"end_line":103,"end_offset":3045,"start_col":28,"start_line":103,"start_offset":3036},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"pos":{"end_col":42,"end_line":103,"end_offset":3050,"start_col":38,"start_line":103,"start_offset":3046},"roles":["Name","Type"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":104,"end_offset":3065,"start_col":9,"start_line":104,"start_offset":3061},"roles":["Name"],"token":"Byte","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":18,"end_line":104,"end_offset":3070,"start_col":14,"start_line":104,"start_offset":3066},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":21,"end_line":104,"end_offset":3073,"start_col":19,"start_line":104,"start_offset":3071},"roles":["Name"],"token":"s1","type":"Identifier"}],"pos":{"end_col":22,"end_line":104,"end_offset":3074,"start_col":18,"start_line":104,"start_offset":3070},"roles":[],"token":"(s1)","type":"List"}],"pos":{"end_col":22,"end_line":104,"end_offset":3074,"start_col":14,"start_line":104,"start_offset":3066},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"pos":{"end_col":35,"end_line":104,"end_offset":3087,"start_col":23,"start_line":104,"start_offset":3075},"roles":["Name"],"token":"Intersection","type":"Identifier"}],"pos":{"end_col":35,"end_line":104,"end_offset":3087,"start_col":14,"start_line":104,"start_offset":3066},"roles":[],"token":"cast(s1).Intersection","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":40,"end_line":104,"end_offset":3092,"start_col":36,"start_line":104,"start_offset":3088},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":43,"end_line":104,"end_offset":3095,"start_col":41,"start_line":104,"start_offset":3093},"roles":["Name"],"token":"s2","type":"Identifier"}],"pos":{"end_col":44,"end_line":104,"end_offset":3096,"start_col":40,"start_line":104,"start_offset":3092},"roles":[],"token":"(s2)","type":"List"}],"pos":{"end_col":44,"end_line":104,"end_offset":3096,"start_col":36,"start_line":104,"start_offset":3088},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"pos":{"end_col":45,"end_line":104,"end_offset":3097,"start_col":35,"start_line":104,"start_offset":3087},"roles":[],"token":"(cast(s2))","type":"List"}],"pos":{"end_col":45,"end_line":104,"end_offset":3097,"start_col":14,"start_line":104,"start_offset":3066},"roles":["Call"],"token":"cast(s1).Intersection(cast(s2))","type":"Call"}],"pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":13,"start_line":104,"start_offset":3065},"roles":[],"token":"(cast(s1).Intersection(cast(s2)))","type":"List"}],"pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":9,"start_line":104,"start_offset":3061},"roles":["Call"],"token":"Byte(cast(s1).Intersection(cast(s2)))","type":"Call"}],"pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":9,"start_line":104,"start_offset":3061},"roles":[],"type":"List"}],"pos":{"end_col":46,"end_line":104,"end_offset":3098,"start_col":2,"start_line":104,"start_offset":3054},"roles":["Return"],"token":"return Byte(cast(s1).Intersection(cast(s2)))","type":"Return"}],"pos":{"end_col":2,"end_line":105,"end_offset":3100,"start_col":43,"start_line":103,"start_offset":3051},"roles":["Body"],"token":"{\n\treturn Byte(cast(s1).Intersection(cast(s2)))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":105,"end_offset":3100,"start_col":1,"start_line":103,"start_offset":3009},"props":{"name":"Intersection"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":9,"end_line":108,"end_offset":3176,"start_col":7,"start_line":108,"start_offset":3174},"roles":["Name"],"token":"s1","type":"Identifier"},{"pos":{"end_col":14,"end_line":108,"end_offset":3181,"start_col":10,"start_line":108,"start_offset":3177},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":14,"end_line":108,"end_offset":3181,"start_col":7,"start_line":108,"start_offset":3174},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":15,"end_line":108,"end_offset":3182,"start_col":6,"start_line":108,"start_offset":3173},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"pos":{"end_col":26,"end_line":108,"end_offset":3193,"start_col":16,"start_line":108,"start_offset":3183},"roles":["Name"],"token":"IsSuperset","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":29,"end_line":108,"end_offset":3196,"start_col":27,"start_line":108,"start_offset":3194},"roles":["Name"],"token":"s2","type":"Identifier"},{"pos":{"end_col":34,"end_line":108,"end_offset":3201,"start_col":30,"start_line":108,"start_offset":3197},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":34,"end_line":108,"end_offset":3201,"start_col":27,"start_line":108,"start_offset":3194},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":35,"end_line":108,"end_offset":3202,"start_col":26,"start_line":108,"start_offset":3193},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"pos":{"end_col":40,"end_line":108,"end_offset":3207,"start_col":36,"start_line":108,"start_offset":3203},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":109,"end_offset":3222,"start_col":9,"start_line":109,"start_offset":3218},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":16,"end_line":109,"end_offset":3225,"start_col":14,"start_line":109,"start_offset":3223},"roles":["Name"],"token":"s1","type":"Identifier"}],"pos":{"end_col":17,"end_line":109,"end_offset":3226,"start_col":13,"start_line":109,"start_offset":3222},"roles":[],"token":"(s1)","type":"List"}],"pos":{"end_col":17,"end_line":109,"end_offset":3226,"start_col":9,"start_line":109,"start_offset":3218},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"pos":{"end_col":28,"end_line":109,"end_offset":3237,"start_col":18,"start_line":109,"start_offset":3227},"roles":["Name"],"token":"IsSuperset","type":"Identifier"}],"pos":{"end_col":28,"end_line":109,"end_offset":3237,"start_col":9,"start_line":109,"start_offset":3218},"roles":[],"token":"cast(s1).IsSuperset","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":33,"end_line":109,"end_offset":3242,"start_col":29,"start_line":109,"start_offset":3238},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":36,"end_line":109,"end_offset":3245,"start_col":34,"start_line":109,"start_offset":3243},"roles":["Name"],"token":"s2","type":"Identifier"}],"pos":{"end_col":37,"end_line":109,"end_offset":3246,"start_col":33,"start_line":109,"start_offset":3242},"roles":[],"token":"(s2)","type":"List"}],"pos":{"end_col":37,"end_line":109,"end_offset":3246,"start_col":29,"start_line":109,"start_offset":3238},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":28,"start_line":109,"start_offset":3237},"roles":[],"token":"(cast(s2))","type":"List"}],"pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":9,"start_line":109,"start_offset":3218},"roles":["Call"],"token":"cast(s1).IsSuperset(cast(s2))","type":"Call"}],"pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":9,"start_line":109,"start_offset":3218},"roles":[],"type":"List"}],"pos":{"end_col":38,"end_line":109,"end_offset":3247,"start_col":2,"start_line":109,"start_offset":3211},"roles":["Return"],"token":"return cast(s1).IsSuperset(cast(s2))","type":"Return"}],"pos":{"end_col":2,"end_line":110,"end_offset":3249,"start_col":41,"start_line":108,"start_offset":3208},"roles":["Body"],"token":"{\n\treturn cast(s1).IsSuperset(cast(s2))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":110,"end_offset":3249,"start_col":1,"start_line":108,"start_offset":3168},"props":{"name":"IsSuperset"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":9,"end_line":115,"end_offset":3447,"start_col":7,"start_line":115,"start_offset":3445},"roles":["Name"],"token":"s1","type":"Identifier"},{"pos":{"end_col":14,"end_line":115,"end_offset":3452,"start_col":10,"start_line":115,"start_offset":3448},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":14,"end_line":115,"end_offset":3452,"start_col":7,"start_line":115,"start_offset":3445},"props":{"name":"s1"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":15,"end_line":115,"end_offset":3453,"start_col":6,"start_line":115,"start_offset":3444},"roles":["Parameter"],"token":"(s1 Byte)","type":"Parameter"},{"pos":{"end_col":21,"end_line":115,"end_offset":3459,"start_col":16,"start_line":115,"start_offset":3454},"roles":["Name"],"token":"Equal","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":24,"end_line":115,"end_offset":3462,"start_col":22,"start_line":115,"start_offset":3460},"roles":["Name"],"token":"s2","type":"Identifier"},{"pos":{"end_col":29,"end_line":115,"end_offset":3467,"start_col":25,"start_line":115,"start_offset":3463},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":29,"end_line":115,"end_offset":3467,"start_col":22,"start_line":115,"start_offset":3460},"props":{"name":"s2"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":30,"end_line":115,"end_offset":3468,"start_col":21,"start_line":115,"start_offset":3459},"roles":["Parameter"],"token":"(s2 Byte)","type":"Parameter"},{"pos":{"end_col":35,"end_line":115,"end_offset":3473,"start_col":31,"start_line":115,"start_offset":3469},"roles":["Name","Type"],"token":"bool","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":116,"end_offset":3488,"start_col":9,"start_line":116,"start_offset":3484},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":16,"end_line":116,"end_offset":3491,"start_col":14,"start_line":116,"start_offset":3489},"roles":["Name"],"token":"s1","type":"Identifier"}],"pos":{"end_col":17,"end_line":116,"end_offset":3492,"start_col":13,"start_line":116,"start_offset":3488},"roles":[],"token":"(s1)","type":"List"}],"pos":{"end_col":17,"end_line":116,"end_offset":3492,"start_col":9,"start_line":116,"start_offset":3484},"roles":["Call"],"token":"cast(s1)","type":"Call"},{"pos":{"end_col":23,"end_line":116,"end_offset":3498,"start_col":18,"start_line":116,"start_offset":3493},"roles":["Name"],"token":"Equal","type":"Identifier"}],"pos":{"end_col":23,"end_line":116,"end_offset":3498,"start_col":9,"start_line":116,"start_offset":3484},"roles":[],"token":"cast(s1).Equal","type":"Synthetic"},{"children":[{"children":[{"pos":{"end_col":28,"end_line":116,"end_offset":3503,"start_col":24,"start_line":116,"start_offset":3499},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":31,"end_line":116,"end_offset":3506,"start_col":29,"start_line":116,"start_offset":3504},"roles":["Name"],"token":"s2","type":"Identifier"}],"pos":{"end_col":32,"end_line":116,"end_offset":3507,"start_col":28,"start_line":116,"start_offset":3503},"roles":[],"token":"(s2)","type":"List"}],"pos":{"end_col":32,"end_line":116,"end_offset":3507,"start_col":24,"start_line":116,"start_offset":3499},"roles":["Call"],"token":"cast(s2)","type":"Call"}],"pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":23,"start_line":116,"start_offset":3498},"roles":[],"token":"(cast(s2))","type":"List"}],"pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":9,"start_line":116,"start_offset":3484},"roles":["Call"],"token":"cast(s1).Equal(cast(s2))","type":"Call"}],"pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":9,"start_line":116,"start_offset":3484},"roles":[],"type":"List"}],"pos":{"end_col":33,"end_line":116,"end_offset":3508,"start_col":2,"start_line":116,"start_offset":3477},"roles":["Return"],"token":"return cast(s1).Equal(cast(s2))","type":"Return"}],"pos":{"end_col":2,"end_line":117,"end_offset":3510,"start_col":36,"start_line":115,"start_offset":3474},"roles":["Body"],"token":"{\n\treturn cast(s1).Equal(cast(s2))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":117,"end_offset":3510,"start_col":1,"start_line":115,"start_offset":3439},"props":{"name":"Equal"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":120,"end_offset":3572,"start_col":7,"start_line":120,"start_offset":3571},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":120,"end_offset":3577,"start_col":9,"start_line":120,"start_offset":3573},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":120,"end_offset":3577,"start_col":7,"start_line":120,"start_offset":3571},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":120,"end_offset":3578,"start_col":6,"start_line":120,"start_offset":3570},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":19,"end_line":120,"end_offset":3583,"start_col":15,"start_line":120,"start_offset":3579},"roles":["Name"],"token":"List","type":"Identifier"},{"pos":{"end_col":21,"end_line":120,"end_offset":3585,"start_col":19,"start_line":120,"start_offset":3583},"roles":["Parameter"],"token":"()","type":"Parameter"},{"children":[{"pos":{"end_col":28,"end_line":120,"end_offset":3592,"start_col":24,"start_line":120,"start_offset":3588},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":28,"end_line":120,"end_offset":3592,"start_col":22,"start_line":120,"start_offset":3586},"roles":[],"token":"[]byte","type":"Slice"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":121,"end_offset":3607,"start_col":9,"start_line":121,"start_offset":3603},"roles":["Name"],"token":"List","type":"Identifier"},{"children":[{"children":[{"pos":{"end_col":18,"end_line":121,"end_offset":3612,"start_col":14,"start_line":121,"start_offset":3608},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":20,"end_line":121,"end_offset":3614,"start_col":19,"start_line":121,"start_offset":3613},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":21,"end_line":121,"end_offset":3615,"start_col":18,"start_line":121,"start_offset":3612},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":21,"end_line":121,"end_offset":3615,"start_col":14,"start_line":121,"start_offset":3608},"roles":["Call"],"token":"cast(s)","type":"Call"}],"pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":13,"start_line":121,"start_offset":3607},"roles":[],"token":"(cast(s))","type":"List"}],"pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":9,"start_line":121,"start_offset":3603},"roles":["Call"],"token":"List(cast(s))","type":"Call"}],"pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":9,"start_line":121,"start_offset":3603},"roles":[],"type":"List"}],"pos":{"end_col":22,"end_line":121,"end_offset":3616,"start_col":2,"start_line":121,"start_offset":3596},"roles":["Return"],"token":"return List(cast(s))","type":"Return"}],"pos":{"end_col":2,"end_line":122,"end_offset":3618,"start_col":29,"start_line":120,"start_offset":3593},"roles":["Body"],"token":"{\n\treturn List(cast(s))\n}","type":"Block"}],"pos":{"end_col":2,"end_line":122,"end_offset":3618,"start_col":1,"start_line":120,"start_offset":3565},"props":{"name":"List"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":125,"end_offset":3692,"start_col":7,"start_line":125,"start_offset":3691},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":125,"end_offset":3697,"start_col":9,"start_line":125,"start_offset":3693},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":125,"end_offset":3697,"start_col":7,"start_line":125,"start_offset":3691},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":125,"end_offset":3698,"start_col":6,"start_line":125,"start_offset":3690},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":27,"end_line":125,"end_offset":3711,"start_col":15,"start_line":125,"start_offset":3699},"roles":["Name"],"token":"UnsortedList","type":"Identifier"},{"pos":{"end_col":29,"end_line":125,"end_offset":3713,"start_col":27,"start_line":125,"start_offset":3711},"roles":["Parameter"],"token":"()","type":"Parameter"},{"children":[{"pos":{"end_col":36,"end_line":125,"end_offset":3720,"start_col":32,"start_line":125,"start_offset":3716},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":36,"end_line":125,"end_offset":3720,"start_col":30,"start_line":125,"start_offset":3714},"roles":[],"token":"[]byte","type":"Slice"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":126,"end_offset":3735,"start_col":9,"start_line":126,"start_offset":3731},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":15,"end_line":126,"end_offset":3737,"start_col":14,"start_line":126,"start_offset":3736},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":16,"end_line":126,"end_offset":3738,"start_col":13,"start_line":126,"start_offset":3735},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":16,"end_line":126,"end_offset":3738,"start_col":9,"start_line":126,"start_offset":3731},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":29,"end_line":126,"end_offset":3751,"start_col":17,"start_line":126,"start_offset":3739},"roles":["Name"],"token":"UnsortedList","type":"Identifier"}],"pos":{"end_col":29,"end_line":126,"end_offset":3751,"start_col":9,"start_line":126,"start_offset":3731},"roles":[],"token":"cast(s).UnsortedList","type":"Synthetic"},{"pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":29,"start_line":126,"start_offset":3751},"roles":[],"token":"()","type":"List"}],"pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":9,"start_line":126,"start_offset":3731},"roles":["Call"],"token":"cast(s).UnsortedList()","type":"Call"}],"pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":9,"start_line":126,"start_offset":3731},"roles":[],"type":"List"}],"pos":{"end_col":31,"end_line":126,"end_offset":3753,"start_col":2,"start_line":126,"start_offset":3724},"roles":["Return"],"token":"return cast(s).UnsortedList()","type":"Return"}],"pos":{"end_col":2,"end_line":127,"end_offset":3755,"start_col":37,"start_line":125,"start_offset":3721},"roles":["Body"],"token":"{\n\treturn cast(s).UnsortedList()\n}","type":"Block"}],"pos":{"end_col":2,"end_line":127,"end_offset":3755,"start_col":1,"start_line":125,"start_offset":3685},"props":{"name":"UnsortedList"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":130,"end_offset":3813,"start_col":7,"start_line":130,"start_offset":3812},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":130,"end_offset":3818,"start_col":9,"start_line":130,"start_offset":3814},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":130,"end_offset":3818,"start_col":7,"start_line":130,"start_offset":3812},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":130,"end_offset":3819,"start_col":6,"start_line":130,"start_offset":3811},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":21,"end_line":130,"end_offset":3826,"start_col":15,"start_line":130,"start_offset":3820},"roles":["Name"],"token":"PopAny","type":"Identifier"},{"pos":{"end_col":23,"end_line":130,"end_offset":3828,"start_col":21,"start_line":130,"start_offset":3826},"roles":["Parameter"],"token":"()","type":"Parameter"},{"children":[{"children":[{"pos":{"end_col":29,"end_line":130,"end_offset":3834,"start_col":25,"start_line":130,"start_offset":3830},"roles":["Name","Type"],"token":"byte","type":"Identifier"}],"pos":{"end_col":29,"end_line":130,"end_offset":3834,"start_col":25,"start_line":130,"start_offset":3830},"roles":["Parameter"],"type":"Parameter"},{"children":[{"pos":{"end_col":35,"end_line":130,"end_offset":3840,"start_col":31,"start_line":130,"start_offset":3836},"roles":["Name","Type"],"token":"bool","type":"Identifier"}],"pos":{"end_col":35,"end_line":130,"end_offset":3840,"start_col":31,"start_line":130,"start_offset":3836},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":36,"end_line":130,"end_offset":3841,"start_col":24,"start_line":130,"start_offset":3829},"roles":["Parameter"],"token":"(byte, bool)","type":"Parameter"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":13,"end_line":131,"end_offset":3856,"start_col":9,"start_line":131,"start_offset":3852},"roles":["Name"],"token":"cast","type":"Identifier"},{"children":[{"pos":{"end_col":15,"end_line":131,"end_offset":3858,"start_col":14,"start_line":131,"start_offset":3857},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":16,"end_line":131,"end_offset":3859,"start_col":13,"start_line":131,"start_offset":3856},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":16,"end_line":131,"end_offset":3859,"start_col":9,"start_line":131,"start_offset":3852},"roles":["Call"],"token":"cast(s)","type":"Call"},{"pos":{"end_col":23,"end_line":131,"end_offset":3866,"start_col":17,"start_line":131,"start_offset":3860},"roles":["Name"],"token":"PopAny","type":"Identifier"}],"pos":{"end_col":23,"end_line":131,"end_offset":3866,"start_col":9,"start_line":131,"start_offset":3852},"roles":[],"token":"cast(s).PopAny","type":"Synthetic"},{"pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":23,"start_line":131,"start_offset":3866},"roles":[],"token":"()","type":"List"}],"pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":9,"start_line":131,"start_offset":3852},"roles":["Call"],"token":"cast(s).PopAny()","type":"Call"}],"pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":9,"start_line":131,"start_offset":3852},"roles":[],"type":"List"}],"pos":{"end_col":25,"end_line":131,"end_offset":3868,"start_col":2,"start_line":131,"start_offset":3845},"roles":["Return"],"token":"return cast(s).PopAny()","type":"Return"}],"pos":{"end_col":2,"end_line":132,"end_offset":3870,"start_col":37,"start_line":130,"start_offset":3842},"roles":["Body"],"token":"{\n\treturn cast(s).PopAny()\n}","type":"Block"}],"pos":{"end_col":2,"end_line":132,"end_offset":3870,"start_col":1,"start_line":130,"start_offset":3806},"props":{"name":"PopAny"},"roles":["Function","Declaration","Member"],"type":"Method"},{"children":[{"children":[{"children":[{"pos":{"end_col":8,"end_line":135,"end_offset":3915,"start_col":7,"start_line":135,"start_offset":3914},"roles":["Name"],"token":"s","type":"Identifier"},{"pos":{"end_col":13,"end_line":135,"end_offset":3920,"start_col":9,"start_line":135,"start_offset":3916},"roles":["Name","Type"],"token":"Byte","type":"Identifier"}],"pos":{"end_col":13,"end_line":135,"end_offset":3920,"start_col":7,"start_line":135,"start_offset":3914},"props":{"name":"s"},"roles":["Parameter"],"type":"Parameter"}],"pos":{"end_col":14,"end_line":135,"end_offset":3921,"start_col":6,"start_line":135,"start_offset":3913},"roles":["Parameter"],"token":"(s Byte)","type":"Parameter"},{"pos":{"end_col":18,"end_line":135,"end_offset":3925,"start_col":15,"start_line":135,"start_offset":3922},"roles":["Name"],"token":"Len","type":"Identifier"},{"pos":{"end_col":20,"end_line":135,"end_offset":3927,"start_col":18,"start_line":135,"start_offset":3925},"roles":["Parameter"],"token":"()","type":"Parameter"},{"pos":{"end_col":24,"end_line":135,"end_offset":3931,"start_col":21,"start_line":135,"start_offset":3928},"roles":["Name","Type"],"token":"int","type":"Identifier"},{"children":[{"children":[{"children":[{"children":[{"pos":{"end_col":12,"end_line":136,"end_offset":3945,"start_col":9,"start_line":136,"start_offset":3942},"roles":["Name"],"token":"len","type":"Identifier"},{"children":[{"pos":{"end_col":14,"end_line":136,"end_offset":3947,"start_col":13,"start_line":136,"start_offset":3946},"roles":["Name"],"token":"s","type":"Identifier"}],"pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":12,"start_line":136,"start_offset":3945},"roles":[],"token":"(s)","type":"List"}],"pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":9,"start_line":136,"start_offset":3942},"roles":["Call"],"token":"len(s)","type":"Call"}],"pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":9,"start_line":136,"start_offset":3942},"roles":[],"type":"List"}],"pos":{"end_col":15,"end_line":136,"end_offset":3948,"start_col":2,"start_line":136,"start_offset":3935},"roles":["Return"],"token":"return len(s)","type":"Return"}],"pos":{"end_col":2,"end_line":137,"end_offset":3950,"start_col":25,"start_line":135,"start_offset":3932},"roles":["Body"],"token":"{\n\treturn len(s)\n}","type":"Block"}],"pos":{"end_col":2,"end_line":137,"end_offset":3950,"start_col":1,"start_line":135,"start_offset":3908},"props":{"name":"Len"},"roles":["Function","Declaration","Member"],"type":"Method"}]} diff --git a/tests/golden/uast/query.count b/tests/golden/uast/query.count deleted file mode 100644 index d00491fd..00000000 --- a/tests/golden/uast/query.count +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/tests/golden/uast/query.json b/tests/golden/uast/query.json deleted file mode 100644 index 969acac7..00000000 --- a/tests/golden/uast/query.json +++ /dev/null @@ -1,7044 +0,0 @@ -{ - "results": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 28, - "end_offset": 863, - "start_col": 6, - "start_line": 28, - "start_offset": 856 - }, - "roles": [ - "Name" - ], - "token": "NewByte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 19, - "end_line": 28, - "end_offset": 869, - "start_col": 14, - "start_line": 28, - "start_offset": 864 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "pos": { - "end_col": 27, - "end_line": 28, - "end_offset": 877, - "start_col": 23, - "start_line": 28, - "start_offset": 873 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 27, - "end_line": 28, - "end_offset": 877, - "start_col": 14, - "start_line": 28, - "start_offset": 864 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 28, - "end_line": 28, - "end_offset": 878, - "start_col": 13, - "start_line": 28, - "start_offset": 863 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 33, - "end_line": 28, - "end_offset": 883, - "start_col": 29, - "start_line": 28, - "start_offset": 879 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 29, - "end_offset": 898, - "start_col": 9, - "start_line": 29, - "start_offset": 894 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 17, - "end_line": 29, - "end_offset": 902, - "start_col": 14, - "start_line": 29, - "start_offset": 899 - }, - "roles": [ - "Name" - ], - "token": "New", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 22, - "end_line": 29, - "end_offset": 907, - "start_col": 18, - "start_line": 29, - "start_offset": 903 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 22, - "end_line": 29, - "end_offset": 907, - "start_col": 18, - "start_line": 29, - "start_offset": 903 - }, - "roles": [], - "type": "Synthetic" - } - ], - "pos": { - "end_col": 23, - "end_line": 29, - "end_offset": 908, - "start_col": 17, - "start_line": 29, - "start_offset": 902 - }, - "roles": [], - "token": "[byte]", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 29, - "end_line": 29, - "end_offset": 914, - "start_col": 24, - "start_line": 29, - "start_offset": 909 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "pos": { - "end_col": 32, - "end_line": 29, - "end_offset": 917, - "start_col": 24, - "start_line": 29, - "start_offset": 909 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 29, - "end_offset": 918, - "start_col": 23, - "start_line": 29, - "start_offset": 908 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 29, - "end_offset": 918, - "start_col": 14, - "start_line": 29, - "start_offset": 899 - }, - "roles": [ - "Call" - ], - "token": "New[byte](items...)", - "type": "Call" - } - ], - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 13, - "start_line": 29, - "start_offset": 898 - }, - "roles": [], - "token": "(New[byte](items...))", - "type": "List" - } - ], - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 9, - "start_line": 29, - "start_offset": 894 - }, - "roles": [ - "Call" - ], - "token": "Byte(New[byte](items...))", - "type": "Call" - } - ], - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 9, - "start_line": 29, - "start_offset": 894 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 34, - "end_line": 29, - "end_offset": 919, - "start_col": 2, - "start_line": 29, - "start_offset": 887 - }, - "roles": [ - "Return" - ], - "token": "return Byte(New[byte](items...))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 30, - "end_offset": 921, - "start_col": 34, - "start_line": 28, - "start_offset": 884 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(New[byte](items...))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 30, - "end_offset": 921, - "start_col": 1, - "start_line": 28, - "start_offset": 851 - }, - "props": { - "name": "NewByte" - }, - "roles": [ - "Function", - "Declaration" - ], - "token": "func NewByte(items ...byte) Byte {\n\treturn Byte(New[byte](items...))\n}", - "type": "Function" - }, - { - "children": [ - { - "pos": { - "end_col": 16, - "end_line": 34, - "end_offset": 1084, - "start_col": 6, - "start_line": 34, - "start_offset": 1074 - }, - "roles": [ - "Name" - ], - "token": "ByteKeySet", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 34, - "end_offset": 1086, - "start_col": 17, - "start_line": 34, - "start_offset": 1085 - }, - "roles": [ - "Name" - ], - "token": "T", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 22, - "end_line": 34, - "end_offset": 1090, - "start_col": 19, - "start_line": 34, - "start_offset": 1087 - }, - "roles": [ - "Name", - "Type" - ], - "token": "any", - "type": "Identifier" - } - ], - "pos": { - "end_col": 22, - "end_line": 34, - "end_offset": 1090, - "start_col": 19, - "start_line": 34, - "start_offset": 1087 - }, - "roles": [], - "type": "Synthetic" - } - ], - "pos": { - "end_col": 22, - "end_line": 34, - "end_offset": 1090, - "start_col": 17, - "start_line": 34, - "start_offset": 1085 - }, - "props": { - "name": "T" - }, - "roles": [ - "Parameter" - ], - "token": "T any", - "type": "Parameter" - } - ], - "pos": { - "end_col": 23, - "end_line": 34, - "end_offset": 1091, - "start_col": 16, - "start_line": 34, - "start_offset": 1084 - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 30, - "end_line": 34, - "end_offset": 1098, - "start_col": 24, - "start_line": 34, - "start_offset": 1092 - }, - "roles": [ - "Name" - ], - "token": "theMap", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 39, - "end_line": 34, - "end_offset": 1107, - "start_col": 35, - "start_line": 34, - "start_offset": 1103 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - }, - { - "pos": { - "end_col": 41, - "end_line": 34, - "end_offset": 1109, - "start_col": 40, - "start_line": 34, - "start_offset": 1108 - }, - "roles": [ - "Name", - "Type" - ], - "token": "T", - "type": "Identifier" - } - ], - "pos": { - "end_col": 41, - "end_line": 34, - "end_offset": 1109, - "start_col": 31, - "start_line": 34, - "start_offset": 1099 - }, - "roles": [], - "token": "map[byte]T", - "type": "Synthetic" - } - ], - "pos": { - "end_col": 41, - "end_line": 34, - "end_offset": 1109, - "start_col": 24, - "start_line": 34, - "start_offset": 1092 - }, - "props": { - "name": "theMap" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 42, - "end_line": 34, - "end_offset": 1110, - "start_col": 23, - "start_line": 34, - "start_offset": 1091 - }, - "roles": [ - "Parameter" - ], - "token": "(theMap map[byte]T)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 47, - "end_line": 34, - "end_offset": 1115, - "start_col": 43, - "start_line": 34, - "start_offset": 1111 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 35, - "end_offset": 1130, - "start_col": 9, - "start_line": 35, - "start_offset": 1126 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 20, - "end_line": 35, - "end_offset": 1137, - "start_col": 14, - "start_line": 35, - "start_offset": 1131 - }, - "roles": [ - "Name" - ], - "token": "KeySet", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 27, - "end_line": 35, - "end_offset": 1144, - "start_col": 21, - "start_line": 35, - "start_offset": 1138 - }, - "roles": [ - "Name" - ], - "token": "theMap", - "type": "Identifier" - } - ], - "pos": { - "end_col": 28, - "end_line": 35, - "end_offset": 1145, - "start_col": 20, - "start_line": 35, - "start_offset": 1137 - }, - "roles": [], - "token": "(theMap)", - "type": "List" - } - ], - "pos": { - "end_col": 28, - "end_line": 35, - "end_offset": 1145, - "start_col": 14, - "start_line": 35, - "start_offset": 1131 - }, - "roles": [ - "Call" - ], - "token": "KeySet(theMap)", - "type": "Call" - } - ], - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 13, - "start_line": 35, - "start_offset": 1130 - }, - "roles": [], - "token": "(KeySet(theMap))", - "type": "List" - } - ], - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 9, - "start_line": 35, - "start_offset": 1126 - }, - "roles": [ - "Call" - ], - "token": "Byte(KeySet(theMap))", - "type": "Call" - } - ], - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 9, - "start_line": 35, - "start_offset": 1126 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 29, - "end_line": 35, - "end_offset": 1146, - "start_col": 2, - "start_line": 35, - "start_offset": 1119 - }, - "roles": [ - "Return" - ], - "token": "return Byte(KeySet(theMap))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 36, - "end_offset": 1148, - "start_col": 48, - "start_line": 34, - "start_offset": 1116 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(KeySet(theMap))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 36, - "end_offset": 1148, - "start_col": 1, - "start_line": 34, - "start_offset": 1069 - }, - "props": { - "name": "ByteKeySet" - }, - "roles": [ - "Function", - "Declaration" - ], - "token": "func ByteKeySet[T any](theMap map[byte]T) Byte {\n\treturn Byte(KeySet(theMap))\n}", - "type": "Function" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 39, - "end_offset": 1190, - "start_col": 7, - "start_line": 39, - "start_offset": 1189 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 39, - "end_offset": 1195, - "start_col": 9, - "start_line": 39, - "start_offset": 1191 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 39, - "end_offset": 1195, - "start_col": 7, - "start_line": 39, - "start_offset": 1189 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 39, - "end_offset": 1196, - "start_col": 6, - "start_line": 39, - "start_offset": 1188 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 21, - "end_line": 39, - "end_offset": 1203, - "start_col": 15, - "start_line": 39, - "start_offset": 1197 - }, - "roles": [ - "Name" - ], - "token": "Insert", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 27, - "end_line": 39, - "end_offset": 1209, - "start_col": 22, - "start_line": 39, - "start_offset": 1204 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "pos": { - "end_col": 35, - "end_line": 39, - "end_offset": 1217, - "start_col": 31, - "start_line": 39, - "start_offset": 1213 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 35, - "end_line": 39, - "end_offset": 1217, - "start_col": 22, - "start_line": 39, - "start_offset": 1204 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 36, - "end_line": 39, - "end_offset": 1218, - "start_col": 21, - "start_line": 39, - "start_offset": 1203 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 41, - "end_line": 39, - "end_offset": 1223, - "start_col": 37, - "start_line": 39, - "start_offset": 1219 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 40, - "end_offset": 1238, - "start_col": 9, - "start_line": 40, - "start_offset": 1234 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 40, - "end_offset": 1243, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 20, - "end_line": 40, - "end_offset": 1245, - "start_col": 19, - "start_line": 40, - "start_offset": 1244 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 21, - "end_line": 40, - "end_offset": 1246, - "start_col": 18, - "start_line": 40, - "start_offset": 1243 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 21, - "end_line": 40, - "end_offset": 1246, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 28, - "end_line": 40, - "end_offset": 1253, - "start_col": 22, - "start_line": 40, - "start_offset": 1247 - }, - "roles": [ - "Name" - ], - "token": "Insert", - "type": "Identifier" - } - ], - "pos": { - "end_col": 28, - "end_line": 40, - "end_offset": 1253, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [], - "token": "cast(s).Insert", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 34, - "end_line": 40, - "end_offset": 1259, - "start_col": 29, - "start_line": 40, - "start_offset": 1254 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "pos": { - "end_col": 37, - "end_line": 40, - "end_offset": 1262, - "start_col": 29, - "start_line": 40, - "start_offset": 1254 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "pos": { - "end_col": 38, - "end_line": 40, - "end_offset": 1263, - "start_col": 28, - "start_line": 40, - "start_offset": 1253 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "pos": { - "end_col": 38, - "end_line": 40, - "end_offset": 1263, - "start_col": 14, - "start_line": 40, - "start_offset": 1239 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Insert(items...)", - "type": "Call" - } - ], - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 13, - "start_line": 40, - "start_offset": 1238 - }, - "roles": [], - "token": "(cast(s).Insert(items...))", - "type": "List" - } - ], - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 9, - "start_line": 40, - "start_offset": 1234 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s).Insert(items...))", - "type": "Call" - } - ], - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 9, - "start_line": 40, - "start_offset": 1234 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 39, - "end_line": 40, - "end_offset": 1264, - "start_col": 2, - "start_line": 40, - "start_offset": 1227 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s).Insert(items...))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 41, - "end_offset": 1266, - "start_col": 42, - "start_line": 39, - "start_offset": 1224 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s).Insert(items...))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 41, - "end_offset": 1266, - "start_col": 1, - "start_line": 39, - "start_offset": 1183 - }, - "props": { - "name": "Insert" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 44, - "end_offset": 1317, - "start_col": 7, - "start_line": 44, - "start_offset": 1316 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 44, - "end_offset": 1322, - "start_col": 9, - "start_line": 44, - "start_offset": 1318 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 44, - "end_offset": 1322, - "start_col": 7, - "start_line": 44, - "start_offset": 1316 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 44, - "end_offset": 1323, - "start_col": 6, - "start_line": 44, - "start_offset": 1315 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 21, - "end_line": 44, - "end_offset": 1330, - "start_col": 15, - "start_line": 44, - "start_offset": 1324 - }, - "roles": [ - "Name" - ], - "token": "Delete", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 27, - "end_line": 44, - "end_offset": 1336, - "start_col": 22, - "start_line": 44, - "start_offset": 1331 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "pos": { - "end_col": 35, - "end_line": 44, - "end_offset": 1344, - "start_col": 31, - "start_line": 44, - "start_offset": 1340 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 35, - "end_line": 44, - "end_offset": 1344, - "start_col": 22, - "start_line": 44, - "start_offset": 1331 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 36, - "end_line": 44, - "end_offset": 1345, - "start_col": 21, - "start_line": 44, - "start_offset": 1330 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 41, - "end_line": 44, - "end_offset": 1350, - "start_col": 37, - "start_line": 44, - "start_offset": 1346 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 45, - "end_offset": 1365, - "start_col": 9, - "start_line": 45, - "start_offset": 1361 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 45, - "end_offset": 1370, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 20, - "end_line": 45, - "end_offset": 1372, - "start_col": 19, - "start_line": 45, - "start_offset": 1371 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 21, - "end_line": 45, - "end_offset": 1373, - "start_col": 18, - "start_line": 45, - "start_offset": 1370 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 21, - "end_line": 45, - "end_offset": 1373, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 28, - "end_line": 45, - "end_offset": 1380, - "start_col": 22, - "start_line": 45, - "start_offset": 1374 - }, - "roles": [ - "Name" - ], - "token": "Delete", - "type": "Identifier" - } - ], - "pos": { - "end_col": 28, - "end_line": 45, - "end_offset": 1380, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [], - "token": "cast(s).Delete", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 34, - "end_line": 45, - "end_offset": 1386, - "start_col": 29, - "start_line": 45, - "start_offset": 1381 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "pos": { - "end_col": 37, - "end_line": 45, - "end_offset": 1389, - "start_col": 29, - "start_line": 45, - "start_offset": 1381 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "pos": { - "end_col": 38, - "end_line": 45, - "end_offset": 1390, - "start_col": 28, - "start_line": 45, - "start_offset": 1380 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "pos": { - "end_col": 38, - "end_line": 45, - "end_offset": 1390, - "start_col": 14, - "start_line": 45, - "start_offset": 1366 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Delete(items...)", - "type": "Call" - } - ], - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 13, - "start_line": 45, - "start_offset": 1365 - }, - "roles": [], - "token": "(cast(s).Delete(items...))", - "type": "List" - } - ], - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 9, - "start_line": 45, - "start_offset": 1361 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s).Delete(items...))", - "type": "Call" - } - ], - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 9, - "start_line": 45, - "start_offset": 1361 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 39, - "end_line": 45, - "end_offset": 1391, - "start_col": 2, - "start_line": 45, - "start_offset": 1354 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s).Delete(items...))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 46, - "end_offset": 1393, - "start_col": 42, - "start_line": 44, - "start_offset": 1351 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s).Delete(items...))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 46, - "end_offset": 1393, - "start_col": 1, - "start_line": 44, - "start_offset": 1310 - }, - "props": { - "name": "Delete" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 49, - "end_offset": 1467, - "start_col": 7, - "start_line": 49, - "start_offset": 1466 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 49, - "end_offset": 1472, - "start_col": 9, - "start_line": 49, - "start_offset": 1468 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 49, - "end_offset": 1472, - "start_col": 7, - "start_line": 49, - "start_offset": 1466 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 49, - "end_offset": 1473, - "start_col": 6, - "start_line": 49, - "start_offset": 1465 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 18, - "end_line": 49, - "end_offset": 1477, - "start_col": 15, - "start_line": 49, - "start_offset": 1474 - }, - "roles": [ - "Name" - ], - "token": "Has", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 23, - "end_line": 49, - "end_offset": 1482, - "start_col": 19, - "start_line": 49, - "start_offset": 1478 - }, - "roles": [ - "Name" - ], - "token": "item", - "type": "Identifier" - }, - { - "pos": { - "end_col": 28, - "end_line": 49, - "end_offset": 1487, - "start_col": 24, - "start_line": 49, - "start_offset": 1483 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 28, - "end_line": 49, - "end_offset": 1487, - "start_col": 19, - "start_line": 49, - "start_offset": 1478 - }, - "props": { - "name": "item" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 29, - "end_line": 49, - "end_offset": 1488, - "start_col": 18, - "start_line": 49, - "start_offset": 1477 - }, - "roles": [ - "Parameter" - ], - "token": "(item byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 34, - "end_line": 49, - "end_offset": 1493, - "start_col": 30, - "start_line": 49, - "start_offset": 1489 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 50, - "end_offset": 1508, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 15, - "end_line": 50, - "end_offset": 1510, - "start_col": 14, - "start_line": 50, - "start_offset": 1509 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 16, - "end_line": 50, - "end_offset": 1511, - "start_col": 13, - "start_line": 50, - "start_offset": 1508 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 16, - "end_line": 50, - "end_offset": 1511, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 20, - "end_line": 50, - "end_offset": 1515, - "start_col": 17, - "start_line": 50, - "start_offset": 1512 - }, - "roles": [ - "Name" - ], - "token": "Has", - "type": "Identifier" - } - ], - "pos": { - "end_col": 20, - "end_line": 50, - "end_offset": 1515, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [], - "token": "cast(s).Has", - "type": "Synthetic" - }, - { - "children": [ - { - "pos": { - "end_col": 25, - "end_line": 50, - "end_offset": 1520, - "start_col": 21, - "start_line": 50, - "start_offset": 1516 - }, - "roles": [ - "Name" - ], - "token": "item", - "type": "Identifier" - } - ], - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 20, - "start_line": 50, - "start_offset": 1515 - }, - "roles": [], - "token": "(item)", - "type": "List" - } - ], - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Has(item)", - "type": "Call" - } - ], - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 9, - "start_line": 50, - "start_offset": 1504 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 26, - "end_line": 50, - "end_offset": 1521, - "start_col": 2, - "start_line": 50, - "start_offset": 1497 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).Has(item)", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 51, - "end_offset": 1523, - "start_col": 35, - "start_line": 49, - "start_offset": 1494 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).Has(item)\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 51, - "end_offset": 1523, - "start_col": 1, - "start_line": 49, - "start_offset": 1460 - }, - "props": { - "name": "Has" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 54, - "end_offset": 1606, - "start_col": 7, - "start_line": 54, - "start_offset": 1605 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 54, - "end_offset": 1611, - "start_col": 9, - "start_line": 54, - "start_offset": 1607 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 54, - "end_offset": 1611, - "start_col": 7, - "start_line": 54, - "start_offset": 1605 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 54, - "end_offset": 1612, - "start_col": 6, - "start_line": 54, - "start_offset": 1604 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 21, - "end_line": 54, - "end_offset": 1619, - "start_col": 15, - "start_line": 54, - "start_offset": 1613 - }, - "roles": [ - "Name" - ], - "token": "HasAll", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 27, - "end_line": 54, - "end_offset": 1625, - "start_col": 22, - "start_line": 54, - "start_offset": 1620 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "pos": { - "end_col": 35, - "end_line": 54, - "end_offset": 1633, - "start_col": 31, - "start_line": 54, - "start_offset": 1629 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 35, - "end_line": 54, - "end_offset": 1633, - "start_col": 22, - "start_line": 54, - "start_offset": 1620 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 36, - "end_line": 54, - "end_offset": 1634, - "start_col": 21, - "start_line": 54, - "start_offset": 1619 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 41, - "end_line": 54, - "end_offset": 1639, - "start_col": 37, - "start_line": 54, - "start_offset": 1635 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 55, - "end_offset": 1654, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 15, - "end_line": 55, - "end_offset": 1656, - "start_col": 14, - "start_line": 55, - "start_offset": 1655 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 16, - "end_line": 55, - "end_offset": 1657, - "start_col": 13, - "start_line": 55, - "start_offset": 1654 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 16, - "end_line": 55, - "end_offset": 1657, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 23, - "end_line": 55, - "end_offset": 1664, - "start_col": 17, - "start_line": 55, - "start_offset": 1658 - }, - "roles": [ - "Name" - ], - "token": "HasAll", - "type": "Identifier" - } - ], - "pos": { - "end_col": 23, - "end_line": 55, - "end_offset": 1664, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [], - "token": "cast(s).HasAll", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 29, - "end_line": 55, - "end_offset": 1670, - "start_col": 24, - "start_line": 55, - "start_offset": 1665 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "pos": { - "end_col": 32, - "end_line": 55, - "end_offset": 1673, - "start_col": 24, - "start_line": 55, - "start_offset": 1665 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 23, - "start_line": 55, - "start_offset": 1664 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [ - "Call" - ], - "token": "cast(s).HasAll(items...)", - "type": "Call" - } - ], - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 9, - "start_line": 55, - "start_offset": 1650 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 55, - "end_offset": 1674, - "start_col": 2, - "start_line": 55, - "start_offset": 1643 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).HasAll(items...)", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 56, - "end_offset": 1676, - "start_col": 42, - "start_line": 54, - "start_offset": 1640 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).HasAll(items...)\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 56, - "end_offset": 1676, - "start_col": 1, - "start_line": 54, - "start_offset": 1599 - }, - "props": { - "name": "HasAll" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 59, - "end_offset": 1747, - "start_col": 7, - "start_line": 59, - "start_offset": 1746 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 59, - "end_offset": 1752, - "start_col": 9, - "start_line": 59, - "start_offset": 1748 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 59, - "end_offset": 1752, - "start_col": 7, - "start_line": 59, - "start_offset": 1746 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 59, - "end_offset": 1753, - "start_col": 6, - "start_line": 59, - "start_offset": 1745 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 21, - "end_line": 59, - "end_offset": 1760, - "start_col": 15, - "start_line": 59, - "start_offset": 1754 - }, - "roles": [ - "Name" - ], - "token": "HasAny", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 27, - "end_line": 59, - "end_offset": 1766, - "start_col": 22, - "start_line": 59, - "start_offset": 1761 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - }, - { - "pos": { - "end_col": 35, - "end_line": 59, - "end_offset": 1774, - "start_col": 31, - "start_line": 59, - "start_offset": 1770 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 35, - "end_line": 59, - "end_offset": 1774, - "start_col": 22, - "start_line": 59, - "start_offset": 1761 - }, - "props": { - "name": "items" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 36, - "end_line": 59, - "end_offset": 1775, - "start_col": 21, - "start_line": 59, - "start_offset": 1760 - }, - "roles": [ - "Parameter" - ], - "token": "(items ...byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 41, - "end_line": 59, - "end_offset": 1780, - "start_col": 37, - "start_line": 59, - "start_offset": 1776 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 60, - "end_offset": 1795, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 15, - "end_line": 60, - "end_offset": 1797, - "start_col": 14, - "start_line": 60, - "start_offset": 1796 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 16, - "end_line": 60, - "end_offset": 1798, - "start_col": 13, - "start_line": 60, - "start_offset": 1795 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 16, - "end_line": 60, - "end_offset": 1798, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 23, - "end_line": 60, - "end_offset": 1805, - "start_col": 17, - "start_line": 60, - "start_offset": 1799 - }, - "roles": [ - "Name" - ], - "token": "HasAny", - "type": "Identifier" - } - ], - "pos": { - "end_col": 23, - "end_line": 60, - "end_offset": 1805, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [], - "token": "cast(s).HasAny", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 29, - "end_line": 60, - "end_offset": 1811, - "start_col": 24, - "start_line": 60, - "start_offset": 1806 - }, - "roles": [ - "Name" - ], - "token": "items", - "type": "Identifier" - } - ], - "pos": { - "end_col": 32, - "end_line": 60, - "end_offset": 1814, - "start_col": 24, - "start_line": 60, - "start_offset": 1806 - }, - "roles": [], - "token": "items...", - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 23, - "start_line": 60, - "start_offset": 1805 - }, - "roles": [], - "token": "(items...)", - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [ - "Call" - ], - "token": "cast(s).HasAny(items...)", - "type": "Call" - } - ], - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 9, - "start_line": 60, - "start_offset": 1791 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 60, - "end_offset": 1815, - "start_col": 2, - "start_line": 60, - "start_offset": 1784 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).HasAny(items...)", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 61, - "end_offset": 1817, - "start_col": 42, - "start_line": 59, - "start_offset": 1781 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).HasAny(items...)\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 61, - "end_offset": 1817, - "start_col": 1, - "start_line": 59, - "start_offset": 1740 - }, - "props": { - "name": "HasAny" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 64, - "end_offset": 1889, - "start_col": 7, - "start_line": 64, - "start_offset": 1888 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 64, - "end_offset": 1894, - "start_col": 9, - "start_line": 64, - "start_offset": 1890 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 64, - "end_offset": 1894, - "start_col": 7, - "start_line": 64, - "start_offset": 1888 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 64, - "end_offset": 1895, - "start_col": 6, - "start_line": 64, - "start_offset": 1887 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 20, - "end_line": 64, - "end_offset": 1901, - "start_col": 15, - "start_line": 64, - "start_offset": 1896 - }, - "roles": [ - "Name" - ], - "token": "Clone", - "type": "Identifier" - }, - { - "pos": { - "end_col": 22, - "end_line": 64, - "end_offset": 1903, - "start_col": 20, - "start_line": 64, - "start_offset": 1901 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "pos": { - "end_col": 27, - "end_line": 64, - "end_offset": 1908, - "start_col": 23, - "start_line": 64, - "start_offset": 1904 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 65, - "end_offset": 1923, - "start_col": 9, - "start_line": 65, - "start_offset": 1919 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 65, - "end_offset": 1928, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 20, - "end_line": 65, - "end_offset": 1930, - "start_col": 19, - "start_line": 65, - "start_offset": 1929 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 21, - "end_line": 65, - "end_offset": 1931, - "start_col": 18, - "start_line": 65, - "start_offset": 1928 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 21, - "end_line": 65, - "end_offset": 1931, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 27, - "end_line": 65, - "end_offset": 1937, - "start_col": 22, - "start_line": 65, - "start_offset": 1932 - }, - "roles": [ - "Name" - ], - "token": "Clone", - "type": "Identifier" - } - ], - "pos": { - "end_col": 27, - "end_line": 65, - "end_offset": 1937, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [], - "token": "cast(s).Clone", - "type": "Synthetic" - }, - { - "pos": { - "end_col": 29, - "end_line": 65, - "end_offset": 1939, - "start_col": 27, - "start_line": 65, - "start_offset": 1937 - }, - "roles": [], - "token": "()", - "type": "List" - } - ], - "pos": { - "end_col": 29, - "end_line": 65, - "end_offset": 1939, - "start_col": 14, - "start_line": 65, - "start_offset": 1924 - }, - "roles": [ - "Call" - ], - "token": "cast(s).Clone()", - "type": "Call" - } - ], - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 13, - "start_line": 65, - "start_offset": 1923 - }, - "roles": [], - "token": "(cast(s).Clone())", - "type": "List" - } - ], - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 9, - "start_line": 65, - "start_offset": 1919 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s).Clone())", - "type": "Call" - } - ], - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 9, - "start_line": 65, - "start_offset": 1919 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 30, - "end_line": 65, - "end_offset": 1940, - "start_col": 2, - "start_line": 65, - "start_offset": 1912 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s).Clone())", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 66, - "end_offset": 1942, - "start_col": 28, - "start_line": 64, - "start_offset": 1909 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s).Clone())\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 66, - "end_offset": 1942, - "start_col": 1, - "start_line": 64, - "start_offset": 1882 - }, - "props": { - "name": "Clone" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 9, - "end_line": 74, - "end_offset": 2133, - "start_col": 7, - "start_line": 74, - "start_offset": 2131 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "pos": { - "end_col": 14, - "end_line": 74, - "end_offset": 2138, - "start_col": 10, - "start_line": 74, - "start_offset": 2134 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 14, - "end_line": 74, - "end_offset": 2138, - "start_col": 7, - "start_line": 74, - "start_offset": 2131 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 15, - "end_line": 74, - "end_offset": 2139, - "start_col": 6, - "start_line": 74, - "start_offset": 2130 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 26, - "end_line": 74, - "end_offset": 2150, - "start_col": 16, - "start_line": 74, - "start_offset": 2140 - }, - "roles": [ - "Name" - ], - "token": "Difference", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 29, - "end_line": 74, - "end_offset": 2153, - "start_col": 27, - "start_line": 74, - "start_offset": 2151 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "pos": { - "end_col": 34, - "end_line": 74, - "end_offset": 2158, - "start_col": 30, - "start_line": 74, - "start_offset": 2154 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 34, - "end_line": 74, - "end_offset": 2158, - "start_col": 27, - "start_line": 74, - "start_offset": 2151 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 35, - "end_line": 74, - "end_offset": 2159, - "start_col": 26, - "start_line": 74, - "start_offset": 2150 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 40, - "end_line": 74, - "end_offset": 2164, - "start_col": 36, - "start_line": 74, - "start_offset": 2160 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 75, - "end_offset": 2179, - "start_col": 9, - "start_line": 75, - "start_offset": 2175 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 75, - "end_offset": 2184, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 21, - "end_line": 75, - "end_offset": 2187, - "start_col": 19, - "start_line": 75, - "start_offset": 2185 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "pos": { - "end_col": 22, - "end_line": 75, - "end_offset": 2188, - "start_col": 18, - "start_line": 75, - "start_offset": 2184 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "pos": { - "end_col": 22, - "end_line": 75, - "end_offset": 2188, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "pos": { - "end_col": 33, - "end_line": 75, - "end_offset": 2199, - "start_col": 23, - "start_line": 75, - "start_offset": 2189 - }, - "roles": [ - "Name" - ], - "token": "Difference", - "type": "Identifier" - } - ], - "pos": { - "end_col": 33, - "end_line": 75, - "end_offset": 2199, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [], - "token": "cast(s1).Difference", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 38, - "end_line": 75, - "end_offset": 2204, - "start_col": 34, - "start_line": 75, - "start_offset": 2200 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 41, - "end_line": 75, - "end_offset": 2207, - "start_col": 39, - "start_line": 75, - "start_offset": 2205 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "pos": { - "end_col": 42, - "end_line": 75, - "end_offset": 2208, - "start_col": 38, - "start_line": 75, - "start_offset": 2204 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "pos": { - "end_col": 42, - "end_line": 75, - "end_offset": 2208, - "start_col": 34, - "start_line": 75, - "start_offset": 2200 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "pos": { - "end_col": 43, - "end_line": 75, - "end_offset": 2209, - "start_col": 33, - "start_line": 75, - "start_offset": 2199 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "pos": { - "end_col": 43, - "end_line": 75, - "end_offset": 2209, - "start_col": 14, - "start_line": 75, - "start_offset": 2180 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Difference(cast(s2))", - "type": "Call" - } - ], - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 13, - "start_line": 75, - "start_offset": 2179 - }, - "roles": [], - "token": "(cast(s1).Difference(cast(s2)))", - "type": "List" - } - ], - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 9, - "start_line": 75, - "start_offset": 2175 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).Difference(cast(s2)))", - "type": "Call" - } - ], - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 9, - "start_line": 75, - "start_offset": 2175 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 44, - "end_line": 75, - "end_offset": 2210, - "start_col": 2, - "start_line": 75, - "start_offset": 2168 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).Difference(cast(s2)))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 76, - "end_offset": 2212, - "start_col": 41, - "start_line": 74, - "start_offset": 2165 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).Difference(cast(s2)))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 76, - "end_offset": 2212, - "start_col": 1, - "start_line": 74, - "start_offset": 2125 - }, - "props": { - "name": "Difference" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 9, - "end_line": 84, - "end_offset": 2487, - "start_col": 7, - "start_line": 84, - "start_offset": 2485 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "pos": { - "end_col": 14, - "end_line": 84, - "end_offset": 2492, - "start_col": 10, - "start_line": 84, - "start_offset": 2488 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 14, - "end_line": 84, - "end_offset": 2492, - "start_col": 7, - "start_line": 84, - "start_offset": 2485 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 15, - "end_line": 84, - "end_offset": 2493, - "start_col": 6, - "start_line": 84, - "start_offset": 2484 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 35, - "end_line": 84, - "end_offset": 2513, - "start_col": 16, - "start_line": 84, - "start_offset": 2494 - }, - "roles": [ - "Name" - ], - "token": "SymmetricDifference", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 38, - "end_line": 84, - "end_offset": 2516, - "start_col": 36, - "start_line": 84, - "start_offset": 2514 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "pos": { - "end_col": 43, - "end_line": 84, - "end_offset": 2521, - "start_col": 39, - "start_line": 84, - "start_offset": 2517 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 43, - "end_line": 84, - "end_offset": 2521, - "start_col": 36, - "start_line": 84, - "start_offset": 2514 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 44, - "end_line": 84, - "end_offset": 2522, - "start_col": 35, - "start_line": 84, - "start_offset": 2513 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 49, - "end_line": 84, - "end_offset": 2527, - "start_col": 45, - "start_line": 84, - "start_offset": 2523 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 85, - "end_offset": 2542, - "start_col": 9, - "start_line": 85, - "start_offset": 2538 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 85, - "end_offset": 2547, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 21, - "end_line": 85, - "end_offset": 2550, - "start_col": 19, - "start_line": 85, - "start_offset": 2548 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "pos": { - "end_col": 22, - "end_line": 85, - "end_offset": 2551, - "start_col": 18, - "start_line": 85, - "start_offset": 2547 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "pos": { - "end_col": 22, - "end_line": 85, - "end_offset": 2551, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "pos": { - "end_col": 42, - "end_line": 85, - "end_offset": 2571, - "start_col": 23, - "start_line": 85, - "start_offset": 2552 - }, - "roles": [ - "Name" - ], - "token": "SymmetricDifference", - "type": "Identifier" - } - ], - "pos": { - "end_col": 42, - "end_line": 85, - "end_offset": 2571, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [], - "token": "cast(s1).SymmetricDifference", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 47, - "end_line": 85, - "end_offset": 2576, - "start_col": 43, - "start_line": 85, - "start_offset": 2572 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 50, - "end_line": 85, - "end_offset": 2579, - "start_col": 48, - "start_line": 85, - "start_offset": 2577 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "pos": { - "end_col": 51, - "end_line": 85, - "end_offset": 2580, - "start_col": 47, - "start_line": 85, - "start_offset": 2576 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "pos": { - "end_col": 51, - "end_line": 85, - "end_offset": 2580, - "start_col": 43, - "start_line": 85, - "start_offset": 2572 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "pos": { - "end_col": 52, - "end_line": 85, - "end_offset": 2581, - "start_col": 42, - "start_line": 85, - "start_offset": 2571 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "pos": { - "end_col": 52, - "end_line": 85, - "end_offset": 2581, - "start_col": 14, - "start_line": 85, - "start_offset": 2543 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).SymmetricDifference(cast(s2))", - "type": "Call" - } - ], - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 13, - "start_line": 85, - "start_offset": 2542 - }, - "roles": [], - "token": "(cast(s1).SymmetricDifference(cast(s2)))", - "type": "List" - } - ], - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 9, - "start_line": 85, - "start_offset": 2538 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).SymmetricDifference(cast(s2)))", - "type": "Call" - } - ], - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 9, - "start_line": 85, - "start_offset": 2538 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 53, - "end_line": 85, - "end_offset": 2582, - "start_col": 2, - "start_line": 85, - "start_offset": 2531 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).SymmetricDifference(cast(s2)))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 86, - "end_offset": 2584, - "start_col": 50, - "start_line": 84, - "start_offset": 2528 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).SymmetricDifference(cast(s2)))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 86, - "end_offset": 2584, - "start_col": 1, - "start_line": 84, - "start_offset": 2479 - }, - "props": { - "name": "SymmetricDifference" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 9, - "end_line": 94, - "end_offset": 2782, - "start_col": 7, - "start_line": 94, - "start_offset": 2780 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "pos": { - "end_col": 14, - "end_line": 94, - "end_offset": 2787, - "start_col": 10, - "start_line": 94, - "start_offset": 2783 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 14, - "end_line": 94, - "end_offset": 2787, - "start_col": 7, - "start_line": 94, - "start_offset": 2780 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 15, - "end_line": 94, - "end_offset": 2788, - "start_col": 6, - "start_line": 94, - "start_offset": 2779 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 21, - "end_line": 94, - "end_offset": 2794, - "start_col": 16, - "start_line": 94, - "start_offset": 2789 - }, - "roles": [ - "Name" - ], - "token": "Union", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 24, - "end_line": 94, - "end_offset": 2797, - "start_col": 22, - "start_line": 94, - "start_offset": 2795 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "pos": { - "end_col": 29, - "end_line": 94, - "end_offset": 2802, - "start_col": 25, - "start_line": 94, - "start_offset": 2798 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 29, - "end_line": 94, - "end_offset": 2802, - "start_col": 22, - "start_line": 94, - "start_offset": 2795 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 30, - "end_line": 94, - "end_offset": 2803, - "start_col": 21, - "start_line": 94, - "start_offset": 2794 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 35, - "end_line": 94, - "end_offset": 2808, - "start_col": 31, - "start_line": 94, - "start_offset": 2804 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 95, - "end_offset": 2823, - "start_col": 9, - "start_line": 95, - "start_offset": 2819 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 95, - "end_offset": 2828, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 21, - "end_line": 95, - "end_offset": 2831, - "start_col": 19, - "start_line": 95, - "start_offset": 2829 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "pos": { - "end_col": 22, - "end_line": 95, - "end_offset": 2832, - "start_col": 18, - "start_line": 95, - "start_offset": 2828 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "pos": { - "end_col": 22, - "end_line": 95, - "end_offset": 2832, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "pos": { - "end_col": 28, - "end_line": 95, - "end_offset": 2838, - "start_col": 23, - "start_line": 95, - "start_offset": 2833 - }, - "roles": [ - "Name" - ], - "token": "Union", - "type": "Identifier" - } - ], - "pos": { - "end_col": 28, - "end_line": 95, - "end_offset": 2838, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [], - "token": "cast(s1).Union", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 33, - "end_line": 95, - "end_offset": 2843, - "start_col": 29, - "start_line": 95, - "start_offset": 2839 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 36, - "end_line": 95, - "end_offset": 2846, - "start_col": 34, - "start_line": 95, - "start_offset": 2844 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "pos": { - "end_col": 37, - "end_line": 95, - "end_offset": 2847, - "start_col": 33, - "start_line": 95, - "start_offset": 2843 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "pos": { - "end_col": 37, - "end_line": 95, - "end_offset": 2847, - "start_col": 29, - "start_line": 95, - "start_offset": 2839 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "pos": { - "end_col": 38, - "end_line": 95, - "end_offset": 2848, - "start_col": 28, - "start_line": 95, - "start_offset": 2838 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "pos": { - "end_col": 38, - "end_line": 95, - "end_offset": 2848, - "start_col": 14, - "start_line": 95, - "start_offset": 2824 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Union(cast(s2))", - "type": "Call" - } - ], - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 13, - "start_line": 95, - "start_offset": 2823 - }, - "roles": [], - "token": "(cast(s1).Union(cast(s2)))", - "type": "List" - } - ], - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 9, - "start_line": 95, - "start_offset": 2819 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).Union(cast(s2)))", - "type": "Call" - } - ], - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 9, - "start_line": 95, - "start_offset": 2819 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 39, - "end_line": 95, - "end_offset": 2849, - "start_col": 2, - "start_line": 95, - "start_offset": 2812 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).Union(cast(s2)))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 96, - "end_offset": 2851, - "start_col": 36, - "start_line": 94, - "start_offset": 2809 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).Union(cast(s2)))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 96, - "end_offset": 2851, - "start_col": 1, - "start_line": 94, - "start_offset": 2774 - }, - "props": { - "name": "Union" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 9, - "end_line": 103, - "end_offset": 3017, - "start_col": 7, - "start_line": 103, - "start_offset": 3015 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "pos": { - "end_col": 14, - "end_line": 103, - "end_offset": 3022, - "start_col": 10, - "start_line": 103, - "start_offset": 3018 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 14, - "end_line": 103, - "end_offset": 3022, - "start_col": 7, - "start_line": 103, - "start_offset": 3015 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 15, - "end_line": 103, - "end_offset": 3023, - "start_col": 6, - "start_line": 103, - "start_offset": 3014 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 28, - "end_line": 103, - "end_offset": 3036, - "start_col": 16, - "start_line": 103, - "start_offset": 3024 - }, - "roles": [ - "Name" - ], - "token": "Intersection", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 31, - "end_line": 103, - "end_offset": 3039, - "start_col": 29, - "start_line": 103, - "start_offset": 3037 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "pos": { - "end_col": 36, - "end_line": 103, - "end_offset": 3044, - "start_col": 32, - "start_line": 103, - "start_offset": 3040 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 36, - "end_line": 103, - "end_offset": 3044, - "start_col": 29, - "start_line": 103, - "start_offset": 3037 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 37, - "end_line": 103, - "end_offset": 3045, - "start_col": 28, - "start_line": 103, - "start_offset": 3036 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 42, - "end_line": 103, - "end_offset": 3050, - "start_col": 38, - "start_line": 103, - "start_offset": 3046 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 104, - "end_offset": 3065, - "start_col": 9, - "start_line": 104, - "start_offset": 3061 - }, - "roles": [ - "Name" - ], - "token": "Byte", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 104, - "end_offset": 3070, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 21, - "end_line": 104, - "end_offset": 3073, - "start_col": 19, - "start_line": 104, - "start_offset": 3071 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "pos": { - "end_col": 22, - "end_line": 104, - "end_offset": 3074, - "start_col": 18, - "start_line": 104, - "start_offset": 3070 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "pos": { - "end_col": 22, - "end_line": 104, - "end_offset": 3074, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "pos": { - "end_col": 35, - "end_line": 104, - "end_offset": 3087, - "start_col": 23, - "start_line": 104, - "start_offset": 3075 - }, - "roles": [ - "Name" - ], - "token": "Intersection", - "type": "Identifier" - } - ], - "pos": { - "end_col": 35, - "end_line": 104, - "end_offset": 3087, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [], - "token": "cast(s1).Intersection", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 40, - "end_line": 104, - "end_offset": 3092, - "start_col": 36, - "start_line": 104, - "start_offset": 3088 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 43, - "end_line": 104, - "end_offset": 3095, - "start_col": 41, - "start_line": 104, - "start_offset": 3093 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "pos": { - "end_col": 44, - "end_line": 104, - "end_offset": 3096, - "start_col": 40, - "start_line": 104, - "start_offset": 3092 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "pos": { - "end_col": 44, - "end_line": 104, - "end_offset": 3096, - "start_col": 36, - "start_line": 104, - "start_offset": 3088 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "pos": { - "end_col": 45, - "end_line": 104, - "end_offset": 3097, - "start_col": 35, - "start_line": 104, - "start_offset": 3087 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "pos": { - "end_col": 45, - "end_line": 104, - "end_offset": 3097, - "start_col": 14, - "start_line": 104, - "start_offset": 3066 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Intersection(cast(s2))", - "type": "Call" - } - ], - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 13, - "start_line": 104, - "start_offset": 3065 - }, - "roles": [], - "token": "(cast(s1).Intersection(cast(s2)))", - "type": "List" - } - ], - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 9, - "start_line": 104, - "start_offset": 3061 - }, - "roles": [ - "Call" - ], - "token": "Byte(cast(s1).Intersection(cast(s2)))", - "type": "Call" - } - ], - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 9, - "start_line": 104, - "start_offset": 3061 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 46, - "end_line": 104, - "end_offset": 3098, - "start_col": 2, - "start_line": 104, - "start_offset": 3054 - }, - "roles": [ - "Return" - ], - "token": "return Byte(cast(s1).Intersection(cast(s2)))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 105, - "end_offset": 3100, - "start_col": 43, - "start_line": 103, - "start_offset": 3051 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn Byte(cast(s1).Intersection(cast(s2)))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 105, - "end_offset": 3100, - "start_col": 1, - "start_line": 103, - "start_offset": 3009 - }, - "props": { - "name": "Intersection" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 9, - "end_line": 108, - "end_offset": 3176, - "start_col": 7, - "start_line": 108, - "start_offset": 3174 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "pos": { - "end_col": 14, - "end_line": 108, - "end_offset": 3181, - "start_col": 10, - "start_line": 108, - "start_offset": 3177 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 14, - "end_line": 108, - "end_offset": 3181, - "start_col": 7, - "start_line": 108, - "start_offset": 3174 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 15, - "end_line": 108, - "end_offset": 3182, - "start_col": 6, - "start_line": 108, - "start_offset": 3173 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 26, - "end_line": 108, - "end_offset": 3193, - "start_col": 16, - "start_line": 108, - "start_offset": 3183 - }, - "roles": [ - "Name" - ], - "token": "IsSuperset", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 29, - "end_line": 108, - "end_offset": 3196, - "start_col": 27, - "start_line": 108, - "start_offset": 3194 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "pos": { - "end_col": 34, - "end_line": 108, - "end_offset": 3201, - "start_col": 30, - "start_line": 108, - "start_offset": 3197 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 34, - "end_line": 108, - "end_offset": 3201, - "start_col": 27, - "start_line": 108, - "start_offset": 3194 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 35, - "end_line": 108, - "end_offset": 3202, - "start_col": 26, - "start_line": 108, - "start_offset": 3193 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 40, - "end_line": 108, - "end_offset": 3207, - "start_col": 36, - "start_line": 108, - "start_offset": 3203 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 109, - "end_offset": 3222, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 16, - "end_line": 109, - "end_offset": 3225, - "start_col": 14, - "start_line": 109, - "start_offset": 3223 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "pos": { - "end_col": 17, - "end_line": 109, - "end_offset": 3226, - "start_col": 13, - "start_line": 109, - "start_offset": 3222 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "pos": { - "end_col": 17, - "end_line": 109, - "end_offset": 3226, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "pos": { - "end_col": 28, - "end_line": 109, - "end_offset": 3237, - "start_col": 18, - "start_line": 109, - "start_offset": 3227 - }, - "roles": [ - "Name" - ], - "token": "IsSuperset", - "type": "Identifier" - } - ], - "pos": { - "end_col": 28, - "end_line": 109, - "end_offset": 3237, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [], - "token": "cast(s1).IsSuperset", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 33, - "end_line": 109, - "end_offset": 3242, - "start_col": 29, - "start_line": 109, - "start_offset": 3238 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 36, - "end_line": 109, - "end_offset": 3245, - "start_col": 34, - "start_line": 109, - "start_offset": 3243 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "pos": { - "end_col": 37, - "end_line": 109, - "end_offset": 3246, - "start_col": 33, - "start_line": 109, - "start_offset": 3242 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "pos": { - "end_col": 37, - "end_line": 109, - "end_offset": 3246, - "start_col": 29, - "start_line": 109, - "start_offset": 3238 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 28, - "start_line": 109, - "start_offset": 3237 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).IsSuperset(cast(s2))", - "type": "Call" - } - ], - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 9, - "start_line": 109, - "start_offset": 3218 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 38, - "end_line": 109, - "end_offset": 3247, - "start_col": 2, - "start_line": 109, - "start_offset": 3211 - }, - "roles": [ - "Return" - ], - "token": "return cast(s1).IsSuperset(cast(s2))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 110, - "end_offset": 3249, - "start_col": 41, - "start_line": 108, - "start_offset": 3208 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s1).IsSuperset(cast(s2))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 110, - "end_offset": 3249, - "start_col": 1, - "start_line": 108, - "start_offset": 3168 - }, - "props": { - "name": "IsSuperset" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 9, - "end_line": 115, - "end_offset": 3447, - "start_col": 7, - "start_line": 115, - "start_offset": 3445 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - }, - { - "pos": { - "end_col": 14, - "end_line": 115, - "end_offset": 3452, - "start_col": 10, - "start_line": 115, - "start_offset": 3448 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 14, - "end_line": 115, - "end_offset": 3452, - "start_col": 7, - "start_line": 115, - "start_offset": 3445 - }, - "props": { - "name": "s1" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 15, - "end_line": 115, - "end_offset": 3453, - "start_col": 6, - "start_line": 115, - "start_offset": 3444 - }, - "roles": [ - "Parameter" - ], - "token": "(s1 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 21, - "end_line": 115, - "end_offset": 3459, - "start_col": 16, - "start_line": 115, - "start_offset": 3454 - }, - "roles": [ - "Name" - ], - "token": "Equal", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 24, - "end_line": 115, - "end_offset": 3462, - "start_col": 22, - "start_line": 115, - "start_offset": 3460 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - }, - { - "pos": { - "end_col": 29, - "end_line": 115, - "end_offset": 3467, - "start_col": 25, - "start_line": 115, - "start_offset": 3463 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 29, - "end_line": 115, - "end_offset": 3467, - "start_col": 22, - "start_line": 115, - "start_offset": 3460 - }, - "props": { - "name": "s2" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 30, - "end_line": 115, - "end_offset": 3468, - "start_col": 21, - "start_line": 115, - "start_offset": 3459 - }, - "roles": [ - "Parameter" - ], - "token": "(s2 Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 35, - "end_line": 115, - "end_offset": 3473, - "start_col": 31, - "start_line": 115, - "start_offset": 3469 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 116, - "end_offset": 3488, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 16, - "end_line": 116, - "end_offset": 3491, - "start_col": 14, - "start_line": 116, - "start_offset": 3489 - }, - "roles": [ - "Name" - ], - "token": "s1", - "type": "Identifier" - } - ], - "pos": { - "end_col": 17, - "end_line": 116, - "end_offset": 3492, - "start_col": 13, - "start_line": 116, - "start_offset": 3488 - }, - "roles": [], - "token": "(s1)", - "type": "List" - } - ], - "pos": { - "end_col": 17, - "end_line": 116, - "end_offset": 3492, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [ - "Call" - ], - "token": "cast(s1)", - "type": "Call" - }, - { - "pos": { - "end_col": 23, - "end_line": 116, - "end_offset": 3498, - "start_col": 18, - "start_line": 116, - "start_offset": 3493 - }, - "roles": [ - "Name" - ], - "token": "Equal", - "type": "Identifier" - } - ], - "pos": { - "end_col": 23, - "end_line": 116, - "end_offset": 3498, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [], - "token": "cast(s1).Equal", - "type": "Synthetic" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 28, - "end_line": 116, - "end_offset": 3503, - "start_col": 24, - "start_line": 116, - "start_offset": 3499 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 31, - "end_line": 116, - "end_offset": 3506, - "start_col": 29, - "start_line": 116, - "start_offset": 3504 - }, - "roles": [ - "Name" - ], - "token": "s2", - "type": "Identifier" - } - ], - "pos": { - "end_col": 32, - "end_line": 116, - "end_offset": 3507, - "start_col": 28, - "start_line": 116, - "start_offset": 3503 - }, - "roles": [], - "token": "(s2)", - "type": "List" - } - ], - "pos": { - "end_col": 32, - "end_line": 116, - "end_offset": 3507, - "start_col": 24, - "start_line": 116, - "start_offset": 3499 - }, - "roles": [ - "Call" - ], - "token": "cast(s2)", - "type": "Call" - } - ], - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 23, - "start_line": 116, - "start_offset": 3498 - }, - "roles": [], - "token": "(cast(s2))", - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [ - "Call" - ], - "token": "cast(s1).Equal(cast(s2))", - "type": "Call" - } - ], - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 9, - "start_line": 116, - "start_offset": 3484 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 33, - "end_line": 116, - "end_offset": 3508, - "start_col": 2, - "start_line": 116, - "start_offset": 3477 - }, - "roles": [ - "Return" - ], - "token": "return cast(s1).Equal(cast(s2))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 117, - "end_offset": 3510, - "start_col": 36, - "start_line": 115, - "start_offset": 3474 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s1).Equal(cast(s2))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 117, - "end_offset": 3510, - "start_col": 1, - "start_line": 115, - "start_offset": 3439 - }, - "props": { - "name": "Equal" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 120, - "end_offset": 3572, - "start_col": 7, - "start_line": 120, - "start_offset": 3571 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 120, - "end_offset": 3577, - "start_col": 9, - "start_line": 120, - "start_offset": 3573 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 120, - "end_offset": 3577, - "start_col": 7, - "start_line": 120, - "start_offset": 3571 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 120, - "end_offset": 3578, - "start_col": 6, - "start_line": 120, - "start_offset": 3570 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 19, - "end_line": 120, - "end_offset": 3583, - "start_col": 15, - "start_line": 120, - "start_offset": 3579 - }, - "roles": [ - "Name" - ], - "token": "List", - "type": "Identifier" - }, - { - "pos": { - "end_col": 21, - "end_line": 120, - "end_offset": 3585, - "start_col": 19, - "start_line": 120, - "start_offset": 3583 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "children": [ - { - "pos": { - "end_col": 28, - "end_line": 120, - "end_offset": 3592, - "start_col": 24, - "start_line": 120, - "start_offset": 3588 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 28, - "end_line": 120, - "end_offset": 3592, - "start_col": 22, - "start_line": 120, - "start_offset": 3586 - }, - "roles": [], - "token": "[]byte", - "type": "Slice" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 121, - "end_offset": 3607, - "start_col": 9, - "start_line": 121, - "start_offset": 3603 - }, - "roles": [ - "Name" - ], - "token": "List", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 18, - "end_line": 121, - "end_offset": 3612, - "start_col": 14, - "start_line": 121, - "start_offset": 3608 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 20, - "end_line": 121, - "end_offset": 3614, - "start_col": 19, - "start_line": 121, - "start_offset": 3613 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 21, - "end_line": 121, - "end_offset": 3615, - "start_col": 18, - "start_line": 121, - "start_offset": 3612 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 21, - "end_line": 121, - "end_offset": 3615, - "start_col": 14, - "start_line": 121, - "start_offset": 3608 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - } - ], - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 13, - "start_line": 121, - "start_offset": 3607 - }, - "roles": [], - "token": "(cast(s))", - "type": "List" - } - ], - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 9, - "start_line": 121, - "start_offset": 3603 - }, - "roles": [ - "Call" - ], - "token": "List(cast(s))", - "type": "Call" - } - ], - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 9, - "start_line": 121, - "start_offset": 3603 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 22, - "end_line": 121, - "end_offset": 3616, - "start_col": 2, - "start_line": 121, - "start_offset": 3596 - }, - "roles": [ - "Return" - ], - "token": "return List(cast(s))", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 122, - "end_offset": 3618, - "start_col": 29, - "start_line": 120, - "start_offset": 3593 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn List(cast(s))\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 122, - "end_offset": 3618, - "start_col": 1, - "start_line": 120, - "start_offset": 3565 - }, - "props": { - "name": "List" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 125, - "end_offset": 3692, - "start_col": 7, - "start_line": 125, - "start_offset": 3691 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 125, - "end_offset": 3697, - "start_col": 9, - "start_line": 125, - "start_offset": 3693 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 125, - "end_offset": 3697, - "start_col": 7, - "start_line": 125, - "start_offset": 3691 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 125, - "end_offset": 3698, - "start_col": 6, - "start_line": 125, - "start_offset": 3690 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 27, - "end_line": 125, - "end_offset": 3711, - "start_col": 15, - "start_line": 125, - "start_offset": 3699 - }, - "roles": [ - "Name" - ], - "token": "UnsortedList", - "type": "Identifier" - }, - { - "pos": { - "end_col": 29, - "end_line": 125, - "end_offset": 3713, - "start_col": 27, - "start_line": 125, - "start_offset": 3711 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "children": [ - { - "pos": { - "end_col": 36, - "end_line": 125, - "end_offset": 3720, - "start_col": 32, - "start_line": 125, - "start_offset": 3716 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 36, - "end_line": 125, - "end_offset": 3720, - "start_col": 30, - "start_line": 125, - "start_offset": 3714 - }, - "roles": [], - "token": "[]byte", - "type": "Slice" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 126, - "end_offset": 3735, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 15, - "end_line": 126, - "end_offset": 3737, - "start_col": 14, - "start_line": 126, - "start_offset": 3736 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 16, - "end_line": 126, - "end_offset": 3738, - "start_col": 13, - "start_line": 126, - "start_offset": 3735 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 16, - "end_line": 126, - "end_offset": 3738, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 29, - "end_line": 126, - "end_offset": 3751, - "start_col": 17, - "start_line": 126, - "start_offset": 3739 - }, - "roles": [ - "Name" - ], - "token": "UnsortedList", - "type": "Identifier" - } - ], - "pos": { - "end_col": 29, - "end_line": 126, - "end_offset": 3751, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [], - "token": "cast(s).UnsortedList", - "type": "Synthetic" - }, - { - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 29, - "start_line": 126, - "start_offset": 3751 - }, - "roles": [], - "token": "()", - "type": "List" - } - ], - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [ - "Call" - ], - "token": "cast(s).UnsortedList()", - "type": "Call" - } - ], - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 9, - "start_line": 126, - "start_offset": 3731 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 31, - "end_line": 126, - "end_offset": 3753, - "start_col": 2, - "start_line": 126, - "start_offset": 3724 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).UnsortedList()", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 127, - "end_offset": 3755, - "start_col": 37, - "start_line": 125, - "start_offset": 3721 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).UnsortedList()\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 127, - "end_offset": 3755, - "start_col": 1, - "start_line": 125, - "start_offset": 3685 - }, - "props": { - "name": "UnsortedList" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 130, - "end_offset": 3813, - "start_col": 7, - "start_line": 130, - "start_offset": 3812 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 130, - "end_offset": 3818, - "start_col": 9, - "start_line": 130, - "start_offset": 3814 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 130, - "end_offset": 3818, - "start_col": 7, - "start_line": 130, - "start_offset": 3812 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 130, - "end_offset": 3819, - "start_col": 6, - "start_line": 130, - "start_offset": 3811 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 21, - "end_line": 130, - "end_offset": 3826, - "start_col": 15, - "start_line": 130, - "start_offset": 3820 - }, - "roles": [ - "Name" - ], - "token": "PopAny", - "type": "Identifier" - }, - { - "pos": { - "end_col": 23, - "end_line": 130, - "end_offset": 3828, - "start_col": 21, - "start_line": 130, - "start_offset": 3826 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 29, - "end_line": 130, - "end_offset": 3834, - "start_col": 25, - "start_line": 130, - "start_offset": 3830 - }, - "roles": [ - "Name", - "Type" - ], - "token": "byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 29, - "end_line": 130, - "end_offset": 3834, - "start_col": 25, - "start_line": 130, - "start_offset": 3830 - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - }, - { - "children": [ - { - "pos": { - "end_col": 35, - "end_line": 130, - "end_offset": 3840, - "start_col": 31, - "start_line": 130, - "start_offset": 3836 - }, - "roles": [ - "Name", - "Type" - ], - "token": "bool", - "type": "Identifier" - } - ], - "pos": { - "end_col": 35, - "end_line": 130, - "end_offset": 3840, - "start_col": 31, - "start_line": 130, - "start_offset": 3836 - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 36, - "end_line": 130, - "end_offset": 3841, - "start_col": 24, - "start_line": 130, - "start_offset": 3829 - }, - "roles": [ - "Parameter" - ], - "token": "(byte, bool)", - "type": "Parameter" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 13, - "end_line": 131, - "end_offset": 3856, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [ - "Name" - ], - "token": "cast", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 15, - "end_line": 131, - "end_offset": 3858, - "start_col": 14, - "start_line": 131, - "start_offset": 3857 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 16, - "end_line": 131, - "end_offset": 3859, - "start_col": 13, - "start_line": 131, - "start_offset": 3856 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 16, - "end_line": 131, - "end_offset": 3859, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [ - "Call" - ], - "token": "cast(s)", - "type": "Call" - }, - { - "pos": { - "end_col": 23, - "end_line": 131, - "end_offset": 3866, - "start_col": 17, - "start_line": 131, - "start_offset": 3860 - }, - "roles": [ - "Name" - ], - "token": "PopAny", - "type": "Identifier" - } - ], - "pos": { - "end_col": 23, - "end_line": 131, - "end_offset": 3866, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [], - "token": "cast(s).PopAny", - "type": "Synthetic" - }, - { - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 23, - "start_line": 131, - "start_offset": 3866 - }, - "roles": [], - "token": "()", - "type": "List" - } - ], - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [ - "Call" - ], - "token": "cast(s).PopAny()", - "type": "Call" - } - ], - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 9, - "start_line": 131, - "start_offset": 3852 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 25, - "end_line": 131, - "end_offset": 3868, - "start_col": 2, - "start_line": 131, - "start_offset": 3845 - }, - "roles": [ - "Return" - ], - "token": "return cast(s).PopAny()", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 132, - "end_offset": 3870, - "start_col": 37, - "start_line": 130, - "start_offset": 3842 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn cast(s).PopAny()\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 132, - "end_offset": 3870, - "start_col": 1, - "start_line": 130, - "start_offset": 3806 - }, - "props": { - "name": "PopAny" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 8, - "end_line": 135, - "end_offset": 3915, - "start_col": 7, - "start_line": 135, - "start_offset": 3914 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - }, - { - "pos": { - "end_col": 13, - "end_line": 135, - "end_offset": 3920, - "start_col": 9, - "start_line": 135, - "start_offset": 3916 - }, - "roles": [ - "Name", - "Type" - ], - "token": "Byte", - "type": "Identifier" - } - ], - "pos": { - "end_col": 13, - "end_line": 135, - "end_offset": 3920, - "start_col": 7, - "start_line": 135, - "start_offset": 3914 - }, - "props": { - "name": "s" - }, - "roles": [ - "Parameter" - ], - "type": "Parameter" - } - ], - "pos": { - "end_col": 14, - "end_line": 135, - "end_offset": 3921, - "start_col": 6, - "start_line": 135, - "start_offset": 3913 - }, - "roles": [ - "Parameter" - ], - "token": "(s Byte)", - "type": "Parameter" - }, - { - "pos": { - "end_col": 18, - "end_line": 135, - "end_offset": 3925, - "start_col": 15, - "start_line": 135, - "start_offset": 3922 - }, - "roles": [ - "Name" - ], - "token": "Len", - "type": "Identifier" - }, - { - "pos": { - "end_col": 20, - "end_line": 135, - "end_offset": 3927, - "start_col": 18, - "start_line": 135, - "start_offset": 3925 - }, - "roles": [ - "Parameter" - ], - "token": "()", - "type": "Parameter" - }, - { - "pos": { - "end_col": 24, - "end_line": 135, - "end_offset": 3931, - "start_col": 21, - "start_line": 135, - "start_offset": 3928 - }, - "roles": [ - "Name", - "Type" - ], - "token": "int", - "type": "Identifier" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "pos": { - "end_col": 12, - "end_line": 136, - "end_offset": 3945, - "start_col": 9, - "start_line": 136, - "start_offset": 3942 - }, - "roles": [ - "Name" - ], - "token": "len", - "type": "Identifier" - }, - { - "children": [ - { - "pos": { - "end_col": 14, - "end_line": 136, - "end_offset": 3947, - "start_col": 13, - "start_line": 136, - "start_offset": 3946 - }, - "roles": [ - "Name" - ], - "token": "s", - "type": "Identifier" - } - ], - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 12, - "start_line": 136, - "start_offset": 3945 - }, - "roles": [], - "token": "(s)", - "type": "List" - } - ], - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 9, - "start_line": 136, - "start_offset": 3942 - }, - "roles": [ - "Call" - ], - "token": "len(s)", - "type": "Call" - } - ], - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 9, - "start_line": 136, - "start_offset": 3942 - }, - "roles": [], - "type": "List" - } - ], - "pos": { - "end_col": 15, - "end_line": 136, - "end_offset": 3948, - "start_col": 2, - "start_line": 136, - "start_offset": 3935 - }, - "roles": [ - "Return" - ], - "token": "return len(s)", - "type": "Return" - } - ], - "pos": { - "end_col": 2, - "end_line": 137, - "end_offset": 3950, - "start_col": 25, - "start_line": 135, - "start_offset": 3932 - }, - "roles": [ - "Body" - ], - "token": "{\n\treturn len(s)\n}", - "type": "Block" - } - ], - "pos": { - "end_col": 2, - "end_line": 137, - "end_offset": 3950, - "start_col": 1, - "start_line": 135, - "start_offset": 3908 - }, - "props": { - "name": "Len" - }, - "roles": [ - "Function", - "Declaration", - "Member" - ], - "type": "Method" - } - ] -} diff --git a/tests/oss-coverage/run.sh b/tests/oss-coverage/run.sh index c9adad8c..279b5762 100755 --- a/tests/oss-coverage/run.sh +++ b/tests/oss-coverage/run.sh @@ -5,8 +5,7 @@ # (manifest.tsv), runs `codefang run -a static/complexity,static/cohesion # --per-file` over each, and prints a per-language function-detection matrix # via summarize.py. Zero-function rates flag uastmap gaps on real code that -# the synthetic corpus test (internal/analyzers/language_coverage_test.go) -# cannot catch. +# the synthetic fixtures in the crate unit tests cannot catch. # # Usage: # tests/oss-coverage/run.sh [workdir] From 78baa16efa10e456e21becd56cfe46047c4aeb46 Mon Sep 17 00:00:00 2001 From: Dmitriy <dmit8815@gmail.com> Date: Mon, 5 Oct 2026 19:38:55 +0300 Subject: [PATCH 2/7] docs: fix markdownlint findings in the analyzer and CLI pages Hard tabs in the Go examples become spaces, code spans with a trailing space are reworded, the global-flags heading follows its parent, a table cell no longer contains a literal pipe, and the people-dict example fence names its language. markdownlint-cli2 over the CI globs: 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiE8YcZ3GtgQW2bDbcgXo --- site/analyzers/halstead.md | 6 ++-- site/explanation/complexity.md | 56 +++++++++++++++++----------------- site/explanation/halstead.md | 6 ++-- site/guide/cli-reference.md | 6 ++-- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/site/analyzers/halstead.md b/site/analyzers/halstead.md index 0431a9b8..3328fb5f 100644 --- a/site/analyzers/halstead.md +++ b/site/analyzers/halstead.md @@ -57,7 +57,7 @@ and `--languages`. | leaf `Identifier` with the `Type` role | operator (type specifier) | its text | | any node with `props["operator"]` | operator | that property, and nothing else from the node | | `Call` / `Index` / `Slice` | operator | fixed `()` / `[]` / `[:]` | -| every other node | operator(s) | the node's *residue*: its source span with its children blanked (` := `, ` + `, `return `, `.`, `++`), split into symbol runs and whitelisted keywords | +| every other node | operator(s) | the node's *residue*: its source span with its children blanked (` := `, ` + `, `return` and its trailing space, `.`, `++`), split into symbol runs and whitelisted keywords | | `Parameter` (type or role), `Comment`, `DocString` | skipped | — | | the function's signature (every child before its body) | skipped | — | @@ -292,5 +292,5 @@ Distribution** by bucket. ## See also -- [Understanding Halstead metrics](../explanation/halstead.md) — the mental model, counting policy, aggregation and limitations. -- [Quick start](../getting-started/quickstart.md) — run static analysis. +* [Understanding Halstead metrics](../explanation/halstead.md) — the mental model, counting policy, aggregation and limitations. +* [Quick start](../getting-started/quickstart.md) — run static analysis. diff --git a/site/explanation/complexity.md b/site/explanation/complexity.md index 3f0e73df..6c2551f1 100644 --- a/site/explanation/complexity.md +++ b/site/explanation/complexity.md @@ -99,26 +99,26 @@ text; a multi-line token is never a name. ```go func decisions(a, b int, xs []int) int { - r := 0 - if a > 0 && b > 0 { // cyclomatic +1 (if) +1 (&&) | cognitive +1 (if) +1 (&& sequence) - r++ - } else if a < 0 || b < 0 { // cyclomatic +1 (if) +1 (||) | cognitive +1 (else if) +1 (|| sequence) - r-- - } else { // | cognitive +1 (else) - r = 2 - } - for _, x := range xs { // cyclomatic +1 | cognitive +1 - r += x - } - switch r { // | cognitive +1 - case 1: // cyclomatic +1 - r = 10 - case 2: // cyclomatic +1 - r = 20 - default: // nothing - r = 0 - } - return r + r := 0 + if a > 0 && b > 0 { // cyclomatic +1 (if) +1 (&&) | cognitive +1 (if) +1 (&& sequence) + r++ + } else if a < 0 || b < 0 { // cyclomatic +1 (if) +1 (||) | cognitive +1 (else if) +1 (|| sequence) + r-- + } else { // | cognitive +1 (else) + r = 2 + } + for _, x := range xs { // cyclomatic +1 | cognitive +1 + r += x + } + switch r { // | cognitive +1 + case 1: // cyclomatic +1 + r = 10 + case 2: // cyclomatic +1 + r = 20 + default: // nothing + r = 0 + } + return r } ``` @@ -129,14 +129,14 @@ Nesting changes the cognitive score but not the cyclomatic one: ```go func nested(a, b, c bool) int { - if a { // cognitive +1 (nesting 0) - if b { // cognitive +2 (nesting 1) - if c { // cognitive +3 (nesting 2) - return 1 - } - } - } - return 0 + if a { // cognitive +1 (nesting 0) + if b { // cognitive +2 (nesting 1) + if c { // cognitive +3 (nesting 2) + return 1 + } + } + } + return 0 } ``` diff --git a/site/explanation/halstead.md b/site/explanation/halstead.md index 134ca62a..4860ca34 100644 --- a/site/explanation/halstead.md +++ b/site/explanation/halstead.md @@ -38,8 +38,8 @@ on: variables, literals, names). From four counts it derives: ```go func hal(a int, b int) int { - c := a + b - return c * 2 + c := a + b + return c * 2 } ``` @@ -70,7 +70,7 @@ Codefang classifies UAST nodes inside each function body with one table operator of calls (`()`), indexing (`[]`) and slicing (`[:]`); otherwise the node's **residue** — its source text with every child blanked out. For `c := a + b` the declaration's residue is ` := ` and the binary - expression's is ` + `; for `return c * 2` it is `return `; for `r++` it + expression's is ` + `; for `return c * 2` it is `return` and its trailing space; for `r++` it is `++`. Residue is tokenized into symbol runs (always operators) and words (operators only when they are keywords: `return`, `if`, `for`, `let`, `int`, `new`, …). Brackets, commas, semicolons and a lone `:` are diff --git a/site/guide/cli-reference.md b/site/guide/cli-reference.md index f6d6b9b4..6be42d57 100644 --- a/site/guide/cli-reference.md +++ b/site/guide/cli-reference.md @@ -13,7 +13,7 @@ has no effect is a bug. The main analysis binary. It runs static and history analyzers against source code and Git repositories. -#### Global flags +### Global flags | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| @@ -333,7 +333,7 @@ codefang run -a '*' --extra-excluded-prefixes '.venv/,target/' . | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--people-dict` | `path` | `""` | File with one person per line, `|`-separated identity tokens (names and emails, matched case-insensitively). With a dictionary, an author matching no line is reported as `<unmatched>`. An unreadable file is an error. | +| `--people-dict` | `path` | `""` | File with one person per line, its identity tokens separated by a vertical bar (names and emails, matched case-insensitively). With a dictionary, an author matching no line is reported as `<unmatched>`. An unreadable file is an error. | | `--exact-signatures` | `bool` | `false` | Match the whole `name <email>` signature instead of merging authors that share a name **or** an email. Yields considerably more identities. | | `--anonymize` | `bool` | `false` | Replace developer names in every report with `Developer-A`, `Developer-B`, … | @@ -343,7 +343,7 @@ lowercased name, and a hit on one token registers the other: `alice <a@x>`, detector serves every history analyzer, so devs, burndown, couples, file-history and anomaly agree on author ids. -``` +```text # people.txt Alice|alice@example.com|alice@other.org Bob|bob@example.com From 171ade000ebd0cc12c6e6a82c27c32e64fe76875 Mon Sep 17 00:00:00 2001 From: codefang-dev <dev@codefang.invalid> Date: Sat, 10 Oct 2026 22:40:15 +0000 Subject: [PATCH 3/7] fix(report): schema matches emitted fields; warn on shallow history Full-report validation over 100 popular open-source repositories found two report-validity defects. - The schema registry described fields the reports did not emit for history/typos and history/imports, and omitted fields they did emit for history/burndown (`interactions`) and static/clones (`message`). The registry now declares exactly the report keys, matching site/analyzers. A new e2e contract test asserts schema keys equal report keys for every analyzer of a full run. - A shallow clone yields an empty history walk, so every history analyzer reported empty results with exit 0 and no explanation. The run still succeeds with the same empty report, and now prints one `warning: shallow clone` line naming `git fetch --unshallow`. Refs: site/how-to/run-in-ci.md, site/release-notes/unreleased.md Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx --- .../tests/report_schema_contract_e2e.rs | 103 +++++++++++++++++ bins/codefang/tests/shallow_history_e2e.rs | 105 ++++++++++++++++++ crates/cf-analyze/src/schema_registry.rs | 77 +++++++++---- crates/cf-commands/src/handlers.rs | 19 ++++ crates/cf-gitlib/src/repository.rs | 23 ++-- site/how-to/run-in-ci.md | 4 +- site/release-notes/unreleased.md | 2 + 7 files changed, 303 insertions(+), 30 deletions(-) create mode 100644 bins/codefang/tests/report_schema_contract_e2e.rs create mode 100644 bins/codefang/tests/shallow_history_e2e.rs diff --git a/bins/codefang/tests/report_schema_contract_e2e.rs b/bins/codefang/tests/report_schema_contract_e2e.rs new file mode 100644 index 00000000..7f4dc34c --- /dev/null +++ b/bins/codefang/tests/report_schema_contract_e2e.rs @@ -0,0 +1,103 @@ +//! End-to-end contract: the unified `codefang.run.v1` report describes every +//! field it emits. +//! +//! For each analyzer, the `schema` object declares exactly the keys of its +//! `report` object. A consumer that reads the schema to learn the report's +//! fields must never meet an undeclared field, nor a declared field the report +//! lacks. This suite runs the full analyzer selection (`-a '*'`) over a real +//! git fixture and checks both directions for every analyzer, so a renamed or +//! added report field without a schema-registry update fails here. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo}; +use std::collections::BTreeSet; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// 2021-01-01T12:00:00Z, the fixture's first commit. +const T0: i64 = 1_609_502_400; +/// One day. +const DAY: i64 = 24 * 3_600; +/// Analyzers a full selection emits (static + history). +const ANALYZER_COUNT: usize = 17; + +fn full_report(repo: &str) -> serde_json::Value { + let out = common::command(BIN) + .arg("run") + .args(["-a", "*", "--format", "json", repo]) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary"); + assert!( + out.status.success(), + "full run failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + serde_json::from_slice(&out.stdout).expect("stdout is one JSON document") +} + +fn keys(value: &serde_json::Value, what: &str) -> BTreeSet<String> { + value + .as_object() + .unwrap_or_else(|| panic!("{what} is not an object: {value}")) + .keys() + .cloned() + .collect() +} + +#[test] +fn every_analyzer_schema_declares_exactly_its_report_fields() { + let test = init_repo().expect("init repo"); + let path = test.path().to_str().expect("utf-8 path").to_string(); + commit_files( + &test, + "init", + T0, + &[ + ( + "main.go", + b"package main\n\nfunc add(a, b int) int {\n\treturn a + b\n}\n", + ), + ("util.py", b"def hi(x):\n return x + 1\n"), + ], + ) + .expect("first commit"); + commit_files( + &test, + "refactor", + T0 + DAY, + &[( + "main.go", + b"package main\n\nfunc add(a, b int) int {\n\tc := a + b\n\treturn c\n}\n", + )], + ) + .expect("second commit"); + + let doc = full_report(&path); + let analyzers = doc["analyzers"].as_array().expect("analyzers array"); + assert_eq!(analyzers.len(), ANALYZER_COUNT, "full selection analyzer count"); + // Collect every mismatch so one run lists them all, not just the first. + let mut mismatches = Vec::new(); + for analyzer in analyzers { + let id = analyzer["id"].as_str().expect("analyzer id"); + let schema = keys(&analyzer["schema"], &format!("{id} schema")); + let report = keys(&analyzer["report"], &format!("{id} report")); + let undeclared: Vec<_> = report.difference(&schema).collect(); + let absent: Vec<_> = schema.difference(&report).collect(); + if !undeclared.is_empty() || !absent.is_empty() { + mismatches.push(format!( + "{id}: report fields undeclared in schema {undeclared:?}; \ + schema fields absent from report {absent:?}" + )); + } + } + assert!( + mismatches.is_empty(), + "schema/report drift:\n{}", + mismatches.join("\n") + ); +} diff --git a/bins/codefang/tests/shallow_history_e2e.rs b/bins/codefang/tests/shallow_history_e2e.rs new file mode 100644 index 00000000..708635c5 --- /dev/null +++ b/bins/codefang/tests/shallow_history_e2e.rs @@ -0,0 +1,105 @@ +//! End-to-end coverage of history analysis on a shallow clone. +//! +//! libgit2 marks a shallow clone with `.git/shallow`, and `Repository::log` +//! yields no commits for such a repository (its history beyond the boundary +//! is absent, so the walk is not meaningful). Before this suite the history +//! analyzers then emitted their empty reports silently with exit 0. The +//! contract pinned here: the run still succeeds with the empty report, and +//! stderr carries one `warning:` line naming the cause and the remedy. A full +//! clone emits no such warning. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// 2021-01-01T12:00:00Z, the fixture's first commit. +const T0: i64 = 1_609_502_400; +/// One day. +const DAY: i64 = 24 * 3_600; +/// Commits in the fixture. +const COMMITS: usize = 3; +/// Stable prefix of the shallow-clone warning. +const SHALLOW_WARNING: &str = "warning: shallow clone"; + +/// A repository with `COMMITS` linear commits by one author. +fn three_commit_repo() -> TestRepo { + let test = init_repo().expect("init repo"); + for i in 0..COMMITS { + let body = format!("package main\n\nfunc f{i}() int {{\n\treturn {i}\n}}\n"); + commit_files( + &test, + &format!("commit {i}"), + T0 + DAY * i as i64, + &[("main.go", body.as_bytes())], + ) + .expect("commit"); + } + test +} + +/// Marks HEAD as the shallow boundary, the on-disk form of `git clone --depth 1`. +fn make_shallow(test: &TestRepo) { + let head = git2::Repository::open(test.path()) + .expect("open with git2") + .head() + .expect("head") + .target() + .expect("head target"); + std::fs::write(test.path().join(".git").join("shallow"), format!("{head}\n")) + .expect("write .git/shallow"); +} + +/// Runs `codefang run -a history/devs --format json` and returns +/// `(total_commits, stderr)`. +fn devs_total_commits(repo: &str) -> (i64, String) { + let out = common::command(BIN) + .arg("run") + .args(["-a", "history/devs", "--format", "json", repo]) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary"); + assert!( + out.status.success(), + "run must still succeed on a shallow clone: {}", + String::from_utf8_lossy(&out.stderr) + ); + let report: serde_json::Value = + serde_json::from_slice(&out.stdout).expect("stdout is one JSON document"); + let total = report["aggregate"]["total_commits"] + .as_i64() + .expect("total_commits"); + (total, String::from_utf8_lossy(&out.stderr).into_owned()) +} + +#[test] +fn full_clone_walks_every_commit_without_a_warning() { + let test = three_commit_repo(); + let (total, stderr) = devs_total_commits(test.path().to_str().expect("utf-8 path")); + assert_eq!(total, COMMITS as i64); + assert!( + !stderr.contains(SHALLOW_WARNING), + "a full clone must not warn: {stderr}" + ); +} + +#[test] +fn shallow_clone_warns_once_and_reports_no_commits() { + let test = three_commit_repo(); + make_shallow(&test); + let (total, stderr) = devs_total_commits(test.path().to_str().expect("utf-8 path")); + assert_eq!(total, 0, "a shallow walk yields no commits"); + assert_eq!( + stderr.matches(SHALLOW_WARNING).count(), + 1, + "exactly one shallow warning per run: {stderr}" + ); + assert!( + stderr.contains("git fetch --unshallow"), + "the warning names the remedy: {stderr}" + ); +} diff --git a/crates/cf-analyze/src/schema_registry.rs b/crates/cf-analyze/src/schema_registry.rs index e69792a0..b985173f 100644 --- a/crates/cf-analyze/src/schema_registry.rs +++ b/crates/cf-analyze/src/schema_registry.rs @@ -197,6 +197,14 @@ fn build_registry() -> BTreeMap<String, AnalyzerSchema> { "Fraction of functions involved in duplication", ), ), + ( + "message", + FieldMeta::new( + "scalar", + "", + "Duplication band message keyed off clone_ratio", + ), + ), ], ); schema( @@ -398,6 +406,14 @@ fn build_registry() -> BTreeMap<String, AnalyzerSchema> { "developer_survival", FieldMeta::new("list", "developer", "Per-developer survival data"), ), + ( + "interactions", + FieldMeta::new( + "list", + "pair", + "Lines each author lost to each modifier (filled with --burndown-people)", + ), + ), ( "aggregate", FieldMeta::new("aggregate", "", "Summary statistics"), @@ -421,34 +437,48 @@ fn build_registry() -> BTreeMap<String, AnalyzerSchema> { "history/imports", &[ ( - "import_list", - FieldMeta::new("list", "import", "Import statements (requires UAST mode)"), + "author_index", + FieldMeta::new("list", "author", "Developer labels by author id"), ), ( - "dependencies", - FieldMeta::new("list", "dependency", "Dependencies (requires UAST mode)"), + "imports", + FieldMeta::new( + "aggregate", + "author", + "Import counts by author, language, file and tick", + ), ), ( - "categories", - FieldMeta::new("aggregate", "", "Import category breakdown"), + "tick_size", + FieldMeta::new("scalar", "", "Tick width in seconds"), ), + ], + ); + schema( + "history/typos", + &[ ( "aggregate", FieldMeta::new("aggregate", "", "Summary statistics"), ), + ( + "typo_list", + FieldMeta::new( + "list", + "identifier", + "Every distinct wrong to correct pair, attributed to its first fix", + ), + ), + ( + "patterns", + FieldMeta::new("list", "pattern", "Wrong to correct pairs with frequency"), + ), + ( + "file_typos", + FieldMeta::new("list", "file", "Typos fixed per file"), + ), ], ); - schema( - "history/typos", - &[( - "typos", - FieldMeta::new( - "list", - "identifier", - "Detected identifier typos (requires UAST mode)", - ), - )], - ); reg } @@ -471,10 +501,17 @@ mod tests { } #[test] - fn typos_has_single_field() { + fn typos_declares_the_fields_the_report_emits() { let s = schema_for_analyzer("history/typos").expect("registered"); - assert_eq!(s.len(), 1); - assert!(s.contains_key("typos")); + let fields: Vec<&str> = s.keys().map(String::as_str).collect(); + assert_eq!(fields, ["aggregate", "file_typos", "patterns", "typo_list"]); + } + + #[test] + fn imports_history_declares_the_fields_the_report_emits() { + let s = schema_for_analyzer("history/imports").expect("registered"); + let fields: Vec<&str> = s.keys().map(String::as_str).collect(); + assert_eq!(fields, ["author_index", "imports", "tick_size"]); } #[test] diff --git a/crates/cf-commands/src/handlers.rs b/crates/cf-commands/src/handlers.rs index 412c9267..b009075e 100644 --- a/crates/cf-commands/src/handlers.rs +++ b/crates/cf-commands/src/handlers.rs @@ -581,6 +581,22 @@ pub struct HistoryWindow { pub since: SinceSpec, } +/// Warns, once per process, that a shallow clone has no walkable history. +/// +/// A shallow repository yields an empty window (see +/// [`cf_gitlib::Repository::log`]), so every history analyzer reports empty +/// results. The warning names the cause and the remedy so the empty report is +/// not mistaken for an analysis result. +fn warn_shallow_history_once() { + static WARNED: std::sync::Once = std::sync::Once::new(); + WARNED.call_once(|| { + eprintln!( + "warning: shallow clone: history analyzers see no commits; \ + run `git fetch --unshallow` for a full history" + ); + }); +} + /// The commit window selected by a [`HistoryWindow`]. /// /// Returns hashes in the **oldest-first** order the history analyzers consume @@ -610,6 +626,9 @@ pub fn load_history_commit_hashes( if window.head { return Some(vec![repo.head().ok()?]); } + if repo.is_shallow() { + warn_shallow_history_once(); + } let since_secs = match &window.since { SinceSpec::Inactive => None, diff --git a/crates/cf-gitlib/src/repository.rs b/crates/cf-gitlib/src/repository.rs index c7cb0738..68158688 100644 --- a/crates/cf-gitlib/src/repository.rs +++ b/crates/cf-gitlib/src/repository.rs @@ -218,6 +218,13 @@ impl Repository { Ok(RevWalk::new(walk, self)) } + /// Reports whether this is a shallow clone (`.git/shallow` lists its graft + /// boundary). A shallow repository's [`Self::log`] is exhausted at once. + #[must_use] + pub fn is_shallow(&self) -> bool { + self.repo.is_shallow() + } + /// Returns a commit iterator starting from HEAD. /// /// Pushes HEAD, applies `SortTime | SortTopological` (plus `SortReverse` @@ -226,19 +233,17 @@ impl Repository { /// part of the report contract; the topological order prevents diffing /// against a descendant (burndown integrity). /// + /// A shallow repository (see [`Self::is_shallow`]) yields an exhausted + /// iterator: the reference binary's libgit2 predates shallow-clone support + /// and fails the revwalk on the grafted commit's missing parents, so the + /// reference reports an empty history. The command layer surfaces this with + /// a warning rather than leaving the empty report unexplained. + /// /// # Errors /// /// Returns revwalk/HEAD errors on failure. pub fn log(&self, opts: &LogOptions) -> Result<CommitIter<'_>> { - // Shallow-clone parity with the reference binary: it links libgit2 1.5.0, - // which predates shallow-clone support, so its revwalk preparation hits the - // grafted commit's missing parent objects and fails on the first next(). - // The reference pipeline swallows that as an empty walk ("planning chunks - // ... commits=0 chunks=0") and every history analyzer emits its empty - // report. Our libgit2 honors `.git/shallow` and would happily walk the - // truncated history, so replicate the reference behavior: a shallow - // repository yields an already-exhausted commit iterator. - if self.repo.is_shallow() { + if self.is_shallow() { return Ok(CommitIter::empty(self)); } let mut walk = self.repo.revwalk().map_err(GitError::CreateRevwalk)?; diff --git a/site/how-to/run-in-ci.md b/site/how-to/run-in-ci.md index 739fd95d..7f6cd524 100644 --- a/site/how-to/run-in-ci.md +++ b/site/how-to/run-in-ci.md @@ -13,7 +13,9 @@ detects issues. - A repository hosted on GitHub (this guide uses the published GitHub Action). - Permission to add workflow files under `.github/workflows/`. - For history analyzers, the checkout step must fetch full history - (`fetch-depth: 0`); a shallow clone only sees the most recent commits. + (`fetch-depth: 0`). A shallow clone has no walkable history: the history + analyzers report empty results and print a `warning: shallow clone` line + on stderr. ## Steps diff --git a/site/release-notes/unreleased.md b/site/release-notes/unreleased.md index da089f69..107e33bc 100644 --- a/site/release-notes/unreleased.md +++ b/site/release-notes/unreleased.md @@ -80,6 +80,8 @@ This is the first release line, so there is no prior version to upgrade from. If ## Fixed +- Full-report schemas now declare exactly the fields each analyzer emits. `history/typos`, `history/imports`, `history/burndown` (`interactions`), and `static/clones` (`message`) had registry entries that named fields the reports did not contain, or omitted fields they did contain. +- A shallow clone no longer produces empty history reports silently. History analyzers still see no commits in a shallow repository, but the run prints one `warning: shallow clone` line on stderr naming the remedy (`git fetch --unshallow`). - Fixed a data race in `internal/framework.PipelineSampler`: `t1Captured` was a plain `bool` read by the sampler goroutine and written by the caller, causing intermittent races under `go test -race`. It is now a `sync/atomic.Bool` using `CompareAndSwap`, so at most one t1 heap profile is captured. Removed the unused `t0Captured` field. ## Security From 75a501fe4ebb06669129803352a87bc4c5194eec Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Wed, 14 Oct 2026 22:27:09 +0000 Subject: [PATCH 4/7] perf(static): skip parsing data files in function analyzers; cap parse size Full-report memory on large repositories was dominated by a few large data files. Alacritty's tests/ref fixtures include a 19 MB JSON file that needed over 3 GB to parse, and the full report was OOM-killed. - The function analyzers (complexity, halstead, cohesion, clones) no longer build a tree for data and config formats (JSON, YAML, TOML, INI, CSV, PSV, dotenv, XML, git and ssh config). Those files have no functions, so an empty file root gives the same zero-function report. Verified byte-identical per-file output against the previous binary on a fixture of every such format and on 12 real repositories (48 runs). alacritty's static/complexity dropped from 4.7 GB and 46 s to 36 MB and 2 s. - The UAST parse size limit is now 8 MiB by default (was 64 MiB). Larger source files are skipped with a warning that names the limit. The new --max-parse-file-size flag (MiB, >= 1) raises it. - StaticFilter::default() now carries the default limit; the derived default would have set it to zero and skipped every file. Refs: site/release-notes/unreleased.md Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx --- bins/codefang/tests/functionless_files_e2e.rs | 141 ++++++++++++++++++ crates/cf-commands/src/flags.rs | 5 + crates/cf-commands/src/handlers.rs | 92 ++++++++++-- .../cf-commands/src/handlers/static_clones.rs | 2 +- .../src/handlers/static_cohesion.rs | 2 +- .../src/handlers/static_complexity.rs | 2 +- .../src/handlers/static_halstead.rs | 2 +- .../cf-commands/src/handlers/static_json.rs | 2 + crates/cf-commands/tests/flag_parity.rs | 1 + site/analyzers/index.md | 2 +- site/architecture/overview.md | 2 +- site/guide/cli-reference.md | 12 +- site/release-notes/unreleased.md | 1 + 13 files changed, 246 insertions(+), 20 deletions(-) create mode 100644 bins/codefang/tests/functionless_files_e2e.rs diff --git a/bins/codefang/tests/functionless_files_e2e.rs b/bins/codefang/tests/functionless_files_e2e.rs new file mode 100644 index 00000000..8be7bc1f --- /dev/null +++ b/bins/codefang/tests/functionless_files_e2e.rs @@ -0,0 +1,141 @@ +//! End-to-end coverage of function-free (data) files and the UAST parse size +//! limit, through the real `codefang` binary. +//! +//! - A data file (JSON, YAML, ...) has no function-like constructs. The +//! function analyzers do not build its tree, but it must still appear in +//! `--per-file` output with zero functions, exactly as before. +//! - A source file over the parse size limit is skipped with a stderr warning +//! that names it; `--max-parse-file-size` raises the limit. + +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo, TestRepo}; + +/// Path to the freshly built `codefang` binary under test. +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +/// 2021-01-01T12:00:00Z. +const T0: i64 = 1_609_502_400; +/// One MiB in bytes. +const MIB: usize = 1024 * 1024; +/// Default parse limit, in MiB. +const DEFAULT_LIMIT_MIB: usize = 8; + +fn run_json(args: &[&str]) -> (serde_json::Value, String) { + let out = common::command(BIN) + .arg("run") + .args(args) + .env("TZ", "UTC") + .env("NO_COLOR", "1") + .env("LANG", "C") + .env("LC_ALL", "C") + .output() + .expect("run the codefang binary"); + assert!( + out.status.success(), + "run failed ({args:?}): {}", + String::from_utf8_lossy(&out.stderr) + ); + let doc = serde_json::from_slice(&out.stdout).expect("stdout is one JSON document"); + (doc, String::from_utf8_lossy(&out.stderr).into_owned()) +} + +/// The `Total Functions` metric value in a report section or file entry. +fn total_functions(entry: &serde_json::Value) -> String { + entry["metrics"] + .as_array() + .expect("metrics array") + .iter() + .find(|m| m["label"] == "Total Functions") + .and_then(|m| m["value"].as_str()) + .expect("Total Functions metric") + .to_string() +} + +/// A git repository whose working tree holds `files`: the static walk reads +/// the checkout, not the object database. +fn repo_with(files: &[(&str, &[u8])]) -> TestRepo { + let test = init_repo().expect("init repo"); + for (name, body) in files { + std::fs::write(test.path().join(name), body).expect("write fixture file"); + } + commit_files(&test, "init", T0, files).expect("commit"); + test +} + +#[test] +fn data_file_is_listed_per_file_with_zero_functions() { + let test = repo_with(&[ + ("main.go", b"package main\n\nfunc add(a, b int) int {\n\treturn a + b\n}\n"), + ("config.json", b"{\"name\": \"x\", \"items\": [1, 2, 3]}\n"), + ("settings.yaml", b"key: value\nlist:\n - one\n"), + ]); + let path = test.path().to_str().expect("utf-8 path"); + let (doc, _) = run_json(&["-a", "static/complexity", "--per-file", "--format", "json", path]); + + let files = doc["sections"][0]["files"].as_array().expect("per-file entries"); + for data in ["config.json", "settings.yaml"] { + let entry = files + .iter() + .find(|f| f["file_path"] == data) + .unwrap_or_else(|| panic!("{data} must appear per file: {files:?}")); + assert_eq!(total_functions(entry), "0", "{data} has no functions"); + assert_eq!(entry["status"], "No functions found", "{data}"); + } + let go = files + .iter() + .find(|f| f["file_path"] == "main.go") + .expect("main.go entry"); + assert_eq!(total_functions(go), "1"); +} + +#[test] +fn source_file_over_the_parse_limit_is_skipped_with_a_warning() { + let mut big = b"package main\n\nfunc big() int {\n\treturn 1\n}\n// ".to_vec(); + big.resize(DEFAULT_LIMIT_MIB * MIB + 1, b'x'); + let test = repo_with(&[ + ("main.go", b"package main\n\nfunc small() int {\n\treturn 2\n}\n"), + ("big.go", &big), + ]); + let path = test.path().to_str().expect("utf-8 path"); + let (doc, stderr) = run_json(&["-a", "static/complexity", "--format", "json", path]); + + assert!( + stderr.contains("warning: skipped") && stderr.contains("big.go"), + "the oversized file is named on stderr: {stderr}" + ); + assert!( + stderr.contains("parse size limit"), + "the warning names the limit: {stderr}" + ); + assert_eq!( + total_functions(&doc["sections"][0]), + "1", + "only main.go is analyzed" + ); +} + +#[test] +fn max_parse_file_size_flag_raises_the_limit() { + let mut big = b"package main\n\nfunc big() int {\n\treturn 1\n}\n// ".to_vec(); + big.resize(DEFAULT_LIMIT_MIB * MIB + 1, b'x'); + let test = repo_with(&[ + ("main.go", b"package main\n\nfunc small() int {\n\treturn 2\n}\n"), + ("big.go", &big), + ]); + let path = test.path().to_str().expect("utf-8 path"); + let (doc, stderr) = run_json(&[ + "-a", + "static/complexity", + "--max-parse-file-size", + "16", + "--format", + "json", + path, + ]); + + assert!( + !stderr.contains("big.go"), + "a file under the raised limit is not skipped: {stderr}" + ); + assert_eq!(total_functions(&doc["sections"][0]), "2"); +} diff --git a/crates/cf-commands/src/flags.rs b/crates/cf-commands/src/flags.rs index 2fab4433..52974916 100644 --- a/crates/cf-commands/src/flags.rs +++ b/crates/cf-commands/src/flags.rs @@ -131,6 +131,11 @@ fn add_literal_run_flags(cmd: Command) -> Command { ".", "Folder/repository path to analyze", )) + .arg(int_arg( + "max-parse-file-size", + crate::handlers::DEFAULT_MAX_PARSE_FILE_MIB, + "Static analyzers skip source files larger than this many MiB (default 8)", + )) .arg(int_arg( "limit", 0, diff --git a/crates/cf-commands/src/handlers.rs b/crates/cf-commands/src/handlers.rs index b009075e..4b04c744 100644 --- a/crates/cf-commands/src/handlers.rs +++ b/crates/cf-commands/src/handlers.rs @@ -123,7 +123,48 @@ pub(crate) fn run_cancelled() -> bool { /// size), lowered to a UAST, and — on the complexity path — deep-copied /// again, so peak RSS is a large multiple of the largest file encountered. /// Oversized files are skipped exactly like unreadable ones. -pub const MAX_STATIC_FILE_SIZE: u64 = 64 * 1024 * 1024; +pub const DEFAULT_MAX_PARSE_FILE_BYTES: u64 = DEFAULT_MAX_PARSE_FILE_MIB as u64 * MIB_BYTES; + +/// Default `--max-parse-file-size`, in MiB. +pub const DEFAULT_MAX_PARSE_FILE_MIB: i64 = 8; + +/// Bytes in one MiB. +const MIB_BYTES: u64 = 1024 * 1024; + +/// Parses `file` for a function analyzer. A data-only file +/// ([`FUNCTIONLESS_LANGUAGES`]) gets an empty file root instead of a parse: +/// it has no functions, so its report is the same and the tree is never built. +/// +/// # Errors +/// +/// The parser's error for a file that must be parsed. +pub fn parse_for_functions( + parser: &cf_uast::Parser, + file: &SourceFile, +) -> Result<cf_uast::Node, cf_uast::ParseError> { + if FUNCTIONLESS_LANGUAGES.contains(&file.language.as_str()) { + return Ok(cf_uast::Node::with_token(cf_uast_node::UAST_FILE, "")); + } + parser.parse(&file.rel_path, &file.bytes) +} + +/// Languages whose UAST has no function-like constructs (data and config +/// formats). The function analyzers do not build their tree: an empty file +/// root stands in, so the file still reports zero functions. +pub const FUNCTIONLESS_LANGUAGES: [&str; 12] = [ + "json", + "yaml", + "toml", + "ini", + "csv", + "psv", + "dotenv", + "xml", + "gitignore", + "gitattributes", + "git_config", + "ssh_config", +]; /// Bytes read per file by [`walk_all_files`]: the classification header /// (binary sniffing, generated markers, shebangs and modelines all live @@ -135,7 +176,7 @@ pub const ALL_FILES_HEADER_BYTES: usize = 8192; pub enum SkipReason { /// The file (or its metadata) could not be read. Unreadable, - /// The file is larger than [`MAX_STATIC_FILE_SIZE`]. + /// The file is larger than the run's parse size limit. Oversized, /// The parser has no usable grammar for the file. Unparseable, @@ -147,7 +188,7 @@ impl SkipReason { pub fn as_str(self) -> &'static str { match self { Self::Unreadable => "unreadable", - Self::Oversized => "larger than 64 MiB", + Self::Oversized => "larger than the parse size limit (--max-parse-file-size)", Self::Unparseable => "unparseable", } } @@ -195,11 +236,11 @@ fn was_skipped(path: &Path) -> bool { .contains(path) } -/// Reads a source file whole, refusing files over [`MAX_STATIC_FILE_SIZE`] -/// before any byte is read. -fn read_source_capped(path: &Path) -> Result<Vec<u8>, SkipReason> { +/// Reads a source file whole, refusing files over `limit` bytes before any +/// byte is read. +fn read_source_capped(path: &Path, limit: u64) -> Result<Vec<u8>, SkipReason> { let meta = std::fs::metadata(path).map_err(|_| SkipReason::Unreadable)?; - if meta.len() > MAX_STATIC_FILE_SIZE { + if meta.len() > limit { return Err(SkipReason::Oversized); } std::fs::read(path).map_err(|_| SkipReason::Unreadable) @@ -280,7 +321,7 @@ pub struct StaticWalk<'a> { /// 3. applies the vendor / generated / `--extra-excluded-prefixes` policy to /// the ROOT-RELATIVE path, so `--extra-excluded-prefixes gen/` works /// whatever the root spelling; -/// 4. reads each file once, refusing files over [`MAX_STATIC_FILE_SIZE`]; +/// 4. reads each file once, refusing files over the filter's parse size limit; /// an unreadable or oversized file is skipped and reported once on /// stderr for the whole run ([`note_skipped`]); /// 5. applies the generated-content marker over the first 512 bytes @@ -318,6 +359,7 @@ pub fn walk_all_files( extra_excluded_prefixes: filter.policy.extra_excluded_prefixes.clone(), }, language_globs: filter.language_globs.clone(), + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, }; StaticWalk::new(root, WalkMode::AllFiles, relaxed) } @@ -352,7 +394,7 @@ impl<'a> StaticWalk<'a> { if !parser.is_supported(&rel_path) || self.filter.skips(&rel_path) { return None; } - let bytes = match read_source_capped(path) { + let bytes = match read_source_capped(path, self.filter.max_parse_bytes) { Ok(bytes) => bytes, Err(reason) => { note_skipped(path, reason); @@ -1588,12 +1630,22 @@ pub const GENERATED_MARKER_HEADER_BYTES: usize = 512; /// `--extra-excluded-prefixes` policy over the path and, through /// [`StaticFilter::skips_with_header`], over the file's first /// [`GENERATED_MARKER_HEADER_BYTES`] bytes. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct StaticFilter { /// Vendor / generated exclusion policy. pub policy: cf_pathpolicy::Options, /// Basename globs from `--languages`; `None` disables the filter. pub language_globs: Option<Vec<String>>, + /// Largest source file, in bytes, that is read and parsed (`--max-parse-file-size`). + pub max_parse_bytes: u64, +} + +impl Default for StaticFilter { + /// The default policy, no language restriction, and the default parse size + /// limit: a zero limit would skip every file. + fn default() -> Self { + Self::from_policy(cf_pathpolicy::Options::default()) + } } impl StaticFilter { @@ -1603,6 +1655,7 @@ impl StaticFilter { StaticFilter { policy, language_globs: None, + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, } } @@ -1647,6 +1700,16 @@ pub(crate) fn static_filter(ctx: &RunContext) -> Result<StaticFilter, String> { let policy = static_path_policy(ctx); let tokens = language_tokens(ctx.matches); let globs = cf_langpath::globs(&tokens).map_err(|e| format!("static --languages: {e}"))?; + let max_mib = ctx + .matches + .try_get_one::<i64>("max-parse-file-size") + .ok() + .flatten() + .copied() + .unwrap_or(DEFAULT_MAX_PARSE_FILE_MIB); + if max_mib < 1 { + return Err("static --max-parse-file-size: must be at least 1 MiB".to_string()); + } Ok(StaticFilter { policy, language_globs: if globs.wants_all { @@ -1654,6 +1717,7 @@ pub(crate) fn static_filter(ctx: &RunContext) -> Result<StaticFilter, String> { } else { Some(globs.globs) }, + max_parse_bytes: u64::try_from(max_mib).unwrap_or(u64::MAX) * MIB_BYTES, }) } @@ -2159,6 +2223,7 @@ mod static_filter_tests { let filter = StaticFilter { policy: permissive_policy(), language_globs: Some(vec!["*.go".to_string()]), + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, }; assert!(!filter.skips("src/pkg/deep/file.go")); assert!(filter.skips("src/pkg/deep/file.py")); @@ -2169,6 +2234,7 @@ mod static_filter_tests { let filter = StaticFilter { policy: permissive_policy(), language_globs: Some(vec!["*.py".to_string()]), + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, }; assert!(filter.skips("vendor/dep.go")); // A language-matched path still falls through to the path policy: @@ -2176,6 +2242,7 @@ mod static_filter_tests { let filter = StaticFilter { policy: cf_pathpolicy::Options::default(), language_globs: Some(vec!["*.go".to_string()]), + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, }; assert!(filter.skips("vendor/dep.go")); } @@ -2185,6 +2252,7 @@ mod static_filter_tests { let filter = StaticFilter { policy: permissive_policy(), language_globs: None, + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, }; assert!(!filter.skips("a.py")); assert!(!filter.skips("vendor/dep.go")); @@ -2197,6 +2265,7 @@ mod static_filter_tests { let filter = StaticFilter { policy: permissive_policy(), language_globs: Some(Vec::new()), + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, }; assert!(filter.skips("a.go")); assert!(filter.skips("a.py")); @@ -2514,7 +2583,7 @@ mod static_walk_tests { // A sparse file: the size is what the cap reads, no bytes are written. std::fs::File::create(&big) .unwrap() - .set_len(MAX_STATIC_FILE_SIZE + 1) + .set_len(DEFAULT_MAX_PARSE_FILE_BYTES + 1) .unwrap(); assert!(!was_skipped(&big)); assert_eq!(source_paths(dir.path(), &permissive()), vec!["a.go"]); @@ -2612,6 +2681,7 @@ mod static_walk_tests { write(dir.path(), "deep/b.py", b"def f():\n return 1\n"); let py_only = StaticFilter { language_globs: Some(vec!["*.py".to_string()]), + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, ..permissive() }; assert_eq!(source_paths(dir.path(), &py_only), vec!["deep/b.py"]); diff --git a/crates/cf-commands/src/handlers/static_clones.rs b/crates/cf-commands/src/handlers/static_clones.rs index 88426304..fe45107a 100644 --- a/crates/cf-commands/src/handlers/static_clones.rs +++ b/crates/cf-commands/src/handlers/static_clones.rs @@ -101,7 +101,7 @@ fn aggregate_report_opts(root_path: &str, filter: &StaticFilter) -> Option<Clone let parser = Parser::new(); let mut agg = Aggregator::new(); for file in super::walk_source_files(root_path, &parser, filter).ok()? { - let Ok(uast_root) = parser.parse(&file.rel_path, &file.bytes) else { + let Ok(uast_root) = super::parse_for_functions(&parser, &file) else { super::note_skipped(&file.path, SkipReason::Unparseable); continue; }; diff --git a/crates/cf-commands/src/handlers/static_cohesion.rs b/crates/cf-commands/src/handlers/static_cohesion.rs index ee67e6b1..7b690630 100644 --- a/crates/cf-commands/src/handlers/static_cohesion.rs +++ b/crates/cf-commands/src/handlers/static_cohesion.rs @@ -37,7 +37,7 @@ fn analyze_tree(root_path: &str, filter: &StaticFilter) -> Option<Vec<AnalyzedFi let files = super::walk_source_files(root_path, &parser, filter) .ok()? .filter_map(|file| { - let Ok(uast_root) = parser.parse(&file.rel_path, &file.bytes) else { + let Ok(uast_root) = super::parse_for_functions(&parser, &file) else { super::note_skipped(&file.path, SkipReason::Unparseable); return None; }; diff --git a/crates/cf-commands/src/handlers/static_complexity.rs b/crates/cf-commands/src/handlers/static_complexity.rs index f282eda5..b3d1ed54 100644 --- a/crates/cf-commands/src/handlers/static_complexity.rs +++ b/crates/cf-commands/src/handlers/static_complexity.rs @@ -87,7 +87,7 @@ pub(crate) fn collect(root_path: &str, filter: &StaticFilter) -> Option<Collecte /// Parses and measures one admitted file; `None` when it fails to parse /// (counted as skipped). fn analyze_file(file: &SourceFile, parser: &Parser) -> Option<AnalyzedFile> { - let Ok(tree) = parser.parse(&file.rel_path, &file.bytes) else { + let Ok(tree) = super::parse_for_functions(parser, file) else { super::note_skipped(&file.path, SkipReason::Unparseable); return None; }; diff --git a/crates/cf-commands/src/handlers/static_halstead.rs b/crates/cf-commands/src/handlers/static_halstead.rs index a0dead9d..97b03ad1 100644 --- a/crates/cf-commands/src/handlers/static_halstead.rs +++ b/crates/cf-commands/src/handlers/static_halstead.rs @@ -116,7 +116,7 @@ fn aggregate_opts(root_path: &str, filter: &StaticFilter) -> Option<Aggregate> { let directory = file.directory(); // A supported file whose grammar is not wired (or that fails to // parse) still counts as an analyzed, function-free file. - let records = match parser.parse(&file.rel_path, &file.bytes) { + let records = match super::parse_for_functions(&parser, &file) { Ok(node) => analyze_file( &node, &file.bytes, diff --git a/crates/cf-commands/src/handlers/static_json.rs b/crates/cf-commands/src/handlers/static_json.rs index e503e25f..29c0f5f3 100644 --- a/crates/cf-commands/src/handlers/static_json.rs +++ b/crates/cf-commands/src/handlers/static_json.rs @@ -130,6 +130,7 @@ fn walk_root(root_path: &str, filter: &StaticFilter, per_file: bool) -> Option<W #[cfg(test)] mod per_file_tests { use super::*; + use crate::handlers::DEFAULT_MAX_PARSE_FILE_BYTES; /// Fixture: a source file, a vendored file, a generated file, a doc /// under `docs/` and an unknown extension. @@ -211,6 +212,7 @@ mod per_file_tests { let filter = StaticFilter { policy: cf_pathpolicy::Options::default(), language_globs: Some(vec!["*.go".to_string()]), + max_parse_bytes: DEFAULT_MAX_PARSE_FILE_BYTES, }; let json = json(&dir, &filter, false); assert!( diff --git a/crates/cf-commands/tests/flag_parity.rs b/crates/cf-commands/tests/flag_parity.rs index 83ae0ad0..01a42c57 100644 --- a/crates/cf-commands/tests/flag_parity.rs +++ b/crates/cf-commands/tests/flag_parity.rs @@ -86,6 +86,7 @@ fn permissive() -> StaticFilter { ..cf_pathpolicy::Options::default() }, language_globs: None, + max_parse_bytes: cf_commands::handlers::DEFAULT_MAX_PARSE_FILE_BYTES, } } diff --git a/site/analyzers/index.md b/site/analyzers/index.md index 325d4dec..f11a0b5b 100644 --- a/site/analyzers/index.md +++ b/site/analyzers/index.md @@ -42,7 +42,7 @@ walk visits directories in lexical order (skipping `.git`, cache-tagged directories and symlinked directories), admits the files the UAST parser supports and `--languages` keeps, applies the vendor / generated / `--extra-excluded-prefixes` policy to the root-relative path, reads each -file once (files over 64 MiB and unreadable files are skipped with one +file once (files over 8 MiB by default, `--max-parse-file-size`, and unreadable files are skipped with one stderr warning each), and applies the generated-content marker (`DO NOT EDIT`, `Code generated`, … in the first 512 bytes) unless `--include-generated`. `static/composition` classifies every file instead, diff --git a/site/architecture/overview.md b/site/architecture/overview.md index 64b70e95..6b5537ac 100644 --- a/site/architecture/overview.md +++ b/site/architecture/overview.md @@ -227,7 +227,7 @@ flowchart LR ### Static mode 1. Walks the source tree once per analyzer (`walk_source_files`), applying - the vendored / generated policy, `--languages` and a 64 MiB per-file cap. + the vendored / generated policy, `--languages` and an 8 MiB per-file parse cap (`--max-parse-file-size`). 2. Parses each file into a UAST using Tree-sitter and the compiled-in mappings. 3. Runs the selected static analyzers (complexity, cohesion, halstead, diff --git a/site/guide/cli-reference.md b/site/guide/cli-reference.md index 6be42d57..33bd2cef 100644 --- a/site/guide/cli-reference.md +++ b/site/guide/cli-reference.md @@ -252,12 +252,18 @@ Every `static/*` analyzer reads the same walk of the path you pass - a file is admitted when the UAST parser supports its extension, its basename matches `--languages`, and its **root-relative** path passes the vendor / generated / `--extra-excluded-prefixes` policy; -- each admitted file is read **once**; a file larger than **64 MiB** or one - that cannot be read is skipped with one stderr line - (`warning: skipped <path>: larger than 64 MiB` / `: unreadable`) for the +- each admitted file is read **once**; a file larger than the parse size + limit (`--max-parse-file-size`, default **8 MiB**) or one that cannot be read + is skipped with one stderr line + (`warning: skipped <path>: larger than the parse size limit (--max-parse-file-size)` / `: unreadable`) for the whole run, however many analyzers are selected, and counted once in the end-of-run `progress: skipped N unreadable/oversized/unparseable files` line; +- data and config files (JSON, YAML, TOML, INI, CSV, PSV, dotenv, XML, and the + git and ssh config formats) have no functions: the function analyzers + (`static/complexity`, `static/halstead`, `static/cohesion`, `static/clones`) + do not parse them and report them with zero functions, so memory and time stay + bounded on large data files; - the generated-content marker (`DO NOT EDIT`, `Code generated`, `@generated`, … in the first 512 bytes) is applied to the bytes just read unless `--include-generated`; diff --git a/site/release-notes/unreleased.md b/site/release-notes/unreleased.md index 107e33bc..aa10eaea 100644 --- a/site/release-notes/unreleased.md +++ b/site/release-notes/unreleased.md @@ -80,6 +80,7 @@ This is the first release line, so there is no prior version to upgrade from. If ## Fixed +- Static analysis of large data files no longer exhausts memory. The function analyzers (`static/complexity`, `static/halstead`, `static/cohesion`, `static/clones`) skip parsing JSON, YAML, TOML, and similar files, which have no functions; reports are byte-identical. A 19 MB JSON file previously needed over 3 GB. Source files over 8 MiB are now skipped with a warning; `--max-parse-file-size` raises the limit. - Full-report schemas now declare exactly the fields each analyzer emits. `history/typos`, `history/imports`, `history/burndown` (`interactions`), and `static/clones` (`message`) had registry entries that named fields the reports did not contain, or omitted fields they did contain. - A shallow clone no longer produces empty history reports silently. History analyzers still see no commits in a shallow repository, but the run prints one `warning: shallow clone` line on stderr naming the remedy (`git fetch --unshallow`). - Fixed a data race in `internal/framework.PipelineSampler`: `t1Captured` was a plain `bool` read by the sampler goroutine and written by the caller, causing intermittent races under `go test -race`. It is now a `sync/atomic.Bool` using `CompareAndSwap`, so at most one t1 heap profile is captured. Removed the unused `t0Captured` field. From 50ac5626219cc18388d244a96d493ba7f5096e96 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Thu, 15 Oct 2026 14:46:27 +0000 Subject: [PATCH 5/7] feat(static): decide function-parse skips from enry's language type and the mapping The skip for data files used a hardcoded list of format names. Replace it with two data-driven checks: - enry's Linguist language type (data, programming, markup, prose), vendored from enry v2.1.0's data.LanguagesType as data/enry-types-v2.1.0.tsv and exposed as cf_langpath::language_type. The table is generated by tools/enrydump, which now writes both tables and reproduces the existing lookup table byte for byte. - cf_uast::Parser::yields_functions: whether any rule of the file's mapping can produce a function node (type Function or Method, or the Function and Declaration roles), the same criteria the complexity analyzer uses. A function parse is skipped only when enry types the file as data AND the mapping cannot yield a function. enry's type alone is not enough: SQL and GraphQL are data-typed but their mappings declare functions (CREATE FUNCTION, query operations), so the type alone would change their reports. Verified: function counts for SQL (1) and GraphQL (2) are unchanged, and the four function analyzers match the previous binary byte-for-byte on the data fixture and on 12 real repositories (48 runs). alacritty static/complexity drops from 84 s to 2.3 s. .gitignore: the new table is listed beside the other vendored tables, since the blanket *.tsv rule would otherwise leave it untracked. Refs: site/guide/cli-reference.md, site/release-notes/unreleased.md Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx --- .gitignore | 1 + crates/cf-commands/src/handlers.rs | 37 +- crates/cf-langpath/data/README.md | 69 +-- crates/cf-langpath/data/enry-types-v2.1.0.tsv | 519 ++++++++++++++++++ crates/cf-langpath/src/lib.rs | 2 + crates/cf-langpath/src/linguist_type.rs | 102 ++++ crates/cf-uast/src/loader.rs | 22 +- crates/cf-uast/src/parser.rs | 26 + crates/cf-uast/src/types.rs | 7 + site/guide/cli-reference.md | 11 +- site/release-notes/unreleased.md | 2 +- tools/enrydump/main.go | 113 ++++ 12 files changed, 828 insertions(+), 83 deletions(-) create mode 100644 crates/cf-langpath/data/enry-types-v2.1.0.tsv create mode 100644 crates/cf-langpath/src/linguist_type.rs create mode 100644 tools/enrydump/main.go diff --git a/.gitignore b/.gitignore index 9bd5b540..197d01f1 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ _testmain.go !crates/cf-langpath/data/enry-v2.1.0.tsv !crates/cf-langpath/data/enry-frequencies-v2.1.0.tsv !crates/cf-langpath/data/enry-interpreters-v2.1.0.tsv +!crates/cf-langpath/data/enry-types-v2.1.0.tsv !crates/cf-sentiment-lexicons/data/** !crates/cf-gojson/tests/oracle_data/** diff --git a/crates/cf-commands/src/handlers.rs b/crates/cf-commands/src/handlers.rs index 4b04c744..c94e66f9 100644 --- a/crates/cf-commands/src/handlers.rs +++ b/crates/cf-commands/src/handlers.rs @@ -131,9 +131,20 @@ pub const DEFAULT_MAX_PARSE_FILE_MIB: i64 = 8; /// Bytes in one MiB. const MIB_BYTES: u64 = 1024 * 1024; -/// Parses `file` for a function analyzer. A data-only file -/// ([`FUNCTIONLESS_LANGUAGES`]) gets an empty file root instead of a parse: -/// it has no functions, so its report is the same and the tree is never built. +/// Whether a function analyzer may skip parsing `file`. enry must type it as +/// data, and the file's mapping must be unable to produce a function node. +/// Both are needed: enry's data type admits SQL and GraphQL, whose mappings do +/// declare functions, so the type alone would change their reports. +fn skips_function_parse(parser: &cf_uast::Parser, file: &SourceFile) -> bool { + let is_data = cf_langpath::language_by_path_with_content(&file.rel_path, &file.bytes) + .and_then(|language| cf_langpath::language_type(&language)) + == Some(cf_langpath::LanguageType::Data); + is_data && !parser.yields_functions(&file.rel_path) +} + +/// Parses `file` for a function analyzer. A file [`skips_function_parse`] +/// admits gets an empty file root: it has no functions, so its report is the +/// same and the tree is never built. /// /// # Errors /// @@ -142,30 +153,12 @@ pub fn parse_for_functions( parser: &cf_uast::Parser, file: &SourceFile, ) -> Result<cf_uast::Node, cf_uast::ParseError> { - if FUNCTIONLESS_LANGUAGES.contains(&file.language.as_str()) { + if skips_function_parse(parser, file) { return Ok(cf_uast::Node::with_token(cf_uast_node::UAST_FILE, "")); } parser.parse(&file.rel_path, &file.bytes) } -/// Languages whose UAST has no function-like constructs (data and config -/// formats). The function analyzers do not build their tree: an empty file -/// root stands in, so the file still reports zero functions. -pub const FUNCTIONLESS_LANGUAGES: [&str; 12] = [ - "json", - "yaml", - "toml", - "ini", - "csv", - "psv", - "dotenv", - "xml", - "gitignore", - "gitattributes", - "git_config", - "ssh_config", -]; - /// Bytes read per file by [`walk_all_files`]: the classification header /// (binary sniffing, generated markers, shebangs and modelines all live /// there), so classifying a tree never reads whole binaries. diff --git a/crates/cf-langpath/data/README.md b/crates/cf-langpath/data/README.md index 173754a0..ca0fe8ce 100644 --- a/crates/cf-langpath/data/README.md +++ b/crates/cf-langpath/data/README.md @@ -33,69 +33,30 @@ Cardinalities (enry v2.1.0): **750** aliases, **504** extension-languages, **234** filename records (1488 lines total, 32053 bytes, pure ASCII). The `vendored_tables_loaded` unit test pins all three counts. +## Language types (`enry-types-v2.1.0.tsv`) + +enry's `GetLanguageType` table, `data.LanguagesType`: one `language<TAB>code` row +per Linguist language, where the code is enry's `Type` enum (1 data, 2 +programming, 3 markup, 4 prose; `unknown` is never stored). It is loaded by +`src/linguist_type.rs`, which exposes `language_type(name)`. Names are enry's +exact canonical names (`JSON`, `Go`), not the UAST's lowercase language names. + ## Regenerating The canonical, committed dumper lives at `tools/enrydump/main.go`. Run it from the Go module root (so enry resolves from `go.sum`): ```sh -go run ./tools/enrydump crates/cf-langpath/data/enry-v2.1.0.tsv +go run ./tools/enrydump crates/cf-langpath/data/enry-v2.1.0.tsv crates/cf-langpath/data/enry-types-v2.1.0.tsv ``` +Run it from the enry v2.1.0 module (`git clone --branch v2.1.0 +https://github.com/src-d/enry`), with the dumper copied to `tools/enrydump` in that +module, so the import resolves to the pinned data. The first file is +byte-identical to the vendored lookup table, and the second has 519 type rows. It prints `aliases=750 extensions=504 filenames=234`. The dumper sorts every section and each map's keys, so its output is byte-stable across runs. If enry is upgraded, regenerate, bump the `include_str!` path in `src/lib.rs`, and update the cardinality assertions in the `vendored_tables_loaded` test. The dumper -source, for reference: - -```go -package main - -import ( - "fmt" - "os" - "sort" - - "github.com/src-d/enry/v2/data" -) - -func main() { - f, _ := os.Create(os.Args[1]) - defer f.Close() - - keys := make([]string, 0, len(data.LanguageByAliasMap)) - for k := range data.LanguageByAliasMap { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - fmt.Fprintf(f, "A\t%s\t%s\n", k, data.LanguageByAliasMap[k]) - } - - ek := make([]string, 0, len(data.ExtensionsByLanguage)) - for k := range data.ExtensionsByLanguage { - ek = append(ek, k) - } - sort.Strings(ek) - for _, k := range ek { - fmt.Fprintf(f, "E\t%s", k) - for _, e := range data.ExtensionsByLanguage[k] { - fmt.Fprintf(f, "\t%s", e) - } - fmt.Fprintln(f) - } - - fk := make([]string, 0, len(data.LanguagesByFilename)) - for k := range data.LanguagesByFilename { - fk = append(fk, k) - } - sort.Strings(fk) - for _, k := range fk { - fmt.Fprintf(f, "F\t%s", k) - for _, l := range data.LanguagesByFilename[k] { - fmt.Fprintf(f, "\t%s", l) - } - fmt.Fprintln(f) - } -} -``` +source is `tools/enrydump/main.go`, committed in this repository. The +lookup table was checked against it byte for byte. diff --git a/crates/cf-langpath/data/enry-types-v2.1.0.tsv b/crates/cf-langpath/data/enry-types-v2.1.0.tsv new file mode 100644 index 00000000..75d46fd8 --- /dev/null +++ b/crates/cf-langpath/data/enry-types-v2.1.0.tsv @@ -0,0 +1,519 @@ +1C Enterprise 2 +ABAP 2 +ABNF 1 +AGS Script 2 +AMPL 2 +ANTLR 2 +API Blueprint 3 +APL 2 +ASN.1 1 +ASP 2 +ATS 2 +ActionScript 2 +Ada 2 +Adobe Font Metrics 1 +Agda 2 +Alloy 2 +Alpine Abuild 2 +Altium Designer 1 +AngelScript 2 +Ant Build System 1 +ApacheConf 1 +Apex 2 +Apollo Guidance Computer 2 +AppleScript 2 +Arc 2 +AsciiDoc 4 +AspectJ 2 +Assembly 2 +Asymptote 2 +Augeas 2 +AutoHotkey 2 +AutoIt 2 +Awk 2 +Ballerina 2 +Batchfile 2 +Befunge 2 +Bison 2 +BitBake 2 +Blade 3 +BlitzBasic 2 +BlitzMax 2 +Bluespec 2 +Boo 2 +Brainfuck 2 +Brightscript 2 +C 2 +C# 2 +C++ 2 +C-ObjDump 1 +C2hs Haskell 2 +CLIPS 2 +CMake 2 +COBOL 2 +COLLADA 1 +CSON 1 +CSS 3 +CSV 1 +CWeb 2 +Cabal Config 1 +Cap'n Proto 2 +CartoCSS 2 +Ceylon 2 +Chapel 2 +Charity 2 +ChucK 2 +Cirru 2 +Clarion 2 +Clean 2 +Click 2 +Clojure 2 +Closure Templates 3 +Cloud Firestore Security Rules 1 +CoNLL-U 1 +CoffeeScript 2 +ColdFusion 2 +ColdFusion CFC 2 +Common Lisp 2 +Common Workflow Language 2 +Component Pascal 2 +Cool 2 +Coq 2 +Cpp-ObjDump 1 +Creole 4 +Crystal 2 +Csound 2 +Csound Document 2 +Csound Score 2 +Cuda 2 +Cycript 2 +Cython 2 +D 2 +D-ObjDump 1 +DIGITAL Command Language 2 +DM 2 +DNS Zone 1 +DTrace 2 +Darcs Patch 1 +Dart 2 +DataWeave 2 +Dhall 2 +Diff 1 +Dockerfile 2 +Dogescript 2 +Dylan 2 +E 2 +EBNF 1 +ECL 2 +ECLiPSe 2 +EJS 3 +EML 1 +EQ 2 +Eagle 1 +Easybuild 1 +Ecere Projects 1 +EditorConfig 1 +Edje Data Collection 1 +Eiffel 2 +Elixir 2 +Elm 2 +Emacs Lisp 2 +EmberScript 2 +Erlang 2 +F# 2 +F* 2 +FIGlet Font 1 +FLUX 2 +Factor 2 +Fancy 2 +Fantom 2 +Filebench WML 2 +Filterscript 2 +Formatted 1 +Forth 2 +Fortran 2 +FreeMarker 2 +Frege 2 +G-code 2 +GAMS 2 +GAP 2 +GCC Machine Description 2 +GDB 2 +GDScript 2 +GLSL 2 +GN 1 +Game Maker Language 2 +Genie 2 +Genshi 2 +Gentoo Ebuild 2 +Gentoo Eclass 2 +Gerber Image 1 +Gettext Catalog 4 +Gherkin 2 +Git Attributes 1 +Git Config 1 +Glyph 2 +Glyph Bitmap Distribution Format 1 +Gnuplot 2 +Go 2 +Golo 2 +Gosu 2 +Grace 2 +Gradle 1 +Grammatical Framework 2 +Graph Modeling Language 1 +GraphQL 1 +Graphviz (DOT) 1 +Groovy 2 +Groovy Server Pages 2 +HAProxy 1 +HCL 2 +HLSL 2 +HTML 3 +HTML+Django 3 +HTML+ECR 3 +HTML+EEX 3 +HTML+ERB 3 +HTML+PHP 3 +HTML+Razor 3 +HTTP 1 +HXML 1 +Hack 2 +Haml 3 +Handlebars 3 +Harbour 2 +Haskell 2 +Haxe 2 +HiveQL 2 +HolyC 2 +Hy 2 +HyPhy 2 +IDL 2 +IGOR Pro 2 +INI 1 +IRC log 1 +Idris 2 +Ignore List 1 +Inform 7 2 +Inno Setup 2 +Io 2 +Ioke 2 +Isabelle 2 +Isabelle ROOT 2 +J 2 +JFlex 2 +JSON 1 +JSON with Comments 1 +JSON5 1 +JSONLD 1 +JSONiq 2 +JSX 2 +Jasmin 2 +Java 2 +Java Properties 1 +Java Server Pages 2 +JavaScript 2 +JavaScript+ERB 2 +Jison 2 +Jison Lex 2 +Jolie 2 +Jsonnet 2 +Julia 2 +Jupyter Notebook 3 +KRL 2 +KiCad Layout 1 +KiCad Legacy Layout 1 +KiCad Schematic 1 +Kit 3 +Kotlin 2 +LFE 2 +LLVM 2 +LOLCODE 2 +LSL 2 +LTspice Symbol 1 +LabVIEW 2 +Lasso 2 +Latte 3 +Lean 2 +Less 3 +Lex 2 +LilyPond 2 +Limbo 2 +Linker Script 1 +Linux Kernel Module 1 +Liquid 3 +Literate Agda 2 +Literate CoffeeScript 2 +Literate Haskell 2 +LiveScript 2 +Logos 2 +Logtalk 2 +LookML 2 +LoomScript 2 +Lua 2 +M 2 +M4 2 +M4Sugar 2 +MATLAB 2 +MAXScript 2 +MQL4 2 +MQL5 2 +MTML 3 +MUF 2 +Makefile 2 +Mako 2 +Markdown 4 +Marko 3 +Mask 3 +Mathematica 2 +Maven POM 1 +Max 2 +MediaWiki 4 +Mercury 2 +Meson 2 +Metal 2 +MiniD 2 +Mirah 2 +Modelica 2 +Modula-2 2 +Modula-3 2 +Module Management System 2 +Monkey 2 +Moocode 2 +MoonScript 2 +Motorola 68K Assembly 2 +Myghty 2 +NCL 2 +NL 1 +NSIS 2 +Nearley 2 +Nemerle 2 +NetLinx 2 +NetLinx+ERB 2 +NetLogo 2 +NewLisp 2 +Nextflow 2 +Nginx 1 +Nim 2 +Ninja 1 +Nit 2 +Nix 2 +Nu 2 +NumPy 2 +OCaml 2 +ObjDump 1 +ObjectScript 2 +Objective-C 2 +Objective-C++ 2 +Objective-J 2 +Omgrofl 2 +Opa 2 +Opal 2 +OpenCL 2 +OpenEdge ABL 2 +OpenRC runscript 2 +OpenSCAD 2 +OpenType Feature File 1 +Org 4 +Ox 2 +Oxygene 2 +Oz 2 +P4 2 +PHP 2 +PLSQL 2 +PLpgSQL 2 +POV-Ray SDL 2 +Pan 2 +Papyrus 2 +Parrot 2 +Parrot Assembly 2 +Parrot Internal Representation 2 +Pascal 2 +Pawn 2 +Pep8 2 +Perl 2 +Perl 6 2 +Pic 3 +Pickle 1 +PicoLisp 2 +PigLatin 2 +Pike 2 +Pod 4 +Pod 6 4 +PogoScript 2 +Pony 2 +PostCSS 3 +PostScript 3 +PowerBuilder 2 +PowerShell 2 +Processing 2 +Prolog 2 +Propeller Spin 2 +Protocol Buffer 1 +Public Key 1 +Pug 3 +Puppet 2 +Pure Data 1 +PureBasic 2 +PureScript 2 +Python 2 +Python console 2 +Python traceback 1 +QML 2 +QMake 2 +Quake 2 +R 2 +RAML 3 +RDoc 4 +REALbasic 2 +REXX 2 +RHTML 3 +RMarkdown 4 +RPC 2 +RPM Spec 1 +RUNOFF 3 +Racket 2 +Ragel 2 +Rascal 2 +Raw token data 1 +Reason 2 +Rebol 2 +Red 2 +Redcode 2 +Regular Expression 1 +Ren'Py 2 +RenderScript 2 +Rich Text Format 3 +Ring 2 +RobotFramework 2 +Roff 3 +Roff Manpage 3 +Rouge 2 +Ruby 2 +Rust 2 +SAS 2 +SCSS 3 +SMT 2 +SPARQL 1 +SQF 2 +SQL 1 +SQLPL 2 +SRecode Template 3 +SSH Config 1 +STON 1 +SVG 1 +Sage 2 +SaltStack 2 +Sass 3 +Scala 2 +Scaml 3 +Scheme 2 +Scilab 2 +Self 2 +ShaderLab 2 +Shell 2 +ShellSession 2 +Shen 2 +Slash 2 +Slice 2 +Slim 3 +Smali 2 +Smalltalk 2 +Smarty 2 +Solidity 2 +SourcePawn 2 +Spline Font Database 1 +Squirrel 2 +Stan 2 +Standard ML 2 +Stata 2 +Stylus 3 +SubRip Text 1 +SugarSS 3 +SuperCollider 2 +Svelte 3 +Swift 2 +SystemVerilog 2 +TI Program 2 +TLA 2 +TOML 1 +TSQL 2 +TSX 2 +TXL 2 +Tcl 2 +Tcsh 2 +TeX 3 +Tea 3 +Terra 2 +Text 4 +Textile 4 +Thrift 2 +Turing 2 +Turtle 1 +Twig 3 +Type Language 1 +TypeScript 2 +Unified Parallel C 2 +Unity3D Asset 1 +Unix Assembly 2 +Uno 2 +UnrealScript 2 +UrWeb 2 +VCL 2 +VHDL 2 +Vala 2 +Verilog 2 +Vim script 2 +Visual Basic 2 +Volt 2 +Vue 3 +Wavefront Material 1 +Wavefront Object 1 +Web Ontology Language 1 +WebAssembly 2 +WebIDL 2 +WebVTT 1 +Windows Registry Entries 1 +Wollok 2 +World of Warcraft Addon Data 1 +X BitMap 1 +X Font Directory Index 1 +X PixMap 1 +X10 2 +XC 2 +XCompose 1 +XML 1 +XPages 1 +XProc 2 +XQuery 2 +XS 2 +XSLT 2 +Xojo 2 +Xtend 2 +YAML 1 +YANG 1 +YARA 2 +YASnippet 3 +Yacc 2 +ZAP 2 +ZIL 2 +Zeek 2 +ZenScript 2 +Zephir 2 +Zig 2 +Zimpl 2 +desktop 1 +eC 2 +edn 1 +fish 2 +mcfunction 2 +mupad 2 +nanorc 1 +nesC 2 +ooc 2 +q 2 +reStructuredText 4 +sed 2 +wdl 2 +wisp 2 +xBase 2 diff --git a/crates/cf-langpath/src/lib.rs b/crates/cf-langpath/src/lib.rs index ce63dd58..4ca777a4 100644 --- a/crates/cf-langpath/src/lib.rs +++ b/crates/cf-langpath/src/lib.rs @@ -32,6 +32,8 @@ use std::sync::OnceLock; pub mod content; pub mod content_heuristics; +pub mod linguist_type; +pub use linguist_type::{language_type, LanguageType}; /// Resolve a candidate token to its canonical Linguist language via enry's /// alias table (`GetLanguageByAlias`), or `None` if unrecognized. diff --git a/crates/cf-langpath/src/linguist_type.rs b/crates/cf-langpath/src/linguist_type.rs new file mode 100644 index 00000000..5445f207 --- /dev/null +++ b/crates/cf-langpath/src/linguist_type.rs @@ -0,0 +1,102 @@ +//! Linguist language types: enry's `GetLanguageType`. +//! +//! Every Linguist language is one of data, programming, markup, or prose. The +//! type answers "is this file data or code?", which is the question skip +//! decisions need. It comes from enry v2.1.0's `data.LanguagesType`, vendored +//! verbatim as `data/enry-types-v2.1.0.tsv` (see `data/README.md`). +//! +//! A type is a coarse signal, not a guarantee. SQL and GraphQL are data-typed, +//! yet they declare functions and operations, so a skip decision also needs the +//! UAST mapping's answer (see `Parser::yields_functions` in `cf-uast`). + +use std::collections::HashMap; +use std::sync::OnceLock; + +/// enry v2.1.0's `data.LanguagesType`, one `language<TAB>code` row per line. +const ENRY_TYPES_TSV: &str = include_str!("../data/enry-types-v2.1.0.tsv"); + +/// The Linguist type of a language. Codes follow enry's `Type` enum +/// (`Unknown` = 0 is never stored, so it has no variant). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LanguageType { + /// Structured data or configuration (JSON, YAML, CSV, SQL, …). + Data, + /// Source code. + Programming, + /// Markup (HTML, Markdown, XML-like documents). + Markup, + /// Natural-language prose (Text, AsciiDoc, Textile). + Prose, +} + +impl LanguageType { + /// Maps enry's integer code to a type; `None` for `Unknown` and any + /// unrecognized code. + fn from_code(code: u8) -> Option<Self> { + match code { + 1 => Some(Self::Data), + 2 => Some(Self::Programming), + 3 => Some(Self::Markup), + 4 => Some(Self::Prose), + _ => None, + } + } +} + +/// The Linguist type of `language` (enry's canonical name, e.g. `"JSON"`), or +/// `None` when enry has no type for it. +#[must_use] +pub fn language_type(language: &str) -> Option<LanguageType> { + types().get(language).copied() +} + +/// The parsed type table, loaded once. +fn types() -> &'static HashMap<String, LanguageType> { + static TYPES: OnceLock<HashMap<String, LanguageType>> = OnceLock::new(); + TYPES.get_or_init(|| parse_types(ENRY_TYPES_TSV)) +} + +/// Parses `language<TAB>code` rows; malformed or `Unknown` rows are skipped. +fn parse_types(tsv: &str) -> HashMap<String, LanguageType> { + tsv.lines() + .filter_map(|line| { + let (name, code) = line.rsplit_once('\t')?; + let kind = LanguageType::from_code(code.parse().ok()?)?; + Some((name.to_string(), kind)) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vendored_table_loads_every_language() { + assert_eq!(types().len(), 519); + } + + #[test] + fn known_languages_have_enry_types() { + assert_eq!(language_type("JSON"), Some(LanguageType::Data)); + assert_eq!(language_type("YAML"), Some(LanguageType::Data)); + assert_eq!(language_type("Go"), Some(LanguageType::Programming)); + assert_eq!(language_type("HTML"), Some(LanguageType::Markup)); + assert_eq!(language_type("Markdown"), Some(LanguageType::Prose)); + assert_eq!(language_type("Text"), Some(LanguageType::Prose)); + } + + #[test] + fn unknown_or_inexact_names_have_no_type() { + assert_eq!(language_type("NotALanguage"), None); + assert_eq!(language_type("json"), None, "enry names are exact"); + } + + #[test] + fn parse_skips_unknown_and_malformed_rows() { + let parsed = parse_types("A\t1\nB\t0\nC\tx\nD\t2\nNoTab\n"); + assert_eq!(parsed.len(), 2); + assert_eq!(parsed.get("A"), Some(&LanguageType::Data)); + assert_eq!(parsed.get("D"), Some(&LanguageType::Programming)); + } +} diff --git a/crates/cf-uast/src/loader.rs b/crates/cf-uast/src/loader.rs index 42a74800..d08697ab 100644 --- a/crates/cf-uast/src/loader.rs +++ b/crates/cf-uast/src/loader.rs @@ -19,7 +19,8 @@ use std::collections::HashMap; use std::sync::{Arc, OnceLock}; use cf_uast_mapping::{LanguageInfo, Parser as MappingParser, PatternMatcher, Rule}; -use cf_uast_node::Node; +use cf_uast_mapping::UastSpec; +use cf_uast_node::{Node, ROLE_DECLARATION, ROLE_FUNCTION, UAST_FUNCTION, UAST_METHOD}; use crate::lowering::{resolve_rules, Lowering, ResolvedRule}; use crate::types::{LanguageParser, ParseError}; @@ -356,6 +357,25 @@ impl LanguageParser for LazyDslParser { fn extensions(&self) -> Vec<String> { self.extensions.clone() } + + fn yields_functions(&self) -> bool { + match self.init() { + Ok(state) => state + .resolved + .iter() + .any(|r| spec_yields_function(&r.rule.uast_spec)), + Err(_) => true, + } + } +} + +/// Whether a rule's output spec is a function node, by the shape the +/// complexity analyzer discovers: type `Function` or `Method`, or the +/// `Function` and `Declaration` roles together. +fn spec_yields_function(spec: &UastSpec) -> bool { + let has_role = |name: &str| spec.roles.iter().any(|r| r == name); + matches!(spec.r#type.as_str(), UAST_FUNCTION | UAST_METHOD) + || (has_role(ROLE_FUNCTION) && has_role(ROLE_DECLARATION)) } /// Returns the embedded precompiled mappings. diff --git a/crates/cf-uast/src/parser.rs b/crates/cf-uast/src/parser.rs index 68a0ef7e..ba5ed5db 100644 --- a/crates/cf-uast/src/parser.rs +++ b/crates/cf-uast/src/parser.rs @@ -117,6 +117,18 @@ impl Parser { } } + /// Whether the mapping for `filename`'s language can produce a function + /// node. `false` means no parse of such a file yields a function, so a + /// function analyzer may skip the parse. A file with no parser is `false` + /// too: it cannot be parsed to yield functions either. + #[must_use] + pub fn yields_functions(&self, filename: &str) -> bool { + let ext = get_file_extension(filename).to_lowercase(); + self.loader + .language_parser(&ext) + .is_some_and(|p| p.yields_functions()) + } + /// Parses `content` for `filename`, returning its UAST root. /// /// # Errors @@ -187,6 +199,20 @@ impl LanguageParser for CustomDslParser { mod tests { use super::*; + #[test] + fn yields_functions_follows_the_mapping_not_the_name() { + let p = Parser::new(); + // Plain data mappings declare no function rule. + assert!(!p.yields_functions("config.json")); + assert!(!p.yields_functions("settings.yaml")); + // A data-typed language whose mapping declares functions (SQL's + // CREATE FUNCTION rule) must be parsed, so it is reported as yielding. + assert!(p.yields_functions("query.sql")); + assert!(p.yields_functions("main.go")); + // No parser: no function can be produced. + assert!(!p.yields_functions("notes.unknownext")); + } + #[test] fn new_parser_supports_go() { let p = Parser::new(); diff --git a/crates/cf-uast/src/types.rs b/crates/cf-uast/src/types.rs index a77ebe4b..bdf8fa7b 100644 --- a/crates/cf-uast/src/types.rs +++ b/crates/cf-uast/src/types.rs @@ -90,6 +90,13 @@ pub trait LanguageParser { /// Returns the supported file extensions (including the leading dot). fn extensions(&self) -> Vec<String>; + + /// Whether any mapping rule of this language can produce a function node + /// (see [`crate::Parser::yields_functions`]). The default is `true`, the + /// answer that never lets a caller skip a parse it needs. + fn yields_functions(&self) -> bool { + true + } } /// Minimum number of dot-separated parts for a filename to have an extension. diff --git a/site/guide/cli-reference.md b/site/guide/cli-reference.md index 33bd2cef..b557eb4e 100644 --- a/site/guide/cli-reference.md +++ b/site/guide/cli-reference.md @@ -259,11 +259,12 @@ Every `static/*` analyzer reads the same walk of the path you pass whole run, however many analyzers are selected, and counted once in the end-of-run `progress: skipped N unreadable/oversized/unparseable files` line; -- data and config files (JSON, YAML, TOML, INI, CSV, PSV, dotenv, XML, and the - git and ssh config formats) have no functions: the function analyzers - (`static/complexity`, `static/halstead`, `static/cohesion`, `static/clones`) - do not parse them and report them with zero functions, so memory and time stay - bounded on large data files; +- a data file is not parsed by the function analyzers (`static/complexity`, + `static/halstead`, `static/cohesion`, `static/clones`) when enry types it as + data (Linguist's `data` type) **and** its UAST mapping cannot produce a + function. The file is still reported, with zero functions. The two checks + are both needed: SQL and GraphQL are data-typed but declare functions, so they + are parsed; - the generated-content marker (`DO NOT EDIT`, `Code generated`, `@generated`, … in the first 512 bytes) is applied to the bytes just read unless `--include-generated`; diff --git a/site/release-notes/unreleased.md b/site/release-notes/unreleased.md index aa10eaea..3111ce15 100644 --- a/site/release-notes/unreleased.md +++ b/site/release-notes/unreleased.md @@ -80,7 +80,7 @@ This is the first release line, so there is no prior version to upgrade from. If ## Fixed -- Static analysis of large data files no longer exhausts memory. The function analyzers (`static/complexity`, `static/halstead`, `static/cohesion`, `static/clones`) skip parsing JSON, YAML, TOML, and similar files, which have no functions; reports are byte-identical. A 19 MB JSON file previously needed over 3 GB. Source files over 8 MiB are now skipped with a warning; `--max-parse-file-size` raises the limit. +- Static analysis of large data files no longer exhausts memory. The function analyzers (`static/complexity`, `static/halstead`, `static/cohesion`, `static/clones`) skip parsing a file that enry types as data and whose UAST mapping cannot produce a function (JSON, YAML, TOML, and similar); reports are byte-identical, and SQL and GraphQL, which declare functions, are still parsed. A 19 MB JSON file previously needed over 3 GB. Source files over 8 MiB are now skipped with a warning; `--max-parse-file-size` raises the limit. - Full-report schemas now declare exactly the fields each analyzer emits. `history/typos`, `history/imports`, `history/burndown` (`interactions`), and `static/clones` (`message`) had registry entries that named fields the reports did not contain, or omitted fields they did contain. - A shallow clone no longer produces empty history reports silently. History analyzers still see no commits in a shallow repository, but the run prints one `warning: shallow clone` line on stderr naming the remedy (`git fetch --unshallow`). - Fixed a data race in `internal/framework.PipelineSampler`: `t1Captured` was a plain `bool` read by the sampler goroutine and written by the caller, causing intermittent races under `go test -race`. It is now a `sync/atomic.Bool` using `CompareAndSwap`, so at most one t1 heap profile is captured. Removed the unused `t0Captured` field. diff --git a/tools/enrydump/main.go b/tools/enrydump/main.go new file mode 100644 index 00000000..25af55ae --- /dev/null +++ b/tools/enrydump/main.go @@ -0,0 +1,113 @@ +// Command enrydump writes the vendored enry v2.1.0 tables that cf-langpath +// embeds. It must run inside the enry module so `github.com/src-d/enry/v2/data` +// resolves to the pinned version: +// +// go run ./tools/enrydump <lookup.tsv> <types.tsv> (from the enry module root) +// +// lookup.tsv receives the alias (A), extension (E) and filename (F) records; +// types.tsv receives the Linguist language type (T) of every language. Keys +// are sorted, so the output is byte-stable. +package main + +import ( + "fmt" + "os" + "sort" + + "github.com/src-d/enry/v2/data" +) + +func main() { + if len(os.Args) != 3 { + fmt.Fprintln(os.Stderr, "usage: enrydump <lookup.tsv> <types.tsv>") + os.Exit(2) + } + if err := writeLookup(os.Args[1]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := writeTypes(os.Args[2]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func writeLookup(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + for _, k := range sortedAliasKeys() { + fmt.Fprintf(f, "A\t%s\t%s\n", k, data.LanguageByAliasMap[k]) + } + for _, k := range sortedExtensionKeys() { + fmt.Fprintf(f, "E\t%s", k) + for _, e := range data.ExtensionsByLanguage[k] { + fmt.Fprintf(f, "\t%s", e) + } + fmt.Fprintln(f) + } + for _, k := range sortedFilenameKeys() { + fmt.Fprintf(f, "F\t%s", k) + for _, l := range data.LanguagesByFilename[k] { + fmt.Fprintf(f, "\t%s", l) + } + fmt.Fprintln(f) + } + return f.Close() +} + +// writeTypes emits `<language>\t<type>` with the type as enry's integer code +// (0 unknown, 1 data, 2 programming, 3 markup, 4 prose). +func writeTypes(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + for _, k := range sortedTypeKeys() { + fmt.Fprintf(f, "%s\t%d\n", k, data.LanguagesType[k]) + } + return f.Close() +} + +// The enry module is go1.12 (no generics), so each map gets its own sort. + +func sortedAliasKeys() []string { + keys := make([]string, 0, len(data.LanguageByAliasMap)) + for k := range data.LanguageByAliasMap { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func sortedExtensionKeys() []string { + keys := make([]string, 0, len(data.ExtensionsByLanguage)) + for k := range data.ExtensionsByLanguage { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func sortedFilenameKeys() []string { + keys := make([]string, 0, len(data.LanguagesByFilename)) + for k := range data.LanguagesByFilename { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func sortedTypeKeys() []string { + keys := make([]string, 0, len(data.LanguagesType)) + for k := range data.LanguagesType { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} From b4dcdf86bd75cf8e6380401f72cfef075baf1754 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Thu, 15 Oct 2026 17:33:29 +0000 Subject: [PATCH 6/7] perf(static): skip parsing data files for the import analyzer The import analyzer now uses the same data-file skip as the function analyzers. A file is not parsed when enry types it as data AND its mapping cannot yield an Import or Call node, the only nodes the import rules start from (cf_imports::IMPORT_TRIGGER_TYPES). Protocol Buffers is data-typed but its mapping yields imports, so it is still parsed and its imports reported. static/comments is deliberately not skipped: YAML, TOML, and INI comments are real parsed nodes that are counted, so an empty tree would change the report. The predicate is generalized to Parser::may_yield(file, wanted), with yields_functions as one caller. cf-commands has a single parse_unless_data used by the function and import analyzers. Verified byte-identical output against the previous binary for static/imports on the data fixtures and on 12 real repositories (60 comparisons across the five analyzers), and comments and complexity on the fixtures. Refs: site/guide/cli-reference.md, site/release-notes/unreleased.md Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx --- bins/codefang/tests/functionless_files_e2e.rs | 66 +++++++++++++++++++ crates/cf-commands/src/handlers.rs | 50 ++++++++++---- .../src/handlers/static_imports.rs | 2 +- crates/cf-imports/src/extract.rs | 6 ++ crates/cf-imports/src/lib.rs | 2 +- crates/cf-uast/src/lib.rs | 3 +- crates/cf-uast/src/loader.rs | 16 ++--- crates/cf-uast/src/parser.rs | 22 +++++-- crates/cf-uast/src/types.rs | 9 +-- site/guide/cli-reference.md | 12 ++-- site/release-notes/unreleased.md | 2 +- 11 files changed, 148 insertions(+), 42 deletions(-) diff --git a/bins/codefang/tests/functionless_files_e2e.rs b/bins/codefang/tests/functionless_files_e2e.rs index 8be7bc1f..5f5d26b9 100644 --- a/bins/codefang/tests/functionless_files_e2e.rs +++ b/bins/codefang/tests/functionless_files_e2e.rs @@ -139,3 +139,69 @@ fn max_parse_file_size_flag_raises_the_limit() { ); assert_eq!(total_functions(&doc["sections"][0]), "2"); } + +/// The `issues` names of a per-file entry, sorted. +fn issue_names(entry: &serde_json::Value) -> Vec<String> { + let mut names: Vec<String> = entry["issues"] + .as_array() + .expect("issues array") + .iter() + .map(|i| i["name"].as_str().expect("issue name").to_string()) + .collect(); + names.sort(); + names +} + +#[test] +fn proto_imports_are_still_reported_from_a_data_typed_file() { + // enry types Protocol Buffer as data, but its mapping yields Import nodes, + // so the import analyzer must still parse it. + let test = repo_with(&[ + ("api.proto", b"syntax = \"proto3\";\nimport \"google/api.proto\";\nimport \"other.proto\";\nmessage M { int32 a = 1; }\n"), + ("config.yaml", b"a: 1\n"), + ]); + let path = test.path().to_str().expect("utf-8 path"); + let (doc, _) = run_json(&["-a", "static/imports", "--per-file", "--format", "json", path]); + + let files = doc["sections"][0]["files"].as_array().expect("per-file entries"); + let proto = files.iter().find(|f| f["file_path"] == "api.proto").expect("api.proto"); + assert_eq!(issue_names(proto), ["google/api.proto", "other.proto"]); + let yaml = files.iter().find(|f| f["file_path"] == "config.yaml").expect("config.yaml"); + assert!(issue_names(yaml).is_empty(), "a YAML file has no imports"); +} + +#[test] +fn comments_in_data_files_are_still_counted() { + // Comments are real nodes of the parsed tree, so a data file's comments + // must not be dropped by the function-parse skip. + let test = repo_with(&[( + "settings.yaml", + b"# top comment\nkey: value # trailing\nlist:\n - one\n", + )]); + let path = test.path().to_str().expect("utf-8 path"); + let (doc, _) = run_json(&["-a", "static/comments", "--per-file", "--format", "json", path]); + + let files = doc["sections"][0]["files"].as_array().expect("per-file entries"); + let yaml = files.iter().find(|f| f["file_path"] == "settings.yaml").expect("entry"); + let total = yaml["metrics"] + .as_array() + .expect("metrics") + .iter() + .find(|m| m["label"] == "Total Comments") + .and_then(|m| m["value"].as_str()) + .expect("Total Comments"); + assert_eq!(total, "2"); +} + +#[test] +fn sql_functions_are_still_counted_from_a_data_typed_file() { + // enry types SQL as data, but its mapping declares CREATE FUNCTION as a + // function, so the function analyzers must parse it. + let test = repo_with(&[( + "query.sql", + b"CREATE FUNCTION add(a int, b int) RETURNS int AS $$ SELECT a + b $$ LANGUAGE sql;\n", + )]); + let path = test.path().to_str().expect("utf-8 path"); + let (doc, _) = run_json(&["-a", "static/complexity", "--format", "json", path]); + assert_eq!(total_functions(&doc["sections"][0]), "1"); +} diff --git a/crates/cf-commands/src/handlers.rs b/crates/cf-commands/src/handlers.rs index c94e66f9..030d963c 100644 --- a/crates/cf-commands/src/handlers.rs +++ b/crates/cf-commands/src/handlers.rs @@ -131,20 +131,33 @@ pub const DEFAULT_MAX_PARSE_FILE_MIB: i64 = 8; /// Bytes in one MiB. const MIB_BYTES: u64 = 1024 * 1024; -/// Whether a function analyzer may skip parsing `file`. enry must type it as -/// data, and the file's mapping must be unable to produce a function node. -/// Both are needed: enry's data type admits SQL and GraphQL, whose mappings do -/// declare functions, so the type alone would change their reports. -fn skips_function_parse(parser: &cf_uast::Parser, file: &SourceFile) -> bool { +/// Parses `file` for an analyzer that looks only for the nodes `wanted` +/// accepts. A data file (enry types it as data) whose mapping cannot yield such +/// a node gets an empty file root instead of a parse: the analyzer would find +/// nothing either way, so its report is the same and the tree is never built. +/// +/// Both checks are needed. enry's data type alone admits SQL and GraphQL, whose +/// mappings declare functions and operations, so the type alone would change +/// their reports. +/// +/// # Errors +/// +/// The parser's error for a file that must be parsed. +pub fn parse_unless_data( + parser: &cf_uast::Parser, + file: &SourceFile, + wanted: &dyn Fn(&cf_uast::UastSpec) -> bool, +) -> Result<cf_uast::Node, cf_uast::ParseError> { let is_data = cf_langpath::language_by_path_with_content(&file.rel_path, &file.bytes) .and_then(|language| cf_langpath::language_type(&language)) == Some(cf_langpath::LanguageType::Data); - is_data && !parser.yields_functions(&file.rel_path) + if is_data && !parser.may_yield(&file.rel_path, wanted) { + return Ok(cf_uast::Node::with_token(cf_uast_node::UAST_FILE, "")); + } + parser.parse(&file.rel_path, &file.bytes) } -/// Parses `file` for a function analyzer. A file [`skips_function_parse`] -/// admits gets an empty file root: it has no functions, so its report is the -/// same and the tree is never built. +/// [`parse_unless_data`] for the function analyzers. /// /// # Errors /// @@ -153,10 +166,21 @@ pub fn parse_for_functions( parser: &cf_uast::Parser, file: &SourceFile, ) -> Result<cf_uast::Node, cf_uast::ParseError> { - if skips_function_parse(parser, file) { - return Ok(cf_uast::Node::with_token(cf_uast_node::UAST_FILE, "")); - } - parser.parse(&file.rel_path, &file.bytes) + parse_unless_data(parser, file, &cf_uast::spec_yields_function) +} + +/// [`parse_unless_data`] for the import analyzer. +/// +/// # Errors +/// +/// The parser's error for a file that must be parsed. +pub fn parse_for_imports( + parser: &cf_uast::Parser, + file: &SourceFile, +) -> Result<cf_uast::Node, cf_uast::ParseError> { + parse_unless_data(parser, file, &|spec| { + cf_imports::IMPORT_TRIGGER_TYPES.contains(&spec.r#type.as_str()) + }) } /// Bytes read per file by [`walk_all_files`]: the classification header diff --git a/crates/cf-commands/src/handlers/static_imports.rs b/crates/cf-commands/src/handlers/static_imports.rs index c74fb688..7385fbf8 100644 --- a/crates/cf-commands/src/handlers/static_imports.rs +++ b/crates/cf-commands/src/handlers/static_imports.rs @@ -190,7 +190,7 @@ fn scan(root_path: &str, filter: &StaticFilter, keep_per_file: bool) -> Option<S let mut scan = Scan::default(); for file in walk { - let imports: Vec<String> = match parser.parse(&file.rel_path, &file.bytes) { + let imports: Vec<String> = match super::parse_for_imports(&parser, &file) { Ok(node) => { let found = extract_imports(&node, &file.language, &file.bytes); scan.aggregate.add_file(&file.language, &found); diff --git a/crates/cf-imports/src/extract.rs b/crates/cf-imports/src/extract.rs index c5a6c608..51ae3039 100644 --- a/crates/cf-imports/src/extract.rs +++ b/crates/cf-imports/src/extract.rs @@ -43,6 +43,12 @@ const TYPE_IDENTIFIER: &str = "Identifier"; const TYPE_SYNTHETIC: &str = "Synthetic"; /// UAST type of a call expression node. const TYPE_CALL: &str = "Call"; + +/// The node types an import rule starts from: `Import` statements, and `Call`s +/// for the JavaScript `require` and `import()` loaders. Every rule consumes one +/// of these, so a language whose mapping has no output of these types cannot +/// yield an import. +pub const IMPORT_TRIGGER_TYPES: [&str; 2] = [TYPE_IMPORT, TYPE_CALL]; /// The CommonJS loader function. const JS_REQUIRE: &str = "require"; /// The token the dynamic-import keyword node carries. diff --git a/crates/cf-imports/src/lib.rs b/crates/cf-imports/src/lib.rs index 2ef9d2bd..4507307c 100644 --- a/crates/cf-imports/src/lib.rs +++ b/crates/cf-imports/src/lib.rs @@ -55,7 +55,7 @@ pub mod stdlib; // Re-exports for the primary public surface. pub use aggregate::{Aggregate, Entry}; -pub use extract::{extract_imports, Import}; +pub use extract::{extract_imports, Import, IMPORT_TRIGGER_TYPES}; pub use history::{HistoryReport, ImportEntry, ImportsMap}; pub use language::Language; pub use metrics::{ diff --git a/crates/cf-uast/src/lib.rs b/crates/cf-uast/src/lib.rs index 1f0f1461..4ba1cd6b 100644 --- a/crates/cf-uast/src/lib.rs +++ b/crates/cf-uast/src/lib.rs @@ -51,7 +51,7 @@ mod types; pub mod languages; pub use changes::detect_changes; -pub use loader::{embedded_mappings_available, Loader, PrecompiledMapping}; +pub use loader::{embedded_mappings_available, spec_yields_function, Loader, PrecompiledMapping}; pub use parser::Parser; pub use types::{ get_file_extension, ChangeType, LanguageParser, Map, NodeChange, ParseError, @@ -61,6 +61,7 @@ pub use types::{ // Re-export the node type so facade callers can use it without an explicit // `cf-uast-node` dependency. pub use cf_uast_node::{Node, Positions}; +pub use cf_uast_mapping::UastSpec; /// The dependency name provided by UAST change detection. pub const DEPENDENCY_UAST_CHANGES: &str = "uast_changes"; diff --git a/crates/cf-uast/src/loader.rs b/crates/cf-uast/src/loader.rs index d08697ab..556a8385 100644 --- a/crates/cf-uast/src/loader.rs +++ b/crates/cf-uast/src/loader.rs @@ -358,21 +358,19 @@ impl LanguageParser for LazyDslParser { self.extensions.clone() } - fn yields_functions(&self) -> bool { + fn may_yield(&self, wanted: &dyn Fn(&UastSpec) -> bool) -> bool { match self.init() { - Ok(state) => state - .resolved - .iter() - .any(|r| spec_yields_function(&r.rule.uast_spec)), + Ok(state) => state.resolved.iter().any(|r| wanted(&r.rule.uast_spec)), Err(_) => true, } } } -/// Whether a rule's output spec is a function node, by the shape the -/// complexity analyzer discovers: type `Function` or `Method`, or the -/// `Function` and `Declaration` roles together. -fn spec_yields_function(spec: &UastSpec) -> bool { +/// Whether an output spec is a function node, by the shape the complexity +/// analyzer discovers: type `Function` or `Method`, or the `Function` and +/// `Declaration` roles together. +#[must_use] +pub fn spec_yields_function(spec: &UastSpec) -> bool { let has_role = |name: &str| spec.roles.iter().any(|r| r == name); matches!(spec.r#type.as_str(), UAST_FUNCTION | UAST_METHOD) || (has_role(ROLE_FUNCTION) && has_role(ROLE_DECLARATION)) diff --git a/crates/cf-uast/src/parser.rs b/crates/cf-uast/src/parser.rs index ba5ed5db..2b222a3b 100644 --- a/crates/cf-uast/src/parser.rs +++ b/crates/cf-uast/src/parser.rs @@ -11,7 +11,8 @@ use std::sync::Arc; use cf_uast_mapping::Parser as MappingParser; use cf_uast_node::Node; -use crate::loader::Loader; +use crate::loader::{spec_yields_function, Loader}; +use crate::UastSpec; use crate::types::{get_file_extension, LanguageParser, Map, ParseError}; /// The main entry point for UAST parsing. @@ -117,16 +118,23 @@ impl Parser { } } - /// Whether the mapping for `filename`'s language can produce a function - /// node. `false` means no parse of such a file yields a function, so a - /// function analyzer may skip the parse. A file with no parser is `false` - /// too: it cannot be parsed to yield functions either. + /// Whether the mapping for `filename`'s language has an output spec that + /// `wanted` accepts. `false` means no parse of such a file yields a node + /// `wanted` matches, so an analyzer that looks only for those nodes may skip + /// the parse. A file with no parser is `false` too: it cannot be parsed to + /// yield such a node either. #[must_use] - pub fn yields_functions(&self, filename: &str) -> bool { + pub fn may_yield(&self, filename: &str, wanted: &dyn Fn(&UastSpec) -> bool) -> bool { let ext = get_file_extension(filename).to_lowercase(); self.loader .language_parser(&ext) - .is_some_and(|p| p.yields_functions()) + .is_some_and(|p| p.may_yield(wanted)) + } + + /// [`Self::may_yield`] for function nodes ([`crate::spec_yields_function`]). + #[must_use] + pub fn yields_functions(&self, filename: &str) -> bool { + self.may_yield(filename, &spec_yields_function) } /// Parses `content` for `filename`, returning its UAST root. diff --git a/crates/cf-uast/src/types.rs b/crates/cf-uast/src/types.rs index bdf8fa7b..dd4b5a06 100644 --- a/crates/cf-uast/src/types.rs +++ b/crates/cf-uast/src/types.rs @@ -91,10 +91,11 @@ pub trait LanguageParser { /// Returns the supported file extensions (including the leading dot). fn extensions(&self) -> Vec<String>; - /// Whether any mapping rule of this language can produce a function node - /// (see [`crate::Parser::yields_functions`]). The default is `true`, the - /// answer that never lets a caller skip a parse it needs. - fn yields_functions(&self) -> bool { + /// Whether any mapping rule of this language has an output spec that + /// `wanted` accepts (see [`crate::Parser::may_yield`]). The default is + /// `true`, the answer that never lets a caller skip a parse it needs. + fn may_yield(&self, wanted: &dyn Fn(&crate::UastSpec) -> bool) -> bool { + let _ = wanted; true } } diff --git a/site/guide/cli-reference.md b/site/guide/cli-reference.md index b557eb4e..db5aed8e 100644 --- a/site/guide/cli-reference.md +++ b/site/guide/cli-reference.md @@ -260,11 +260,13 @@ Every `static/*` analyzer reads the same walk of the path you pass end-of-run `progress: skipped N unreadable/oversized/unparseable files` line; - a data file is not parsed by the function analyzers (`static/complexity`, - `static/halstead`, `static/cohesion`, `static/clones`) when enry types it as - data (Linguist's `data` type) **and** its UAST mapping cannot produce a - function. The file is still reported, with zero functions. The two checks - are both needed: SQL and GraphQL are data-typed but declare functions, so they - are parsed; + `static/halstead`, `static/cohesion`, `static/clones`) or by the import + analyzer (`static/imports`) when enry types it as data (Linguist's `data` + type) **and** its UAST mapping cannot produce the nodes those analyzers look + for (a function, or an import). The file is still reported, with zero results. + Both checks are needed: SQL and GraphQL are data-typed but declare functions, + and Protocol Buffers declare imports, so they are parsed. `static/comments` + always parses, because comments in YAML, TOML, and INI files are counted; - the generated-content marker (`DO NOT EDIT`, `Code generated`, `@generated`, … in the first 512 bytes) is applied to the bytes just read unless `--include-generated`; diff --git a/site/release-notes/unreleased.md b/site/release-notes/unreleased.md index 3111ce15..e6d89e16 100644 --- a/site/release-notes/unreleased.md +++ b/site/release-notes/unreleased.md @@ -80,7 +80,7 @@ This is the first release line, so there is no prior version to upgrade from. If ## Fixed -- Static analysis of large data files no longer exhausts memory. The function analyzers (`static/complexity`, `static/halstead`, `static/cohesion`, `static/clones`) skip parsing a file that enry types as data and whose UAST mapping cannot produce a function (JSON, YAML, TOML, and similar); reports are byte-identical, and SQL and GraphQL, which declare functions, are still parsed. A 19 MB JSON file previously needed over 3 GB. Source files over 8 MiB are now skipped with a warning; `--max-parse-file-size` raises the limit. +- Static analysis of large data files no longer exhausts memory. The function analyzers (`static/complexity`, `static/halstead`, `static/cohesion`, `static/clones`) skip parsing a file that enry types as data and whose UAST mapping cannot produce a function or an import (JSON, YAML, TOML, and similar); reports are byte-identical. SQL and GraphQL, which declare functions, and Protocol Buffers, which declare imports, are still parsed. `static/comments` always parses, since YAML, TOML, and INI comments are counted. A 19 MB JSON file previously needed over 3 GB. Source files over 8 MiB are now skipped with a warning; `--max-parse-file-size` raises the limit. - Full-report schemas now declare exactly the fields each analyzer emits. `history/typos`, `history/imports`, `history/burndown` (`interactions`), and `static/clones` (`message`) had registry entries that named fields the reports did not contain, or omitted fields they did contain. - A shallow clone no longer produces empty history reports silently. History analyzers still see no commits in a shallow repository, but the run prints one `warning: shallow clone` line on stderr naming the remedy (`git fetch --unshallow`). - Fixed a data race in `internal/framework.PipelineSampler`: `t1Captured` was a plain `bool` read by the sampler goroutine and written by the caller, causing intermittent races under `go test -race`. It is now a `sync/atomic.Bool` using `CompareAndSwap`, so at most one t1 heap profile is captured. Removed the unused `t0Captured` field. From 48a918aacb2f95e2db6ca0412025e8b513aa392f Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Fri, 16 Oct 2026 06:10:15 +0000 Subject: [PATCH 7/7] style: cargo fmt the workspace The Format check in CI failed on the formatting of the new e2e tests and the cf-uast re-exports. This is a formatting-only change; no code changed. Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx --- bins/codefang/tests/functionless_files_e2e.rs | 69 +++++++++++++++---- .../tests/report_schema_contract_e2e.rs | 6 +- bins/codefang/tests/shallow_history_e2e.rs | 7 +- crates/cf-uast/src/lib.rs | 2 +- crates/cf-uast/src/loader.rs | 2 +- crates/cf-uast/src/parser.rs | 2 +- 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/bins/codefang/tests/functionless_files_e2e.rs b/bins/codefang/tests/functionless_files_e2e.rs index 5f5d26b9..d7d1f05f 100644 --- a/bins/codefang/tests/functionless_files_e2e.rs +++ b/bins/codefang/tests/functionless_files_e2e.rs @@ -65,14 +65,26 @@ fn repo_with(files: &[(&str, &[u8])]) -> TestRepo { #[test] fn data_file_is_listed_per_file_with_zero_functions() { let test = repo_with(&[ - ("main.go", b"package main\n\nfunc add(a, b int) int {\n\treturn a + b\n}\n"), + ( + "main.go", + b"package main\n\nfunc add(a, b int) int {\n\treturn a + b\n}\n", + ), ("config.json", b"{\"name\": \"x\", \"items\": [1, 2, 3]}\n"), ("settings.yaml", b"key: value\nlist:\n - one\n"), ]); let path = test.path().to_str().expect("utf-8 path"); - let (doc, _) = run_json(&["-a", "static/complexity", "--per-file", "--format", "json", path]); + let (doc, _) = run_json(&[ + "-a", + "static/complexity", + "--per-file", + "--format", + "json", + path, + ]); - let files = doc["sections"][0]["files"].as_array().expect("per-file entries"); + let files = doc["sections"][0]["files"] + .as_array() + .expect("per-file entries"); for data in ["config.json", "settings.yaml"] { let entry = files .iter() @@ -93,7 +105,10 @@ fn source_file_over_the_parse_limit_is_skipped_with_a_warning() { let mut big = b"package main\n\nfunc big() int {\n\treturn 1\n}\n// ".to_vec(); big.resize(DEFAULT_LIMIT_MIB * MIB + 1, b'x'); let test = repo_with(&[ - ("main.go", b"package main\n\nfunc small() int {\n\treturn 2\n}\n"), + ( + "main.go", + b"package main\n\nfunc small() int {\n\treturn 2\n}\n", + ), ("big.go", &big), ]); let path = test.path().to_str().expect("utf-8 path"); @@ -119,7 +134,10 @@ fn max_parse_file_size_flag_raises_the_limit() { let mut big = b"package main\n\nfunc big() int {\n\treturn 1\n}\n// ".to_vec(); big.resize(DEFAULT_LIMIT_MIB * MIB + 1, b'x'); let test = repo_with(&[ - ("main.go", b"package main\n\nfunc small() int {\n\treturn 2\n}\n"), + ( + "main.go", + b"package main\n\nfunc small() int {\n\treturn 2\n}\n", + ), ("big.go", &big), ]); let path = test.path().to_str().expect("utf-8 path"); @@ -161,12 +179,27 @@ fn proto_imports_are_still_reported_from_a_data_typed_file() { ("config.yaml", b"a: 1\n"), ]); let path = test.path().to_str().expect("utf-8 path"); - let (doc, _) = run_json(&["-a", "static/imports", "--per-file", "--format", "json", path]); + let (doc, _) = run_json(&[ + "-a", + "static/imports", + "--per-file", + "--format", + "json", + path, + ]); - let files = doc["sections"][0]["files"].as_array().expect("per-file entries"); - let proto = files.iter().find(|f| f["file_path"] == "api.proto").expect("api.proto"); + let files = doc["sections"][0]["files"] + .as_array() + .expect("per-file entries"); + let proto = files + .iter() + .find(|f| f["file_path"] == "api.proto") + .expect("api.proto"); assert_eq!(issue_names(proto), ["google/api.proto", "other.proto"]); - let yaml = files.iter().find(|f| f["file_path"] == "config.yaml").expect("config.yaml"); + let yaml = files + .iter() + .find(|f| f["file_path"] == "config.yaml") + .expect("config.yaml"); assert!(issue_names(yaml).is_empty(), "a YAML file has no imports"); } @@ -179,10 +212,22 @@ fn comments_in_data_files_are_still_counted() { b"# top comment\nkey: value # trailing\nlist:\n - one\n", )]); let path = test.path().to_str().expect("utf-8 path"); - let (doc, _) = run_json(&["-a", "static/comments", "--per-file", "--format", "json", path]); + let (doc, _) = run_json(&[ + "-a", + "static/comments", + "--per-file", + "--format", + "json", + path, + ]); - let files = doc["sections"][0]["files"].as_array().expect("per-file entries"); - let yaml = files.iter().find(|f| f["file_path"] == "settings.yaml").expect("entry"); + let files = doc["sections"][0]["files"] + .as_array() + .expect("per-file entries"); + let yaml = files + .iter() + .find(|f| f["file_path"] == "settings.yaml") + .expect("entry"); let total = yaml["metrics"] .as_array() .expect("metrics") diff --git a/bins/codefang/tests/report_schema_contract_e2e.rs b/bins/codefang/tests/report_schema_contract_e2e.rs index 7f4dc34c..739c190b 100644 --- a/bins/codefang/tests/report_schema_contract_e2e.rs +++ b/bins/codefang/tests/report_schema_contract_e2e.rs @@ -79,7 +79,11 @@ fn every_analyzer_schema_declares_exactly_its_report_fields() { let doc = full_report(&path); let analyzers = doc["analyzers"].as_array().expect("analyzers array"); - assert_eq!(analyzers.len(), ANALYZER_COUNT, "full selection analyzer count"); + assert_eq!( + analyzers.len(), + ANALYZER_COUNT, + "full selection analyzer count" + ); // Collect every mismatch so one run lists them all, not just the first. let mut mismatches = Vec::new(); for analyzer in analyzers { diff --git a/bins/codefang/tests/shallow_history_e2e.rs b/bins/codefang/tests/shallow_history_e2e.rs index 708635c5..27fbe63d 100644 --- a/bins/codefang/tests/shallow_history_e2e.rs +++ b/bins/codefang/tests/shallow_history_e2e.rs @@ -47,8 +47,11 @@ fn make_shallow(test: &TestRepo) { .expect("head") .target() .expect("head target"); - std::fs::write(test.path().join(".git").join("shallow"), format!("{head}\n")) - .expect("write .git/shallow"); + std::fs::write( + test.path().join(".git").join("shallow"), + format!("{head}\n"), + ) + .expect("write .git/shallow"); } /// Runs `codefang run -a history/devs --format json` and returns diff --git a/crates/cf-uast/src/lib.rs b/crates/cf-uast/src/lib.rs index 4ba1cd6b..986b0a63 100644 --- a/crates/cf-uast/src/lib.rs +++ b/crates/cf-uast/src/lib.rs @@ -60,8 +60,8 @@ pub use types::{ // Re-export the node type so facade callers can use it without an explicit // `cf-uast-node` dependency. -pub use cf_uast_node::{Node, Positions}; pub use cf_uast_mapping::UastSpec; +pub use cf_uast_node::{Node, Positions}; /// The dependency name provided by UAST change detection. pub const DEPENDENCY_UAST_CHANGES: &str = "uast_changes"; diff --git a/crates/cf-uast/src/loader.rs b/crates/cf-uast/src/loader.rs index 556a8385..22fbfe7c 100644 --- a/crates/cf-uast/src/loader.rs +++ b/crates/cf-uast/src/loader.rs @@ -18,8 +18,8 @@ use std::collections::HashMap; use std::sync::{Arc, OnceLock}; -use cf_uast_mapping::{LanguageInfo, Parser as MappingParser, PatternMatcher, Rule}; use cf_uast_mapping::UastSpec; +use cf_uast_mapping::{LanguageInfo, Parser as MappingParser, PatternMatcher, Rule}; use cf_uast_node::{Node, ROLE_DECLARATION, ROLE_FUNCTION, UAST_FUNCTION, UAST_METHOD}; use crate::lowering::{resolve_rules, Lowering, ResolvedRule}; diff --git a/crates/cf-uast/src/parser.rs b/crates/cf-uast/src/parser.rs index 2b222a3b..69101905 100644 --- a/crates/cf-uast/src/parser.rs +++ b/crates/cf-uast/src/parser.rs @@ -12,8 +12,8 @@ use cf_uast_mapping::Parser as MappingParser; use cf_uast_node::Node; use crate::loader::{spec_yields_function, Loader}; -use crate::UastSpec; use crate::types::{get_file_extension, LanguageParser, Map, ParseError}; +use crate::UastSpec; /// The main entry point for UAST parsing. ///